locksy 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|