suo 0.1.1 → 0.1.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 +4 -4
- data/CHANGELOG.md +5 -1
- data/README.md +1 -2
- data/lib/suo/client/base.rb +115 -24
- data/lib/suo/client/memcached.rb +9 -115
- data/lib/suo/client/redis.rb +18 -141
- data/lib/suo/version.rb +1 -1
- data/test/client_test.rb +5 -4
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6334c57e0648ae8fefdeb1113664c3e6631dd03c
|
4
|
+
data.tar.gz: 0f35accaea812d463d911b1602a30372952ca19b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 51081aad925fa7032551ff81be52f724ed8fcd9592dfbfb7aea70d1a837597bdeabdcbe0bb9946629cbc2f853a81d1e8b501ec2b1d1ba8ba3c57a4ff6d43a77a
|
7
|
+
data.tar.gz: cce84bf639f8303f241a3c901d92991ae5e72b95c918601db06b75ca98cef250c85d3a00c0a2e3412ce539f2eb6acfedfeda1297ae1fbf033844477bc26b79fa
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
+
## 0.1.2
|
2
|
+
|
3
|
+
- Fix retry_timeout to properly use the full time (was being calculated incorrectly).
|
4
|
+
- Refactor client implementations to re-use more code.
|
5
|
+
|
1
6
|
## 0.1.1
|
2
7
|
|
3
8
|
- Use [MessagePack](https://github.com/msgpack/msgpack-ruby) for semaphore serialization.
|
4
9
|
|
5
|
-
|
6
10
|
## 0.1.0
|
7
11
|
|
8
12
|
- First release.
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Suo [](https://travis-ci.org/nickelser/suo) [](http://badge.fury.io/rb/suo)
|
2
2
|
|
3
3
|
:lock: Distributed semaphores using Memcached or Redis in Ruby.
|
4
4
|
|
@@ -42,7 +42,6 @@ end
|
|
42
42
|
## TODO
|
43
43
|
- better stale key handling (refresh blocks)
|
44
44
|
- more race condition tests
|
45
|
-
- refactor clients to re-use more code
|
46
45
|
|
47
46
|
## History
|
48
47
|
|
data/lib/suo/client/base.rb
CHANGED
@@ -2,13 +2,13 @@ module Suo
|
|
2
2
|
module Client
|
3
3
|
class Base
|
4
4
|
DEFAULT_OPTIONS = {
|
5
|
-
|
5
|
+
retry_timeout: 0.1,
|
6
6
|
retry_delay: 0.01,
|
7
7
|
stale_lock_expiration: 3600
|
8
8
|
}.freeze
|
9
9
|
|
10
10
|
def initialize(options = {})
|
11
|
-
@options = self.class.merge_defaults(options)
|
11
|
+
@options = self.class.merge_defaults(options)
|
12
12
|
end
|
13
13
|
|
14
14
|
def lock(key, resources = 1, options = {})
|
@@ -33,32 +33,86 @@ module Suo
|
|
33
33
|
end
|
34
34
|
|
35
35
|
class << self
|
36
|
-
def lock(key, resources = 1, options = {})
|
37
|
-
|
36
|
+
def lock(key, resources = 1, options = {})
|
37
|
+
options = merge_defaults(options)
|
38
|
+
acquisition_token = nil
|
39
|
+
token = SecureRandom.base64(16)
|
40
|
+
|
41
|
+
retry_with_timeout(key, options) do
|
42
|
+
val, cas = get(key, options)
|
43
|
+
|
44
|
+
if val.nil?
|
45
|
+
set_initial(key, options)
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
locks = deserialize_and_clear_locks(val, options)
|
50
|
+
|
51
|
+
if locks.size < resources
|
52
|
+
add_lock(locks, token)
|
53
|
+
|
54
|
+
newval = serialize_locks(locks)
|
55
|
+
|
56
|
+
if set(key, newval, cas, options)
|
57
|
+
acquisition_token = token
|
58
|
+
break
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
acquisition_token
|
38
64
|
end
|
39
65
|
|
40
66
|
def locked?(key, resources = 1, options = {})
|
41
|
-
options
|
42
|
-
client = options[:client]
|
43
|
-
locks = deserialize_locks(client.get(key))
|
44
|
-
|
45
|
-
locks.size >= resources
|
67
|
+
locks(key, options).size >= resources
|
46
68
|
end
|
47
69
|
|
48
70
|
def locks(key, options)
|
49
71
|
options = merge_defaults(options)
|
50
|
-
|
51
|
-
locks = deserialize_locks(
|
72
|
+
val, _ = get(key, options)
|
73
|
+
locks = deserialize_locks(val)
|
52
74
|
|
53
|
-
locks
|
75
|
+
locks
|
54
76
|
end
|
55
77
|
|
56
|
-
def refresh(key, acquisition_token, options = {})
|
57
|
-
|
78
|
+
def refresh(key, acquisition_token, options = {})
|
79
|
+
options = merge_defaults(options)
|
80
|
+
|
81
|
+
retry_with_timeout(key, options) do
|
82
|
+
val, cas = get(key, options)
|
83
|
+
|
84
|
+
if val.nil?
|
85
|
+
set_initial(key, options)
|
86
|
+
next
|
87
|
+
end
|
88
|
+
|
89
|
+
locks = deserialize_and_clear_locks(val, options)
|
90
|
+
|
91
|
+
refresh_lock(locks, acquisition_token)
|
92
|
+
|
93
|
+
break if set(key, serialize_locks(locks), cas, options)
|
94
|
+
end
|
58
95
|
end
|
59
96
|
|
60
|
-
def unlock(key, acquisition_token, options = {})
|
61
|
-
|
97
|
+
def unlock(key, acquisition_token, options = {})
|
98
|
+
options = merge_defaults(options)
|
99
|
+
|
100
|
+
return unless acquisition_token
|
101
|
+
|
102
|
+
retry_with_timeout(key, options) do
|
103
|
+
val, cas = get(key, options)
|
104
|
+
|
105
|
+
break if val.nil?
|
106
|
+
|
107
|
+
locks = deserialize_and_clear_locks(val, options)
|
108
|
+
|
109
|
+
acquisition_lock = remove_lock(locks, acquisition_token)
|
110
|
+
|
111
|
+
break unless acquisition_lock
|
112
|
+
break if set(key, serialize_locks(locks), cas, options)
|
113
|
+
end
|
114
|
+
rescue FailedToAcquireLock => _ # rubocop:disable Lint/HandleExceptions
|
115
|
+
# ignore - assume success due to optimistic locking
|
62
116
|
end
|
63
117
|
|
64
118
|
def clear(key, options = {}) # rubocop:disable Lint/UnusedMethodArgument
|
@@ -66,27 +120,64 @@ module Suo
|
|
66
120
|
end
|
67
121
|
|
68
122
|
def merge_defaults(options = {})
|
69
|
-
|
70
|
-
options = self::DEFAULT_OPTIONS.merge(options)
|
123
|
+
options = self::DEFAULT_OPTIONS.merge(options)
|
71
124
|
|
72
|
-
|
73
|
-
end
|
125
|
+
fail "Client required" unless options[:client]
|
74
126
|
|
75
|
-
|
76
|
-
options[:retry_count] = (options[:retry_timeout] / options[:retry_delay].to_f).floor
|
77
|
-
end
|
127
|
+
options[:retry_count] = (options[:retry_timeout] / options[:retry_delay].to_f).ceil
|
78
128
|
|
79
129
|
options
|
80
130
|
end
|
81
131
|
|
82
132
|
private
|
83
133
|
|
134
|
+
def get(key, options) # rubocop:disable Lint/UnusedMethodArgument
|
135
|
+
fail NotImplementedError
|
136
|
+
end
|
137
|
+
|
138
|
+
def set(key, newval, oldval, options) # rubocop:disable Lint/UnusedMethodArgument
|
139
|
+
fail NotImplementedError
|
140
|
+
end
|
141
|
+
|
142
|
+
def set_initial(key, options) # rubocop:disable Lint/UnusedMethodArgument
|
143
|
+
fail NotImplementedError
|
144
|
+
end
|
145
|
+
|
146
|
+
def synchronize(key, options)
|
147
|
+
yield(key, options)
|
148
|
+
end
|
149
|
+
|
150
|
+
def retry_with_timeout(key, options)
|
151
|
+
start = Time.now.to_f
|
152
|
+
|
153
|
+
options[:retry_count].times do
|
154
|
+
if options[:retry_timeout]
|
155
|
+
now = Time.now.to_f
|
156
|
+
break if now - start > options[:retry_timeout]
|
157
|
+
end
|
158
|
+
|
159
|
+
synchronize(key, options) do
|
160
|
+
yield
|
161
|
+
end
|
162
|
+
|
163
|
+
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
164
|
+
end
|
165
|
+
rescue => _
|
166
|
+
raise FailedToAcquireLock
|
167
|
+
end
|
168
|
+
|
84
169
|
def serialize_locks(locks)
|
85
170
|
MessagePack.pack(locks.map { |time, token| [time.to_f, token] })
|
86
171
|
end
|
87
172
|
|
173
|
+
def deserialize_and_clear_locks(val, options)
|
174
|
+
clear_expired_locks(deserialize_locks(val), options)
|
175
|
+
end
|
176
|
+
|
88
177
|
def deserialize_locks(val)
|
89
|
-
MessagePack.unpack(val)
|
178
|
+
unpacked = (val.nil? || val == "") ? [] : MessagePack.unpack(val)
|
179
|
+
|
180
|
+
unpacked.map do |time, token|
|
90
181
|
[Time.at(time), token]
|
91
182
|
end
|
92
183
|
rescue EOFError => _
|
data/lib/suo/client/memcached.rb
CHANGED
@@ -7,129 +7,23 @@ module Suo
|
|
7
7
|
end
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def
|
10
|
+
def clear(key, options = {})
|
11
11
|
options = merge_defaults(options)
|
12
|
-
|
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
|
-
if options[:retry_timeout]
|
21
|
-
now = Time.now.to_f
|
22
|
-
break if now - start > options[:retry_timeout]
|
23
|
-
end
|
24
|
-
|
25
|
-
val, cas = client.get_cas(key)
|
26
|
-
|
27
|
-
# no key has been set yet; we could simply set it, but would lead to race conditions on the initial setting
|
28
|
-
if val.nil?
|
29
|
-
client.set(key, "")
|
30
|
-
next
|
31
|
-
end
|
32
|
-
|
33
|
-
locks = clear_expired_locks(deserialize_locks(val.to_s), options)
|
34
|
-
|
35
|
-
if locks.size < resources
|
36
|
-
add_lock(locks, token)
|
37
|
-
|
38
|
-
newval = serialize_locks(locks)
|
39
|
-
|
40
|
-
if client.set_cas(key, newval, cas)
|
41
|
-
acquisition_token = token
|
42
|
-
break
|
43
|
-
end
|
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
|
12
|
+
options[:client].delete(key)
|
53
13
|
end
|
54
14
|
|
55
|
-
|
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
|
-
if options[:retry_timeout]
|
64
|
-
now = Time.now.to_f
|
65
|
-
break if now - start > options[:retry_timeout]
|
66
|
-
end
|
67
|
-
|
68
|
-
val, cas = client.get_cas(key)
|
69
|
-
|
70
|
-
# much like with initial set - ensure the key is here
|
71
|
-
if val.nil?
|
72
|
-
client.set(key, "")
|
73
|
-
next
|
74
|
-
end
|
75
|
-
|
76
|
-
locks = clear_expired_locks(deserialize_locks(val), options)
|
77
|
-
|
78
|
-
refresh_lock(locks, acquisition_token)
|
15
|
+
private
|
79
16
|
|
80
|
-
|
81
|
-
|
82
|
-
break if client.set_cas(key, newval, cas)
|
83
|
-
|
84
|
-
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
85
|
-
end
|
86
|
-
rescue => _
|
87
|
-
raise FailedToAcquireLock
|
88
|
-
end
|
17
|
+
def get(key, options)
|
18
|
+
options[:client].get_cas(key)
|
89
19
|
end
|
90
20
|
|
91
|
-
def
|
92
|
-
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
|
-
if options[:retry_timeout]
|
102
|
-
now = Time.now.to_f
|
103
|
-
break if now - start > options[:retry_timeout]
|
104
|
-
end
|
105
|
-
|
106
|
-
val, cas = client.get_cas(key)
|
107
|
-
|
108
|
-
break if val.nil? # lock has expired totally
|
109
|
-
|
110
|
-
locks = clear_expired_locks(deserialize_locks(val), options)
|
111
|
-
|
112
|
-
acquisition_lock = remove_lock(locks, acquisition_token)
|
113
|
-
|
114
|
-
break unless acquisition_lock
|
115
|
-
|
116
|
-
newval = serialize_locks(locks)
|
117
|
-
|
118
|
-
break if client.set_cas(key, newval, cas)
|
119
|
-
|
120
|
-
# another client cleared a token in the interim - try again!
|
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
|
21
|
+
def set(key, newval, cas, options)
|
22
|
+
options[:client].set_cas(key, newval, cas)
|
128
23
|
end
|
129
24
|
|
130
|
-
def
|
131
|
-
options
|
132
|
-
options[:client].delete(key)
|
25
|
+
def set_initial(key, options)
|
26
|
+
options[:client].set(key, "")
|
133
27
|
end
|
134
28
|
end
|
135
29
|
end
|
data/lib/suo/client/redis.rb
CHANGED
@@ -7,158 +7,35 @@ module Suo
|
|
7
7
|
end
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def
|
10
|
+
def clear(key, options = {})
|
11
11
|
options = merge_defaults(options)
|
12
|
-
|
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
|
-
if options[:retry_timeout]
|
21
|
-
now = Time.now.to_f
|
22
|
-
break if now - start > options[:retry_timeout]
|
23
|
-
end
|
24
|
-
|
25
|
-
client.watch(key) do
|
26
|
-
begin
|
27
|
-
val = client.get(key)
|
28
|
-
|
29
|
-
locks = clear_expired_locks(deserialize_locks(val.to_s), options)
|
30
|
-
|
31
|
-
if locks.size < resources
|
32
|
-
add_lock(locks, token)
|
33
|
-
|
34
|
-
newval = serialize_locks(locks)
|
35
|
-
|
36
|
-
ret = client.multi do |multi|
|
37
|
-
multi.set(key, newval)
|
38
|
-
end
|
39
|
-
|
40
|
-
acquisition_token = token if ret[0] == "OK"
|
41
|
-
end
|
42
|
-
ensure
|
43
|
-
client.unwatch
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
break if acquisition_token
|
48
|
-
|
49
|
-
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
50
|
-
end
|
51
|
-
rescue => _
|
52
|
-
raise Suo::Client::FailedToAcquireLock
|
53
|
-
end
|
54
|
-
|
55
|
-
acquisition_token
|
12
|
+
options[:client].del(key)
|
56
13
|
end
|
57
14
|
|
58
|
-
|
59
|
-
options = merge_defaults(options)
|
60
|
-
client = options[:client]
|
61
|
-
refreshed = false
|
62
|
-
|
63
|
-
begin
|
64
|
-
start = Time.now.to_f
|
65
|
-
|
66
|
-
options[:retry_count].times do
|
67
|
-
if options[:retry_timeout]
|
68
|
-
now = Time.now.to_f
|
69
|
-
break if now - start > options[:retry_timeout]
|
70
|
-
end
|
71
|
-
|
72
|
-
client.watch(key) do
|
73
|
-
begin
|
74
|
-
val = client.get(key)
|
75
|
-
|
76
|
-
locks = clear_expired_locks(deserialize_locks(val), options)
|
77
|
-
|
78
|
-
refresh_lock(locks, acquisition_token)
|
79
|
-
|
80
|
-
newval = serialize_locks(locks)
|
81
|
-
|
82
|
-
ret = client.multi do |multi|
|
83
|
-
multi.set(key, newval)
|
84
|
-
end
|
15
|
+
private
|
85
16
|
|
86
|
-
|
87
|
-
|
88
|
-
client.unwatch
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
break if refreshed
|
93
|
-
|
94
|
-
sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
|
95
|
-
end
|
96
|
-
rescue => _
|
97
|
-
raise Suo::Client::FailedToAcquireLock
|
98
|
-
end
|
17
|
+
def get(key, options)
|
18
|
+
[options[:client].get(key), nil]
|
99
19
|
end
|
100
20
|
|
101
|
-
def
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
return unless acquisition_token
|
106
|
-
|
107
|
-
begin
|
108
|
-
start = Time.now.to_f
|
109
|
-
|
110
|
-
options[:retry_count].times do
|
111
|
-
cleared = false
|
112
|
-
|
113
|
-
if options[:retry_timeout]
|
114
|
-
now = Time.now.to_f
|
115
|
-
break if now - start > options[:retry_timeout]
|
116
|
-
end
|
117
|
-
|
118
|
-
client.watch(key) do
|
119
|
-
begin
|
120
|
-
val = client.get(key)
|
121
|
-
|
122
|
-
if val.nil?
|
123
|
-
cleared = true
|
124
|
-
break
|
125
|
-
end
|
126
|
-
|
127
|
-
locks = clear_expired_locks(deserialize_locks(val), options)
|
128
|
-
|
129
|
-
acquisition_lock = remove_lock(locks, acquisition_token)
|
130
|
-
|
131
|
-
unless acquisition_lock
|
132
|
-
# token was already cleared
|
133
|
-
cleared = true
|
134
|
-
break
|
135
|
-
end
|
136
|
-
|
137
|
-
newval = serialize_locks(locks)
|
138
|
-
|
139
|
-
ret = client.multi do |multi|
|
140
|
-
multi.set(key, newval)
|
141
|
-
end
|
142
|
-
|
143
|
-
cleared = ret[0] == "OK"
|
144
|
-
ensure
|
145
|
-
client.unwatch
|
146
|
-
end
|
147
|
-
end
|
21
|
+
def set(key, newval, _, options)
|
22
|
+
ret = options[:client].multi do |multi|
|
23
|
+
multi.set(key, newval)
|
24
|
+
end
|
148
25
|
|
149
|
-
|
26
|
+
ret[0] == "OK"
|
27
|
+
end
|
150
28
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
# since it's optimistic locking - fine if we are unable to release
|
155
|
-
raise boom if ENV["SUO_TEST"]
|
29
|
+
def synchronize(key, options)
|
30
|
+
options[:client].watch(key) do
|
31
|
+
yield
|
156
32
|
end
|
33
|
+
ensure
|
34
|
+
options[:client].unwatch
|
157
35
|
end
|
158
36
|
|
159
|
-
def
|
160
|
-
options
|
161
|
-
options[:client].del(key)
|
37
|
+
def set_initial(key, options)
|
38
|
+
options[:client].set(key, "")
|
162
39
|
end
|
163
40
|
end
|
164
41
|
end
|
data/lib/suo/version.rb
CHANGED
data/test/client_test.rb
CHANGED
@@ -24,6 +24,7 @@ module ClientTests
|
|
24
24
|
@klass.unlock(TEST_KEY, lock1, client: @klass_client)
|
25
25
|
|
26
26
|
locked = @klass.locked?(TEST_KEY, 1, client: @klass_client)
|
27
|
+
|
27
28
|
assert_equal false, locked
|
28
29
|
end
|
29
30
|
|
@@ -74,8 +75,8 @@ module ClientTests
|
|
74
75
|
|
75
76
|
100.times.map do |i|
|
76
77
|
Thread.new do
|
77
|
-
success = @client.lock(TEST_KEY, 50, retry_timeout: 0.
|
78
|
-
sleep(
|
78
|
+
success = @client.lock(TEST_KEY, 50, retry_timeout: 0.5) do
|
79
|
+
sleep(2)
|
79
80
|
success_counter << i
|
80
81
|
end
|
81
82
|
|
@@ -94,7 +95,7 @@ module ClientTests
|
|
94
95
|
100.times.map do |i|
|
95
96
|
Thread.new do
|
96
97
|
success = @client.lock(TEST_KEY, 50, retry_timeout: 2) do
|
97
|
-
sleep(
|
98
|
+
sleep(0.5)
|
98
99
|
success_counter << i
|
99
100
|
end
|
100
101
|
|
@@ -114,7 +115,7 @@ class TestBaseClient < Minitest::Test
|
|
114
115
|
|
115
116
|
def test_not_implemented
|
116
117
|
assert_raises(NotImplementedError) do
|
117
|
-
@klass.
|
118
|
+
@klass.send(:get, TEST_KEY, {})
|
118
119
|
end
|
119
120
|
end
|
120
121
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: suo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Elser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-04-
|
11
|
+
date: 2015-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dalli
|