Class: Locksy::DynamoDB

Inherits:
BaseLock show all
Extended by:
Forwardable
Defined in:
lib/locksy/dynamodb.rb

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes inherited from BaseLock

#_clock, #default_expiry, #default_extension, #lock_name, #owner

Attributes inherited from LockInterface

#default_expiry, #default_extension, #lock_name, #owner

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseLock

shutting_down?, #with_lock

Methods inherited from LockInterface

#with_lock

Constructor Details

#initialize(dynamo_client: default_client, table_name: default_table, **_args) ⇒ DynamoDB

Returns a new instance of DynamoDB.



12
13
14
15
16
17
18
19
20
# 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

Class Attribute Details

.default_clientObject



109
110
111
# File 'lib/locksy/dynamodb.rb', line 109

def default_client
  @default_client ||= create_client
end

.default_tableObject



105
106
107
# File 'lib/locksy/dynamodb.rb', line 105

def default_table
  @default_table ||= 'default_locks'
end

Instance Attribute Details

#dynamo_clientObject (readonly)

Returns the value of attribute dynamo_client.



8
9
10
# File 'lib/locksy/dynamodb.rb', line 8

def dynamo_client
  @dynamo_client
end

#table_nameObject (readonly)

Returns the value of attribute table_name.



8
9
10
# File 'lib/locksy/dynamodb.rb', line 8

def table_name
  @table_name
end

Class Method Details

.create_client(**args) ⇒ Object



113
114
115
116
117
# 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

Instance Method Details

#_interrupt_waitingObject



124
125
126
# File 'lib/locksy/dynamodb.rb', line 124

def _interrupt_waiting
  @_timeout_mutex.synchronize { @_timeout_stopper.broadcast }
end

#_wait_for_timeout(timeout) ⇒ Object



120
121
122
# File 'lib/locksy/dynamodb.rb', line 120

def _wait_for_timeout(timeout)
  @_timeout_mutex.synchronize { @_timeout_stopper.wait(@_timeout_mutex, timeout) }
end

#create_tableObject



84
85
86
87
88
89
90
91
92
93
94
95
96
# 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!Object



98
99
100
# 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) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 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) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# 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_lockObject



57
58
59
60
61
62
63
64
65
66
# 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