Class AmazonDynamoDBLockClient

  • All Implemented Interfaces:
    Closeable, AutoCloseable, Runnable

    @ThreadSafe
    public class AmazonDynamoDBLockClient
    extends Object
    implements Runnable, Closeable

    Provides a simple library for using DynamoDB's consistent read/write feature to use it for managing distributed locks.

    In order to use this library, the client must create a table in DynamoDB, although the library provides a convenience method for creating that table (createLockTableInDynamoDB.)

    Here is some example code for how to use the lock client for leader election to work on a resource called "database-3" (it assumes you already have a DynamoDB table named lockTable, which can be created with the static createLockTableInDynamoDB helper method):

     
      AmazonDynamoDBLockClient lockClient = new AmazonDynamoDBLockClient(
          AmazonDynamoDBLockClientOptions.builder(dynamoDBClient, "lockTable").build();
      try {
          // Attempt to acquire the lock indefinitely, polling DynamoDB every 2 seconds for the lock
          LockItem lockItem = lockClient.acquireLock(
              AcquireLockOptions.builder("database-3")
                  .withRefreshPeriod(120L)
                  .withAdditionalTimeToWaitForLock(Long.MAX_VALUE / 2L)
                  .withTimeUnit(TimeUnit.MILLISECONDS)
                  .build());
          if (!lockItem.isExpired()) {
              // do business logic, you can call lockItem.isExpired() to periodically check to make sure you still have the lock
              // the background thread will keep the lock valid for you by sending heartbeats (default is every 5 seconds)
          }
      } catch (LockNotGrantedException x) {
          // Should only be thrown if the lock could not be acquired for Long.MAX_VALUE / 2L milliseconds.
      }
     
     

    Here is an example that involves a bunch of workers getting customer IDs from a queue, taking a lock on that Customer ID, then releasing that lock when complete:

     
      AmazonDynamoDBLockClient lockClient = new AmazonDynamoDBLockClient(
          AmazonDynamoDBLockClient.builder(dynamoDBClient, "lockTable").build();
      while (true) {
          // Somehow find out about what work needs to be done
          String customerID = getCustomerIDFromQueue();
    
         try {
              // Don't try indefinitely -- if someone else has a lock on this Customer ID, just move onto the next customer
              // (note that, if there is a lock on this customer ID, this method will still wait at least 20 seconds in order to be
              // able to determine
              // if that lock is stale)
              LockItem lockItem = lockClient.acquireLock(AcquireLockOptions.builder(customerID).build());
              if (!lockItem.isExpired()) {
                  // Perform operation on this customer
              }
              lockItem.close();
          } catch (LockNotGrantedException x) {
              logger.info("We failed to acquire the lock for customer " + customerID, x);
          }
      }
     
     
    Author:
    Sasha Slutsker, Alexander Patrikalakis
    • Field Detail

      • LEASE_DURATION_PATH_VALUE_EXPRESSION_VARIABLE

        protected static final String LEASE_DURATION_PATH_VALUE_EXPRESSION_VARIABLE
        See Also:
        Constant Field Values
      • LEASE_DURATION_VALUE_EXPRESSION_VARIABLE

        protected static final String LEASE_DURATION_VALUE_EXPRESSION_VARIABLE
        See Also:
        Constant Field Values
      • OWNER_NAME_PATH_EXPRESSION_VARIABLE

        protected static final String OWNER_NAME_PATH_EXPRESSION_VARIABLE
        See Also:
        Constant Field Values
      • OWNER_NAME_VALUE_EXPRESSION_VARIABLE

        protected static final String OWNER_NAME_VALUE_EXPRESSION_VARIABLE
        See Also:
        Constant Field Values
      • IS_RELEASED_PATH_EXPRESSION_VARIABLE

        protected static final String IS_RELEASED_PATH_EXPRESSION_VARIABLE
        See Also:
        Constant Field Values
      • IS_RELEASED_VALUE_EXPRESSION_VARIABLE

        protected static final String IS_RELEASED_VALUE_EXPRESSION_VARIABLE
        See Also:
        Constant Field Values
      • ACQUIRE_LOCK_THAT_DOESNT_EXIST_PK_CONDITION

        protected static final String ACQUIRE_LOCK_THAT_DOESNT_EXIST_PK_CONDITION
      • ACQUIRE_LOCK_THAT_DOESNT_EXIST_PK_SK_CONDITION

        protected static final String ACQUIRE_LOCK_THAT_DOESNT_EXIST_PK_SK_CONDITION
      • PK_EXISTS_AND_IS_RELEASED_CONDITION

        protected static final String PK_EXISTS_AND_IS_RELEASED_CONDITION
      • PK_EXISTS_AND_SK_EXISTS_AND_IS_RELEASED_CONDITION

        protected static final String PK_EXISTS_AND_SK_EXISTS_AND_IS_RELEASED_CONDITION
      • PK_EXISTS_AND_SK_EXISTS_AND_RVN_IS_THE_SAME_AND_IS_RELEASED_CONDITION

        protected static final String PK_EXISTS_AND_SK_EXISTS_AND_RVN_IS_THE_SAME_AND_IS_RELEASED_CONDITION
      • PK_EXISTS_AND_SK_EXISTS_AND_RVN_IS_THE_SAME_CONDITION

        protected static final String PK_EXISTS_AND_SK_EXISTS_AND_RVN_IS_THE_SAME_CONDITION
      • PK_EXISTS_AND_SK_EXISTS_AND_OWNER_NAME_SAME_AND_RVN_SAME_CONDITION

        protected static final String PK_EXISTS_AND_SK_EXISTS_AND_OWNER_NAME_SAME_AND_RVN_SAME_CONDITION
      • PK_EXISTS_AND_RVN_IS_THE_SAME_AND_IS_RELEASED_CONDITION

        protected static final String PK_EXISTS_AND_RVN_IS_THE_SAME_AND_IS_RELEASED_CONDITION
      • PK_EXISTS_AND_RVN_IS_THE_SAME_CONDITION

        protected static final String PK_EXISTS_AND_RVN_IS_THE_SAME_CONDITION
      • PK_EXISTS_AND_OWNER_NAME_SAME_AND_RVN_SAME_CONDITION

        protected static final String PK_EXISTS_AND_OWNER_NAME_SAME_AND_RVN_SAME_CONDITION
      • UPDATE_IS_RELEASED

        protected static final String UPDATE_IS_RELEASED
      • UPDATE_IS_RELEASED_AND_DATA

        protected static final String UPDATE_IS_RELEASED_AND_DATA
      • UPDATE_LEASE_DURATION_AND_RVN

        protected static final String UPDATE_LEASE_DURATION_AND_RVN
      • UPDATE_LEASE_DURATION_AND_RVN_AND_REMOVE_DATA

        protected static final String UPDATE_LEASE_DURATION_AND_RVN_AND_REMOVE_DATA
      • UPDATE_LEASE_DURATION_AND_RVN_AND_DATA

        protected static final String UPDATE_LEASE_DURATION_AND_RVN_AND_DATA
      • REMOVE_IS_RELEASED_UPDATE_EXPRESSION

        protected static final String REMOVE_IS_RELEASED_UPDATE_EXPRESSION
      • QUERY_PK_EXPRESSION

        protected static final String QUERY_PK_EXPRESSION
      • dynamoDB

        protected final software.amazon.awssdk.services.dynamodb.DynamoDbClient dynamoDB
      • tableName

        protected final String tableName
      • IS_RELEASED_ATTRIBUTE_VALUE

        protected static final software.amazon.awssdk.services.dynamodb.model.AttributeValue IS_RELEASED_ATTRIBUTE_VALUE
      • lockClientId

        protected static volatile AtomicInteger lockClientId
      • IS_RELEASED_INDICATOR

        protected static final Boolean IS_RELEASED_INDICATOR
    • Constructor Detail

      • AmazonDynamoDBLockClient

        public AmazonDynamoDBLockClient​(AmazonDynamoDBLockClientOptions amazonDynamoDBLockClientOptions)
        Initializes an AmazonDynamoDBLockClient using the lock client options specified in the AmazonDynamoDBLockClientOptions object.
        Parameters:
        amazonDynamoDBLockClientOptions - The options to use when initializing the client, i.e. the table name, sort key value, etc.
    • Method Detail

      • lockTableExists

        public boolean lockTableExists()
        Checks whether the lock table exists in DynamoDB.
        Returns:
        true if the table exists, false otherwise.
      • assertLockTableExists

        public void assertLockTableExists()
                                   throws LockTableDoesNotExistException
        Asserts that the lock table exists in DynamoDB. You can use this method during application initialization to ensure that the lock client will be usable. Since this is a no-arg assertion as opposed to a check that returns a value, this method is also suitable as an init-method for a Spring bean.
        Throws:
        LockTableDoesNotExistException - if the table doesn't exist.
      • createLockTableInDynamoDB

        public static void createLockTableInDynamoDB​(CreateDynamoDBTableOptions createDynamoDBTableOptions)
        Creates a DynamoDB table with the right schema for it to be used by this locking library. The table should be set up in advance, because it takes a few minutes for DynamoDB to provision a new instance. Also, if the table already exists, this will throw an exception.

        This method lets you specify a sort key to be used by the lock client. This sort key then needs to be specified in the AmazonDynamoDBLockClientOptions when the lock client object is created.

        Parameters:
        createDynamoDBTableOptions - The options for the lock client
      • acquireLock

        public LockItem acquireLock​(AcquireLockOptions options)
                             throws LockNotGrantedException,
                                    InterruptedException

        Attempts to acquire a lock until it either acquires the lock, or a specified additionalTimeToWaitForLock is reached. This method will poll DynamoDB based on the refreshPeriod. If it does not see the lock in DynamoDB, it will immediately return the lock to the caller. If it does see the lock, it will note the lease expiration on the lock. If the lock is deemed stale, (that is, there is no heartbeat on it for at least the length of its lease duration) then this will acquire and return it. Otherwise, if it waits for as long as additionalTimeToWaitForLock without acquiring the lock, then it will throw a LockNotGrantedException.

        Note that this method will wait for at least as long as the leaseDuration in order to acquire a lock that already exists. If the lock is not acquired in that time, it will wait an additional amount of time specified in additionalTimeToWaitForLock before giving up.

        See the defaults set when constructing a new AcquireLockOptions object for any fields that you do not set explicitly.

        Parameters:
        options - A combination of optional arguments that may be passed in for acquiring the lock
        Returns:
        the lock
        Throws:
        LockNotGrantedException - if the lock is not acquired before the configured wait time elapses, or if the lock cannot be acquired because of a failed conditional write or exceeded provisioned throughput.
        LockCurrentlyUnavailableException - if shouldSkipBlockingWait is true and the lock is currently held by another owner, or if another owner acquires the lock first during a skip-blocking acquire attempt.
        InterruptedException - in case the Thread.sleep call was interrupted while waiting to refresh.
      • hasLock

        public boolean hasLock​(String key,
                               Optional<String> sortKey)
        Returns true if the client currently owns the lock with @param key and @param sortKey. It returns false otherwise.
        Parameters:
        key - The partition key representing the lock.
        sortKey - The sort key if present.
        Returns:
        true if the client owns the lock. It returns false otherwise.
      • tryAcquireLock

        public Optional<LockItem> tryAcquireLock​(AcquireLockOptions options)
                                          throws InterruptedException
        Attempts to acquire lock. If successful, returns the lock. Otherwise, returns Optional.empty(). This includes cases where acquireLock would throw LockNotGrantedException or LockCurrentlyUnavailableException. For more details on behavior, please see acquireLock.
        Parameters:
        options - The options to use when acquiring the lock.
        Returns:
        the lock if successful.
        Throws:
        InterruptedException - in case this.acquireLock was interrupted.
      • releaseLock

        public boolean releaseLock​(LockItem lockItem)
        Releases the given lock if the current user still has it, returning true if the lock was successfully released, and false if someone else already stole the lock. Deletes the lock item if it is released and deleteLockItemOnClose is set.
        Parameters:
        lockItem - The lock item to release
        Returns:
        true if the lock is released, false otherwise
      • getLock

        public Optional<LockItem> getLock​(String key,
                                          Optional<String> sortKey)
        Finds out who owns the given lock, but does not acquire the lock. It returns the metadata currently associated with the given lock. If the client currently has the lock, it will return the lock, and operations such as releaseLock will work. However, if the client does not have the lock, then operations like releaseLock will not work (after calling getLock, the caller should check lockItem.isExpired() to figure out if it currently has the lock.)
        Parameters:
        key - The partition key representing the lock.
        sortKey - The sort key if present.
        Returns:
        A LockItem that represents the lock, if the lock exists.
      • getLockFromDynamoDB

        public Optional<LockItem> getLockFromDynamoDB​(GetLockOptions options)
        Retrieves the lock item from DynamoDB. Note that this will return a LockItem even if it was released -- do NOT use this method if your goal is to acquire a lock for doing work.
        Parameters:
        options - The options such as the key, etc.
        Returns:
        The LockItem, or absent if it is not present. Note that the item can exist in the table even if it is released, as noted by isReleased().
      • getAllLocksFromDynamoDB

        public Stream<LockItem> getAllLocksFromDynamoDB​(boolean deleteOnRelease)

        Retrieves all the lock items from DynamoDB.

        Not that this will may return a lock item even if it was released.

        Parameters:
        deleteOnRelease - Whether or not the LockItem should delete the item when LockItem.close() is called on it.
        Returns:
        A non parallel Stream of all the LockItems in DynamoDB. Note that the item can exist in the table even if it is released, as noted by LockItem.isReleased().
      • getLocksByPartitionKey

        public Stream<LockItem> getLocksByPartitionKey​(String key,
                                                       boolean deleteOnRelease)

        Retrieves the locks with partition_key = key.

        Not that this may return a lock item even if it was released.

        Parameters:
        key - the partition key
        deleteOnRelease - Whether or not the LockItem should delete the item when LockItem.close() is called on it.
        Returns:
        A non parallel Stream of LockItems that has the partition key in DynamoDB. Note that the item can exist in the table even if it is released, as noted by LockItem.isReleased().
      • sendHeartbeat

        public void sendHeartbeat​(LockItem lockItem)

        Sends a heartbeat to indicate that the given lock is still being worked on. If using createHeartbeatBackgroundThread=true when setting up this object, then this method is unnecessary, because the background thread will be periodically calling it and sending heartbeats. However, if createHeartbeatBackgroundThread=false, then this method must be called to instruct DynamoDB that the lock should not be expired.

        The lease duration of the lock will be set to the default specified in the constructor of this class.

        Parameters:
        lockItem - the lock item row to send a heartbeat and extend lock expiry.
      • sendHeartbeat

        public void sendHeartbeat​(SendHeartbeatOptions options)

        Sends a heartbeat to indicate that the given lock is still being worked on. If using createHeartbeatBackgroundThread=true when setting up this object, then this method is unnecessary, because the background thread will be periodically calling it and sending heartbeats. However, if createHeartbeatBackgroundThread=false, then this method must be called to instruct DynamoDB that the lock should not be expired.

        This method will also set the lease duration of the lock to the given value.

        This will also either update or delete the data from the lock, as specified in the options

        Parameters:
        options - a set of optional arguments for how to send the heartbeat
      • run

        public void run()
        Loops forever, sending hearbeats for all the locks this thread needs to keep track of.
        Specified by:
        run in interface Runnable