Class JdbcTokenStore

java.lang.Object
org.axonframework.eventhandling.tokenstore.jdbc.JdbcTokenStore
All Implemented Interfaces:
TokenStore

public class JdbcTokenStore extends Object implements TokenStore
A TokenStore implementation that uses JDBC to save and load TrackingToken instances.

Before using this store make sure the database contains a table named TokenSchema.tokenTable() in which to store the tokens. For convenience, this table can be constructed through the createSchema(TokenTableFactory) operation.

Since:
3.0
Author:
Rene de Waele
  • Constructor Details

  • Method Details

    • builder

      public static JdbcTokenStore.Builder builder()
      Instantiate a Builder to be able to create a JdbcTokenStore.

      The schema is defaulted to an TokenSchema, the claimTimeout to a 10 seconds duration, nodeId is defaulted to the name of the managed bean for the runtime system of the Java virtual machine and the contentType to a byte[] Class. The ConnectionProvider and Serializer are hard requirements and as such should be provided.

      Returns:
      a Builder to be able to create a JdbcTokenStore
    • createSchema

      public void createSchema(TokenTableFactory schemaFactory)
      Performs the DDL queries to create the schema necessary for this token store implementation.
      Parameters:
      schemaFactory - factory of the token entry schema
    • initializeTokenSegments

      public void initializeTokenSegments(@Nonnull String processorName, int segmentCount) throws UnableToClaimTokenException
      Description copied from interface: TokenStore
      Initializes the given segmentCount number of segments for the given processorName to track its tokens. This method should only be invoked when no tokens have been stored for the given processor, yet.

      This method will initialize the tokens, but not claim them. It will create the segments ranging from 0 until segmentCount - 1.

      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.

      Specified by:
      initializeTokenSegments in interface TokenStore
      Parameters:
      processorName - The name of the processor to initialize segments for
      segmentCount - The number of segments to initialize
      Throws:
      UnableToClaimTokenException - when a segment has already been created
    • initializeTokenSegments

      public void initializeTokenSegments(@Nonnull String processorName, int segmentCount, TrackingToken initialToken) throws UnableToClaimTokenException
      Description copied from interface: TokenStore
      Initializes the given segmentCount number of segments for the given processorName to track its tokens. This method should only be invoked when no tokens have been stored for the given processor, yet.

      This method will store initialToken for all segments as starting point for processor, but not claim them. It will create the segments ranging from 0 until segmentCount - 1.

      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.

      Specified by:
      initializeTokenSegments in interface TokenStore
      Parameters:
      processorName - The name of the processor to initialize segments for
      segmentCount - The number of segments to initialize
      initialToken - The initial token which is used as a starting point for processor
      Throws:
      UnableToClaimTokenException - when a segment has already been created
    • initializeSegment

      public void initializeSegment(@Nullable TrackingToken token, @Nonnull String processorName, int segment) throws UnableToInitializeTokenException
      Description copied from interface: TokenStore
      Initializes a segment with given segment for the processor with given processorName to contain the given token.

      This method fails if a Token already exists for the given processor and segment, even if that token has been claimed by the active instance.

      This method will not claim the initialized segment. Use TokenStore.fetchToken(String, int) to retrieve and claim the token.

      Specified by:
      initializeSegment in interface TokenStore
      Parameters:
      token - The token to initialize the segment with
      processorName - The name of the processor to create the segment for
      segment - The identifier of the segment to initialize
      Throws:
      UnableToInitializeTokenException - if a Token already exists
    • requiresExplicitSegmentInitialization

      public boolean requiresExplicitSegmentInitialization()
      Description copied from interface: TokenStore
      Indicates whether this TokenStore instance requires segments to be explicitly initialized, before any tokens can be claimed for that segment.
      Specified by:
      requiresExplicitSegmentInitialization in interface TokenStore
      Returns:
      true if this instance requires tokens to be explicitly initialized, otherwise false.
      See Also:
    • retrieveStorageIdentifier

      public Optional<String> retrieveStorageIdentifier() throws UnableToRetrieveIdentifierException
      Description copied from interface: TokenStore
      Returns a unique identifier that uniquely identifies the storage location of the tokens in this store. Two token store implementations that share state, must return the same identifier. Two token store implementations that do not share a location, must return a different identifier (or an empty optional if identifiers are not supported).

      Note that this method may require the implementation to consult its underlying storage. Therefore, a Transaction should be active when this method is called, similarly to invocations like TokenStore.fetchToken(String, int), TokenStore.fetchSegments(String), etc. When no Transaction is active, the behavior is undefined.

      Specified by:
      retrieveStorageIdentifier in interface TokenStore
      Returns:
      an identifier to uniquely identify the storage location of tokens in this TokenStore.
      Throws:
      UnableToRetrieveIdentifierException - when the implementation was unable to determine its identifier
    • serializer

      public Serializer serializer()
      Returns the serializer used by the Token Store to serialize tokens.
      Returns:
      the serializer used by the Token Store to serialize tokens
    • storeToken

      public void storeToken(TrackingToken token, @Nonnull String processorName, int segment) throws UnableToClaimTokenException
      Description copied from interface: TokenStore
      Stores the given token in the store. The token marks the current position of the process with given processorName and segment. The given token may be null.

      Any claims made by the current process have their timestamp updated.

      This method should throw an UnableToClaimTokenException when the given segment has not been initialized with a Token (albeit null) yet. In that case, a segment must have been explicitly initialized. A TokenStore implementation's ability to do so is exposed by the TokenStore.requiresExplicitSegmentInitialization() method. If that method returns false, this method may implicitly initialize a token and return that token upon invocation.

      Specified by:
      storeToken in interface TokenStore
      Parameters:
      token - The token to store for a given process and segment. May be null.
      processorName - The name of the process for which to store the token
      segment - The index of the segment for which to store the token
      Throws:
      UnableToClaimTokenException - when the token being updated has been claimed by another process.
    • fetchToken

      public TrackingToken fetchToken(@Nonnull String processorName, int segment) throws UnableToClaimTokenException
      Description copied from interface: TokenStore
      Returns the last stored token for the given processorName and segment. Returns null if the stored token for the given process and segment is null.

      This method should throw an UnableToClaimTokenException when the given segment has not been initialized with a Token (albeit null) yet. In that case, a segment must have been explicitly initialized. A TokenStore implementation's ability to do so is exposed by the TokenStore.requiresExplicitSegmentInitialization() method. If that method returns false, this method may implicitly initialize a token and return that token upon invocation.

      The token will be claimed by the current process (JVM instance), preventing access by other instances. To release the claim, use TokenStore.releaseClaim(String, int)

      Specified by:
      fetchToken in interface TokenStore
      Parameters:
      processorName - The process name for which to fetch the token
      segment - The segment index for which to fetch the token
      Returns:
      The last stored TrackingToken or null if the store holds no token for given process and segment
      Throws:
      UnableToClaimTokenException - if there is a token for given processorName and segment, but they are claimed by another process.
    • fetchToken

      public TrackingToken fetchToken(@Nonnull String processorName, @Nonnull Segment segment) throws UnableToClaimTokenException
      Description copied from interface: TokenStore
      Returns the last stored token for the given processorName and segment. Returns null if the stored token for the given process and segment is null.

      This method should throw an UnableToClaimTokenException when the given segment has not been initialized with a Token (albeit null) yet. In that case, a segment must have been explicitly initialized. A TokenStore implementation's ability to do so is exposed by the TokenStore.requiresExplicitSegmentInitialization() method. If that method returns false, this method may implicitly initialize a token and return that token upon invocation.

      The token will be claimed by the current process (JVM instance), preventing access by other instances. To release the claim, use TokenStore.releaseClaim(String, int)

      Specified by:
      fetchToken in interface TokenStore
      Parameters:
      processorName - The process name for which to fetch the token
      segment - The segment for which to fetch the token
      Returns:
      The last stored TrackingToken or null if the store holds no token for given process and segment
      Throws:
      UnableToClaimTokenException - if there is a token for given processorName and segment, but they are claimed by another process, or if the segment has been split or merged concurrently
    • releaseClaim

      public void releaseClaim(@Nonnull String processorName, int segment)
      Description copied from interface: TokenStore
      Release a claim of the token for given processorName and segment. If no such claim existed, nothing happens.

      The caller must ensure not to use any streams opened based on the token for which the claim is released.

      Specified by:
      releaseClaim in interface TokenStore
      Parameters:
      processorName - The name of the process owning the token (e.g. a TrackingEventProcessor name)
      segment - the segment for which a token was obtained
    • deleteToken

      public void deleteToken(@Nonnull String processorName, int segment)
      Description copied from interface: TokenStore
      Deletes the token for the processor with given processorName and segment. The token must be owned by the current node, to be able to delete it.

      Implementations should implement this method only when TokenStore.requiresExplicitSegmentInitialization() is overridden to return true. Deleting tokens using implementations that do not require explicit token initialization is unsafe, as a claim will automatically recreate the deleted token instance, which may result in concurrency issues.

      Specified by:
      deleteToken in interface TokenStore
      Parameters:
      processorName - The name of the processor to remove the token for
      segment - The segment to delete
    • fetchSegments

      public int[] fetchSegments(@Nonnull String processorName)
      Description copied from interface: TokenStore
      Returns an array of known segments for a given processorName.

      The segments returned are segments for which a token has been stored previously. When the TokenStore is empty, an empty array is returned.

      Specified by:
      fetchSegments in interface TokenStore
      Parameters:
      processorName - The process name for which to fetch the segments
      Returns:
      an array of segment identifiers.
    • fetchAvailableSegments

      public List<Segment> fetchAvailableSegments(@Nonnull String processorName)
      Description copied from interface: TokenStore
      Returns a List of known available segments for a given processorName. A segment is considered available if it is not claimed by any other event processor.

      The segments returned are segments for which a token has been stored previously and have not been claimed by another processor. When the TokenStore is empty, an empty list is returned. By default, if this method is not implemented, we will return all segments instead, whether they are available or not.

      Specified by:
      fetchAvailableSegments in interface TokenStore
      Parameters:
      processorName - the processor's name for which to fetch the segments
      Returns:
      a List of available segment identifiers for the specified processorName
    • selectForSegments

      protected PreparedStatement selectForSegments(Connection connection, String processorName) throws SQLException
      Returns a PreparedStatement to select all segments ids for a given processorName from the underlying storage.
      Parameters:
      connection - the connection to the underlying database
      processorName - the name of the processor to fetch the segments for
      Returns:
      a PreparedStatement that will fetch segments when executed
      Throws:
      SQLException - when an exception occurs while creating the prepared statement
    • selectTokenEntries

      protected PreparedStatement selectTokenEntries(Connection connection, String processorName) throws SQLException
      Returns a PreparedStatement to select all TokenEntries for a given processorName from the underlying storage.
      Parameters:
      connection - the connection to the underlying database
      processorName - the name of the processor to fetch the segments for
      Returns:
      a PreparedStatement that will fetch TokenEntries when executed
      Throws:
      SQLException - when an exception occurs while creating the prepared statement
    • storeUpdate

      protected PreparedStatement storeUpdate(Connection connection, TrackingToken token, String processorName, int segment) throws SQLException
      Returns a PreparedStatement which updates the given token for the given processorName and segment combination.
      Parameters:
      connection - the connection to the underlying database
      token - the new token to store
      processorName - the name of the processor executing the update
      segment - the segment of the processor to executing the update
      Returns:
      a PreparedStatement that will update a token entry when executed
      Throws:
      SQLException - when an exception occurs while creating the prepared statement
    • selectForUpdate

      protected PreparedStatement selectForUpdate(Connection connection, String processorName, int segment) throws SQLException
      Returns a PreparedStatement to select a token entry from the underlying storage. The ResultSet that is returned when this statement is executed should be updatable.
      Parameters:
      connection - the connection to the underlying database
      processorName - the name of the processor to fetch the entry for
      segment - the segment of the processor to fetch the entry for
      Returns:
      a PreparedStatement that will fetch an updatable token entry when executed
      Throws:
      SQLException - when an exception occurs while creating the prepared statement
    • select

      protected PreparedStatement select(Connection connection, String processorName, int segment, boolean forUpdate) throws SQLException
      Returns a PreparedStatement to select a token entry from the underlying storage, either for updating or just for reading.
      Parameters:
      connection - the connection to the underlying database
      processorName - the name of the processor to fetch the entry for
      segment - the segment of the processor to fetch the entry for
      forUpdate - whether the returned token should be updatable
      Returns:
      a PreparedStatement that will fetch an updatable token entry when executed
      Throws:
      SQLException - when an exception occurs while creating the prepared statement
    • updateToken

      protected void updateToken(Connection connection, ResultSet resultSet, TrackingToken token, String processorName, int segment) throws SQLException
      If the given resultSet has an entry, attempts to replace the token in the entry with the given token and claim ownership.
      Parameters:
      connection - the connection to the underlying database
      resultSet - the updatable query result set of an executed PreparedStatement
      token - the token for the new or updated entry
      processorName - the name of the processor owning the token
      segment - the segment of the processor owning the token
      Throws:
      UnableToClaimTokenException - if the token cannot be claimed because another node currently owns the token
      SQLException - when an exception occurs while updating the result set
    • claimToken

      protected TrackingToken claimToken(Connection connection, AbstractTokenEntry<?> entry) throws SQLException
      Tries to claim the given token entry. If the claim fails an UnableToClaimTokenException should be thrown. Otherwise the given resultSet should be updated to reflect the claim.
      Parameters:
      connection - the connection to the underlying database
      entry - the entry extracted from the given result set
      Returns:
      the claimed tracking token
      Throws:
      UnableToClaimTokenException - if the token cannot be claimed because another node currently owns the token
      SQLException - when an exception occurs while claiming the token entry
    • loadToken

      protected TrackingToken loadToken(Connection connection, ResultSet resultSet, String processorName, int segment) throws SQLException
      Tries loading an existing token owned by a processor with given processorName and 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 UnableToClaimTokenException will be thrown.

      If no such token exists yet, a new token entry will be inserted with null token owned by this node and return null.

      Parameters:
      connection - the connection to the underlying database
      resultSet - the updatable result set from a prior select for update query
      processorName - the name of the processor to load or insert a token entry for
      segment - the segment of the processor to load or insert a token entry for
      Returns:
      the tracking token of the fetched entry or null if a new entry was inserted
      Throws:
      UnableToClaimTokenException - if the token cannot be claimed because another node currently owns the token
      SQLException - when an exception occurs while loading or inserting the entry
    • loadToken

      protected TrackingToken loadToken(Connection connection, ResultSet resultSet, String processorName, Segment segment) throws SQLException
      Tries loading an existing token owned by a processor with given processorName and 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 UnableToClaimTokenException will be thrown.

      If no such token exists yet, a new token entry will be inserted with a null token, owned by this node, and this method returns null.

      If a token has been claimed, the segment will be validated by checking the database for the split and merge candidate segments. If a concurrent split or merge operation has been detected, the calim will be released and an UnableToClaimTokenException will be thrown.}

      Parameters:
      connection - the connection to the underlying database
      resultSet - the updatable result set from a prior select for update query
      processorName - the name of the processor to load or insert a token entry for
      segment - the segment of the processor to load or insert a token entry for
      Returns:
      the tracking token of the fetched entry or null if a new entry was inserted
      Throws:
      UnableToClaimTokenException - if the token cannot be claimed because another node currently owns the token or if the segment has been split or merged concurrently
      SQLException - when an exception occurs while loading or inserting the entry
    • validateSegment

      protected void validateSegment(String processorName, Segment segment)
      Validate a segment by checking for the existence of a split or merge candidate segment.

      If the segment has been split concurrently, the split segment candidate will be found, indicating that we have claimed an incorrect segment. If the segment has been merged concurrently, the merge candidate segment will no longer exist, also indicating that we have claimed an incorrect segment.

      Parameters:
      processorName - the name of the processor to load or insert a token entry for
      segment - the segment of the processor to load or insert a token entry for
    • selectSegments

      protected PreparedStatement selectSegments(Connection connection, String processorName, int splitSegmentId, int mergeableSegmentId) throws SQLException
      Returns a PreparedStatement for the count of segments that can be found after searching for the splitSegmentId and mergeableSegmentId.
      Parameters:
      connection - the connection to the underlying database
      processorName - the name of the processor to load or insert a token entry for
      splitSegmentId - the id of the split candidate segment
      mergeableSegmentId - the id of the merge candidate segment
      Returns:
      The PreparedStatement to execute
      Throws:
      SQLException - when an Exception occurs in building the PreparedStatement
    • insertTokenEntry

      protected TrackingToken insertTokenEntry(Connection connection, TrackingToken token, String processorName, int segment) throws SQLException
      Inserts a new token entry via the given updatable resultSet.
      Parameters:
      connection - the connection to the underlying database
      token - the token of the entry to insert
      processorName - the name of the processor to insert a token for
      segment - the segment of the processor to insert a token for
      Returns:
      the tracking token of the inserted entry
      Throws:
      SQLException - when an exception occurs while inserting a token entry
    • readTokenEntry

      protected AbstractTokenEntry<?> readTokenEntry(ResultSet resultSet) throws SQLException
      Convert given resultSet to an AbstractTokenEntry. The result set contains a single token entry.
      Parameters:
      resultSet - the result set of a prior select statement containing a single token entry
      Returns:
      an token entry with data extracted from the result set
      Throws:
      SQLException - if the result set cannot be converted to an entry
    • releaseClaim

      protected PreparedStatement releaseClaim(Connection connection, String processorName, int segment) throws SQLException
      Creates a new PreparedStatement to release the current claim this node has on a token belonging to a processor with given processorName and segment.
      Parameters:
      connection - the connection that should be used to create a PreparedStatement
      processorName - the name of the processor for which to release this node's claim
      segment - the segment of the processor for which to release this node's claim
      Returns:
      a PreparedStatement that will release the claim this node has on the token entry
      Throws:
      SQLException - if the statement to release a claim cannot be created
    • deleteToken

      protected PreparedStatement deleteToken(Connection connection, String processorName, int segment) throws SQLException
      Creates a new PreparedStatement to release the current claim this node has on a token belonging to a processor with given processorName and segment.
      Parameters:
      connection - the connection that should be used to create a PreparedStatement
      processorName - the name of the processor for which to release this node's claim
      segment - the segment of the processor for which to release this node's claim
      Returns:
      a PreparedStatement that will release the claim this node has on the token entry
      Throws:
      SQLException - if the statement to release a claim cannot be created
    • readSerializedData

      protected <T> T readSerializedData(ResultSet resultSet, String columnName) throws SQLException
      Returns the serialized token data from the given resultSet at given columnName.
      Type Parameters:
      T - the type of data to return
      Parameters:
      resultSet - the result set to get serialized data from
      columnName - the name of the column containing the serialized token
      Returns:
      the serialized data of the token
      Throws:
      SQLException - if the token cannot be read from the entry
    • getConnection

      protected Connection getConnection()
      Returns a Connection to the database.
      Returns:
      a database Connection