@Override public void updateToken(TrackingToken token, Serializer serializer) { updateToken(token, serializer, contentType); }
/** * 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); }
/** * 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); } }
/** * Tries to claim the given token {@code entry}. If the claim fails an {@link UnableToClaimTokenException} should be * thrown. Otherwise the given {@code resultSet} should be updated to reflect the claim. * * @param resultSet the updatable query result of an executed {@link PreparedStatement} * @param entry the entry extracted from the given result set * @return the claimed tracking token * * @throws UnableToClaimTokenException if the token cannot be claimed because another node currently owns the token * @throws SQLException when an exception occurs while claiming the token entry */ protected TrackingToken claimToken(ResultSet resultSet, AbstractTokenEntry<?> entry) throws SQLException { if (!entry.claim(nodeId, claimTimeout)) { throw new UnableToClaimTokenException( format("Unable to claim token '%s[%s]'. It is owned by '%s'", entry.getProcessorName(), entry.getSegment(), entry.getOwner())); } resultSet.updateString(schema.ownerColum(), entry.getOwner()); resultSet.updateString(schema.timestampColumn(), entry.timestampAsString()); resultSet.updateRow(); return entry.getToken(serializer); }
/** * Inserts a new token entry via the given updatable {@code resultSet}. * * @param resultSet the updatable result set to add the entry to * @param token the token of the entry to insert * @param processorName the name of the processor to insert a token for * @param segment the segment of the processor to insert a token for * @return the tracking token of the inserted entry * * @throws SQLException when an exception occurs while inserting a token entry */ protected TrackingToken insertTokenEntry(ResultSet resultSet, TrackingToken token, String processorName, int segment) throws SQLException { AbstractTokenEntry<?> entry = new GenericTokenEntry<>(token, serializer, contentType, processorName, segment); entry.claim(nodeId, claimTimeout); resultSet.moveToInsertRow(); resultSet.updateObject(schema.tokenColumn(), token == null ? null : entry.getSerializedToken().getData()); resultSet.updateString(schema.tokenTypeColumn(), token == null ? null : entry.getSerializedToken().getType().getName()); resultSet.updateString(schema.timestampColumn(), entry.timestampAsString()); resultSet.updateString(schema.ownerColum(), entry.getOwner()); resultSet.updateString(schema.processorNameColumn(), processorName); resultSet.updateInt(schema.segmentColumn(), segment); resultSet.insertRow(); return token; }
/** * Convert given {@code resultSet} to an {@link AbstractTokenEntry}. The result set contains a single token entry. * * @param resultSet the result set of a prior select statement containing a single token entry * @return an token entry with data extracted from the result set * * @throws SQLException if the result set cannot be converted to an entry */ protected AbstractTokenEntry<?> readTokenEntry(ResultSet resultSet) throws SQLException { return new GenericTokenEntry<>(readSerializedData(resultSet, schema.tokenColumn()), resultSet.getString(schema.tokenTypeColumn()), resultSet.getString(schema.timestampColumn()), resultSet.getString(schema.ownerColum()), resultSet.getString(schema.processorNameColumn()), resultSet.getInt(schema.segmentColumn()), contentType); }
@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))); }
@Override public void initializeTokenSegments(String processorName, int segmentCount, TrackingToken initialToken) throws UnableToClaimTokenException { Connection connection = getConnection(); try { executeQuery(connection, c -> selectForUpdate(c, processorName, 0), resultSet -> { for (int segment = 0; segment < segmentCount; segment++) { insertTokenEntry(resultSet, initialToken, processorName, segment); } if (!connection.getAutoCommit()) { connection.commit(); } return null; }, e -> new UnableToClaimTokenException( "Could not initialize segments. Some segments were already present.", e )); } finally { closeQuietly(connection); } }
@Override public void initializeTokenSegments(String processorName, int segmentCount, TrackingToken initialToken) throws UnableToClaimTokenException { if (fetchSegments(processorName).length > 0) { throw new UnableToClaimTokenException("Could not initialize segments. Some segments were already present."); } for (int segment = 0; segment < segmentCount; segment++) { tokens.put(new ProcessAndSegment(processorName, segment), getOrDefault(initialToken, NULL_TOKEN)); } }
@Override public void initializeTokenSegments(String processorName, int segmentCount, TrackingToken initialToken) throws UnableToClaimTokenException { EntityManager entityManager = entityManagerProvider.getEntityManager(); if (fetchSegments(processorName).length > 0) { throw new UnableToClaimTokenException("Could not initialize segments. Some segments were already present."); } for (int segment = 0; segment < segmentCount; segment++) { TokenEntry token = new TokenEntry(processorName, segment, initialToken, serializer); entityManager.persist(token); } entityManager.flush(); }
/** * 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); } }
@Override public void extendClaim(String processorName, int segment) throws UnableToClaimTokenException { EntityManager entityManager = entityManagerProvider.getEntityManager(); int updates = entityManager.createQuery("UPDATE TokenEntry te SET te.timestamp = :timestamp " + "WHERE te.processorName = :processorName " + "AND te.segment = :segment " + "AND te.owner = :owner") .setParameter("processorName", processorName) .setParameter("segment", segment) .setParameter("owner", nodeId) .setParameter("timestamp", formatInstant(TokenEntry.clock.instant())) .executeUpdate(); if (updates == 0) { throw new UnableToClaimTokenException("Unable to extend the claim on token for processor '" + processorName + "[" + segment + "]'. It is either claimed " + "by another process, or there is no such token."); } }
/** * Returns the token, deserializing it with given {@code serializer} * * @param serializer The serialize to deserialize the token with * @return the deserialized token stored in this entry */ public TrackingToken getToken(Serializer serializer) { return token == null ? null : serializer.deserialize(getSerializedToken()); }
private boolean expired(TemporalAmount claimTimeout) { return timestamp().plus(claimTimeout).isBefore(clock.instant()); }
/** * Check if given {@code owner} may claim this token. * * @param owner The name of the current node, to register as owner. This name must be unique for multiple * instances of the same logical processor * @param claimTimeout The time after which a claim may be 'stolen' from its current owner * @return {@code true} if the claim may be made, {@code false} otherwise */ public boolean mayClaim(String owner, TemporalAmount claimTimeout) { return this.owner == null || owner.equals(this.owner) || expired(claimTimeout); }
/** * Returns the serialized token. * * @return the serialized token stored in this entry */ public SerializedObject<T> getSerializedToken() { if (token == null) { return null; } return new SimpleSerializedObject<>(token, (Class<T>) token.getClass(), getTokenType()); }
/** * Attempt to claim ownership of this token. When successful, this method returns {@code true}, otherwise * {@code false}. When a claim fails, this token should not be used, as it is already being used in another process. * <p> * If a claim exists, but it is older than given {@code claimTimeout}, the claim may be 'stolen'. * * @param owner The name of the current node, to register as owner. This name must be unique for multiple * instances of the same logical processor * @param claimTimeout The time after which a claim may be 'stolen' from its current owner * @return {@code true} if the claim succeeded, otherwise false */ public boolean claim(String owner, TemporalAmount claimTimeout) { if (!mayClaim(owner, claimTimeout)) { return false; } this.timestamp = formatInstant(clock.instant()); this.owner = owner; return true; }
private void releaseToken(Segment segment) { try { transactionManager.executeInTransaction(() -> tokenStore.releaseClaim(getName(), segment.getSegmentId())); } catch (Exception e) { // Ignore exception } }
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; }