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



101
102
103
# File 'lib/locksy/dynamodb.rb', line 101

def default_client
  @default_client ||= create_client
end

.default_tableObject



97
98
99
# File 'lib/locksy/dynamodb.rb', line 97

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



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

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



116
117
118
# File 'lib/locksy/dynamodb.rb', line 116

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

#_wait_for_timeout(timeout) ⇒ Object



112
113
114
# File 'lib/locksy/dynamodb.rb', line 112

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

#create_tableObject



76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/locksy/dynamodb.rb', line 76

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



90
91
92
# File 'lib/locksy/dynamodb.rb', line 90

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
# 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
  expire_at = expiry(expire_after)
  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 < :expires) ' \
                             'OR (attribute_not_exists(lock_owner) OR lock_owner = :owner)',
       expression_attribute_values: { ':expires' => now, ':owner' => owner } })
  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.
    _wait_for_timeout \
      ( if (current = retrieve_current_lock).nil?
          0.1
        else
          [stop_waiting_at - now, [(current[:expires] - now) / 2, 0.1].max].min
        end)
    retry unless self.class.shutting_down?
  end
  raise build_not_owned_error_from_remote
end

#refresh_lock(expire_after: default_extension, **_args) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/locksy/dynamodb.rb', line 60

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



49
50
51
52
53
54
55
56
57
58
# File 'lib/locksy/dynamodb.rb', line 49

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