class Locksy::DynamoDB

Attributes

default_client[W]
default_table[W]
dynamo_client[R]
table_name[R]

Public Class Methods

create_client(**args) click to toggle source
# File lib/locksy/dynamodb.rb, line 113
def create_client(**args)
  # require at runtime to avoid a gem dependency
  require 'aws-sdk-dynamodb'
  Aws::DynamoDB::Client.new(**args)
end
default_client() click to toggle source
# File lib/locksy/dynamodb.rb, line 109
def default_client
  @default_client ||= create_client
end
default_table() click to toggle source
# File lib/locksy/dynamodb.rb, line 105
def default_table
  @default_table ||= 'default_locks'
end
new(dynamo_client: default_client, table_name: default_table, **_args) click to toggle source
Calls superclass method Locksy::BaseLock::new
# File lib/locksy/dynamodb.rb, line 12
def initialize(dynamo_client: default_client, table_name: default_table, **_args)
  # lazy-load the gem to avoid forcing a dependency on the implementation
  require 'aws-sdk-dynamodb'
  @dynamo_client = dynamo_client
  @table_name = table_name
  @_timeout_stopper = ConditionVariable.new
  @_timeout_mutex = Mutex.new
  super
end

Public Instance Methods

_interrupt_waiting() click to toggle source
# File lib/locksy/dynamodb.rb, line 124
def _interrupt_waiting
  @_timeout_mutex.synchronize { @_timeout_stopper.broadcast }
end
_wait_for_timeout(timeout) click to toggle source
# File lib/locksy/dynamodb.rb, line 120
def _wait_for_timeout(timeout)
  @_timeout_mutex.synchronize { @_timeout_stopper.wait(@_timeout_mutex, timeout) }
end
create_table() click to toggle source
# File lib/locksy/dynamodb.rb, line 84
def create_table
  dynamo_client.create_table(table_name: table_name,
                             key_schema: [{ attribute_name: 'id', key_type: 'HASH' }],
                             attribute_definitions: [{ attribute_name: 'id',
                                                       attribute_type: 'S' }],
                             provisioned_throughput: { read_capacity_units: 10,
                                                       write_capacity_units: 10 })
rescue Aws::DynamoDB::Errors::ResourceInUseException => ex
  unless ex.message == 'Cannot create preexisting table' ||
      ex.message.start_with?('Table already exists')
    raise ex
  end
end
force_unlock!() click to toggle source
# File lib/locksy/dynamodb.rb, line 98
def force_unlock!
  dynamo_client.delete_item(table_name: table_name, key: { id: lock_name })
end
obtain_lock(expire_after: default_expiry, wait_for: nil, **_args) click to toggle source
# File lib/locksy/dynamodb.rb, line 22
def obtain_lock(expire_after: default_expiry, wait_for: nil, **_args)
  stop_waiting_at = wait_for ? now + wait_for : nil
  begin
    expire_at = expiry(expire_after)
    logger.debug "trying to obtain lock #{lock_name} for #{owner} to be held until #{expire_at}"
    dynamo_client.put_item \
      ({ table_name: table_name,
         item: { id: lock_name, expires: expire_at, lock_owner: owner },
         condition_expression: '(attribute_not_exists(expires) OR expires < :now) ' \
                               'OR (attribute_not_exists(lock_owner) OR lock_owner = :owner)',
         expression_attribute_values: { ':now' => now, ':owner' => owner } })
    logger.debug "acquired lock #{lock_name} for #{owner} to be held until #{expire_at}"
    expire_at
  rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
    if stop_waiting_at && stop_waiting_at > now
      # Retry at a maximum of 1/2 of the remaining time until the
      # current lock expires or the remaining time from the what the
      # caller was willing to wait, subject to a minimum of 0.1s to
      # prevent busy looping.
      if (current = retrieve_current_lock).nil?
        retry_wait = 0.1
      else
        retry_wait = [stop_waiting_at - now, [(current[:expires] - now) / 2, 0.1].max].min
      end
      logger.debug "Attempt to acquire lock #{lock_name} for #{owner} failed - "\
        "lock owned by #{current[:owner]} until #{format('%0.02f', current[:expires])}. " \
        "Will retry in #{format('%0.02f', retry_wait)}s"
      _wait_for_timeout retry_wait
      retry unless self.class.shutting_down?
    end
    logger.debug "Attempt to acquire lock #{lock_name} for #{owner} failed. Giving up"
    raise build_not_owned_error_from_remote
  end
end
refresh_lock(expire_after: default_extension, **_args) click to toggle source
# File lib/locksy/dynamodb.rb, line 68
def refresh_lock(expire_after: default_extension, **_args)
  expire_at = expiry(expire_after)
  dynamo_client.update_item \
    ({ table_name: table_name,
       key: { id: lock_name },
       update_expression: 'SET expires = :expires',
       condition_expression: 'attribute_exists(expires) AND expires > :now ' \
                             'AND lock_owner = :owner',
       expression_attribute_values: { ':expires' => expire_at,
                                      ':owner' => owner,
                                      ':now' => now } })
  expire_at
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
  obtain_lock expire_after: expire_after
end
release_lock() click to toggle source
# File lib/locksy/dynamodb.rb, line 57
def release_lock
  dynamo_client.delete_item \
    ({ table_name: table_name,
       key: { id: lock_name },
       condition_expression: '(attribute_not_exists(lock_owner) OR lock_owner = :owner) ' \
                             'OR (attribute_not_exists(expires) OR expires < :expires)',
       expression_attribute_values: { ':owner' => owner, ':expires' => now } })
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
  raise build_not_owned_error_from_remote
end

Private Instance Methods

build_not_owned_error_from_remote() click to toggle source
# File lib/locksy/dynamodb.rb, line 136
def build_not_owned_error_from_remote
  current = retrieve_current_lock || {}
  LockNotOwnedError.new(lock: self, current_owner: current['owner'],
                        current_expiry: current['expires'])
rescue RuntimeError # in the case that there is a different error raised, ignore it
  LockNotOwnedError.new(lock: self)
end
retrieve_current_lock() click to toggle source
# File lib/locksy/dynamodb.rb, line 130
def retrieve_current_lock
  item = dynamo_client.get_item(table_name: table_name, key: { id: lock_name }).item
  return nil if item.nil?
  { lock_name: item['id'], owner: item['lock_owner'], expires: item['expires'] }
end