locksy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/locksy/base_lock.rb +76 -0
- data/lib/locksy/dynamodb.rb +136 -0
- data/lib/locksy/errors.rb +19 -0
- data/lib/locksy/lock_interface.rb +41 -0
- data/lib/locksy/memory.rb +88 -0
- data/lib/locksy/version.rb +5 -0
- data/lib/locksy.rb +2 -0
- metadata +148 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 43b6b34290b0dedbc0793205bb5cbb1d6e5442ebbcc241998529ed5e4d73d913
|
4
|
+
data.tar.gz: b59b6d7e9c999093e30a6119bf96c782d66db865f803ac2c0f2054b76e0e9a27
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 06f41badc92181f8d4d28e66fa59723488f78110aa3fbfcc7d974b89953db30684c529b5d08d6d0705900abdcc3f4959267f41bcb0a0e7acd1c6a3b77c547f41
|
7
|
+
data.tar.gz: ffe4a652c57acbe76680eb1204f647c0fcf8cf378f7839d4778fe3e6f16279cd3189f59f4ee18f275dd496a8363b4a43c715ec93f433e177a997bd912eeb7922
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require_relative './lock_interface'
|
2
|
+
require_relative './errors'
|
3
|
+
|
4
|
+
require 'securerandom'
|
5
|
+
require 'socket'
|
6
|
+
|
7
|
+
module Locksy
|
8
|
+
class BaseLock < LockInterface
|
9
|
+
attr_reader :owner, :default_expiry, :default_extension, :lock_name
|
10
|
+
attr_writer :logger
|
11
|
+
|
12
|
+
# allow injection of a clock to assist testing
|
13
|
+
# do not call or set this outside of tests
|
14
|
+
attr_writer :_clock
|
15
|
+
|
16
|
+
# add a class-level flag to allow children to know to stop loops etc.
|
17
|
+
@_shutting_down = false
|
18
|
+
|
19
|
+
def initialize(lock_name: generate_default_lock_name, owner: generate_default_owner,
|
20
|
+
default_expiry: 10, default_extension: 10, logger: nil, **_args)
|
21
|
+
@owner = owner
|
22
|
+
@default_expiry = default_expiry
|
23
|
+
@default_extension = default_extension
|
24
|
+
@lock_name = lock_name
|
25
|
+
@logger = logger
|
26
|
+
end
|
27
|
+
|
28
|
+
# disabling here because we have a no-op implementation... we're expecting this
|
29
|
+
# class to be extended
|
30
|
+
# rubocop:disable Style/EmptyMethod
|
31
|
+
def obtain_lock(expire_after: default_expiry, **_args); end
|
32
|
+
|
33
|
+
def refresh_lock(expire_after: default_extension, **_args); end
|
34
|
+
|
35
|
+
def release_lock(**_args); end
|
36
|
+
# rubocop:enable Style/EmptyMethod
|
37
|
+
|
38
|
+
def with_lock(expire_after: default_expiry, **args)
|
39
|
+
if (lock_obtained = obtain_lock(expire_after: expire_after, **args))
|
40
|
+
yield
|
41
|
+
end
|
42
|
+
ensure
|
43
|
+
release_lock if lock_obtained
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.shutting_down?
|
47
|
+
@_shutting_down
|
48
|
+
end
|
49
|
+
|
50
|
+
at_exit do
|
51
|
+
@_shutting_down = true
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def now
|
57
|
+
(@_clock ||= Time).now.to_f
|
58
|
+
end
|
59
|
+
|
60
|
+
def logger
|
61
|
+
@logger ||= Logger.new
|
62
|
+
end
|
63
|
+
|
64
|
+
def expiry(after)
|
65
|
+
now + after
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_default_owner
|
69
|
+
"#{Thread.current.object_id}-#{Process.pid}@#{Socket.gethostname}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def generate_default_lock_name
|
73
|
+
"#{SecureRandom.base64(12)}.#{generate_default_owner}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require_relative './base_lock'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Locksy
|
5
|
+
class DynamoDB < BaseLock
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_reader :dynamo_client, :table_name
|
9
|
+
|
10
|
+
def_delegators 'self.class'.to_sym, :default_table, :default_client
|
11
|
+
|
12
|
+
def initialize(dynamo_client: default_client, table_name: default_table, **_args)
|
13
|
+
# lazy-load the gem to avoid forcing a dependency on the implementation
|
14
|
+
require 'aws-sdk-dynamodb'
|
15
|
+
@dynamo_client = dynamo_client
|
16
|
+
@table_name = table_name
|
17
|
+
@_timeout_stopper = ConditionVariable.new
|
18
|
+
@_timeout_mutex = Mutex.new
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def obtain_lock(expire_after: default_expiry, wait_for: nil, **_args)
|
23
|
+
stop_waiting_at = wait_for ? now + wait_for : nil
|
24
|
+
expire_at = expiry(expire_after)
|
25
|
+
dynamo_client.put_item \
|
26
|
+
({ table_name: table_name,
|
27
|
+
item: { id: lock_name, expires: expire_at, lock_owner: owner },
|
28
|
+
condition_expression: '(attribute_not_exists(expires) OR expires < :expires) ' \
|
29
|
+
'OR (attribute_not_exists(lock_owner) OR lock_owner = :owner)',
|
30
|
+
expression_attribute_values: { ':expires' => now, ':owner' => owner } })
|
31
|
+
expire_at
|
32
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
33
|
+
if stop_waiting_at && stop_waiting_at > now
|
34
|
+
# Retry at a maximum of 1/2 of the remaining time until the
|
35
|
+
# current lock expires or the remaining time from the what the
|
36
|
+
# caller was willing to wait, subject to a minimum of 0.1s to
|
37
|
+
# prevent busy looping.
|
38
|
+
_wait_for_timeout \
|
39
|
+
( if (current = retrieve_current_lock).nil?
|
40
|
+
0.1
|
41
|
+
else
|
42
|
+
[stop_waiting_at - now, [(current[:expires] - now) / 2, 0.1].max].min
|
43
|
+
end)
|
44
|
+
retry unless self.class.shutting_down?
|
45
|
+
end
|
46
|
+
raise build_not_owned_error_from_remote
|
47
|
+
end
|
48
|
+
|
49
|
+
def release_lock
|
50
|
+
dynamo_client.delete_item \
|
51
|
+
({ table_name: table_name,
|
52
|
+
key: { id: lock_name },
|
53
|
+
condition_expression: '(attribute_not_exists(lock_owner) OR lock_owner = :owner) ' \
|
54
|
+
'OR (attribute_not_exists(expires) OR expires < :expires)',
|
55
|
+
expression_attribute_values: { ':owner' => owner, ':expires' => now } })
|
56
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
57
|
+
raise build_not_owned_error_from_remote
|
58
|
+
end
|
59
|
+
|
60
|
+
def refresh_lock(expire_after: default_extension, **_args)
|
61
|
+
expire_at = expiry(expire_after)
|
62
|
+
dynamo_client.update_item \
|
63
|
+
({ table_name: table_name,
|
64
|
+
key: { id: lock_name },
|
65
|
+
update_expression: 'SET expires = :expires',
|
66
|
+
condition_expression: 'attribute_exists(expires) AND expires > :now ' \
|
67
|
+
'AND lock_owner = :owner',
|
68
|
+
expression_attribute_values: { ':expires' => expire_at,
|
69
|
+
':owner' => owner,
|
70
|
+
':now' => now } })
|
71
|
+
expire_at
|
72
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
73
|
+
obtain_lock expire_after: expire_after
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_table
|
77
|
+
dynamo_client.create_table(table_name: table_name,
|
78
|
+
key_schema: [{ attribute_name: 'id', key_type: 'HASH' }],
|
79
|
+
attribute_definitions: [{ attribute_name: 'id',
|
80
|
+
attribute_type: 'S' }],
|
81
|
+
provisioned_throughput: { read_capacity_units: 10,
|
82
|
+
write_capacity_units: 10 })
|
83
|
+
rescue Aws::DynamoDB::Errors::ResourceInUseException => ex
|
84
|
+
unless ex.message == 'Cannot create preexisting table' ||
|
85
|
+
ex.message.start_with?('Table already exists')
|
86
|
+
raise ex
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def force_unlock!
|
91
|
+
dynamo_client.delete_item(table_name: table_name, key: { id: lock_name })
|
92
|
+
end
|
93
|
+
|
94
|
+
class << self
|
95
|
+
attr_writer :default_client, :default_table
|
96
|
+
|
97
|
+
def default_table
|
98
|
+
@default_table ||= 'default_locks'
|
99
|
+
end
|
100
|
+
|
101
|
+
def default_client
|
102
|
+
@default_client ||= create_client
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_client(**args)
|
106
|
+
# require at runtime to avoid a gem dependency
|
107
|
+
require 'aws-sdk-dynamodb'
|
108
|
+
Aws::DynamoDB::Client.new(**args)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def _wait_for_timeout(timeout)
|
113
|
+
@_timeout_mutex.synchronize { @_timeout_stopper.wait(@_timeout_mutex, timeout) }
|
114
|
+
end
|
115
|
+
|
116
|
+
def _interrupt_waiting
|
117
|
+
@_timeout_mutex.synchronize { @_timeout_stopper.broadcast }
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def retrieve_current_lock
|
123
|
+
item = dynamo_client.get_item(table_name: table_name, key: { id: lock_name }).item
|
124
|
+
return nil if item.nil?
|
125
|
+
{ lock_name: item['id'], owner: item['lock_owner'], expires: item['expires'] }
|
126
|
+
end
|
127
|
+
|
128
|
+
def build_not_owned_error_from_remote
|
129
|
+
current = retrieve_current_lock || {}
|
130
|
+
LockNotOwnedError.new(lock: self, current_owner: current['owner'],
|
131
|
+
current_expiry: current['expires'])
|
132
|
+
rescue RuntimeError # in the case that there is a different error raised, ignore it
|
133
|
+
LockNotOwnedError.new(lock: self)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Locksy
|
2
|
+
class LockNotOwnedError < RuntimeError
|
3
|
+
attr_reader :lock, :current_owner, :current_expiry
|
4
|
+
|
5
|
+
def initialize(msg = nil, lock:, current_owner: nil, current_expiry: nil)
|
6
|
+
@lock = lock
|
7
|
+
@current_owner = current_owner
|
8
|
+
@current_expiry = current_expiry
|
9
|
+
|
10
|
+
if msg.nil?
|
11
|
+
msg = "Unable to manipulate lock #{lock.lock_name} for #{lock.owner}."
|
12
|
+
msg += " Lock currently owned by #{current_owner}." if current_owner
|
13
|
+
msg += " Lock unnavailable until #{current_expiry}." if current_expiry
|
14
|
+
end
|
15
|
+
|
16
|
+
super msg
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Locksy
|
2
|
+
# AbstractLock allows us to declare the interface for locks.
|
3
|
+
# Any locks created should follow this interface
|
4
|
+
#
|
5
|
+
# disabling this cop because we are declaring this as an interface and deliberately not using here
|
6
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
7
|
+
class LockInterface
|
8
|
+
attr_reader :owner, :default_expiry, :default_extension, :lock_name
|
9
|
+
attr_writer :logger
|
10
|
+
|
11
|
+
def initialize(lock_name: generate_default_lock_name, owner: generate_default_owner,
|
12
|
+
default_expiry: nil, default_extension: nil, logger: nil, **_args)
|
13
|
+
raise NotImplementedError.new 'This is an abstract class - instantiation is not supported'
|
14
|
+
end
|
15
|
+
|
16
|
+
# should return a boolean denoting lock obtained (true) or not (false)
|
17
|
+
def obtain_lock(expire_after: default_expiry, **_args)
|
18
|
+
raise NotImplementedError.new 'Obtaining a lock is not supported'
|
19
|
+
end
|
20
|
+
|
21
|
+
# should raise a LockNotOwnedError if the lock is not owned by the requested owner
|
22
|
+
def refresh_lock(expire_after: default_extension, **_args)
|
23
|
+
raise NotImplementedError.new 'Refreshing a lock is not supported'
|
24
|
+
end
|
25
|
+
|
26
|
+
# should raise a LockNotOwnedError if the lock is not owned by the requested owner
|
27
|
+
def release_lock(**_args)
|
28
|
+
raise NotImplementedError.new 'Releasing a lock is not supported'
|
29
|
+
end
|
30
|
+
|
31
|
+
# should raise a LockNotOwnedError if the lock is not owned by the requested owner
|
32
|
+
def with_lock(expire_after: default_expiry, **_args)
|
33
|
+
raise NotImplementedError.new 'Working with a lock is not supported'
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
attr_reader :logger, :generate_default_owner, :generate_default_lock_name
|
39
|
+
end
|
40
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
41
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require_relative './base_lock'
|
2
|
+
|
3
|
+
module Locksy
|
4
|
+
class Memory < BaseLock
|
5
|
+
@_singleton_mutex = Mutex.new
|
6
|
+
@_datastore = {}
|
7
|
+
@_data_change = ConditionVariable.new
|
8
|
+
|
9
|
+
def obtain_lock(expire_after: default_expiry, wait_for: nil, **_args)
|
10
|
+
stop_waiting_at = wait_for ? now + wait_for : nil
|
11
|
+
begin
|
12
|
+
current = nil
|
13
|
+
self.class._synchronize do
|
14
|
+
current = _in_mutex_retrieve_lock
|
15
|
+
self.class._datastore[lock_name] = [owner, expiry(expire_after)]
|
16
|
+
self.class._notify_data_change
|
17
|
+
end
|
18
|
+
rescue LockNotOwnedError => ex
|
19
|
+
if stop_waiting_at && stop_waiting_at > now
|
20
|
+
# Maximum wait time for the condition variable before retrying
|
21
|
+
# Because it is possible that a condition variable will not be
|
22
|
+
# triggered, or may be triggered by something that is not what
|
23
|
+
# was expected.
|
24
|
+
# Retry at a maximum of 1/2 of the remaining time until the
|
25
|
+
# current lock expires or the remaining time from the what the
|
26
|
+
# caller was willing to wait, subject to a minimum of 0.1s to
|
27
|
+
# prevent busy looping.
|
28
|
+
cv_timeout = [stop_waiting_at - now, [(ex.current_expiry - now) / 2, 0.1].max].min
|
29
|
+
self.class._synchronize { self.class._wait_for_data_change(cv_timeout) }
|
30
|
+
retry unless self.class.shutting_down?
|
31
|
+
end
|
32
|
+
raise ex
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def release_lock
|
37
|
+
self.class._synchronize do
|
38
|
+
_in_mutex_retrieve_lock
|
39
|
+
self.class._datastore.delete lock_name
|
40
|
+
self.class._notify_data_change
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def refresh_lock(expire_after: default_extension, **_args)
|
45
|
+
obtain_lock expire_after: expire_after
|
46
|
+
end
|
47
|
+
|
48
|
+
class << self
|
49
|
+
attr_reader :_datastore
|
50
|
+
|
51
|
+
# This is needed to allow tests to inject and control the condition variable
|
52
|
+
attr_writer :_data_change
|
53
|
+
|
54
|
+
def release_all!
|
55
|
+
_synchronize { @_datastore = {} }
|
56
|
+
end
|
57
|
+
|
58
|
+
def _synchronize(&blk)
|
59
|
+
@_singleton_mutex.synchronize(&blk)
|
60
|
+
end
|
61
|
+
|
62
|
+
# THIS IS DANGEROUS... CALL ONLY WHEN SYNCHRONIZED IN THE MUTEX
|
63
|
+
def _wait_for_data_change(timeout = nil)
|
64
|
+
@_data_change.wait(@_singleton_mutex, timeout)
|
65
|
+
end
|
66
|
+
|
67
|
+
# THIS IS DANGEROUS... CALL ONLY WHEN SYNCHRONIZED IN THE MUTEX
|
68
|
+
def _notify_data_change
|
69
|
+
@_data_change.broadcast
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
at_exit do
|
74
|
+
_synchronize { _notify_data_change }
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def _in_mutex_retrieve_lock
|
80
|
+
current = self.class._datastore[lock_name]
|
81
|
+
if current && current[0] != owner && current[1] > now
|
82
|
+
raise LockNotOwnedError.new(lock: self, current_owner: current[0],
|
83
|
+
current_expiry: current[1])
|
84
|
+
end
|
85
|
+
current
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/locksy.rb
ADDED
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: locksy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- dan@52degreesnorth.com
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-10-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-its
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.57.2
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.57.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: aws-sdk-dynamodb
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: A tool to provide distributed locking
|
112
|
+
email:
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- lib/locksy.rb
|
118
|
+
- lib/locksy/base_lock.rb
|
119
|
+
- lib/locksy/dynamodb.rb
|
120
|
+
- lib/locksy/errors.rb
|
121
|
+
- lib/locksy/lock_interface.rb
|
122
|
+
- lib/locksy/memory.rb
|
123
|
+
- lib/locksy/version.rb
|
124
|
+
homepage: https://github.com/danleyden/locksy
|
125
|
+
licenses:
|
126
|
+
- MIT
|
127
|
+
metadata: {}
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
requirements: []
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 2.7.10
|
145
|
+
signing_key:
|
146
|
+
specification_version: 4
|
147
|
+
summary: A tool to provide distributed locking
|
148
|
+
test_files: []
|