@Test public void testProcessorExtendsClaimOnSegment() throws InterruptedException { tokenStore.storeToken(new GlobalSequenceTrackingToken(1L), "test", 0); tokenStore.storeToken(new GlobalSequenceTrackingToken(2L), "test", 1); testSubject.start(); // give it some time to split segments from the store and submit to executor service. Thread.sleep(200); eventBus.publish(createEvents(10)); assertWithin(200, MILLISECONDS, () -> verify(tokenStore, atLeast(1)).extendClaim("test", 0)); assertWithin(200, MILLISECONDS, () -> verify(tokenStore, atLeast(1)).extendClaim("test", 1)); assertWithin(1, SECONDS, () -> assertThat(testSubject.activeProcessorThreads(), is(2))); }
/** * Resets tokens to the given {@code startPosition}. This effectively causes a replay of events since that position. * <p> * Note that the new token must represent a position that is <em>before</em> the current position of the processor. * <p> * Before attempting to reset the tokens, the caller must stop this processor, as well as any instances of the * same logical processor that may be running in the cluster. Failure to do so will cause the reset to fail, * as a processor can only reset the tokens if it is able to claim them all. * * @param startPosition The token representing the position to reset the processor to. */ public void resetTokens(TrackingToken startPosition) { Assert.state(supportsReset(), () -> "The handlers assigned to this Processor do not support a reset"); Assert.state(!isRunning() && activeProcessorThreads() == 0, () -> "TrackingProcessor must be shut down before triggering a reset"); transactionManager.executeInTransaction(() -> { int[] segments = tokenStore.fetchSegments(getName()); TrackingToken[] tokens = new TrackingToken[segments.length]; for (int i = 0; i < segments.length; i++) { tokens[i] = tokenStore.fetchToken(getName(), segments[i]); } // we now have all tokens, hurray eventHandlerInvoker().performReset(); for (int i = 0; i < tokens.length; i++) { tokenStore.storeToken(ReplayToken.createReplayToken(tokens[i], startPosition), getName(), segments[i]); } }); }
tokenStoreCurrentSegments = tokenStore.fetchSegments(processorName); () -> { TrackingToken initialToken = initialTrackingTokenBuilder.apply(messageSource); tokenStore.initializeTokenSegments(processorName, segmentsSize, initialToken); return tokenStore.fetchSegments(processorName); }); try { transactionManager.executeInTransaction(() -> { TrackingToken token = tokenStore.fetchToken(processorName, segment.getSegmentId()); activeSegments.putIfAbsent(segment.getSegmentId(), new TrackerStatus(segment, token)); });
/** * Initializes the given {@code segmentCount} number of segments for the given {@code processorName} to track its * tokens. This method should only be invoked when no tokens have been stored for the given processor, yet. * <p> * This method will store {@code initialToken} for all segments as starting point for processor, but not claim them. * It will create the segments ranging from {@code 0} until {@code segmentCount - 1}. * <p> * The exact behavior when this method is called while tokens were already present, is undefined in case the token * already present is not owned by the initializing process. * * @param processorName The name of the processor to initialize segments for * @param segmentCount The number of segments to initialize * @param initialToken The initial token which is used as a starting point for processor * @throws UnableToClaimTokenException when a segment has already been created */ default void initializeTokenSegments(String processorName, int segmentCount, TrackingToken initialToken) throws UnableToClaimTokenException { for (int segment = 0; segment < segmentCount; segment++) { storeToken(initialToken, processorName, segment); releaseClaim(processorName, segment); } }
/** * Initializes the given {@code segmentCount} number of segments for the given {@code processorName} to track its * tokens. This method should only be invoked when no tokens have been stored for the given processor, yet. * <p> * This method will initialize the tokens, but not claim them. It will create the segments ranging from {@code 0} * until {@code segmentCount - 1}. * <p> * The exact behavior when this method is called while tokens were already present, is undefined in case the token * already present is not owned by the initializing process. * * @param processorName The name of the processor to initialize segments for * @param segmentCount The number of segments to initialize * @throws UnableToClaimTokenException when a segment has already been created */ default void initializeTokenSegments(String processorName, int segmentCount) throws UnableToClaimTokenException { for (int segment = 0; segment < segmentCount; segment++) { fetchToken(processorName, segment); releaseClaim(processorName, segment); } }
@Test public void testResetRejectedIfNotAllTokensCanBeClaimed() { tokenStore.initializeTokenSegments("test", 4); when(tokenStore.fetchToken("test", 3)).thenThrow(new UnableToClaimTokenException("Mock")); try { testSubject.resetTokens(); fail("Expected exception"); } catch (UnableToClaimTokenException e) { // expected } verify(tokenStore, never()).storeToken(isNull(), anyString(), anyInt()); }
/** * This processor won't be able to handle any segments, as claiming a segment will fail. */ @Test public void testProcessorWorkerCountWithMultipleSegmentsClaimFails() throws InterruptedException { tokenStore.storeToken(new GlobalSequenceTrackingToken(1L), "test", 0); tokenStore.storeToken(new GlobalSequenceTrackingToken(2L), "test", 1); // Will skip segments. doThrow(new UnableToClaimTokenException("Failed")).when(tokenStore).extendClaim("test", 0); doThrow(new UnableToClaimTokenException("Failed")).when(tokenStore).fetchToken("test", 0); doThrow(new UnableToClaimTokenException("Failed")).when(tokenStore).extendClaim("test", 1); doThrow(new UnableToClaimTokenException("Failed")).when(tokenStore).fetchToken("test", 1); testSubject.start(); // give it some time to split segments from the store and submit to executor service. Thread.sleep(200); assertWithin(1, SECONDS, () -> assertThat(testSubject.activeProcessorThreads(), is(0))); }
@Test public void testMultiThreadTokenIsStoredWhenEventIsRead() throws Exception { CountDownLatch countDownLatch = new CountDownLatch(2); testSubject.registerHandlerInterceptor(((unitOfWork, interceptorChain) -> { unitOfWork.onCleanup(uow -> countDownLatch.countDown()); return interceptorChain.proceed(); })); testSubject.start(); eventBus.publish(createEvents(2)); assertTrue("Expected Unit of Work to have reached clean up phase", countDownLatch.await(5, SECONDS)); verify(tokenStore, atLeastOnce()).storeToken(any(), any(), anyInt()); assertThat(tokenStore.fetchToken(testSubject.getName(), 0), notNullValue()); assertThat(tokenStore.fetchToken(testSubject.getName(), 1), notNullValue()); }
/** * Extends the claim on the current token held by the this node for the given {@code processorName} and * {@code segment}. * * @param processorName The process name for which to fetch the token * @param segment The segment index for which to fetch the token * @throws UnableToClaimTokenException if there is no token for given {@code processorName} and {@code segment}, or * if it has been claimed by another process. * @implSpec By default, this method invokes {@link #fetchToken(String, int)}, which also extends the claim if the * token is held. TokenStore implementations may choose to implement this method if they can provide a more efficient * way of extending this claim. */ default void extendClaim(String processorName, int segment) throws UnableToClaimTokenException { fetchToken(processorName, segment); }
@Test public void testWhenFailureDuringInit() throws InterruptedException { when(tokenStore.fetchSegments(anyString())) .thenThrow(new RuntimeException("Faking issue during fetchSegments")) .thenReturn(new int[]{}) .thenReturn(new int[]{0}); doThrow(new RuntimeException("Faking issue during initializeTokenSegments")) // and on further calls .doNothing() .when(tokenStore).initializeTokenSegments(anyString(), anyInt()); testSubject.start(); Thread.sleep(2500); assertEquals(1, testSubject.activeProcessorThreads()); }
@Test public void testBlacklistingSegmentWillHaveProcessorClaimAnotherOne() { tokenStore.storeToken(new GlobalSequenceTrackingToken(1L), "test", 0); tokenStore.storeToken(new GlobalSequenceTrackingToken(2L), "test", 1); tokenStore.storeToken(new GlobalSequenceTrackingToken(2L), "test", 2); testSubject.start(); assertWithin(1, SECONDS, () -> assertEquals(0, testSubject.availableProcessorThreads())); assertEquals(new HashSet<>(asList(0, 1)), testSubject.processingStatus().keySet()); testSubject.releaseSegment(0); assertWithin(5, SECONDS, () -> assertTrue(testSubject.processingStatus().containsKey(2))); assertEquals(new HashSet<>(asList(2, 1)), testSubject.processingStatus().keySet()); assertWithin(2, SECONDS, () -> assertEquals(0, testSubject.availableProcessorThreads())); }
@Test public void testProcessorInitializesMoreTokensThanWorkerCount() throws InterruptedException { configureProcessor(TrackingEventProcessorConfiguration.forParallelProcessing(2) .andInitialSegmentsCount(4)); testSubject.start(); // give it some time to split segments from the store and submit to executor service. Thread.sleep(200); assertThat(testSubject.activeProcessorThreads(), is(2)); int[] actual = tokenStore.fetchSegments(testSubject.getName()); Arrays.sort(actual); assertArrayEquals(new int[]{0, 1, 2, 3}, actual); }
private void releaseToken(Segment segment) { try { transactionManager.executeInTransaction(() -> tokenStore.releaseClaim(getName(), segment.getSegmentId())); } catch (Exception e) { // Ignore exception } }
@Test public void testTokenIsStoredWhenEventIsRead() throws Exception { CountDownLatch countDownLatch = new CountDownLatch(1); testSubject.registerHandlerInterceptor(((unitOfWork, interceptorChain) -> { unitOfWork.onCleanup(uow -> countDownLatch.countDown()); return interceptorChain.proceed(); })); testSubject.start(); // give it a bit of time to start Thread.sleep(200); eventBus.publish(createEvent()); assertTrue("Expected Unit of Work to have reached clean up phase", countDownLatch.await(5, TimeUnit.SECONDS)); verify(tokenStore).extendClaim(eq(testSubject.getName()), anyInt()); verify(tokenStore).storeToken(any(), any(), anyInt()); assertNotNull(tokenStore.fetchToken(testSubject.getName(), 0)); }
@Test public void testFirstTokenIsStoredWhenUnitOfWorkIsRolledBackOnSecondEvent() throws Exception { List<? extends EventMessage<?>> events = createEvents(2); CountDownLatch countDownLatch = new CountDownLatch(2); testSubject.registerHandlerInterceptor(((unitOfWork, interceptorChain) -> { unitOfWork.onCommit(uow -> { if (uow.getMessage().equals(events.get(1))) { throw new MockException(); } }); return interceptorChain.proceed(); })); testSubject.registerHandlerInterceptor(((unitOfWork, interceptorChain) -> { unitOfWork.onCleanup(uow -> countDownLatch.countDown()); return interceptorChain.proceed(); })); testSubject.start(); // give it a bit of time to start Thread.sleep(200); eventBus.publish(events); assertTrue("Expected Unit of Work to have reached clean up phase", countDownLatch.await(5, TimeUnit.SECONDS)); verify(tokenStore, atLeastOnce()).storeToken(any(), any(), anyInt()); assertNotNull(tokenStore.fetchToken(testSubject.getName(), 0)); }
private BlockingStream<TrackedEventMessage<?>> ensureEventStreamOpened( BlockingStream<TrackedEventMessage<?>> eventStreamIn, Segment segment) { BlockingStream<TrackedEventMessage<?>> eventStream = eventStreamIn; if (eventStream == null && state.get().isRunning()) { final TrackingToken trackingToken = transactionManager.fetchInTransaction( () -> tokenStore.fetchToken(getName(), segment.getSegmentId()) ); logger.info("Fetched token: {} for segment: {}", trackingToken, segment); eventStream = transactionManager.fetchInTransaction( () -> doOpenStream(trackingToken)); } return eventStream; }
/** * Initializes the given {@code segmentCount} number of segments for the given {@code processorName} to track its * tokens. This method should only be invoked when no tokens have been stored for the given processor, yet. * <p> * This method will store {@code initialToken} for all segments as starting point for processor, but not claim them. * It will create the segments ranging from {@code 0} until {@code segmentCount - 1}. * <p> * The exact behavior when this method is called while tokens were already present, is undefined in case the token * already present is not owned by the initializing process. * * @param processorName The name of the processor to initialize segments for * @param segmentCount The number of segments to initialize * @param initialToken The initial token which is used as a starting point for processor * @throws UnableToClaimTokenException when a segment has already been created */ default void initializeTokenSegments(String processorName, int segmentCount, TrackingToken initialToken) throws UnableToClaimTokenException { for (int segment = 0; segment < segmentCount; segment++) { storeToken(initialToken, processorName, segment); releaseClaim(processorName, segment); } }
/** * Initializes the given {@code segmentCount} number of segments for the given {@code processorName} to track its * tokens. This method should only be invoked when no tokens have been stored for the given processor, yet. * <p> * This method will initialize the tokens, but not claim them. It will create the segments ranging from {@code 0} * until {@code segmentCount - 1}. * <p> * The exact behavior when this method is called while tokens were already present, is undefined in case the token * already present is not owned by the initializing process. * * @param processorName The name of the processor to initialize segments for * @param segmentCount The number of segments to initialize * @throws UnableToClaimTokenException when a segment has already been created */ default void initializeTokenSegments(String processorName, int segmentCount) throws UnableToClaimTokenException { for (int segment = 0; segment < segmentCount; segment++) { fetchToken(processorName, segment); releaseClaim(processorName, segment); } }
@Test public void testProcessorWorkerCountWithMultipleSegmentsWithOneThread() throws InterruptedException { tokenStore.storeToken(new GlobalSequenceTrackingToken(1L), "test", 0); tokenStore.storeToken(new GlobalSequenceTrackingToken(2L), "test", 1); configureProcessor(TrackingEventProcessorConfiguration.forSingleThreadedProcessing()); testSubject.start(); // give it some time to split segments from the store and submit to executor service. Thread.sleep(200); assertThat(testSubject.activeProcessorThreads(), is(1)); }
@Test public void testProcessorInitializesAndUsesSameTokens() { configureProcessor(TrackingEventProcessorConfiguration.forParallelProcessing(6) .andInitialSegmentsCount(6)); testSubject.start(); assertWithin(5, SECONDS, () -> assertThat(testSubject.activeProcessorThreads(), is(6))); int[] actual = tokenStore.fetchSegments(testSubject.getName()); Arrays.sort(actual); assertArrayEquals(new int[]{0, 1, 2, 3, 4, 5}, actual); }