Skip to content

KAFKA-8620: fix NPE due to race condition during shutdown while rebalancing#7021

Merged
mjsax merged 10 commits into
apache:trunkfrom
abbccdda:system_test_fix
Jul 15, 2019
Merged

KAFKA-8620: fix NPE due to race condition during shutdown while rebalancing#7021
mjsax merged 10 commits into
apache:trunkfrom
abbccdda:system_test_fix

Conversation

@abbccdda

@abbccdda abbccdda commented Jul 1, 2019

Copy link
Copy Markdown

We have detected a race condition under system test failure. The problem was that the task manager internal active tasks should be guarded against state changes on the stream thread. Could definitely consider other fixes but this is currently the make-sense one.

Committer Checklist (excluded from commit message)

  • Verify design and implementation
  • Verify test coverage and CI build status
  • Verify documentation (including upgrade notes)

@mjsax mjsax added the streams label Jul 2, 2019
@abbccdda

abbccdda commented Jul 8, 2019

Copy link
Copy Markdown
Author

@mjsax mjsax changed the title KAFKA-8620: fix stream task concurrent access issue KAFKA-8620: fix NPE due to race condition during shutdown while rebalancing Jul 9, 2019
return null;
} else if (state == State.PARTITIONS_REVOKED && newState == State.PARTITIONS_REVOKED) {
log.debug("Invalid transition from PARTITIONS_REVOKED to PARTITIONS_REVOKED: " +
"self transition is not allowed");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this, I am wondering if we should just change the FSM to allow this transition and simplify the code here? \cc @guozhangwang

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this when tightening the FSM before but the unit tests reminds me of one thing: our current contract is that we only transit to PARTITIONS_REVOKED when calling onPartitionsRevoked, which is called only once at the beginning of the rebalance today, so keeping it strict is better just in case we have incorrect partial rebalance procedure.

With KIP-429 this may be violated so we need to revisit our FSM once Streams adopt cooperative protocols. cc @ableegoldman who's working on this.

return;
}
if (streamThread.assignmentErrorCode.get() == StreamsPartitionAssignor.Error.NONE.code()) {
log.error("State transition from {} to PARTITIONS_ASSIGNED failed. " +

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this an ERROR? If we receive a SHUTDOWN signal, it's just an "eager exit" to not create tasks IMHO. We might want to log a DEBUG though.

Would also update the message:

log.debug("Skipping task creation in rebalance because we are already in shutdown phase.");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

setState(State.PENDING_SHUTDOWN);

log.info("Shutting down");
log.info("Shutting down with cleanRun {}", cleanRun);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we need this? If the shutdown is not clean, we logged an ERROR before in StreamThread#run()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not use variable names in log messages.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could avoid using variable name in the log, but still feel needed to log the flag here, as we can't control future changes to set cleanRun flag.


// assign single partition
assignedPartitions.add(t1p1);
assignedPartitions.add(t1p2);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need two partitions?

beginOffsets.put(t1p2, 0L);
mockConsumer.updateBeginningOffsets(beginOffsets);

final Thread callbackThread = new Thread(() -> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a Thread here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a separate thread to trigger callbacks, otherwise the state won't proceed. Maybe there is a better way to do that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate? What do you mean by

otherwise the state won't proceed

@cadonna cadonna left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abbccdda, Thank you for the PR.

if (streamThread.assignmentErrorCode.get() == StreamsPartitionAssignor.Error.NONE.code()) {
log.error("State transition from {} to PARTITIONS_ASSIGNED failed. " +
"Will skip the task initialization", streamThread.state());
} else if (streamThread.assignmentErrorCode.get() != StreamsPartitionAssignor.Error.NONE.code()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a rather brittle fix? How does this code guarantee that after streamThread.assignmentErrorCode.get() != StreamsPartitionAssignor.Error.NONE.code() is false, the result of streamThread.assignmentErrorCode.get() does not change again before the tasks are created?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in PartitionsAssigned we should have already passed the assignment error placement, so I won't expect another flip for its value during this round rebalance.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

setState(State.PENDING_SHUTDOWN);

log.info("Shutting down");
log.info("Shutting down with cleanRun {}", cleanRun);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not use variable names in log messages.

@vvcephei vvcephei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Just one minor suggestion.

@cadonna cadonna left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
@mjsax, What is so bad about using a mock for the task manager?

if (streamThread.assignmentErrorCode.get() == StreamsPartitionAssignor.Error.NONE.code()) {
log.error("State transition from {} to PARTITIONS_ASSIGNED failed. " +
"Will skip the task initialization", streamThread.state());
} else if (streamThread.assignmentErrorCode.get() != StreamsPartitionAssignor.Error.NONE.code()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

}

@Test
public void shouldNotAccessTaskManagerWhenPendingShutdownInRunOnce() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would give this test a more meaningful name, e.g. shouldNotThrowWhenPendingShutdownInRunOnce.

}

@Test
public void shouldHaveNoErrorAccessTaskManagerInRunOnceNoShutdown() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, e.g. shouldNotThrowWithoutPendingShutdownInRunOnce

return;
}
if (streamThread.assignmentErrorCode.get() == StreamsPartitionAssignor.Error.NONE.code()) {
log.debug("Skipping task creation in rebalance because we are already in {} state.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the indentation be as follows?

log.debug(
    "Skipping task creation in rebalance because we are already in {} state.",
    streamThread.state()
);

streamThread.state());
} else if (streamThread.assignmentErrorCode.get() != StreamsPartitionAssignor.Error.NONE.code()) {
log.debug("Encountered assignment error during partition assignment: {}. " +
"Will skip the task initialization", streamThread.assignmentErrorCode);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the indentation be as follows?

log.debug(
    "Encountered assignment error during partition assignment: {}. Will skip the task initialization", 
    streamThread.assignmentErrorCode
);

The first parameter is one or two characters too long, though.

@cadonna

cadonna commented Jul 10, 2019

Copy link
Copy Markdown
Member

Retest this, please

@abbccdda

Copy link
Copy Markdown
Author

@cadonna Thank you for the review, addressed!

@mjsax

mjsax commented Jul 10, 2019

Copy link
Copy Markdown
Member

@cadonna The bug is, that if we don't create tasks on the TaskManager and when we later try to receive tasks from it, we hit an NPE. Hence, the TaskManager is on the test code path -- mocking it seems to make the test weak IMHO.

@abbccdda

Copy link
Copy Markdown
Author

Retest this please

@cadonna

cadonna commented Jul 10, 2019

Copy link
Copy Markdown
Member

@mjsax I do not agree. The TaskManager is on the test code path, but it does not need to be tested. A unit test should test the class under test which is StreamThread in this case and avoid as much as possible testing other classes. With a TaskManager mock, you could easily verify if the calls to the TaskManager that would result in a NullPointerException are done or not. In addition, with a TaskManager mock the tests would also better document what the productive code is supposed to do because you have to specify it explicitly on the mock.

@mjsax

mjsax commented Jul 10, 2019

Copy link
Copy Markdown
Member

@cadonna Maybe. Do you want that we switch back to a mock? If yes, please let @abbccdda know. If not, please let me know so the PR can be merged. Your call.

@abbccdda

Copy link
Copy Markdown
Author

Retest this please

@abbccdda

Copy link
Copy Markdown
Author

@cadonna @mjsax My take is that the purpose of mocking taskManager is to create a conditional behavior path: if you set an active task, you could retrieve it later; if you don't, you will get NPE, while the setup of real taskManger is too much compared with a mock. Actually in our case, the mock examples in other tests doesn't quite fit to implement this mentioned conditional logic, and the task manager setup is not bad either. So we should be fine just creating a new client in this case.

@ijuma

ijuma commented Jul 11, 2019

Copy link
Copy Markdown
Member

Reopening to trigger Jenkins build.

@ijuma ijuma closed this Jul 11, 2019
@ijuma ijuma reopened this Jul 11, 2019
@cadonna

cadonna commented Jul 11, 2019

Copy link
Copy Markdown
Member

@abbccdda: What you describe is also a valid use case of a mock, but it only achieves the isolation of the class under test. It does not document well the expected behaviour of productive code. Furthermore, IMO conditional behaviour in a mock should be avoided if possible, because it introduces code that needs maintenance and could be wrong. A mock should be as simple as possible. Admittedly, in this case the code would probably not be that complex, but if it could be avoided, I would avoid it.

@mjsax @abbccdda: I do not want to block this fix further. The test seems to be correct. My concern is about clean and maintainable code. Up to you to follow my reasoning or not.

@bbejeck

bbejeck commented Jul 11, 2019

Copy link
Copy Markdown
Member

retest this please

2 similar comments
@bbejeck

bbejeck commented Jul 11, 2019

Copy link
Copy Markdown
Member

retest this please

@bbejeck

bbejeck commented Jul 11, 2019

Copy link
Copy Markdown
Member

retest this please

@abbccdda

Copy link
Copy Markdown
Author

Checked log and the single test failure was in RebalanceSourceConnectorsIntegrationTest in Connect, not related

@bbejeck

bbejeck commented Jul 11, 2019

Copy link
Copy Markdown
Member

retest this please

2 similar comments
@abbccdda

Copy link
Copy Markdown
Author

retest this please

@abbccdda

Copy link
Copy Markdown
Author

retest this please

@bbejeck

bbejeck commented Jul 12, 2019

Copy link
Copy Markdown
Member

failures unrelated
retest this please

@abbccdda

Copy link
Copy Markdown
Author

All flaky tests:
ExampleConnectIntegrationTest > testSourceConnector

@abbccdda

Copy link
Copy Markdown
Author

retest this please

@abbccdda

Copy link
Copy Markdown
Author

@mjsax @bbejeck I have witnessed 4 times 2/3 green builds, and all failures are flaky tests including this time: ExampleConnectIntegrationTest.testSourceConnector
I personally don't think there is any further meaning to trigger another jenkins build, but you will make the call.

@mjsax
mjsax merged commit 3e48bdb into apache:trunk Jul 15, 2019
mjsax pushed a commit that referenced this pull request Jul 15, 2019
…ancing (#7021)

Reviewers: Matthias J. Sax <matthias@confluent.io>, Bruno Cadonna <bruno@confluent.io>, John Roesler <john@confluent.io>
mjsax pushed a commit that referenced this pull request Jul 15, 2019
…ancing (#7021)

Reviewers: Matthias J. Sax <matthias@confluent.io>, Bruno Cadonna <bruno@confluent.io>, John Roesler <john@confluent.io>
return null;
} else if (state == State.PARTITIONS_REVOKED && newState == State.PARTITIONS_REVOKED) {
log.debug("Invalid transition from PARTITIONS_REVOKED to PARTITIONS_REVOKED: " +
"self transition is not allowed");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this when tightening the FSM before but the unit tests reminds me of one thing: our current contract is that we only transit to PARTITIONS_REVOKED when calling onPartitionsRevoked, which is called only once at the beginning of the rebalance today, so keeping it strict is better just in case we have incorrect partial rebalance procedure.

With KIP-429 this may be violated so we need to revisit our FSM once Streams adopt cooperative protocols. cc @ableegoldman who's working on this.

"will abort the current process and re-throw at the end of rebalance: {}",
t
);
"will abort the current process and re-throw at the end of rebalance", t);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch.

mjsax pushed a commit that referenced this pull request Jul 15, 2019
…ancing (#7021)

Reviewers: Matthias J. Sax <matthias@confluent.io>, Bruno Cadonna <bruno@confluent.io>, John Roesler <john@confluent.io>
@mjsax

mjsax commented Jul 15, 2019

Copy link
Copy Markdown
Member

Merged to trunk and cherry-picked to 2.3, 2.2, and 2.1 branches.

ijuma added a commit to confluentinc/kafka that referenced this pull request Jul 20, 2019
* apache-github/2.3:
  MINOR: Update documentation for enabling optimizations (apache#7099)
  MINOR: Remove stale streams producer retry default docs. (apache#6844)
  KAFKA-8635; Skip client poll in Sender loop when no request is sent (apache#7085)
  KAFKA-8615: Change to track partition time breaks TimestampExtractor (apache#7054)
  KAFKA-8670; Fix exception for kafka-topics.sh --describe without --topic mentioned (apache#7094)
  KAFKA-8602: Separate PR for 2.3 branch (apache#7092)
  KAFKA-8530; Check for topic authorization errors in OffsetFetch response (apache#6928)
  KAFKA-8662; Fix producer metadata error handling and consumer manual assignment (apache#7086)
  KAFKA-8637: WriteBatch objects leak off-heap memory (apache#7050)
  KAFKA-8620: fix NPE due to race condition during shutdown while rebalancing (apache#7021)
  HOT FIX: close RocksDB objects in correct order (apache#7076)
  KAFKA-7157: Fix handling of nulls in TimestampConverter (apache#7070)
  KAFKA-6605: Fix NPE in Flatten when optional Struct is null (apache#5705)
  Fixes apache#8198 KStreams testing docs use non-existent method pipe (apache#6678)
  KAFKA-5998: fix checkpointableOffsets handling (apache#7030)
  KAFKA-8653; Default rebalance timeout to session timeout for JoinGroup v0 (apache#7072)
  KAFKA-8591; WorkerConfigTransformer NPE on connector configuration reloading (apache#6991)
  MINOR: add upgrade text (apache#7013)
  Bump version to 2.3.1-SNAPSHOT
xiowu0 pushed a commit to linkedin/kafka that referenced this pull request Aug 22, 2019
…during shutdown while rebalancing (apache#7021)

TICKET = KAFKA-8620
LI_DESCRIPTION =
EXIT_CRITERIA = HASH [0f0093c]
ORIGINAL_DESCRIPTION =

Reviewers: Matthias J. Sax <matthias@confluent.io>, Bruno Cadonna <bruno@confluent.io>, John Roesler <john@confluent.io>
(cherry picked from commit 0f0093c)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants