suo 0.1.0
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 +216 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +3 -0
- data/README.md +58 -0
- data/Rakefile +7 -0
- data/bin/console +7 -0
- data/bin/setup +5 -0
- data/lib/suo/client/base.rb +116 -0
- data/lib/suo/client/errors.rb +7 -0
- data/lib/suo/client/memcached.rb +137 -0
- data/lib/suo/client/redis.rb +167 -0
- data/lib/suo/clients.rb +12 -0
- data/lib/suo/version.rb +3 -0
- data/lib/suo.rb +2 -0
- data/suo.gemspec +28 -0
- metadata +144 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 200ad3e821080a0c0674af2abd4622de1c5088fb
|
|
4
|
+
data.tar.gz: 81a59e78a7396bd76b4a5d9c1e3cab46b0eac260
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f5ee6c325526dbe4ea6f4a68cd60a310c33b563a3b05495de4197093061634086581702ab25973df67410ef28345484a0ddf224a6d580ab863fe7ce732f777eb
|
|
7
|
+
data.tar.gz: 6f308c0243d9af16a65b33e8c35be667c27ce070500264ed236deb128c3bdfb4f9563aa879ece10d22386f4a92c4d56a42f2242003674a9038a585950f3d5839
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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/TrailingComma:
|
|
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/SpaceAfterControlKeyword:
|
|
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: 8
|
|
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
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# 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.
|
|
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(connection: "127.0.0.1:11211")
|
|
22
|
+
|
|
23
|
+
# Redis
|
|
24
|
+
suo = Suo::Client::Redis.new(connection: {host: "10.0.1.1"})
|
|
25
|
+
|
|
26
|
+
# Pre-existing client
|
|
27
|
+
suo = Suo::Client::Memcached.new(client: some_dalli_client)
|
|
28
|
+
|
|
29
|
+
suo.lock("some_key") do
|
|
30
|
+
# critical code here
|
|
31
|
+
@puppies.pet!
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
2.times do
|
|
35
|
+
Thread.new do
|
|
36
|
+
# second argument is the number of resources - so this will run twice
|
|
37
|
+
suo.lock("other_key", 2, timeout: 0.5) { puts "Will run twice!" }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## TODO
|
|
43
|
+
- better stale key handling (refresh blocks)
|
|
44
|
+
- more race condition tests
|
|
45
|
+
- refactor clients to re-use more code
|
|
46
|
+
|
|
47
|
+
## History
|
|
48
|
+
|
|
49
|
+
View the [changelog](https://github.com/nickelser/suo/blob/master/CHANGELOG.md)
|
|
50
|
+
|
|
51
|
+
## Contributing
|
|
52
|
+
|
|
53
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
|
54
|
+
|
|
55
|
+
- [Report bugs](https://github.com/nickelser/suo/issues)
|
|
56
|
+
- Fix bugs and [submit pull requests](https://github.com/nickelser/suo/pulls)
|
|
57
|
+
- Write, clarify, or fix documentation
|
|
58
|
+
- Suggest or add new features
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Suo
|
|
2
|
+
module Client
|
|
3
|
+
class Base
|
|
4
|
+
DEFAULT_OPTIONS = {
|
|
5
|
+
retry_count: 3,
|
|
6
|
+
retry_delay: 0.01,
|
|
7
|
+
stale_lock_expiration: 3600
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(options = {})
|
|
11
|
+
@options = self.class.merge_defaults(options).merge(_initialized: true)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def lock(key, resources = 1, options = {})
|
|
15
|
+
options = self.class.merge_defaults(@options.merge(options))
|
|
16
|
+
token = self.class.lock(key, resources, options)
|
|
17
|
+
|
|
18
|
+
if token
|
|
19
|
+
begin
|
|
20
|
+
yield if block_given?
|
|
21
|
+
ensure
|
|
22
|
+
self.class.unlock(key, token, options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
true
|
|
26
|
+
else
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def locked?(key, resources = 1)
|
|
32
|
+
self.class.locked?(key, resources, @options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
def lock(key, resources = 1, options = {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
37
|
+
fail NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def locked?(key, resources = 1, options = {})
|
|
41
|
+
options = merge_defaults(options)
|
|
42
|
+
client = options[:client]
|
|
43
|
+
locks = deserialize_locks(client.get(key))
|
|
44
|
+
|
|
45
|
+
locks.size >= resources
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def locks(key, options)
|
|
49
|
+
options = merge_defaults(options)
|
|
50
|
+
client = options[:client]
|
|
51
|
+
locks = deserialize_locks(client.get(key))
|
|
52
|
+
|
|
53
|
+
locks.size
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def refresh(key, acquisition_token, options = {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
57
|
+
fail NotImplementedError
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def unlock(key, acquisition_token, options = {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
61
|
+
fail NotImplementedError
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def clear(key, options = {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
65
|
+
fail NotImplementedError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def merge_defaults(options = {})
|
|
69
|
+
unless options[:_initialized]
|
|
70
|
+
options = self::DEFAULT_OPTIONS.merge(options)
|
|
71
|
+
|
|
72
|
+
fail "Client required" unless options[:client]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if options[:retry_timeout]
|
|
76
|
+
options[:retry_count] = (options[:retry_timeout] / options[:retry_delay].to_f).floor
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
options
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def serialize_locks(locks)
|
|
85
|
+
locks.map { |time, token| [time.to_f, token].join(":") }.join(",")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def deserialize_locks(str)
|
|
89
|
+
str.split(",").map do |s|
|
|
90
|
+
time, token = s.split(":", 2)
|
|
91
|
+
[Time.at(time.to_f), token]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def clear_expired_locks(locks, options)
|
|
96
|
+
expired = Time.now - options[:stale_lock_expiration]
|
|
97
|
+
locks.reject { |time, _| time < expired }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_lock(locks, token)
|
|
101
|
+
locks << [Time.now.to_f, token]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def remove_lock(locks, acquisition_token)
|
|
105
|
+
lock = locks.find { |_, token| token == acquisition_token }
|
|
106
|
+
locks.delete(lock)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def refresh_lock(locks, acquisition_token)
|
|
110
|
+
remove_lock(locks, acquisition_token)
|
|
111
|
+
add_lock(locks, token)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module Suo
|
|
2
|
+
module Client
|
|
3
|
+
class Memcached < Base
|
|
4
|
+
def initialize(options = {})
|
|
5
|
+
options[:client] ||= Dalli::Client.new(options[:connection] || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def lock(key, resources = 1, options = {})
|
|
11
|
+
options = merge_defaults(options)
|
|
12
|
+
acquisition_token = nil
|
|
13
|
+
token = SecureRandom.base64(16)
|
|
14
|
+
client = options[:client]
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
start = Time.now.to_f
|
|
18
|
+
|
|
19
|
+
options[:retry_count].times do |i|
|
|
20
|
+
val, cas = client.get_cas(key)
|
|
21
|
+
|
|
22
|
+
# no key has been set yet; we could simply set it, but would lead to race conditions on the initial setting
|
|
23
|
+
if val.nil?
|
|
24
|
+
client.set(key, "")
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
locks = clear_expired_locks(deserialize_locks(val.to_s), options)
|
|
29
|
+
|
|
30
|
+
if locks.size < resources
|
|
31
|
+
add_lock(locks, token)
|
|
32
|
+
|
|
33
|
+
newval = serialize_locks(locks)
|
|
34
|
+
|
|
35
|
+
if client.set_cas(key, newval, cas)
|
|
36
|
+
acquisition_token = token
|
|
37
|
+
break
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if options[:retry_timeout]
|
|
42
|
+
now = Time.now.to_f
|
|
43
|
+
break if now - start > options[:retry_timeout]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
|
47
|
+
end
|
|
48
|
+
rescue => _
|
|
49
|
+
raise FailedToAcquireLock
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
acquisition_token
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def refresh(key, acquisition_token, options = {})
|
|
56
|
+
options = merge_defaults(options)
|
|
57
|
+
client = options[:client]
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
start = Time.now.to_f
|
|
61
|
+
|
|
62
|
+
options[:retry_count].times do
|
|
63
|
+
val, cas = client.get_cas(key)
|
|
64
|
+
|
|
65
|
+
# much like with initial set - ensure the key is here
|
|
66
|
+
if val.nil?
|
|
67
|
+
client.set(key, "")
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
locks = clear_expired_locks(deserialize_locks(val), options)
|
|
72
|
+
|
|
73
|
+
refresh_lock(locks, acquisition_token)
|
|
74
|
+
|
|
75
|
+
newval = serialize_locks(locks)
|
|
76
|
+
|
|
77
|
+
break if client.set_cas(key, newval, cas)
|
|
78
|
+
|
|
79
|
+
if options[:retry_timeout]
|
|
80
|
+
now = Time.now.to_f
|
|
81
|
+
break if now - start > options[:retry_timeout]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
|
85
|
+
end
|
|
86
|
+
rescue => _
|
|
87
|
+
raise FailedToAcquireLock
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def unlock(key, acquisition_token, options = {})
|
|
92
|
+
options = merge_defaults(options)
|
|
93
|
+
client = options[:client]
|
|
94
|
+
|
|
95
|
+
return unless acquisition_token
|
|
96
|
+
|
|
97
|
+
begin
|
|
98
|
+
start = Time.now.to_f
|
|
99
|
+
|
|
100
|
+
options[:retry_count].times do
|
|
101
|
+
val, cas = client.get_cas(key)
|
|
102
|
+
|
|
103
|
+
break if val.nil? # lock has expired totally
|
|
104
|
+
|
|
105
|
+
locks = clear_expired_locks(deserialize_locks(val), options)
|
|
106
|
+
|
|
107
|
+
acquisition_lock = remove_lock(locks, acquisition_token)
|
|
108
|
+
|
|
109
|
+
break unless acquisition_lock
|
|
110
|
+
|
|
111
|
+
newval = serialize_locks(locks)
|
|
112
|
+
|
|
113
|
+
break if client.set_cas(key, newval, cas)
|
|
114
|
+
|
|
115
|
+
# another client cleared a token in the interim - try again!
|
|
116
|
+
|
|
117
|
+
if options[:retry_timeout]
|
|
118
|
+
now = Time.now.to_f
|
|
119
|
+
break if now - start > options[:retry_timeout]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
|
123
|
+
end
|
|
124
|
+
rescue => boom # rubocop:disable Lint/HandleExceptions
|
|
125
|
+
# since it's optimistic locking - fine if we are unable to release
|
|
126
|
+
raise boom if ENV["SUO_TEST"]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def clear(key, options = {})
|
|
131
|
+
options = merge_defaults(options)
|
|
132
|
+
options[:client].delete(key)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
module Suo
|
|
2
|
+
module Client
|
|
3
|
+
class Redis < Base
|
|
4
|
+
def initialize(options = {})
|
|
5
|
+
options[:client] ||= ::Redis.new(options[:connection] || {})
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def lock(key, resources = 1, options = {})
|
|
11
|
+
options = merge_defaults(options)
|
|
12
|
+
acquisition_token = nil
|
|
13
|
+
token = SecureRandom.base64(16)
|
|
14
|
+
client = options[:client]
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
start = Time.now.to_f
|
|
18
|
+
|
|
19
|
+
options[:retry_count].times do
|
|
20
|
+
client.watch(key) do
|
|
21
|
+
begin
|
|
22
|
+
val = client.get(key)
|
|
23
|
+
|
|
24
|
+
locks = clear_expired_locks(deserialize_locks(val.to_s), options)
|
|
25
|
+
|
|
26
|
+
if locks.size < resources
|
|
27
|
+
add_lock(locks, token)
|
|
28
|
+
|
|
29
|
+
newval = serialize_locks(locks)
|
|
30
|
+
|
|
31
|
+
ret = client.multi do |multi|
|
|
32
|
+
multi.set(key, newval)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
acquisition_token = token if ret[0] == "OK"
|
|
36
|
+
end
|
|
37
|
+
ensure
|
|
38
|
+
client.unwatch
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
break if acquisition_token
|
|
43
|
+
|
|
44
|
+
if options[:retry_timeout]
|
|
45
|
+
now = Time.now.to_f
|
|
46
|
+
break if now - start > options[:retry_timeout]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
|
50
|
+
end
|
|
51
|
+
rescue => boom
|
|
52
|
+
raise boom
|
|
53
|
+
raise Suo::Client::FailedToAcquireLock
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
acquisition_token
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def refresh(key, acquisition_token, options = {})
|
|
60
|
+
options = merge_defaults(options)
|
|
61
|
+
client = options[:client]
|
|
62
|
+
refreshed = false
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
start = Time.now.to_f
|
|
66
|
+
|
|
67
|
+
options[:retry_count].times do
|
|
68
|
+
client.watch(key) do
|
|
69
|
+
begin
|
|
70
|
+
val = client.get(key)
|
|
71
|
+
|
|
72
|
+
locks = clear_expired_locks(deserialize_locks(val), options)
|
|
73
|
+
|
|
74
|
+
refresh_lock(locks, acquisition_token)
|
|
75
|
+
|
|
76
|
+
newval = serialize_locks(locks)
|
|
77
|
+
|
|
78
|
+
ret = client.multi do |multi|
|
|
79
|
+
multi.set(key, newval)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
refreshed = ret[0] == "OK"
|
|
83
|
+
ensure
|
|
84
|
+
client.unwatch
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
break if refreshed
|
|
89
|
+
|
|
90
|
+
if options[:retry_timeout]
|
|
91
|
+
now = Time.now.to_f
|
|
92
|
+
break if now - start > options[:retry_timeout]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
|
96
|
+
end
|
|
97
|
+
rescue => _
|
|
98
|
+
raise Suo::Client::FailedToAcquireLock
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def unlock(key, acquisition_token, options = {})
|
|
103
|
+
options = merge_defaults(options)
|
|
104
|
+
client = options[:client]
|
|
105
|
+
|
|
106
|
+
return unless acquisition_token
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
start = Time.now.to_f
|
|
110
|
+
|
|
111
|
+
options[:retry_count].times do
|
|
112
|
+
cleared = false
|
|
113
|
+
|
|
114
|
+
client.watch(key) do
|
|
115
|
+
begin
|
|
116
|
+
val = client.get(key)
|
|
117
|
+
|
|
118
|
+
if val.nil?
|
|
119
|
+
cleared = true
|
|
120
|
+
break
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
locks = clear_expired_locks(deserialize_locks(val), options)
|
|
124
|
+
|
|
125
|
+
acquisition_lock = remove_lock(locks, acquisition_token)
|
|
126
|
+
|
|
127
|
+
unless acquisition_lock
|
|
128
|
+
# token was already cleared
|
|
129
|
+
cleared = true
|
|
130
|
+
break
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
newval = serialize_locks(locks)
|
|
134
|
+
|
|
135
|
+
ret = client.multi do |multi|
|
|
136
|
+
multi.set(key, newval)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
cleared = ret[0] == "OK"
|
|
140
|
+
ensure
|
|
141
|
+
client.unwatch
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
break if cleared
|
|
146
|
+
|
|
147
|
+
if options[:retry_timeout]
|
|
148
|
+
now = Time.now.to_f
|
|
149
|
+
break if now - start > options[:retry_timeout]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
|
153
|
+
end
|
|
154
|
+
rescue => boom # rubocop:disable Lint/HandleExceptions
|
|
155
|
+
# since it's optimistic locking - fine if we are unable to release
|
|
156
|
+
raise boom if ENV["SUO_TEST"]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def clear(key, options = {})
|
|
161
|
+
options = merge_defaults(options)
|
|
162
|
+
options[:client].del(key)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/suo/clients.rb
ADDED
data/lib/suo/version.rb
ADDED
data/lib/suo.rb
ADDED
data/suo.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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"
|
|
8
|
+
spec.version = Suo::VERSION
|
|
9
|
+
spec.authors = ["Nick Elser"]
|
|
10
|
+
spec.email = ["nick.elser@gmail.com"]
|
|
11
|
+
|
|
12
|
+
spec.summary = %q(Distributed semaphores using Memcached or Redis.)
|
|
13
|
+
# spec.description = %q{TODO: Long description}
|
|
14
|
+
spec.homepage = "https://github.com/nickelser/suo"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
17
|
+
spec.bindir = "bin"
|
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_dependency "dalli"
|
|
22
|
+
spec.add_dependency "redis"
|
|
23
|
+
spec.add_dependency "msgpack"
|
|
24
|
+
|
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
27
|
+
spec.add_development_dependency "rubocop", "~> 0.30.0"
|
|
28
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: suo
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nick Elser
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-04-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dalli
|
|
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: redis
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
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: msgpack
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: bundler
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.5'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.5'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rake
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '10.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '10.0'
|
|
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.30.0
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 0.30.0
|
|
97
|
+
description:
|
|
98
|
+
email:
|
|
99
|
+
- nick.elser@gmail.com
|
|
100
|
+
executables: []
|
|
101
|
+
extensions: []
|
|
102
|
+
extra_rdoc_files: []
|
|
103
|
+
files:
|
|
104
|
+
- ".gitignore"
|
|
105
|
+
- ".rubocop.yml"
|
|
106
|
+
- ".travis.yml"
|
|
107
|
+
- CHANGELOG.md
|
|
108
|
+
- Gemfile
|
|
109
|
+
- README.md
|
|
110
|
+
- Rakefile
|
|
111
|
+
- bin/console
|
|
112
|
+
- bin/setup
|
|
113
|
+
- lib/suo.rb
|
|
114
|
+
- lib/suo/client/base.rb
|
|
115
|
+
- lib/suo/client/errors.rb
|
|
116
|
+
- lib/suo/client/memcached.rb
|
|
117
|
+
- lib/suo/client/redis.rb
|
|
118
|
+
- lib/suo/clients.rb
|
|
119
|
+
- lib/suo/version.rb
|
|
120
|
+
- suo.gemspec
|
|
121
|
+
homepage: https://github.com/nickelser/suo
|
|
122
|
+
licenses: []
|
|
123
|
+
metadata: {}
|
|
124
|
+
post_install_message:
|
|
125
|
+
rdoc_options: []
|
|
126
|
+
require_paths:
|
|
127
|
+
- lib
|
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: '0'
|
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
requirements: []
|
|
139
|
+
rubyforge_project:
|
|
140
|
+
rubygems_version: 2.4.5
|
|
141
|
+
signing_key:
|
|
142
|
+
specification_version: 4
|
|
143
|
+
summary: Distributed semaphores using Memcached or Redis.
|
|
144
|
+
test_files: []
|