/** * Executes the given {@code task} in a new {@link Transaction}. The transaction is committed when the task * completes normally, and rolled back when it throws an exception. * * @param task The task to execute */ default void executeInTransaction(Runnable task) { Transaction transaction = startTransaction(); try { task.run(); transaction.commit(); } catch (Throwable e) { transaction.rollback(); throw e; } }
@Override public void scheduleSnapshot(Class<?> aggregateType, String aggregateIdentifier) { executor.execute(new SilentTask(() -> transactionManager .executeInTransaction(createSnapshotterTask(aggregateType, aggregateIdentifier)))); }
@Override @SuppressWarnings("unchecked") protected List<? extends DomainEventData<?>> fetchDomainEvents(String aggregateIdentifier, long firstSequenceNumber, int batchSize) { return transactionManager.fetchInTransaction( () -> entityManager() .createQuery( "SELECT new org.axonframework.eventhandling.GenericDomainEventEntry(" + "e.type, e.aggregateIdentifier, e.sequenceNumber, e.eventIdentifier, e.timeStamp, " + "e.payloadType, e.payloadRevision, e.payload, e.metaData) FROM " + domainEventEntryEntityName() + " e WHERE e.aggregateIdentifier = :id " + "AND e.sequenceNumber >= :seq ORDER BY e.sequenceNumber ASC" ) .setParameter("id", aggregateIdentifier) .setParameter("seq", firstSequenceNumber) .setMaxResults(batchSize) .getResultList()); }
/** * Creates a statement to read domain event entries for an aggregate with given identifier starting with the first * entry having a sequence number that is equal or larger than the given {@code firstSequenceNumber}. * * @param connection The connection to the database. * @param identifier The identifier of the aggregate. * @param firstSequenceNumber The expected sequence number of the first returned entry. * @param batchSize The number of items to include in the batch * @return A {@link PreparedStatement} that returns event entries for the given query when executed. * * @throws SQLException when an exception occurs while creating the prepared statement. */ protected PreparedStatement readEventData(Connection connection, String identifier, long firstSequenceNumber, int batchSize) throws SQLException { Transaction tx = transactionManager.startTransaction(); try { final String sql = "SELECT " + trackedEventFields() + " FROM " + schema.domainEventTable() + " WHERE " + schema.aggregateIdentifierColumn() + " = ? AND " + schema.sequenceNumberColumn() + " >= ? AND " + schema.sequenceNumberColumn() + " < ? ORDER BY " + schema.sequenceNumberColumn() + " ASC"; PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, identifier); preparedStatement.setLong(2, firstSequenceNumber); preparedStatement.setLong(3, firstSequenceNumber + batchSize); return preparedStatement; } finally { tx.commit(); } }
/** * Invokes the given {@code supplier} in a transaction managed by the current TransactionManager. Upon completion * of the call, the transaction will be committed in the case of a regular return value, or rolled back in case an * exception occurred. * <p> * This method is an alternative to {@link #executeInTransaction(Runnable)} in cases where a result needs to be * returned from the code to be executed transactionally. * * @param supplier The supplier of the value to return * @param <T> The type of value to return * @return The value returned by the supplier */ default <T> T fetchInTransaction(Supplier<T> supplier) { Transaction transaction = startTransaction(); try { T result = supplier.get(); transaction.commit(); return result; } catch (Throwable e) { transaction.rollback(); throw e; } } }
@Override @SuppressWarnings("unchecked") protected Stream<? extends DomainEventData<?>> readSnapshotData(String aggregateIdentifier) { return transactionManager.fetchInTransaction( () -> entityManager() .createQuery( "SELECT new org.axonframework.eventhandling.GenericDomainEventEntry(" + "e.type, e.aggregateIdentifier, e.sequenceNumber, e.eventIdentifier, " + "e.timeStamp, e.payloadType, e.payloadRevision, e.payload, e.metaData) FROM " + snapshotEventEntryEntityName() + " e " + "WHERE e.aggregateIdentifier = :id " + "ORDER BY e.sequenceNumber DESC" ) .setParameter("id", aggregateIdentifier) .setMaxResults(1) .getResultList() .stream() ); }
private void releaseToken(Segment segment) { try { transactionManager.executeInTransaction(() -> tokenStore.releaseClaim(getName(), segment.getSegmentId())); } catch (Exception e) { // Ignore exception } }
@Override public Object handle(UnitOfWork<? extends T> unitOfWork, InterceptorChain interceptorChain) throws Exception { Transaction transaction = transactionManager.startTransaction(); unitOfWork.onCommit(u -> transaction.commit()); unitOfWork.onRollback(u -> transaction.rollback()); return interceptorChain.proceed(); } }
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; }
@Override protected void storeSnapshot(DomainEventMessage<?> snapshot, Serializer serializer) { transactionManager.executeInTransaction(() -> { try { executeUpdates( getConnection(), e -> handlePersistenceException(e, snapshot), connection -> appendSnapshot(connection, snapshot, serializer), connection -> deleteSnapshots( connection, snapshot.getAggregateIdentifier(), snapshot.getSequenceNumber() ) ); } catch (ConcurrencyException e) { // Ignore duplicate key issues in snapshot. It just means a snapshot already exists } }); }
/** * Attach a transaction to this Unit of Work, using the given {@code transactionManager}. The transaction will be * managed in the lifecycle of this Unit of Work. Failure to start a transaction will cause this Unit of Work * to be rolled back. * * @param transactionManager The Transaction Manager to create, commit and/or rollback the transaction */ default void attachTransaction(TransactionManager transactionManager) { try { Transaction transaction = transactionManager.startTransaction(); onCommit(u -> transaction.commit()); onRollback(u -> transaction.rollback()); } catch (Throwable t) { rollback(t); throw t; } }
@Override protected Stream<? extends DomainEventData<?>> readSnapshotData(String aggregateIdentifier) { return transactionManager.fetchInTransaction(() -> { List<DomainEventData<?>> result = executeQuery(getConnection(), connection -> readSnapshotData(connection, aggregateIdentifier), JdbcUtils.listResults(this::getSnapshotData), e -> new EventStoreException( format("Error reading aggregate snapshot [%s]", aggregateIdentifier), e)); return result.stream(); }); }
/** * 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]); } }); }
@Override protected void storeEvents(EventMessage<?>... events) { UnitOfWork<?> unitOfWork = CurrentUnitOfWork.get(); Transaction transaction = transactionManager.startTransaction(); unitOfWork.onCommit(u -> transaction.commit()); unitOfWork.onRollback(u -> transaction.rollback()); super.storeEvents(events); } }
@Override protected List<? extends DomainEventData<?>> fetchDomainEvents(String aggregateIdentifier, long firstSequenceNumber, int batchSize) { return transactionManager.fetchInTransaction( () -> executeQuery( getConnection(), connection -> readEventData(connection, aggregateIdentifier, firstSequenceNumber, batchSize), JdbcUtils.listResults(this::getDomainEventData), e -> new EventStoreException( format("Failed to read events for aggregate [%s]", aggregateIdentifier), e ) )); }
@Test public void testHandlerIsInvokedInTransactionScope() throws Exception { CountDownLatch countDownLatch = new CountDownLatch(1); AtomicInteger counter = new AtomicInteger(); AtomicInteger counterAtHandle = new AtomicInteger(); when(mockTransactionManager.startTransaction()).thenAnswer(i -> { counter.incrementAndGet(); return mockTransaction; }); doAnswer(i -> counter.decrementAndGet()).when(mockTransaction).rollback(); doAnswer(i -> counter.decrementAndGet()).when(mockTransaction).commit(); doAnswer(invocation -> { counterAtHandle.set(counter.get()); countDownLatch.countDown(); return null; }).when(mockHandler).handle(any()); testSubject.start(); // give it a bit of time to start Thread.sleep(200); eventBus.publish(createEvents(2)); assertTrue("Expected Handler to have received 2 published events", countDownLatch.await(5, TimeUnit.SECONDS)); assertEquals(1, counterAtHandle.get()); }
@Override public TrackingToken createTailToken() { String sql = "SELECT min(" + schema.globalIndexColumn() + ") - 1 FROM " + schema.domainEventTable(); Long index = transactionManager.fetchInTransaction( () -> executeQuery(getConnection(), connection -> connection.prepareStatement(sql), resultSet -> nextAndExtract(resultSet, 1, Long.class), e -> new EventStoreException("Failed to get tail token", e))); return Optional.ofNullable(index) .map(seq -> GapAwareTrackingToken.newInstance(seq, Collections.emptySet())) .orElse(null); }
@Override public TrackingToken createHeadToken() { String sql = "SELECT max(" + schema.globalIndexColumn() + ") FROM " + schema.domainEventTable(); Long index = transactionManager.fetchInTransaction( () -> executeQuery(getConnection(), connection -> connection.prepareStatement(sql), resultSet -> nextAndExtract(resultSet, 1, Long.class), e -> new EventStoreException("Failed to get head token", e))); return Optional.ofNullable(index) .map(seq -> GapAwareTrackingToken.newInstance(seq, Collections.emptySet())) .orElse(null); }
@Override public Optional<Long> lastSequenceNumberFor(String aggregateIdentifier) { String sql = "SELECT max(" + schema.sequenceNumberColumn() + ") FROM " + schema.domainEventTable() + " WHERE " + schema.aggregateIdentifierColumn() + " = ?"; return Optional.ofNullable(transactionManager.fetchInTransaction( () -> executeQuery(getConnection(), connection -> { PreparedStatement stmt = connection.prepareStatement(sql); stmt.setString(1, aggregateIdentifier); return stmt; }, resultSet -> nextAndExtract(resultSet, 1, Long.class), e -> new EventStoreException( format("Failed to read events for aggregate [%s]", aggregateIdentifier), e ) ))); }
@Override public TrackingToken createTokenAt(Instant dateTime) { String sql = "SELECT min(" + schema.globalIndexColumn() + ") - 1 FROM " + schema.domainEventTable() + " WHERE " + schema.timestampColumn() + " >= ?"; Long index = transactionManager.fetchInTransaction( () -> executeQuery(getConnection(), connection -> { PreparedStatement stmt = connection.prepareStatement(sql); stmt.setString(1, formatInstant(dateTime)); return stmt; }, resultSet -> nextAndExtract(resultSet, 1, Long.class), e -> new EventStoreException(format("Failed to get token at [%s]", dateTime), e))); if (index == null) { return null; } return GapAwareTrackingToken.newInstance(index, Collections.emptySet()); }