/** * Initializes a {@link JdbcTokenStore} as specified through this Builder. * * @return a {@link JdbcTokenStore} as specified through this Builder */ public JdbcTokenStore build() { return new JdbcTokenStore(this); }
/** * Initializes the default TokenSchema */ public TokenSchema() { this(builder()); }
/** * Returns a new {@link Builder} initialized with default settings. * * @return a new builder for the event schema */ public static Builder builder() { return new Builder(); }
@Override public PreparedStatement createTable(Connection connection, TokenSchema schema) throws SQLException { String sql = "CREATE TABLE IF NOT EXISTS " + schema.tokenTable() + " (\n" + schema.processorNameColumn() + " VARCHAR(255) NOT NULL,\n" + schema.segmentColumn() + " INTEGER NOT NULL,\n" + schema.tokenColumn() + " " + tokenType() + " NULL,\n" + schema.tokenTypeColumn() + " VARCHAR(255) NULL,\n" + schema.timestampColumn() + " VARCHAR(255) NULL,\n" + schema.ownerColum() + " VARCHAR(255) NULL,\n" + "PRIMARY KEY (" + schema.processorNameColumn() + "," + schema.segmentColumn() + ")\n" + ")"; return connection.prepareStatement(sql); }
/** * 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); }
/** * If the given {@code resultSet} has no items this method should insert a new token entry. If a token already * exists it should be attempted to replace the token in the entry with the given {@code token} and claim ownership. * * @param resultSet the updatable query result set of an executed {@link PreparedStatement} * @param token the token for the new or updated entry * @param processorName the name of the processor owning the token * @param segment the segment of the processor owning the token * @throws UnableToClaimTokenException if the token cannot be claimed because another node currently owns the token * @throws SQLException when an exception occurs while updating the result set */ protected void insertOrUpdateToken(ResultSet resultSet, TrackingToken token, String processorName, int segment) throws SQLException { if (resultSet.next()) { AbstractTokenEntry<?> entry = readTokenEntry(resultSet); entry.updateToken(token, serializer); resultSet.updateObject(schema.tokenColumn(), entry.getSerializedToken().getData()); resultSet.updateString(schema.tokenTypeColumn(), entry.getSerializedToken().getType().getName()); resultSet.updateString(schema.timestampColumn(), entry.timestampAsString()); claimToken(resultSet, entry); } else { insertTokenEntry(resultSet, token, processorName, segment); } }
/** * Tries loading an existing token owned by a processor with given {@code processorName} and {@code segment}. If * such a token entry exists an attempt will be made to claim the token. If that succeeds the token will be * returned. If the token is already owned by another node an {@link UnableToClaimTokenException} will be thrown. * <p> * If no such token exists yet, a new token entry will be inserted with {@code null} token owned by this node and * return {@code null}. * * @param resultSet the updatable result set from a prior select for update query * @param processorName the name of the processor to load or insert a token entry for * @param segment the segment of the processor to load or insert a token entry for * @return the tracking token of the fetched entry or {@code null} if a new entry was inserted * * @throws UnableToClaimTokenException if the token cannot be claimed because another node currently owns the token * @throws SQLException when an exception occurs while loading or inserting the entry */ protected TrackingToken loadOrInsertToken(ResultSet resultSet, String processorName, int segment) throws SQLException { if (!resultSet.next()) { return insertTokenEntry(resultSet, null, processorName, segment); } return claimToken(resultSet, readTokenEntry(resultSet)); }
/** * Returns a {@link PreparedStatement} to select all segments ids for a given processorName from the underlying * storage. * * @param connection the connection to the underlying database * @param processorName the name of the processor to fetch the segments for * @return a {@link PreparedStatement} that will fetch segments when executed * * @throws SQLException when an exception occurs while creating the prepared statement */ protected PreparedStatement selectForSegments(Connection connection, String processorName) throws SQLException { final String sql = "SELECT " + schema.segmentColumn() + " FROM " + schema.tokenTable() + " WHERE " + schema.processorNameColumn() + " = ? ORDER BY " + schema.segmentColumn() + " ASC"; PreparedStatement preparedStatement = connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); preparedStatement.setString(1, processorName); return preparedStatement; }
@Override public int[] fetchSegments(String processorName) { Connection connection = getConnection(); try { List<Integer> integers = executeQuery(connection, c -> selectForSegments(c, processorName), listResults(rs -> rs.getInt(schema.segmentColumn())), e -> new JdbcException(format( "Could not load segments for processor [%s]", processorName ), e) ); return integers.stream().mapToInt(i -> i).toArray(); } finally { closeQuietly(connection); } }
@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 TrackingToken fetchToken(String processorName, int segment) throws UnableToClaimTokenException { Connection connection = getConnection(); try { return executeQuery(connection, c -> selectForUpdate(c, processorName, segment), resultSet -> { TrackingToken result = loadOrInsertToken(resultSet, processorName, segment); if (!connection.getAutoCommit()) { connection.commit(); } return result; }, e -> new JdbcException( format("Could not load token for processor [%s] and segment [%d]", processorName, segment), e)); } finally { closeQuietly(connection); } }
@ConditionalOnMissingBean @Bean public TokenStore tokenStore(ConnectionProvider connectionProvider, Serializer serializer) { return JdbcTokenStore.builder() .connectionProvider(connectionProvider) .serializer(serializer) .build(); }
/** * Performs the DDL queries to create the schema necessary for this token store implementation. * * @param schemaFactory factory of the token entry schema */ public void createSchema(TokenTableFactory schemaFactory) { Connection c = getConnection(); try { executeUpdates(c, e -> { throw new JdbcException("Failed to create token tables", e); }, connection -> schemaFactory.createTable(connection, schema)); } finally { closeQuietly(c); } }
@Override public void initializeTokenSegments(String processorName, int segmentCount) throws UnableToClaimTokenException { initializeTokenSegments(processorName, segmentCount, null); }
/** * Builds a new {@link TokenSchema} from builder values. * * @return TokenSchema from this builder */ public TokenSchema build() { return new TokenSchema(this); } }
/** * Sets the {@code nodeId} to identify ownership of the tokens. Defaults to the name of the managed bean for * the runtime system of the Java virtual machine * * @param nodeId the id as a {@link String} to identify ownership of the tokens * @return the current Builder instance, for fluent interfacing */ public Builder nodeId(String nodeId) { assertNodeId(nodeId, "The nodeId may not be null or empty"); this.nodeId = nodeId; return this; }
/** * Instantiate a Builder to be able to create a {@link JdbcTokenStore}. * <p> * The {@code schema} is defaulted to an {@link TokenSchema}, the {@code claimTimeout} to a 10 seconds duration, * {@code nodeId} is defaulted to the name of the managed bean for the runtime system of the Java virtual machine * and the {@code contentType} to a {@code byte[]} {@link Class}. The {@link ConnectionProvider} and * {@link Serializer} are <b>hard requirements</b> and as such should be provided. * * @return a Builder to be able to create a {@link JdbcTokenStore} */ public static Builder builder() { return new Builder(); }
/** * Instantiate a {@link JdbcTokenStore} based on the fields contained in the {@link Builder}. * <p> * Will assert that the {@link ConnectionProvider}, {@link Serializer}, {@link TokenSchema}, {@code claimTimeout}, * {@code nodeId} and {@code contentType} are not {@code null}, and will throw an {@link AxonConfigurationException} * if any of them is {@code null}. * * @param builder the {@link Builder} used to instantiate a {@link JdbcTokenStore} instance */ protected JdbcTokenStore(Builder builder) { builder.validate(); this.connectionProvider = builder.connectionProvider; this.serializer = builder.serializer; this.schema = builder.schema; this.claimTimeout = builder.claimTimeout; this.nodeId = builder.nodeId; this.contentType = builder.contentType; }
@Override public PreparedStatement createTable(Connection connection, TokenSchema schema) throws SQLException { String sql = "CREATE TABLE " + schema.tokenTable() + " (\n" + schema.processorNameColumn() + " VARCHAR(255) NOT NULL,\n" + schema.segmentColumn() + " INTEGER NOT NULL,\n" + schema.tokenColumn() + " " + " BLOB NULL,\n" + schema.tokenTypeColumn() + " VARCHAR(255) NULL,\n" + schema.timestampColumn() + " VARCHAR(255) NULL,\n" + schema.ownerColum() + " VARCHAR(255) NULL,\n" + "PRIMARY KEY (" + schema.processorNameColumn() + "," + schema.segmentColumn() + ")\n" + ")"; return connection.prepareStatement(sql); } }
/** * Validates whether the fields contained in this Builder are set accordingly. * * @throws AxonConfigurationException if one field is asserted to be incorrect according to the Builder's * specifications */ protected void validate() throws AxonConfigurationException { assertNonNull(connectionProvider, "The ConnectionProvider is a hard requirement and should be provided"); assertNonNull(serializer, "The Serializer is a hard requirement and should be provided"); assertNodeId(nodeId, "The nodeId is a hard requirement and should be provided"); }