Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8dc0f50
remove commit from handleCorruption and handle TaskCorrupted in handl…
ableegoldman Mar 23, 2021
549a5f7
fixin up the tests
ableegoldman Mar 26, 2021
a843f11
skip only corrupted tasks when checkpointing in handleRevocation
ableegoldman Mar 26, 2021
dbec22d
checkstyle
ableegoldman Mar 26, 2021
4ce51e9
comments
ableegoldman Mar 26, 2021
7282884
clean up logic
ableegoldman Mar 26, 2021
5950b39
simplify even further
ableegoldman Mar 26, 2021
d6f8c0b
handle corrupted tasks
ableegoldman Mar 26, 2021
1a579a4
use own catch block
ableegoldman Mar 26, 2021
4d4f41c
add flag to closeAndRevive to conditionally mark changelogs as corru…
ableegoldman Mar 27, 2021
dc91e75
cleanup
ableegoldman Mar 27, 2021
1446ec2
fix test I messed with before
ableegoldman Mar 27, 2021
9c8730a
add unit test
ableegoldman Mar 27, 2021
723520b
backtrack on handleCorrupted, tick the task timer and add another test
ableegoldman Mar 27, 2021
e5cca19
Revert "backtrack on handleCorrupted, tick the task timer and add ano…
ableegoldman Mar 28, 2021
4a9ffff
add second unit test
ableegoldman Mar 28, 2021
669367f
Merge branch 'trunk' of https://github.com/apache/kafka into 12523-im…
ableegoldman Mar 28, 2021
4d23c99
cleanup in handleRevocation
ableegoldman Mar 28, 2021
0d21ff4
javadocs for commit
ableegoldman Mar 28, 2021
ecb7e7c
clear timeout in revive and refactor commit
ableegoldman Mar 28, 2021
9ea3256
update/remove comment
ableegoldman Mar 28, 2021
f6361c0
need to clear commit statuses during close
ableegoldman Mar 28, 2021
4aed51d
clear status in all kinds of close
ableegoldman Mar 28, 2021
9db590b
add timed out tasks to dirty tasks in handleRevocation
ableegoldman Mar 28, 2021
4c78a79
add StreamTaskTest
ableegoldman Mar 28, 2021
55c076b
add KAFKA-12569 jira number to TODOs
ableegoldman Mar 28, 2021
6fcdd56
add unit tests for eos
ableegoldman Mar 29, 2021
fa4cde5
fix flaky test due to strict mock
ableegoldman Mar 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public final Task.State state() {
@Override
public void revive() {
if (state == CLOSED) {
clearTaskTimeout();
transitionTo(CREATED);
} else {
throw new IllegalStateException("Illegal state " + state() + " while reviving task " + id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,40 +155,54 @@ void handleRebalanceComplete() {
* @throws TaskMigratedException
*/
void handleCorruption(final Set<TaskId> corruptedTasks) {
final Map<Task, Collection<TopicPartition>> corruptedStandbyTasks = new HashMap<>();
final Map<Task, Collection<TopicPartition>> corruptedActiveTasks = new HashMap<>();
final Set<Task> corruptedActiveTasks = new HashSet<>();
final Set<Task> corruptedStandbyTasks = new HashSet<>();

for (final TaskId taskId : corruptedTasks) {
final Task task = tasks.task(taskId);
if (task.isActive()) {
corruptedActiveTasks.put(task, task.changelogPartitions());
corruptedActiveTasks.add(task);

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.

Thanks for the cleanup! I think in the past we may only mark some subset of changelog partitions as corrupted, but later we would always just mark all of them as corrupted. Just following that thought, maybe in task.markChangelogAsCorrupted we do not need to pass in parameters either but just mark all changelog partitions as corrupted?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I didn't want to go all the way with consolidating this logic since eventually we may want to have it so that only the subset of partitions/stores which are actually corrupted will need to be wiped out. So I'd prefer to leave this as-is for now and keep the places in which we infer the changelogs from the task restricted to just the TaskManager for now

} else {
corruptedStandbyTasks.put(task, task.changelogPartitions());
corruptedStandbyTasks.add(task);
}
}

// Make sure to clean up any corrupted standby tasks in their entirety before committing
// since TaskMigrated can be thrown and the resulting handleLostAll will only clean up active tasks
closeAndRevive(corruptedStandbyTasks);

commit(tasks()
Comment thread
ableegoldman marked this conversation as resolved.
.values()
.stream()
.filter(t -> t.state() == Task.State.RUNNING || t.state() == Task.State.RESTORING)
.filter(t -> !corruptedTasks.contains(t.id()))
.collect(Collectors.toSet())
);
closeDirtyAndRevive(corruptedStandbyTasks, true);

// We need to commit before closing the corrupted active tasks since this will force the ongoing txn to abort
try {
doCommit(tasks()
.values()
.stream()
.filter(t -> t.state() == Task.State.RUNNING || t.state() == Task.State.RESTORING)
.filter(t -> !corruptedTasks.contains(t.id()))
.collect(Collectors.toSet())
);
} catch (final TaskCorruptedException e) {
Comment thread
ableegoldman marked this conversation as resolved.
log.info("Some additional tasks were found corrupted while trying to commit, these will be added to the " +
"tasks to clean and revive: {}", e.corruptedTasks());
corruptedActiveTasks.addAll(tasks.tasks(e.corruptedTasks()));
} catch (final TimeoutException e) {
log.info("Hit TimeoutException when committing all non-corrupted tasks, these will be closed and revived");
final Collection<Task> uncorruptedTasks = new HashSet<>(tasks.activeTasks());
uncorruptedTasks.removeAll(corruptedActiveTasks);
// Those tasks which just timed out can just be closed dirty without marking changelogs as corrupted
closeDirtyAndRevive(uncorruptedTasks, false);

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.

If closeDirtyAndRevive throws here, then the next closeDirtyAndRevive would not be triggered. Is that okay, or do we guarantee that closeDirtyAndRevive would not throw at all now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It seems we guarantee that closeDirtyAndRevive does not throw -- this isn't a new assumption, since prior to this it was possible for closeDirtyAndRevive to throw for standby tasks which means we would not invoke it for active tasks. We're just doing the same thing here. (Even if we did throw I think it would be ok under both ALOS or EOS, as for EOS this would cause an unclean shutdown which would mean wiping the store anyway, and for ALOS we would just be closing dirty which again is what we were about to do anyway)

}

closeAndRevive(corruptedActiveTasks);
closeDirtyAndRevive(corruptedActiveTasks, true);
}

private void closeAndRevive(final Map<Task, Collection<TopicPartition>> taskWithChangelogs) {
for (final Map.Entry<Task, Collection<TopicPartition>> entry : taskWithChangelogs.entrySet()) {
final Task task = entry.getKey();
private void closeDirtyAndRevive(final Collection<Task> taskWithChangelogs, final boolean markAsCorrupted) {
for (final Task task : taskWithChangelogs) {
final Collection<TopicPartition> corruptedPartitions = task.changelogPartitions();

// mark corrupted partitions to not be checkpointed, and then close the task as dirty
final Collection<TopicPartition> corruptedPartitions = entry.getValue();
task.markChangelogAsCorrupted(corruptedPartitions);
if (markAsCorrupted) {
Comment thread
ableegoldman marked this conversation as resolved.
task.markChangelogAsCorrupted(corruptedPartitions);
}

try {
// we do not need to take the returned offsets since we are not going to commit anyways;
Expand Down Expand Up @@ -332,8 +346,8 @@ private void handleCloseAndRecycle(final Set<Task> tasksToRecycle,
// write the checkpoint file.
final Map<TopicPartition, OffsetAndMetadata> offsets = task.prepareCommit();
if (!offsets.isEmpty()) {
log.error("Task {} should has been committed when it was suspended, but it reports non-empty " +
"offsets {} to commit; it means it fails during last commit and hence should be closed dirty",
log.error("Task {} should have been committed when it was suspended, but it reports non-empty " +
"offsets {} to commit; this means it failed during last commit and hence should be closed dirty",
task.id(), offsets);

tasksToCloseDirty.add(task);
Expand Down Expand Up @@ -509,30 +523,46 @@ void handleRevocation(final Collection<TopicPartition> revokedPartitions) {
prepareCommitAndAddOffsetsToMap(commitNeededActiveTasks, consumedOffsetsPerTask);
}

// even if commit failed, we should still continue and complete suspending those tasks,
// so we would capture any exception and throw
// even if commit failed, we should still continue and complete suspending those tasks, so we would capture
// any exception and rethrow it at the end. some exceptions may be handled immediately and then swallowed,
// as such we just need to skip those dirty tasks in the checkpoint
final Set<Task> dirtyTasks = new HashSet<>();
try {
commitOffsetsOrTransaction(consumedOffsetsPerTask);
} catch (final TaskCorruptedException e) {
log.warn("Some tasks were corrupted when trying to commit offsets, these will be cleaned and revived: {}",
e.corruptedTasks());

// If we hit a TaskCorruptedException it must be EOS, just handle the cleanup for those corrupted tasks right here
dirtyTasks.addAll(tasks.tasks(e.corruptedTasks()));
closeDirtyAndRevive(dirtyTasks, true);
} catch (final TimeoutException e) {
log.warn("Timed out while trying to commit all tasks during revocation, these will be cleaned and revived");

// If we hit a TimeoutException it must be ALOS, just close dirty and revive without wiping the state
Comment thread
ableegoldman marked this conversation as resolved.
closeDirtyAndRevive(consumedOffsetsPerTask.keySet(), false);
} catch (final RuntimeException e) {
log.error("Exception caught while committing those revoked tasks " + revokedActiveTasks, e);
firstException.compareAndSet(null, e);
dirtyTasks.addAll(consumedOffsetsPerTask.keySet());
}

// only try to complete post-commit if committing succeeded;
// we enforce checkpointing upon suspending a task: if it is resumed later we just
// proceed normally, if it is going to be closed we would checkpoint by then
if (firstException.get() == null) {
for (final Task task : revokedActiveTasks) {
// we enforce checkpointing upon suspending a task: if it is resumed later we just proceed normally, if it is
// going to be closed we would checkpoint by then
for (final Task task : revokedActiveTasks) {
if (!dirtyTasks.contains(task)) {
try {
task.postCommit(true);
} catch (final RuntimeException e) {
log.error("Exception caught while post-committing task " + task.id(), e);
firstException.compareAndSet(null, e);
}
}
}

if (shouldCommitAdditionalTasks) {
for (final Task task : commitNeededActiveTasks) {
if (shouldCommitAdditionalTasks) {
for (final Task task : commitNeededActiveTasks) {
if (!dirtyTasks.contains(task)) {
try {
// for non-revoking active tasks, we should not enforce checkpoint
// since if it is EOS enabled, no checkpoint should be written while
Expand Down Expand Up @@ -972,42 +1002,60 @@ void addRecordsToTasks(final ConsumerRecords<byte[], byte[]> records) {
/**
* @throws TaskMigratedException if committing offsets failed (non-EOS)
* or if the task producer got fenced (EOS)
* @throws TimeoutException if task.timeout.ms has been exceeded (non-EOS)
* @throws TaskCorruptedException if committing offsets failed due to TimeoutException (EOS)
* @return number of committed offsets, or -1 if we are in the middle of a rebalance and cannot commit
*/
int commit(final Collection<Task> tasksToCommit) {
int committed = 0;
if (rebalanceInProgress) {
return -1;
committed = -1;
} else {
int committed = 0;
final Map<Task, Map<TopicPartition, OffsetAndMetadata>> consumedOffsetsAndMetadataPerTask = new HashMap<>();
for (final Task task : tasksToCommit) {
if (task.commitNeeded()) {
final Map<TopicPartition, OffsetAndMetadata> offsetAndMetadata = task.prepareCommit();
if (task.isActive()) {
consumedOffsetsAndMetadataPerTask.put(task, offsetAndMetadata);
}
}
}

try {
commitOffsetsOrTransaction(consumedOffsetsAndMetadataPerTask);

for (final Task task : tasksToCommit) {
if (task.commitNeeded()) {
task.clearTaskTimeout();
++committed;
task.postCommit(false);
}
}
committed = commitAndFillInConsumedOffsetsAndMetadataPerTaskMap(tasksToCommit, consumedOffsetsAndMetadataPerTask);
} catch (final TimeoutException timeoutException) {
consumedOffsetsAndMetadataPerTask
.keySet()
.forEach(t -> t.maybeInitTaskTimeoutOrThrow(time.milliseconds(), timeoutException));
}
}
return committed;
}

/**
* Prepare, commit, and post-commit all tasks.
*/
private void doCommit(final Collection<Task> tasksToCommit) {
Comment thread
ableegoldman marked this conversation as resolved.
Outdated
final Map<Task, Map<TopicPartition, OffsetAndMetadata>> consumedOffsetsAndMetadataPerTask = new HashMap<>();
commitAndFillInConsumedOffsetsAndMetadataPerTaskMap(tasksToCommit, consumedOffsetsAndMetadataPerTask);
Comment thread
ableegoldman marked this conversation as resolved.
Outdated
}

/**
* @param consumedOffsetsAndMetadataPerTask an empty map that will be filled in with the prepared offsets
*/
private int commitAndFillInConsumedOffsetsAndMetadataPerTaskMap(final Collection<Task> tasksToCommit, final Map<Task, Map<TopicPartition, OffsetAndMetadata>> consumedOffsetsAndMetadataPerTask) {
Comment thread
ableegoldman marked this conversation as resolved.
Outdated
int committed = 0;

return committed;
for (final Task task : tasksToCommit) {
if (task.commitNeeded()) {
final Map<TopicPartition, OffsetAndMetadata> offsetAndMetadata = task.prepareCommit();
if (task.isActive()) {
consumedOffsetsAndMetadataPerTask.put(task, offsetAndMetadata);
}
}
}

commitOffsetsOrTransaction(consumedOffsetsAndMetadataPerTask);

for (final Task task : tasksToCommit) {
if (task.commitNeeded()) {
task.clearTaskTimeout();
++committed;
task.postCommit(false);
}
}
return committed;
}

/**
Expand All @@ -1027,6 +1075,11 @@ int maybeCommitActiveTasksPerUserRequested() {
}
}

/**
* @throws TaskMigratedException if committing offsets failed due to CommitFailedException (non-EOS)
* @throws TimeoutException if committing offsets failed due to TimeoutException (non-EOS)
* @throws TaskCorruptedException if committing offsets failed due to TimeoutException (EOS)
*/
private void commitOffsetsOrTransaction(final Map<Task, Map<TopicPartition, OffsetAndMetadata>> offsetsPerTask) {
log.debug("Committing task offsets {}", offsetsPerTask.entrySet().stream().collect(Collectors.toMap(t -> t.getKey().id(), Entry::getValue))); // avoid logging actual Task objects

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.streams.processor.TaskId;
import org.apache.kafka.streams.processor.internals.metrics.StreamsMetricsImpl;

import java.util.HashSet;
import org.slf4j.Logger;

import java.util.Collection;
Expand Down Expand Up @@ -234,6 +236,14 @@ Task task(final TaskId taskId) {
return allTasksPerId.get(taskId);
}

Collection<Task> tasks(final Collection<TaskId> taskIds) {
final Set<Task> tasks = new HashSet<>();
for (final TaskId taskId : taskIds) {
tasks.add(task(taskId));
}
return tasks;
}

// TODO: change return type to `StreamTask`
Collection<Task> activeTasks() {
return readOnlyActiveTasks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

@RunWith(EasyMockRunner.class)
public class TaskManagerTest {
Expand Down Expand Up @@ -816,6 +817,106 @@ public Map<TopicPartition, OffsetAndMetadata> prepareCommit() {
verify(consumer);
}

@Test
public void shouldCloseAndReviveUncorruptedTasksWhenTimeoutExceptionThrownFromCommit() {

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.

The test says: should close and revive

How do we exactly verify this? Maybe we do, but it's not clear to me from the code. Can you elaborate?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We verify the revive by asserting that it went from RUNNING back to CREATED

final ProcessorStateManager stateManager = EasyMock.createStrictMock(ProcessorStateManager.class);
stateManager.markChangelogAsCorrupted(taskId00Partitions);
replay(stateManager);

final StateMachineTask corruptedActive = new StateMachineTask(taskId00, taskId00Partitions, true, stateManager);
final StateMachineTask unCorruptedActive = new StateMachineTask(taskId01, taskId01Partitions, true, stateManager) {
@Override
public void markChangelogAsCorrupted(final Collection<TopicPartition> partitions) {
fail("Should not try to mark changelogs as corrupted for uncorrupted task");
}

@Override
public void maybeInitTaskTimeoutOrThrow(final long currentWallClockMs,
Comment thread
ableegoldman marked this conversation as resolved.
Outdated
final Exception cause) {
throw new TimeoutException();
}
};
final Map<TopicPartition, OffsetAndMetadata> offsets = singletonMap(t1p1, new OffsetAndMetadata(0L, null));
unCorruptedActive.setCommittableOffsetsAndMetadata(offsets);

// handleAssignment
final Map<TaskId, Set<TopicPartition>> assignment = new HashMap<>();
assignment.putAll(taskId00Assignment);
assignment.putAll(taskId01Assignment);
expect(activeTaskCreator.createTasks(anyObject(), eq(assignment))).andStubReturn(asList(corruptedActive, unCorruptedActive));
topologyBuilder.addSubscribedTopicsFromAssignment(anyObject(), anyString());
expectLastCall().anyTimes();

expectRestoreToBeCompleted(consumer, changeLogReader);

consumer.commitSync(offsets);
expectLastCall().andThrow(new TimeoutException());

expect(consumer.assignment()).andStubReturn(union(HashSet::new, taskId00Partitions, taskId01Partitions));

replay(activeTaskCreator, standbyTaskCreator, topologyBuilder, consumer, changeLogReader);

taskManager.handleAssignment(assignment, emptyMap());
assertThat(taskManager.tryToCompleteRestoration(time.milliseconds(), null), is(true));

// make sure this will be committed and throw
assertThat(unCorruptedActive.state(), is(Task.State.RUNNING));
assertThat(corruptedActive.state(), is(Task.State.RUNNING));

unCorruptedActive.setCommitNeeded();

corruptedActive.setChangelogOffsets(singletonMap(t1p0, 0L));
taskManager.handleCorruption(singleton(taskId00));

assertThat(corruptedActive.commitPrepared, is(true));

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.

The corrupted tasks should be revived? Those this case, should this flag be reset?

@ableegoldman ableegoldman Mar 28, 2021

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Seems like we don't reset the commitPrepared during revive, good point. I guess we should reset all of those in the StateMachineTask#revive
edit: actually I think for commitPrepared at least we should not reset it, since we just use this to verify that we did, indeed, prepare a commit. But commitNeeded should probably be cleared in StateMachineTask#revive (and ultimately in StateMachineTask#close but I don't want to mess with this in this PR since it's used very heavily in these tests, see below)

@ableegoldman ableegoldman Mar 28, 2021

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hm...seems like we actually may not even clear commitNeeded (or the other commit-related flags) in the actual StreamTask's close or revive. We need to be clearing those in revive or closeDirty (closeClean would have cleared during postCommit) This has probably been a long lurking bug, although a minor one

@ableegoldman ableegoldman Mar 28, 2021

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Side note: seems weird that StateMachineTask has its own commitNeeded field rather than making the one in StreamTask protected and use it for active tasks (edit: there's actually a valid-ish reason for this, the StateMachineTask just mocks the behavior of most methods and rarely calls the super's method, so even if we used the same commitNeeded flag across the field we'd still have to remember to manually set/clear it in the same way in StateMachineTask any time we do so in Abstract/StreamTask.).

Looks like we use commitNeeded in kind of a risky way in the tests, eg to indirectly indicate that it was closed clean, or infer that we successfully committed, etc Cleaner/safer to not reuse this variable to mean so many different things and just introduce a closedClean, commitSuccessful, etc wherever needed...
But I don't want to mess with it in this PR so I'll just file a ticket to clean this up later if that makes sense.

assertThat(corruptedActive.state(), is(Task.State.CREATED));
assertThat(unCorruptedActive.state(), is(Task.State.CREATED));
verify(consumer);
}

@Test
public void shouldCloseAndReviveUncorruptedTasksWhenTimeoutExceptionThrownFromCommitDuringRevocation() {

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 question as above. (Or is the fact that we don't crash good enough as criteria?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ditto the above (back to CREATED is the key verification, but also it should not crash)

final StateMachineTask task00 = new StateMachineTask(taskId00, taskId00Partitions, true);
final Map<TopicPartition, OffsetAndMetadata> offsets00 = singletonMap(t1p0, new OffsetAndMetadata(0L, null));
task00.setCommittableOffsetsAndMetadata(offsets00);
task00.setCommitNeeded();

final StateMachineTask task01 = new StateMachineTask(taskId01, taskId01Partitions, true);
final Map<TopicPartition, OffsetAndMetadata> offsets01 = singletonMap(t1p1, new OffsetAndMetadata(1L, null));
task01.setCommittableOffsetsAndMetadata(offsets01);
task01.setCommitNeeded();

final Map<TopicPartition, OffsetAndMetadata> expectedCommittedOffsets = new HashMap<>();
expectedCommittedOffsets.putAll(offsets00);
expectedCommittedOffsets.putAll(offsets01);

final Map<TaskId, Set<TopicPartition>> assignmentActive = mkMap(
mkEntry(taskId00, taskId00Partitions),
mkEntry(taskId01, taskId01Partitions)
);

expectRestoreToBeCompleted(consumer, changeLogReader);

expect(activeTaskCreator.createTasks(anyObject(), eq(assignmentActive))).andReturn(asList(task00, task01));
activeTaskCreator.closeAndRemoveTaskProducerIfNeeded(taskId00);
expectLastCall();
consumer.commitSync(expectedCommittedOffsets);
expectLastCall().andThrow(new TimeoutException());
expect(consumer.assignment()).andStubReturn(union(HashSet::new, taskId00Partitions, taskId01Partitions));

replay(activeTaskCreator, standbyTaskCreator, consumer, changeLogReader);

taskManager.handleAssignment(assignmentActive, emptyMap());
assertThat(taskManager.tryToCompleteRestoration(time.milliseconds(), null), is(true));
assertThat(task00.state(), is(Task.State.RUNNING));
assertThat(task01.state(), is(Task.State.RUNNING));

taskManager.handleRevocation(taskId00Partitions);

assertThat(task00.state(), is(State.SUSPENDED));
assertThat(task01.state(), is(State.CREATED));
}

@Test
public void shouldCloseStandbyUnassignedTasksWhenCreatingNewTasks() {
final Task task00 = new StateMachineTask(taskId00, taskId00Partitions, false);
Expand Down