Fix for invalid ShardConsumer state transitions due to rejected executions (#560)
* Fix to prevent ShardConsumer state transition, when the source state task execution is rejected by the executor service. * Unit test case improvements * Optimized imports * Removed unnecessary sleep in unit test case * Fixing imports * Fixing import again with wildcard removed * Adding asserts to exception cases in SharConsumerTest
This commit is contained in:
parent
b6236d8077
commit
9e2d6fa497
2 changed files with 187 additions and 0 deletions
|
|
@ -20,6 +20,7 @@ import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
|
|
@ -164,6 +165,9 @@ public class ShardConsumer {
|
||||||
} else if (needsInitialization) {
|
} else if (needsInitialization) {
|
||||||
if (stateChangeFuture != null) {
|
if (stateChangeFuture != null) {
|
||||||
if (stateChangeFuture.get()) {
|
if (stateChangeFuture.get()) {
|
||||||
|
// Task rejection during the subscribe() call will not be propagated back as it not executed
|
||||||
|
// in the context of the Scheduler thread. Hence we should not assume the subscription will
|
||||||
|
// always be successful.
|
||||||
subscribe();
|
subscribe();
|
||||||
needsInitialization = false;
|
needsInitialization = false;
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +181,11 @@ public class ShardConsumer {
|
||||||
//
|
//
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
|
} catch (RejectedExecutionException e) {
|
||||||
|
// It is possible the tasks submitted to the executor service by the Scheduler thread might get rejected
|
||||||
|
// due to various reasons. Such failed executions must be captured and marked as failure to prevent
|
||||||
|
// the state transitions.
|
||||||
|
taskOutcome = TaskOutcome.FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConsumerStates.ShardConsumerState.PROCESSING.equals(currentState.state())) {
|
if (ConsumerStates.ShardConsumerState.PROCESSING.equals(currentState.state())) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.instanceOf;
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
import static org.hamcrest.Matchers.notNullValue;
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.anyLong;
|
import static org.mockito.Matchers.anyLong;
|
||||||
import static org.mockito.Matchers.eq;
|
import static org.mockito.Matchers.eq;
|
||||||
|
|
@ -28,6 +30,7 @@ import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
@ -58,6 +61,7 @@ import org.junit.Test;
|
||||||
import org.junit.rules.TestName;
|
import org.junit.rules.TestName;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.invocation.InvocationOnMock;
|
||||||
import org.mockito.runners.MockitoJUnitRunner;
|
import org.mockito.runners.MockitoJUnitRunner;
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
import org.reactivestreams.Subscription;
|
import org.reactivestreams.Subscription;
|
||||||
|
|
@ -69,6 +73,7 @@ import software.amazon.kinesis.common.InitialPositionInStreamExtended;
|
||||||
import software.amazon.kinesis.leases.ShardInfo;
|
import software.amazon.kinesis.leases.ShardInfo;
|
||||||
import software.amazon.kinesis.lifecycle.events.ProcessRecordsInput;
|
import software.amazon.kinesis.lifecycle.events.ProcessRecordsInput;
|
||||||
import software.amazon.kinesis.lifecycle.events.TaskExecutionListenerInput;
|
import software.amazon.kinesis.lifecycle.events.TaskExecutionListenerInput;
|
||||||
|
import software.amazon.kinesis.lifecycle.ConsumerStates.ShardConsumerState;
|
||||||
import software.amazon.kinesis.retrieval.RecordsPublisher;
|
import software.amazon.kinesis.retrieval.RecordsPublisher;
|
||||||
import software.amazon.kinesis.retrieval.RecordsRetrieved;
|
import software.amazon.kinesis.retrieval.RecordsRetrieved;
|
||||||
import software.amazon.kinesis.retrieval.kpl.ExtendedSequenceNumber;
|
import software.amazon.kinesis.retrieval.kpl.ExtendedSequenceNumber;
|
||||||
|
|
@ -95,6 +100,10 @@ public class ShardConsumerTest {
|
||||||
@Mock
|
@Mock
|
||||||
private ShutdownNotification shutdownNotification;
|
private ShutdownNotification shutdownNotification;
|
||||||
@Mock
|
@Mock
|
||||||
|
private ConsumerState blockedOnParentsState;
|
||||||
|
@Mock
|
||||||
|
private ConsumerTask blockedOnParentsTask;
|
||||||
|
@Mock
|
||||||
private ConsumerState initialState;
|
private ConsumerState initialState;
|
||||||
@Mock
|
@Mock
|
||||||
private ConsumerTask initializeTask;
|
private ConsumerTask initializeTask;
|
||||||
|
|
@ -111,6 +120,8 @@ public class ShardConsumerTest {
|
||||||
@Mock
|
@Mock
|
||||||
private TaskResult processingTaskResult;
|
private TaskResult processingTaskResult;
|
||||||
@Mock
|
@Mock
|
||||||
|
private TaskResult blockOnParentsTaskResult;
|
||||||
|
@Mock
|
||||||
private ConsumerState shutdownCompleteState;
|
private ConsumerState shutdownCompleteState;
|
||||||
@Mock
|
@Mock
|
||||||
private ShardConsumerArgument shardConsumerArgument;
|
private ShardConsumerArgument shardConsumerArgument;
|
||||||
|
|
@ -441,6 +452,144 @@ public class ShardConsumerTest {
|
||||||
verify(initialState, never()).shutdownTransition(any());
|
verify(initialState, never()).shutdownTransition(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test method to verify consumer undergoes the transition WAITING_ON_PARENT_SHARDS -> INITIALIZING -> PROCESSING
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
public final void testSuccessfulConsumerStateTransition() throws Exception {
|
||||||
|
ExecutorService directExecutorService = spy(executorService);
|
||||||
|
|
||||||
|
doAnswer(invocation -> directlyExecuteRunnable(invocation))
|
||||||
|
.when(directExecutorService).execute(any());
|
||||||
|
|
||||||
|
ShardConsumer consumer = new ShardConsumer(recordsPublisher, directExecutorService, shardInfo,
|
||||||
|
logWarningForTaskAfterMillis, shardConsumerArgument, blockedOnParentsState,
|
||||||
|
t -> t, 1, taskExecutionListener, 0);
|
||||||
|
|
||||||
|
mockSuccessfulUnblockOnParents();
|
||||||
|
mockSuccessfulInitializeWithFailureTransition();
|
||||||
|
mockSuccessfulProcessing(null);
|
||||||
|
|
||||||
|
int arbitraryExecutionCount = 3;
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
consumer.executeLifecycle();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Suppress any exception like the scheduler.
|
||||||
|
fail("Unexpected exception while executing consumer lifecycle");
|
||||||
|
}
|
||||||
|
} while (--arbitraryExecutionCount > 0);
|
||||||
|
|
||||||
|
assertEquals(ShardConsumerState.PROCESSING.consumerState().state(), consumer.currentState().state());
|
||||||
|
verify(directExecutorService, times(2)).execute(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test method to verify consumer does not transition to PROCESSING from WAITING_ON_PARENT_SHARDS when
|
||||||
|
* INITIALIZING tasks gets rejected.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
public final void testConsumerNotTransitionsToProcessingWhenInitializationFails() {
|
||||||
|
ExecutorService failingService = spy(executorService);
|
||||||
|
ShardConsumer consumer = new ShardConsumer(recordsPublisher, failingService, shardInfo,
|
||||||
|
logWarningForTaskAfterMillis, shardConsumerArgument, blockedOnParentsState,
|
||||||
|
t -> t, 1, taskExecutionListener, 0);
|
||||||
|
|
||||||
|
mockSuccessfulUnblockOnParents();
|
||||||
|
mockSuccessfulInitializeWithFailureTransition();
|
||||||
|
mockSuccessfulProcessing(null);
|
||||||
|
|
||||||
|
// Failing the initialization task and all other attempts after that.
|
||||||
|
doAnswer(invocation -> directlyExecuteRunnable(invocation))
|
||||||
|
.doThrow(new RejectedExecutionException())
|
||||||
|
.when(failingService).execute(any());
|
||||||
|
|
||||||
|
int arbitraryExecutionCount = 5;
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
consumer.executeLifecycle();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Suppress any exception like the scheduler.
|
||||||
|
fail("Unexpected exception while executing consumer lifecycle");
|
||||||
|
}
|
||||||
|
} while (--arbitraryExecutionCount > 0);
|
||||||
|
|
||||||
|
assertEquals(ShardConsumerState.INITIALIZING.consumerState().state(), consumer.currentState().state());
|
||||||
|
verify(failingService, times(5)).execute(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test method to verify consumer transition to PROCESSING from WAITING_ON_PARENT_SHARDS with
|
||||||
|
* intermittent INITIALIZING task rejections.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
public final void testConsumerTransitionsToProcessingWithIntermittentInitializationFailures() {
|
||||||
|
ExecutorService failingService = spy(executorService);
|
||||||
|
ShardConsumer consumer = new ShardConsumer(recordsPublisher, failingService, shardInfo,
|
||||||
|
logWarningForTaskAfterMillis, shardConsumerArgument, blockedOnParentsState,
|
||||||
|
t -> t, 1, taskExecutionListener, 0);
|
||||||
|
|
||||||
|
mockSuccessfulUnblockOnParents();
|
||||||
|
mockSuccessfulInitializeWithFailureTransition();
|
||||||
|
mockSuccessfulProcessing(null);
|
||||||
|
|
||||||
|
// Failing the initialization task and few other attempts after that.
|
||||||
|
doAnswer(invocation -> directlyExecuteRunnable(invocation))
|
||||||
|
.doThrow(new RejectedExecutionException())
|
||||||
|
.doThrow(new RejectedExecutionException())
|
||||||
|
.doThrow(new RejectedExecutionException())
|
||||||
|
.doAnswer(invocation -> directlyExecuteRunnable(invocation))
|
||||||
|
.when(failingService).execute(any());
|
||||||
|
|
||||||
|
int arbitraryExecutionCount = 6;
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
consumer.executeLifecycle();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Suppress any exception like the scheduler.
|
||||||
|
fail("Unexpected exception while executing consumer lifecycle");
|
||||||
|
}
|
||||||
|
} while (--arbitraryExecutionCount > 0);
|
||||||
|
|
||||||
|
assertEquals(ShardConsumerState.PROCESSING.consumerState().state(), consumer.currentState().state());
|
||||||
|
verify(failingService, times(5)).execute(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test method to verify consumer does not transition to INITIALIZING when WAITING_ON_PARENT_SHARDS task rejected.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Test
|
||||||
|
public final void testConsumerNotTransitionsToInitializingWhenWaitingOnParentsFails() {
|
||||||
|
ExecutorService failingService = spy(executorService);
|
||||||
|
ShardConsumer consumer = new ShardConsumer(recordsPublisher, failingService, shardInfo,
|
||||||
|
logWarningForTaskAfterMillis, shardConsumerArgument, blockedOnParentsState,
|
||||||
|
t -> t, 1, taskExecutionListener, 0);
|
||||||
|
|
||||||
|
mockSuccessfulUnblockOnParentsWithFailureTransition();
|
||||||
|
mockSuccessfulInitializeWithFailureTransition();
|
||||||
|
|
||||||
|
// Failing the waiting_on_parents task and few other attempts after that.
|
||||||
|
doThrow(new RejectedExecutionException())
|
||||||
|
.when(failingService).execute(any());
|
||||||
|
|
||||||
|
int arbitraryExecutionCount = 5;
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
consumer.executeLifecycle();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Suppress any exception like the scheduler.
|
||||||
|
fail("Unexpected exception while executing consumer lifecycle");
|
||||||
|
}
|
||||||
|
} while (--arbitraryExecutionCount > 0);
|
||||||
|
|
||||||
|
assertEquals(ShardConsumerState.WAITING_ON_PARENT_SHARDS.consumerState().state(), consumer.currentState().state());
|
||||||
|
verify(failingService, times(5)).execute(any());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test method to verify consumer stays in INITIALIZING state when InitializationTask fails.
|
* Test method to verify consumer stays in INITIALIZING state when InitializationTask fails.
|
||||||
*/
|
*/
|
||||||
|
|
@ -742,6 +891,11 @@ public class ShardConsumerTest {
|
||||||
when(processingState.state()).thenReturn(ConsumerStates.ShardConsumerState.PROCESSING);
|
when(processingState.state()).thenReturn(ConsumerStates.ShardConsumerState.PROCESSING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void mockSuccessfulInitializeWithFailureTransition() {
|
||||||
|
mockSuccessfulInitialize(null, null);
|
||||||
|
when(initialState.failureTransition()).thenReturn(initialState);
|
||||||
|
}
|
||||||
|
|
||||||
private void mockSuccessfulInitialize(CyclicBarrier taskCallBarrier) {
|
private void mockSuccessfulInitialize(CyclicBarrier taskCallBarrier) {
|
||||||
mockSuccessfulInitialize(taskCallBarrier, null);
|
mockSuccessfulInitialize(taskCallBarrier, null);
|
||||||
}
|
}
|
||||||
|
|
@ -763,6 +917,22 @@ public class ShardConsumerTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void mockSuccessfulUnblockOnParentsWithFailureTransition() {
|
||||||
|
mockSuccessfulUnblockOnParents();
|
||||||
|
when(blockedOnParentsState.failureTransition()).thenReturn(blockedOnParentsState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mockSuccessfulUnblockOnParents() {
|
||||||
|
when(blockedOnParentsState.createTask(eq(shardConsumerArgument), any(), any())).thenReturn(blockedOnParentsTask);
|
||||||
|
when(blockedOnParentsState.taskType()).thenReturn(TaskType.BLOCK_ON_PARENT_SHARDS);
|
||||||
|
when(blockedOnParentsTask.taskType()).thenReturn(TaskType.BLOCK_ON_PARENT_SHARDS);
|
||||||
|
when(blockedOnParentsTask.call()).thenAnswer(i -> blockOnParentsTaskResult);
|
||||||
|
when(blockOnParentsTaskResult.getException()).thenReturn(null);
|
||||||
|
when(blockedOnParentsState.requiresDataAvailability()).thenReturn(false);
|
||||||
|
when(blockedOnParentsState.successTransition()).thenReturn(initialState);
|
||||||
|
when(blockedOnParentsState.state()).thenReturn(ShardConsumerState.WAITING_ON_PARENT_SHARDS);
|
||||||
|
}
|
||||||
|
|
||||||
private void awaitBarrier(CyclicBarrier barrier) throws Exception {
|
private void awaitBarrier(CyclicBarrier barrier) throws Exception {
|
||||||
if (barrier != null) {
|
if (barrier != null) {
|
||||||
barrier.await();
|
barrier.await();
|
||||||
|
|
@ -773,4 +943,12 @@ public class ShardConsumerTest {
|
||||||
barrier.await();
|
barrier.await();
|
||||||
barrier.reset();
|
barrier.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Object directlyExecuteRunnable(InvocationOnMock invocation) {
|
||||||
|
Object[] args = invocation.getArguments();
|
||||||
|
Runnable runnable = (Runnable) args[0];
|
||||||
|
runnable.run();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue