-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-10150: task state transitions/management and committing cleanup #8856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3789a7c
f621c3a
0c3c080
a680583
fb64b97
7b08e28
dd03646
1f989fc
cf21fb5
d2ba0df
bd032c9
0349663
3ff79ea
2529fb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,8 +107,6 @@ public class StreamTask extends AbstractTask implements ProcessorNodePunctuator, | |
| private boolean commitNeeded = false; | ||
| private boolean commitRequested = false; | ||
|
|
||
| private Map<TopicPartition, Long> checkpoint = null; | ||
|
|
||
| public StreamTask(final TaskId id, | ||
| final Set<TopicPartition> partitions, | ||
| final ProcessorTopology topology, | ||
|
|
@@ -250,14 +248,9 @@ public void completeRestoration() { | |
| public void suspend() { | ||
| switch (state()) { | ||
| case CREATED: | ||
| case SUSPENDED: | ||
|
ableegoldman marked this conversation as resolved.
Outdated
|
||
| log.info("Skip suspending since state is {}", state()); | ||
|
|
||
| break; | ||
|
|
||
| case RESTORING: | ||
| log.info("Suspended {}", state()); | ||
| transitionTo(State.SUSPENDED); | ||
| log.info("Suspended restoring"); | ||
|
|
||
| break; | ||
|
|
||
|
|
@@ -272,6 +265,12 @@ public void suspend() { | |
|
|
||
| break; | ||
|
|
||
| case SUSPENDED: | ||
| log.info("Skip suspending since state is {}", state()); | ||
|
|
||
| break; | ||
|
|
||
|
|
||
| case CLOSED: | ||
| throw new IllegalStateException("Illegal state " + state() + " while suspending active task " + id); | ||
|
|
||
|
|
@@ -342,7 +341,6 @@ public Map<TopicPartition, OffsetAndMetadata> prepareCommit() { | |
| case RUNNING: | ||
| case RESTORING: | ||
| case SUSPENDED: | ||
| maybeScheduleCheckpoint(); | ||
| stateMgr.flush(); | ||
| recordCollector.flush(); | ||
|
|
||
|
|
@@ -409,30 +407,38 @@ private Map<TopicPartition, OffsetAndMetadata> committableOffsetsAndMetadata() { | |
| return committableOffsets; | ||
| } | ||
|
|
||
| /** | ||
| * This should only be called if the attempted commit succeeded for this task | ||
| */ | ||
| @Override | ||
| public void postCommit() { | ||
| commitRequested = false; | ||
| commitNeeded = false; | ||
|
|
||
| switch (state()) { | ||
| case RESTORING: | ||
| writeCheckpointIfNeed(); | ||
| writeCheckpoint(); | ||
|
|
||
| break; | ||
|
|
||
| case RUNNING: | ||
| if (!eosEnabled) { // if RUNNING, checkpoint only for non-eos | ||
| writeCheckpointIfNeed(); | ||
| if (!eosEnabled) { | ||
| writeCheckpoint(); | ||
| } | ||
|
|
||
| break; | ||
|
|
||
| case SUSPENDED: | ||
| writeCheckpointIfNeed(); | ||
| // we cannot `clear()` the `PartitionGroup` in `suspend()` already, but only after committing, | ||
| // because otherwise we loose the partition-time information | ||
| /* | ||
| * We must clear the `PartitionGroup` only after committing, and not in `suspend()`, | ||
| * because otherwise we lose the partition-time information. | ||
| * We also must clear it when the task is revoked, and not in `close()`, as the consumer will clear | ||
| * its internal buffer when the corresponding partition is revoked but the task may be reassigned | ||
| */ | ||
| partitionGroup.clear(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we still need to make this call -- in eager rebalancing, we suspend a task when we get a partition revoked. For this case, we "forget" the current offset within the consumer and thus need to clear the partition grouper. Otherwise, we might read the data a second time, if the partition is reassigned (what would violate EOS).
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. So we should only clear it here, and not in Just curious, why do we "forget" the current offset? I mean, haven't we just committed the current offset before suspending (and if that failed we would close all tasks right away). Maybe I'm misunderstanding what you mean
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The consumer tracks offset internal, however, we buffer data in our internal queue. Thus, the offset tracked by the consumer, might be larger than the offset we commit (we take the offset we commit not from the consumer, but it's based on the records we did take out of the queue and processed). In eager rebalancing, the consumer clears its internal state if a partition in revoked (and we only suspend the task), including the tracked offsets. If the partition in re-assigned, the consumer fetches the last committed offset to start fetching. Thus, if we don't clear the queue, we might fetch same data that is already in the queue a second time. Does this make sense?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah that's a good catch. Makes sense to me.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, thanks for the explanation. I'll move it back to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not 100% certain that the Consumer does clear its internal buffer on revocation. At least, I couldn't find it in the code, but maybe I'm looking in the wrong place. Not arguing we shouldn't clear the partition group here, was just wondering about this for my own sake. Hm
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not 100% familiar with the consumer code, but in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah that seems right. Thanks! |
||
|
|
||
| writeCheckpoint(); | ||
|
|
||
| break; | ||
|
|
||
| case CREATED: | ||
|
|
@@ -474,84 +480,47 @@ public void update(final Set<TopicPartition> topicPartitions, final Map<String, | |
|
|
||
| @Override | ||
| public void closeAndRecycleState() { | ||
| suspend(); | ||
| prepareCommit(); | ||
| writeCheckpointIfNeed(); | ||
|
|
||
| switch (state()) { | ||
| case CREATED: | ||
| case SUSPENDED: | ||
| stateMgr.recycle(); | ||
| recordCollector.close(); | ||
|
|
||
| break; | ||
|
|
||
| case RESTORING: // we should have transitioned to `SUSPENDED` already | ||
| case RUNNING: // we should have transitioned to `SUSPENDED` already | ||
| case CREATED: | ||
| case RESTORING: | ||
| case RUNNING: | ||
| case CLOSED: | ||
| throw new IllegalStateException("Illegal state " + state() + " while recycling active task " + id); | ||
| default: | ||
| throw new IllegalStateException("Unknown state " + state() + " while recycling active task " + id); | ||
| } | ||
|
|
||
| partitionGroup.clear(); | ||
| closeTaskSensor.record(); | ||
|
|
||
| transitionTo(State.CLOSED); | ||
|
|
||
| log.info("Closed clean and recycled state"); | ||
| } | ||
|
|
||
| private void maybeScheduleCheckpoint() { | ||
| switch (state()) { | ||
| case RESTORING: | ||
| case SUSPENDED: | ||
| this.checkpoint = checkpointableOffsets(); | ||
|
|
||
| break; | ||
|
|
||
| case RUNNING: | ||
| if (!eosEnabled) { | ||
| this.checkpoint = checkpointableOffsets(); | ||
| } | ||
|
|
||
| break; | ||
|
|
||
| case CREATED: | ||
| case CLOSED: | ||
| throw new IllegalStateException("Illegal state " + state() + " while scheduling checkpoint for active task " + id); | ||
|
|
||
| default: | ||
| throw new IllegalStateException("Unknown state " + state() + " while scheduling checkpoint for active task " + id); | ||
| } | ||
| } | ||
|
|
||
| private void writeCheckpointIfNeed() { | ||
| private void writeCheckpoint() { | ||
| if (commitNeeded) { | ||
| log.error("Tried to write a checkpoint with pending uncommitted data, should complete the commit first."); | ||
| throw new IllegalStateException("A checkpoint should only be written if no commit is needed."); | ||
| } | ||
| if (checkpoint != null) { | ||
| stateMgr.checkpoint(checkpoint); | ||
| checkpoint = null; | ||
| } | ||
| stateMgr.checkpoint(checkpointableOffsets()); | ||
| } | ||
|
|
||
| /** | ||
| * <pre> | ||
| * the following order must be followed: | ||
| * 1. checkpoint the state manager -- even if we crash before this step, EOS is still guaranteed | ||
| * 2. then if we are closing on EOS and dirty, wipe out the state store directory | ||
| * 3. finally release the state manager lock | ||
| * </pre> | ||
| * You must commit a task and checkpoint the state manager before closing as this will release the state dir lock | ||
| */ | ||
| private void close(final boolean clean) { | ||
| if (clean) { | ||
| executeAndMaybeSwallow(true, this::writeCheckpointIfNeed, "state manager checkpoint", log); | ||
| if (clean && commitNeeded) { | ||
| log.debug("Tried to close clean but there was pending uncommitted data, this means we failed to" | ||
| + " commit and should close as dirty instead"); | ||
| throw new StreamsException("Tried to close dirty task as clean"); | ||
| } | ||
|
|
||
| switch (state()) { | ||
| case CREATED: | ||
| case RESTORING: | ||
| case SUSPENDED: | ||
| // first close state manager (which is idempotent) then close the record collector | ||
| // if the latter throws and we re-close dirty which would close the state manager again. | ||
|
|
@@ -577,6 +546,8 @@ private void close(final boolean clean) { | |
| log.trace("Skip closing since state is {}", state()); | ||
| return; | ||
|
|
||
| case CREATED: | ||
| case RESTORING: | ||
| case RUNNING: | ||
| throw new IllegalStateException("Illegal state " + state() + " while closing active task " + id); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get that standbys should never really be in RESTORING state, but it still doesn't seem like it's philosophically any more illegal to suspend from RESTORING than it is from RUNNING. I'd vote to legalize RESTORING here. It does seem like a useful sanity check for CLOSED to be illegal, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree 100%, but at some point in the past we started checking for
RESTORINGand throwing IllegalStateException all over StandbyTask. I wanted to keep the changes here to a minimum and figured we should at least be consistent with the current pattern elsewhere