suo-dependless 0.3.2
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/.gitignore +18 -0
- data/.rubocop.yml +219 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +48 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +89 -0
- data/Rakefile +8 -0
- data/bin/console +7 -0
- data/bin/setup +5 -0
- data/lib/suo/client/base.rb +194 -0
- data/lib/suo/client/memcached.rb +28 -0
- data/lib/suo/client/redis.rb +42 -0
- data/lib/suo/errors.rb +3 -0
- data/lib/suo/version.rb +3 -0
- data/lib/suo.rb +16 -0
- data/suo-dependless.gemspec +32 -0
- data/test/client_test.rb +397 -0
- data/test/test_helper.rb +13 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a874831b56f527f3e7bd154e5b7811be14580c7d
|
4
|
+
data.tar.gz: 03eb5515ae43d4ac7bb6fc1c6da82629f9831cb2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 851915fd21541fc735e2c7053d88e47d5cdb02fd8036fa56ce2cccc492fe2c39477916fda2dffa443d186913e09e9aac71fb7036f9fa91bd13825f6dfea14e0c
|
7
|
+
data.tar.gz: 3c1f9ebd4f654d64eef795ecfe81a15ec335e617c71b1af01f08e1b59693c75b81a73aa453106f6afab03c8de80b7f6c726e422d32e5882c18bf1f457d79dc9c
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
AllCops:
|
2
|
+
Exclude:
|
3
|
+
- .git/**/*
|
4
|
+
- tmp/**/*
|
5
|
+
- suo.gemspec
|
6
|
+
|
7
|
+
Lint/DuplicateMethods:
|
8
|
+
Enabled: true
|
9
|
+
|
10
|
+
Lint/DeprecatedClassMethods:
|
11
|
+
Enabled: true
|
12
|
+
|
13
|
+
Style/TrailingWhitespace:
|
14
|
+
Enabled: true
|
15
|
+
|
16
|
+
Style/Tab:
|
17
|
+
Enabled: true
|
18
|
+
|
19
|
+
Style/TrailingBlankLines:
|
20
|
+
Enabled: true
|
21
|
+
|
22
|
+
Style/NilComparison:
|
23
|
+
Enabled: true
|
24
|
+
|
25
|
+
Style/NonNilCheck:
|
26
|
+
Enabled: true
|
27
|
+
|
28
|
+
Style/Not:
|
29
|
+
Enabled: true
|
30
|
+
|
31
|
+
Style/RedundantReturn:
|
32
|
+
Enabled: true
|
33
|
+
|
34
|
+
Style/ClassCheck:
|
35
|
+
Enabled: true
|
36
|
+
|
37
|
+
Style/EmptyLines:
|
38
|
+
Enabled: true
|
39
|
+
|
40
|
+
Style/EmptyLiteral:
|
41
|
+
Enabled: true
|
42
|
+
|
43
|
+
Style/Alias:
|
44
|
+
Enabled: true
|
45
|
+
|
46
|
+
Style/MethodCallParentheses:
|
47
|
+
Enabled: true
|
48
|
+
|
49
|
+
Style/MethodDefParentheses:
|
50
|
+
Enabled: true
|
51
|
+
|
52
|
+
Style/SpaceBeforeBlockBraces:
|
53
|
+
Enabled: true
|
54
|
+
|
55
|
+
Style/SpaceInsideBlockBraces:
|
56
|
+
Enabled: true
|
57
|
+
|
58
|
+
Style/SpaceInsideParens:
|
59
|
+
Enabled: true
|
60
|
+
|
61
|
+
Style/DeprecatedHashMethods:
|
62
|
+
Enabled: true
|
63
|
+
|
64
|
+
Style/HashSyntax:
|
65
|
+
Enabled: true
|
66
|
+
|
67
|
+
Style/SpaceInsideHashLiteralBraces:
|
68
|
+
Enabled: true
|
69
|
+
EnforcedStyle: no_space
|
70
|
+
|
71
|
+
Style/SpaceInsideBrackets:
|
72
|
+
Enabled: true
|
73
|
+
|
74
|
+
Style/AndOr:
|
75
|
+
Enabled: false
|
76
|
+
|
77
|
+
Style/TrailingCommaInLiteral:
|
78
|
+
Enabled: true
|
79
|
+
|
80
|
+
Style/SpaceBeforeComma:
|
81
|
+
Enabled: true
|
82
|
+
|
83
|
+
Style/SpaceBeforeComment:
|
84
|
+
Enabled: true
|
85
|
+
|
86
|
+
Style/SpaceBeforeSemicolon:
|
87
|
+
Enabled: true
|
88
|
+
|
89
|
+
Style/SpaceAroundBlockParameters:
|
90
|
+
Enabled: true
|
91
|
+
|
92
|
+
Style/SpaceAroundOperators:
|
93
|
+
Enabled: true
|
94
|
+
|
95
|
+
Style/SpaceAfterColon:
|
96
|
+
Enabled: true
|
97
|
+
|
98
|
+
Style/SpaceAfterComma:
|
99
|
+
Enabled: true
|
100
|
+
|
101
|
+
Style/SpaceAroundKeyword:
|
102
|
+
Enabled: true
|
103
|
+
|
104
|
+
Style/SpaceAfterNot:
|
105
|
+
Enabled: true
|
106
|
+
|
107
|
+
Style/SpaceAfterSemicolon:
|
108
|
+
Enabled: true
|
109
|
+
|
110
|
+
Lint/UselessComparison:
|
111
|
+
Enabled: true
|
112
|
+
|
113
|
+
Lint/InvalidCharacterLiteral:
|
114
|
+
Enabled: true
|
115
|
+
|
116
|
+
Lint/LiteralInInterpolation:
|
117
|
+
Enabled: true
|
118
|
+
|
119
|
+
Lint/LiteralInCondition:
|
120
|
+
Enabled: true
|
121
|
+
|
122
|
+
Lint/UnusedBlockArgument:
|
123
|
+
Enabled: true
|
124
|
+
|
125
|
+
Style/VariableInterpolation:
|
126
|
+
Enabled: true
|
127
|
+
|
128
|
+
Style/RedundantSelf:
|
129
|
+
Enabled: true
|
130
|
+
|
131
|
+
Style/ParenthesesAroundCondition:
|
132
|
+
Enabled: true
|
133
|
+
|
134
|
+
Style/WhileUntilDo:
|
135
|
+
Enabled: true
|
136
|
+
|
137
|
+
Style/EmptyLineBetweenDefs:
|
138
|
+
Enabled: true
|
139
|
+
|
140
|
+
Style/EmptyLinesAroundAccessModifier:
|
141
|
+
Enabled: true
|
142
|
+
|
143
|
+
Style/EmptyLinesAroundMethodBody:
|
144
|
+
Enabled: true
|
145
|
+
|
146
|
+
Style/ColonMethodCall:
|
147
|
+
Enabled: true
|
148
|
+
|
149
|
+
Lint/SpaceBeforeFirstArg:
|
150
|
+
Enabled: true
|
151
|
+
|
152
|
+
Lint/UnreachableCode:
|
153
|
+
Enabled: true
|
154
|
+
|
155
|
+
Style/UnlessElse:
|
156
|
+
Enabled: true
|
157
|
+
|
158
|
+
Style/ClassVars:
|
159
|
+
Enabled: true
|
160
|
+
|
161
|
+
Style/StringLiterals:
|
162
|
+
Enabled: true
|
163
|
+
EnforcedStyle: double_quotes
|
164
|
+
|
165
|
+
Metrics/CyclomaticComplexity:
|
166
|
+
Max: 10
|
167
|
+
|
168
|
+
Metrics/LineLength:
|
169
|
+
Max: 128
|
170
|
+
|
171
|
+
Metrics/MethodLength:
|
172
|
+
Max: 32
|
173
|
+
|
174
|
+
Metrics/PerceivedComplexity:
|
175
|
+
Max: 8
|
176
|
+
|
177
|
+
# Disabled
|
178
|
+
|
179
|
+
Style/EvenOdd:
|
180
|
+
Enabled: false
|
181
|
+
|
182
|
+
Style/AsciiComments:
|
183
|
+
Enabled: false
|
184
|
+
|
185
|
+
Style/NumericLiterals:
|
186
|
+
Enabled: false
|
187
|
+
|
188
|
+
Style/UnneededPercentQ:
|
189
|
+
Enabled: false
|
190
|
+
|
191
|
+
Style/SpecialGlobalVars:
|
192
|
+
Enabled: false
|
193
|
+
|
194
|
+
Style/TrivialAccessors:
|
195
|
+
Enabled: false
|
196
|
+
|
197
|
+
Style/PerlBackrefs:
|
198
|
+
Enabled: false
|
199
|
+
|
200
|
+
Metrics/AbcSize:
|
201
|
+
Enabled: false
|
202
|
+
|
203
|
+
Metrics/BlockNesting:
|
204
|
+
Enabled: false
|
205
|
+
|
206
|
+
Metrics/ClassLength:
|
207
|
+
Enabled: false
|
208
|
+
|
209
|
+
Metrics/MethodLength:
|
210
|
+
Enabled: false
|
211
|
+
|
212
|
+
Metrics/ParameterLists:
|
213
|
+
Enabled: false
|
214
|
+
|
215
|
+
Metrics/PerceivedComplexity:
|
216
|
+
Enabled: false
|
217
|
+
|
218
|
+
Style/Documentation:
|
219
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
## 0.3.2
|
2
|
+
|
3
|
+
- Custom lock tokens (thanks to avokhmin).
|
4
|
+
|
5
|
+
## 0.3.1
|
6
|
+
|
7
|
+
- Slight memory leak fix.
|
8
|
+
|
9
|
+
## 0.3.0
|
10
|
+
|
11
|
+
- Dramatically simplify the interface by forcing clients to specify the key & resources at lock initialization instead of every method call.
|
12
|
+
|
13
|
+
## 0.2.3
|
14
|
+
|
15
|
+
- Clarify documentation further with respect to semaphores.
|
16
|
+
|
17
|
+
## 0.2.2
|
18
|
+
|
19
|
+
- Fix bug with refresh - typo would've prevented real use.
|
20
|
+
- Clean up code.
|
21
|
+
- Improve documentation a bit.
|
22
|
+
- 100% test coverage.
|
23
|
+
|
24
|
+
## 0.2.1
|
25
|
+
|
26
|
+
- Fix bug when dealing with real-world Redis error conditions.
|
27
|
+
|
28
|
+
## 0.2.0
|
29
|
+
|
30
|
+
- Refactor class methods into instance methods to simplify implementation.
|
31
|
+
- Increase thread safety with Memcached implementation.
|
32
|
+
|
33
|
+
## 0.1.3
|
34
|
+
|
35
|
+
- Properly throw Suo::LockClientError when the connection itself fails (Memcache server not reachable, etc.)
|
36
|
+
|
37
|
+
## 0.1.2
|
38
|
+
|
39
|
+
- Fix retry_timeout to properly use the full time (was being calculated incorrectly).
|
40
|
+
- Refactor client implementations to re-use more code.
|
41
|
+
|
42
|
+
## 0.1.1
|
43
|
+
|
44
|
+
- Use [MessagePack](https://github.com/msgpack/msgpack-ruby) for lock serialization.
|
45
|
+
|
46
|
+
## 0.1.0
|
47
|
+
|
48
|
+
- First release.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Nick Elser
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Suo [](https://travis-ci.org/nickelser/suo) [](https://codeclimate.com/github/nickelser/suo) [](https://codeclimate.com/github/nickelser/suo) [](http://badge.fury.io/rb/suo)
|
2
|
+
|
3
|
+
:lock: Distributed semaphores using Memcached or Redis in Ruby.
|
4
|
+
|
5
|
+
Suo provides a very performant distributed lock solution using Compare-And-Set (`CAS`) commands in Memcached, and `WATCH/MULTI` in Redis. It allows locking both single exclusion (like a mutex - sharing one resource), as well as multiple resources.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application’s Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'suo'
|
13
|
+
```
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
### Basic
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# Memcached
|
21
|
+
suo = Suo::Client::Memcached.new("foo_resource", connection: "127.0.0.1:11211")
|
22
|
+
|
23
|
+
# Redis
|
24
|
+
suo = Suo::Client::Redis.new("baz_resource", connection: {host: "10.0.1.1"})
|
25
|
+
|
26
|
+
# Pre-existing client
|
27
|
+
suo = Suo::Client::Memcached.new("bar_resource", client: some_dalli_client)
|
28
|
+
|
29
|
+
suo.lock do
|
30
|
+
# critical code here
|
31
|
+
@puppies.pet!
|
32
|
+
end
|
33
|
+
|
34
|
+
# The resources argument is the number of resources the semaphore will allow to lock (defaulting to one - a mutex)
|
35
|
+
suo = Suo::Client::Memcached.new("bar_resource", client: some_dalli_client, resources: 2)
|
36
|
+
|
37
|
+
Thread.new { suo.lock { puts "One"; sleep 2 } }
|
38
|
+
Thread.new { suo.lock { puts "Two"; sleep 2 } }
|
39
|
+
Thread.new { suo.lock { puts "Three" } }
|
40
|
+
|
41
|
+
# will print "One" "Two", but not "Three", as there are only 2 resources
|
42
|
+
|
43
|
+
# custom acquisition timeouts (time to acquire)
|
44
|
+
suo = Suo::Client::Memcached.new("protected_key", client: some_dalli_client, acquisition_timeout: 1) # in seconds
|
45
|
+
|
46
|
+
# manually locking/unlocking
|
47
|
+
# the return value from lock without a block is a unique token valid only for the current lock
|
48
|
+
# which must be unlocked manually
|
49
|
+
token = suo.lock
|
50
|
+
foo.baz!
|
51
|
+
suo.unlock(token)
|
52
|
+
|
53
|
+
# custom stale lock expiration (cleaning of dead locks)
|
54
|
+
suo = Suo::Client::Redis.new("other_key", client: some_redis_client, stale_lock_expiration: 60*5)
|
55
|
+
```
|
56
|
+
|
57
|
+
### Stale locks
|
58
|
+
|
59
|
+
"Stale locks" - those acquired more than `stale_lock_expiration` (defaulting to 3600 or one hour) ago - are automatically cleared during any operation on the key (`lock`, `unlock`, `refresh`). The `locked?` method will not return true if only stale locks exist, but will not modify the key itself.
|
60
|
+
|
61
|
+
To re-acquire a lock in the middle of a block, you can use the refresh method on client.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
suo = Suo::Client::Redis.new("foo")
|
65
|
+
|
66
|
+
# lock is the same token as seen in the manual example, above
|
67
|
+
suo.lock do |token|
|
68
|
+
5.times do
|
69
|
+
baz.bar!
|
70
|
+
suo.refresh(token)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
## TODO
|
76
|
+
- more race condition tests
|
77
|
+
|
78
|
+
## History
|
79
|
+
|
80
|
+
View the [changelog](https://github.com/nickelser/suo/blob/master/CHANGELOG.md).
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
85
|
+
|
86
|
+
- [Report bugs](https://github.com/nickelser/suo/issues)
|
87
|
+
- Fix bugs and [submit pull requests](https://github.com/nickelser/suo/pulls)
|
88
|
+
- Write, clarify, or fix documentation
|
89
|
+
- Suggest or add new features
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,194 @@
|
|
1
|
+
module Suo
|
2
|
+
module Client
|
3
|
+
class Base
|
4
|
+
DEFAULT_OPTIONS = {
|
5
|
+
acquisition_timeout: 0.1,
|
6
|
+
acquisition_delay: 0.01,
|
7
|
+
stale_lock_expiration: 3600,
|
8
|
+
resources: 1
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
BLANK_STR = "".freeze
|
12
|
+
|
13
|
+
attr_accessor :client, :key, :resources, :options
|
14
|
+
|
15
|
+
include MonitorMixin
|
16
|
+
|
17
|
+
def initialize(key, options = {})
|
18
|
+
fail "Client required" unless options[:client]
|
19
|
+
|
20
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
21
|
+
@retry_count = (@options[:acquisition_timeout] / @options[:acquisition_delay].to_f).ceil
|
22
|
+
@client = @options[:client]
|
23
|
+
@resources = @options[:resources].to_i
|
24
|
+
@key = key
|
25
|
+
|
26
|
+
super() # initialize Monitor mixin for thread safety
|
27
|
+
end
|
28
|
+
|
29
|
+
def lock(custom_token = nil)
|
30
|
+
token = acquire_lock(custom_token)
|
31
|
+
|
32
|
+
if block_given? && token
|
33
|
+
begin
|
34
|
+
yield
|
35
|
+
ensure
|
36
|
+
unlock(token)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
token
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def locked?
|
44
|
+
locks.size >= resources
|
45
|
+
end
|
46
|
+
|
47
|
+
def locks
|
48
|
+
val, _ = get
|
49
|
+
cleared_locks = deserialize_and_clear_locks(val)
|
50
|
+
|
51
|
+
cleared_locks
|
52
|
+
end
|
53
|
+
|
54
|
+
def refresh(token)
|
55
|
+
retry_with_timeout do
|
56
|
+
val, cas = get
|
57
|
+
|
58
|
+
if val.nil?
|
59
|
+
initial_set
|
60
|
+
next
|
61
|
+
end
|
62
|
+
|
63
|
+
cleared_locks = deserialize_and_clear_locks(val)
|
64
|
+
|
65
|
+
refresh_lock(cleared_locks, token)
|
66
|
+
|
67
|
+
break if set(serialize_locks(cleared_locks), cas)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def unlock(token)
|
72
|
+
return unless token
|
73
|
+
|
74
|
+
retry_with_timeout do
|
75
|
+
val, cas = get
|
76
|
+
|
77
|
+
break if val.nil?
|
78
|
+
|
79
|
+
cleared_locks = deserialize_and_clear_locks(val)
|
80
|
+
|
81
|
+
acquisition_lock = remove_lock(cleared_locks, token)
|
82
|
+
|
83
|
+
break unless acquisition_lock
|
84
|
+
break if set(serialize_locks(cleared_locks), cas)
|
85
|
+
end
|
86
|
+
rescue LockClientError => _ # rubocop:disable Lint/HandleExceptions
|
87
|
+
# ignore - assume success due to optimistic locking
|
88
|
+
end
|
89
|
+
|
90
|
+
def clear
|
91
|
+
fail NotImplementedError
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
attr_accessor :retry_count
|
97
|
+
|
98
|
+
def acquire_lock(token = nil)
|
99
|
+
token ||= SecureRandom.base64(16)
|
100
|
+
|
101
|
+
retry_with_timeout do
|
102
|
+
val, cas = get
|
103
|
+
|
104
|
+
if val.nil?
|
105
|
+
initial_set
|
106
|
+
next
|
107
|
+
end
|
108
|
+
|
109
|
+
cleared_locks = deserialize_and_clear_locks(val)
|
110
|
+
|
111
|
+
if cleared_locks.size < resources
|
112
|
+
add_lock(cleared_locks, token)
|
113
|
+
|
114
|
+
newval = serialize_locks(cleared_locks)
|
115
|
+
|
116
|
+
return token if set(newval, cas)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def get
|
124
|
+
fail NotImplementedError
|
125
|
+
end
|
126
|
+
|
127
|
+
def set(newval, cas) # rubocop:disable Lint/UnusedMethodArgument
|
128
|
+
fail NotImplementedError
|
129
|
+
end
|
130
|
+
|
131
|
+
def initial_set(val = BLANK_STR) # rubocop:disable Lint/UnusedMethodArgument
|
132
|
+
fail NotImplementedError
|
133
|
+
end
|
134
|
+
|
135
|
+
def synchronize
|
136
|
+
mon_synchronize { yield }
|
137
|
+
end
|
138
|
+
|
139
|
+
def retry_with_timeout
|
140
|
+
start = Time.now.to_f
|
141
|
+
|
142
|
+
retry_count.times do
|
143
|
+
elapsed = Time.now.to_f - start
|
144
|
+
break if elapsed >= options[:acquisition_timeout]
|
145
|
+
|
146
|
+
synchronize do
|
147
|
+
yield
|
148
|
+
end
|
149
|
+
|
150
|
+
sleep(rand(options[:acquisition_delay] * 1000).to_f / 1000)
|
151
|
+
end
|
152
|
+
rescue => _
|
153
|
+
raise LockClientError
|
154
|
+
end
|
155
|
+
|
156
|
+
def serialize_locks(locks)
|
157
|
+
MessagePack.pack(locks.map { |time, token| [time.to_f, token] })
|
158
|
+
end
|
159
|
+
|
160
|
+
def deserialize_and_clear_locks(val)
|
161
|
+
clear_expired_locks(deserialize_locks(val))
|
162
|
+
end
|
163
|
+
|
164
|
+
def deserialize_locks(val)
|
165
|
+
unpacked = (val.nil? || val == BLANK_STR) ? [] : MessagePack.unpack(val)
|
166
|
+
|
167
|
+
unpacked.map do |time, token|
|
168
|
+
[Time.at(time), token]
|
169
|
+
end
|
170
|
+
rescue EOFError, MessagePack::MalformedFormatError => _
|
171
|
+
[]
|
172
|
+
end
|
173
|
+
|
174
|
+
def clear_expired_locks(locks)
|
175
|
+
expired = Time.now - options[:stale_lock_expiration]
|
176
|
+
locks.reject { |time, _| time < expired }
|
177
|
+
end
|
178
|
+
|
179
|
+
def add_lock(locks, token, time = Time.now.to_f)
|
180
|
+
locks << [time, token]
|
181
|
+
end
|
182
|
+
|
183
|
+
def remove_lock(locks, acquisition_token)
|
184
|
+
lock = locks.find { |_, token| token == acquisition_token }
|
185
|
+
locks.delete(lock)
|
186
|
+
end
|
187
|
+
|
188
|
+
def refresh_lock(locks, acquisition_token)
|
189
|
+
remove_lock(locks, acquisition_token)
|
190
|
+
add_lock(locks, acquisition_token)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Suo
|
2
|
+
module Client
|
3
|
+
class Memcached < Base
|
4
|
+
def initialize(key, options = {})
|
5
|
+
options[:client] ||= Dalli::Client.new(options[:connection] || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def clear
|
10
|
+
@client.delete(@key)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def get
|
16
|
+
@client.get_cas(@key)
|
17
|
+
end
|
18
|
+
|
19
|
+
def set(newval, cas)
|
20
|
+
@client.set_cas(@key, newval, cas)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initial_set(val = BLANK_STR)
|
24
|
+
@client.set(@key, val)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Suo
|
2
|
+
module Client
|
3
|
+
class Redis < Base
|
4
|
+
OK_STR = "OK".freeze
|
5
|
+
|
6
|
+
def initialize(key, options = {})
|
7
|
+
options[:client] ||= ::Redis.new(options[:connection] || {})
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def clear
|
12
|
+
@client.del(@key)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def get
|
18
|
+
[@client.get(@key), nil]
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(newval, _)
|
22
|
+
ret = @client.multi do |multi|
|
23
|
+
multi.set(@key, newval)
|
24
|
+
end
|
25
|
+
|
26
|
+
ret && ret[0] == OK_STR
|
27
|
+
end
|
28
|
+
|
29
|
+
def synchronize
|
30
|
+
@client.watch(@key) do
|
31
|
+
yield
|
32
|
+
end
|
33
|
+
ensure
|
34
|
+
@client.unwatch
|
35
|
+
end
|
36
|
+
|
37
|
+
def initial_set(val = BLANK_STR)
|
38
|
+
@client.set(@key, val)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/suo/errors.rb
ADDED
data/lib/suo/version.rb
ADDED
data/lib/suo.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "monitor"
|
3
|
+
|
4
|
+
require "dalli"
|
5
|
+
require "dalli/cas/client"
|
6
|
+
|
7
|
+
require "redis"
|
8
|
+
|
9
|
+
require "msgpack"
|
10
|
+
|
11
|
+
require "suo/version"
|
12
|
+
|
13
|
+
require "suo/errors"
|
14
|
+
require "suo/client/base"
|
15
|
+
require "suo/client/memcached"
|
16
|
+
require "suo/client/redis"
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "suo/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "suo-dependless"
|
8
|
+
spec.version = Suo::VERSION
|
9
|
+
spec.authors = ["Nick Elser"]
|
10
|
+
spec.email = ["nick.elser@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q(Distributed locks (mutexes & semaphores) using Memcached or Redis.)
|
13
|
+
spec.description = %q(Distributed locks (mutexes & semaphores) using Memcached or Redis.)
|
14
|
+
spec.homepage = "https://github.com/bernstein/suo"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.bindir = "bin"
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.required_ruby_version = "~> 2.0"
|
24
|
+
|
25
|
+
spec.add_dependency "msgpack"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rubocop", "~> 0.30.0"
|
30
|
+
spec.add_development_dependency "minitest", "~> 5.5.0"
|
31
|
+
spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4.7"
|
32
|
+
end
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,397 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
TEST_KEY = "suo_test_key".freeze
|
4
|
+
|
5
|
+
module ClientTests
|
6
|
+
def client(options = {})
|
7
|
+
@client.class.new(options[:key] || TEST_KEY, options.merge(client: @client.client))
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_throws_failed_error_on_bad_client
|
11
|
+
assert_raises(Suo::LockClientError) do
|
12
|
+
client = @client.class.new(TEST_KEY, client: {})
|
13
|
+
client.lock
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_single_resource_locking
|
18
|
+
lock1 = @client.lock
|
19
|
+
refute_nil lock1
|
20
|
+
|
21
|
+
locked = @client.locked?
|
22
|
+
assert_equal true, locked
|
23
|
+
|
24
|
+
lock2 = @client.lock
|
25
|
+
assert_nil lock2
|
26
|
+
|
27
|
+
@client.unlock(lock1)
|
28
|
+
|
29
|
+
locked = @client.locked?
|
30
|
+
|
31
|
+
assert_equal false, locked
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_lock_with_custom_token
|
35
|
+
token = 'foo-bar'
|
36
|
+
lock = @client.lock token
|
37
|
+
assert_equal lock, token
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_empty_lock_on_invalid_data
|
41
|
+
@client.send(:initial_set, "bad value")
|
42
|
+
assert_equal false, @client.locked?
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_clear
|
46
|
+
lock1 = @client.lock
|
47
|
+
refute_nil lock1
|
48
|
+
|
49
|
+
@client.clear
|
50
|
+
|
51
|
+
assert_equal false, @client.locked?
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_multiple_resource_locking
|
55
|
+
@client = client(resources: 2)
|
56
|
+
|
57
|
+
lock1 = @client.lock
|
58
|
+
refute_nil lock1
|
59
|
+
|
60
|
+
assert_equal false, @client.locked?
|
61
|
+
|
62
|
+
lock2 = @client.lock
|
63
|
+
refute_nil lock2
|
64
|
+
|
65
|
+
assert_equal true, @client.locked?
|
66
|
+
|
67
|
+
@client.unlock(lock1)
|
68
|
+
|
69
|
+
assert_equal false, @client.locked?
|
70
|
+
|
71
|
+
assert_equal 1, @client.locks.size
|
72
|
+
|
73
|
+
@client.unlock(lock2)
|
74
|
+
|
75
|
+
assert_equal false, @client.locked?
|
76
|
+
assert_equal 0, @client.locks.size
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_block_single_resource_locking
|
80
|
+
locked = false
|
81
|
+
|
82
|
+
@client.lock { locked = true }
|
83
|
+
|
84
|
+
assert_equal true, locked
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_block_unlocks_on_exception
|
88
|
+
assert_raises(RuntimeError) do
|
89
|
+
@client.lock{ fail "Test" }
|
90
|
+
end
|
91
|
+
|
92
|
+
assert_equal false, @client.locked?
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_readme_example
|
96
|
+
output = Queue.new
|
97
|
+
@client = client(resources: 2)
|
98
|
+
threads = []
|
99
|
+
|
100
|
+
threads << Thread.new { @client.lock { output << "One"; sleep 0.5 } }
|
101
|
+
threads << Thread.new { @client.lock { output << "Two"; sleep 0.5 } }
|
102
|
+
sleep 0.1
|
103
|
+
threads << Thread.new { @client.lock { output << "Three" } }
|
104
|
+
|
105
|
+
threads.each(&:join)
|
106
|
+
|
107
|
+
ret = []
|
108
|
+
|
109
|
+
ret << (output.size > 0 ? output.pop : nil)
|
110
|
+
ret << (output.size > 0 ? output.pop : nil)
|
111
|
+
|
112
|
+
ret.sort!
|
113
|
+
|
114
|
+
assert_equal 0, output.size
|
115
|
+
assert_equal %w(One Two), ret
|
116
|
+
assert_equal false, @client.locked?
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_block_multiple_resource_locking
|
120
|
+
success_counter = Queue.new
|
121
|
+
failure_counter = Queue.new
|
122
|
+
|
123
|
+
@client = client(acquisition_timeout: 0.9, resources: 50)
|
124
|
+
|
125
|
+
100.times.map do |i|
|
126
|
+
Thread.new do
|
127
|
+
success = @client.lock do
|
128
|
+
sleep(3)
|
129
|
+
success_counter << i
|
130
|
+
end
|
131
|
+
|
132
|
+
failure_counter << i unless success
|
133
|
+
end
|
134
|
+
end.each(&:join)
|
135
|
+
|
136
|
+
assert_equal 50, success_counter.size
|
137
|
+
assert_equal 50, failure_counter.size
|
138
|
+
assert_equal false, @client.locked?
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_block_multiple_resource_locking_longer_timeout
|
142
|
+
success_counter = Queue.new
|
143
|
+
failure_counter = Queue.new
|
144
|
+
|
145
|
+
@client = client(acquisition_timeout: 3, resources: 50)
|
146
|
+
|
147
|
+
100.times.map do |i|
|
148
|
+
Thread.new do
|
149
|
+
success = @client.lock do
|
150
|
+
sleep(0.5)
|
151
|
+
success_counter << i
|
152
|
+
end
|
153
|
+
|
154
|
+
failure_counter << i unless success
|
155
|
+
end
|
156
|
+
end.each(&:join)
|
157
|
+
|
158
|
+
assert_equal 100, success_counter.size
|
159
|
+
assert_equal 0, failure_counter.size
|
160
|
+
assert_equal false, @client.locked?
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_unstale_lock_acquisition
|
164
|
+
success_counter = Queue.new
|
165
|
+
failure_counter = Queue.new
|
166
|
+
|
167
|
+
@client = client(stale_lock_expiration: 0.5)
|
168
|
+
|
169
|
+
t1 = Thread.new { @client.lock { sleep 0.6; success_counter << 1 } }
|
170
|
+
sleep 0.3
|
171
|
+
t2 = Thread.new do
|
172
|
+
locked = @client.lock { success_counter << 1 }
|
173
|
+
failure_counter << 1 unless locked
|
174
|
+
end
|
175
|
+
|
176
|
+
[t1, t2].each(&:join)
|
177
|
+
|
178
|
+
assert_equal 1, success_counter.size
|
179
|
+
assert_equal 1, failure_counter.size
|
180
|
+
assert_equal false, @client.locked?
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_stale_lock_acquisition
|
184
|
+
success_counter = Queue.new
|
185
|
+
failure_counter = Queue.new
|
186
|
+
|
187
|
+
@client = client(stale_lock_expiration: 0.5)
|
188
|
+
|
189
|
+
t1 = Thread.new { @client.lock { sleep 0.6; success_counter << 1 } }
|
190
|
+
sleep 0.55
|
191
|
+
t2 = Thread.new do
|
192
|
+
locked = @client.lock { success_counter << 1 }
|
193
|
+
failure_counter << 1 unless locked
|
194
|
+
end
|
195
|
+
|
196
|
+
[t1, t2].each(&:join)
|
197
|
+
|
198
|
+
assert_equal 2, success_counter.size
|
199
|
+
assert_equal 0, failure_counter.size
|
200
|
+
assert_equal false, @client.locked?
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_refresh
|
204
|
+
@client = client(stale_lock_expiration: 0.5)
|
205
|
+
|
206
|
+
lock1 = @client.lock
|
207
|
+
|
208
|
+
assert_equal true, @client.locked?
|
209
|
+
|
210
|
+
@client.refresh(lock1)
|
211
|
+
|
212
|
+
assert_equal true, @client.locked?
|
213
|
+
|
214
|
+
sleep 0.55
|
215
|
+
|
216
|
+
assert_equal false, @client.locked?
|
217
|
+
|
218
|
+
lock2 = @client.lock
|
219
|
+
|
220
|
+
@client.refresh(lock1)
|
221
|
+
|
222
|
+
assert_equal true, @client.locked?
|
223
|
+
|
224
|
+
@client.unlock(lock1)
|
225
|
+
|
226
|
+
# edge case with refresh lock in the middle
|
227
|
+
assert_equal true, @client.locked?
|
228
|
+
|
229
|
+
@client.clear
|
230
|
+
|
231
|
+
assert_equal false, @client.locked?
|
232
|
+
|
233
|
+
@client.refresh(lock2)
|
234
|
+
|
235
|
+
assert_equal true, @client.locked?
|
236
|
+
|
237
|
+
@client.unlock(lock2)
|
238
|
+
|
239
|
+
# now finally unlocked
|
240
|
+
assert_equal false, @client.locked?
|
241
|
+
end
|
242
|
+
|
243
|
+
def test_block_refresh
|
244
|
+
success_counter = Queue.new
|
245
|
+
failure_counter = Queue.new
|
246
|
+
|
247
|
+
@client = client(stale_lock_expiration: 0.5)
|
248
|
+
|
249
|
+
t1 = Thread.new do
|
250
|
+
@client.lock do |token|
|
251
|
+
sleep 0.6
|
252
|
+
@client.refresh(token)
|
253
|
+
sleep 1
|
254
|
+
success_counter << 1
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
t2 = Thread.new do
|
259
|
+
sleep 0.8
|
260
|
+
locked = @client.lock { success_counter << 1 }
|
261
|
+
failure_counter << 1 unless locked
|
262
|
+
end
|
263
|
+
|
264
|
+
[t1, t2].each(&:join)
|
265
|
+
|
266
|
+
assert_equal 1, success_counter.size
|
267
|
+
assert_equal 1, failure_counter.size
|
268
|
+
assert_equal false, @client.locked?
|
269
|
+
end
|
270
|
+
|
271
|
+
def test_refresh_multi
|
272
|
+
success_counter = Queue.new
|
273
|
+
failure_counter = Queue.new
|
274
|
+
|
275
|
+
@client = client(stale_lock_expiration: 0.5, resources: 2)
|
276
|
+
|
277
|
+
t1 = Thread.new do
|
278
|
+
@client.lock do |token|
|
279
|
+
sleep 0.4
|
280
|
+
@client.refresh(token)
|
281
|
+
success_counter << 1
|
282
|
+
sleep 0.5
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
t2 = Thread.new do
|
287
|
+
sleep 0.55
|
288
|
+
locked = @client.lock do
|
289
|
+
success_counter << 1
|
290
|
+
sleep 0.5
|
291
|
+
end
|
292
|
+
|
293
|
+
failure_counter << 1 unless locked
|
294
|
+
end
|
295
|
+
|
296
|
+
t3 = Thread.new do
|
297
|
+
sleep 0.75
|
298
|
+
locked = @client.lock { success_counter << 1 }
|
299
|
+
failure_counter << 1 unless locked
|
300
|
+
end
|
301
|
+
|
302
|
+
[t1, t2, t3].each(&:join)
|
303
|
+
|
304
|
+
assert_equal 2, success_counter.size
|
305
|
+
assert_equal 1, failure_counter.size
|
306
|
+
assert_equal false, @client.locked?
|
307
|
+
end
|
308
|
+
|
309
|
+
def test_increment_reused_client
|
310
|
+
i = 0
|
311
|
+
|
312
|
+
threads = 2.times.map do
|
313
|
+
Thread.new do
|
314
|
+
@client.lock { i += 1 }
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
threads.each(&:join)
|
319
|
+
|
320
|
+
assert_equal 2, i
|
321
|
+
assert_equal false, @client.locked?
|
322
|
+
end
|
323
|
+
|
324
|
+
def test_increment_new_client
|
325
|
+
i = 0
|
326
|
+
|
327
|
+
threads = 2.times.map do
|
328
|
+
Thread.new do
|
329
|
+
# note this is the method that generates a *new* client
|
330
|
+
client.lock { i += 1 }
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
threads.each(&:join)
|
335
|
+
|
336
|
+
assert_equal 2, i
|
337
|
+
assert_equal false, @client.locked?
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
class TestBaseClient < Minitest::Test
|
342
|
+
def setup
|
343
|
+
@client = Suo::Client::Base.new(TEST_KEY, client: {})
|
344
|
+
end
|
345
|
+
|
346
|
+
def test_not_implemented
|
347
|
+
assert_raises(NotImplementedError) do
|
348
|
+
@client.send(:get)
|
349
|
+
end
|
350
|
+
|
351
|
+
assert_raises(NotImplementedError) do
|
352
|
+
@client.send(:set, "", "")
|
353
|
+
end
|
354
|
+
|
355
|
+
assert_raises(NotImplementedError) do
|
356
|
+
@client.send(:initial_set)
|
357
|
+
end
|
358
|
+
|
359
|
+
assert_raises(NotImplementedError) do
|
360
|
+
@client.send(:clear)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
class TestMemcachedClient < Minitest::Test
|
366
|
+
include ClientTests
|
367
|
+
|
368
|
+
def setup
|
369
|
+
@dalli = Dalli::Client.new("127.0.0.1:11211")
|
370
|
+
@client = Suo::Client::Memcached.new(TEST_KEY)
|
371
|
+
teardown
|
372
|
+
end
|
373
|
+
|
374
|
+
def teardown
|
375
|
+
@dalli.delete(TEST_KEY)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
class TestRedisClient < Minitest::Test
|
380
|
+
include ClientTests
|
381
|
+
|
382
|
+
def setup
|
383
|
+
@redis = Redis.new
|
384
|
+
@client = Suo::Client::Redis.new(TEST_KEY)
|
385
|
+
teardown
|
386
|
+
end
|
387
|
+
|
388
|
+
def teardown
|
389
|
+
@redis.del(TEST_KEY)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
class TestLibrary < Minitest::Test
|
394
|
+
def test_that_it_has_a_version_number
|
395
|
+
refute_nil ::Suo::VERSION
|
396
|
+
end
|
397
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
2
|
+
|
3
|
+
if ENV["CODECLIMATE_REPO_TOKEN"]
|
4
|
+
require "codeclimate-test-reporter"
|
5
|
+
CodeClimate::TestReporter.start
|
6
|
+
end
|
7
|
+
|
8
|
+
require "suo"
|
9
|
+
require "thread"
|
10
|
+
require "minitest/autorun"
|
11
|
+
require "minitest/benchmark"
|
12
|
+
|
13
|
+
ENV["SUO_TEST"] = "true"
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: suo-dependless
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Elser
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: msgpack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
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: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.5'
|
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: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.30.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.30.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 5.5.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 5.5.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: codeclimate-test-reporter
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.4.7
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.4.7
|
97
|
+
description: Distributed locks (mutexes & semaphores) using Memcached or Redis.
|
98
|
+
email:
|
99
|
+
- nick.elser@gmail.com
|
100
|
+
executables:
|
101
|
+
- console
|
102
|
+
- setup
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- ".gitignore"
|
107
|
+
- ".rubocop.yml"
|
108
|
+
- ".travis.yml"
|
109
|
+
- CHANGELOG.md
|
110
|
+
- Gemfile
|
111
|
+
- LICENSE.txt
|
112
|
+
- README.md
|
113
|
+
- Rakefile
|
114
|
+
- bin/console
|
115
|
+
- bin/setup
|
116
|
+
- lib/suo.rb
|
117
|
+
- lib/suo/client/base.rb
|
118
|
+
- lib/suo/client/memcached.rb
|
119
|
+
- lib/suo/client/redis.rb
|
120
|
+
- lib/suo/errors.rb
|
121
|
+
- lib/suo/version.rb
|
122
|
+
- suo-dependless.gemspec
|
123
|
+
- test/client_test.rb
|
124
|
+
- test/test_helper.rb
|
125
|
+
homepage: https://github.com/bernstein/suo
|
126
|
+
licenses:
|
127
|
+
- MIT
|
128
|
+
metadata: {}
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '2.0'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 2.6.9
|
146
|
+
signing_key:
|
147
|
+
specification_version: 4
|
148
|
+
summary: Distributed locks (mutexes & semaphores) using Memcached or Redis.
|
149
|
+
test_files:
|
150
|
+
- test/client_test.rb
|
151
|
+
- test/test_helper.rb
|