suo 0.2.3 → 0.3.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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +19 -21
- data/lib/suo/client/base.rb +45 -40
- data/lib/suo/client/memcached.rb +9 -9
- data/lib/suo/client/redis.rb +11 -11
- data/lib/suo/version.rb +1 -1
- data/suo.gemspec +2 -2
- data/test/client_test.rb +90 -91
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 32aa3b3d87eea1a5332765503443960363b69d96
|
4
|
+
data.tar.gz: 57feb579d0f4c168e811ba46d14712fc4cd41159
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0742cbe948509ebc83ece59c59c9179dbfc900a620a9d4beccf8e784f1dfe1ed5ab037b2686f18b988899634d23ac018c5741c823c840848c33fb5af87bc4e55
|
7
|
+
data.tar.gz: 7864bf86c8197c73d8016fb76a77ba9c8f506d2e1d8aef7c59798d2c0d9c368e5fa6ab556233e788c1ff307103ab242d0cb081596d69b40b38e7ff2b819b7c82
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
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
2
|
|
3
|
-
:lock: Distributed
|
3
|
+
:lock: Distributed semaphores using Memcached or Redis in Ruby.
|
4
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 (a mutex - sharing one resource),
|
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
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -18,38 +18,40 @@ gem 'suo'
|
|
18
18
|
|
19
19
|
```ruby
|
20
20
|
# Memcached
|
21
|
-
suo = Suo::Client::Memcached.new(connection: "127.0.0.1:11211")
|
21
|
+
suo = Suo::Client::Memcached.new("foo_resource", connection: "127.0.0.1:11211")
|
22
22
|
|
23
23
|
# Redis
|
24
|
-
suo = Suo::Client::Redis.new(connection: {host: "10.0.1.1"})
|
24
|
+
suo = Suo::Client::Redis.new("baz_resource", connection: {host: "10.0.1.1"})
|
25
25
|
|
26
26
|
# Pre-existing client
|
27
|
-
suo = Suo::Client::Memcached.new(client: some_dalli_client)
|
27
|
+
suo = Suo::Client::Memcached.new("bar_resource", client: some_dalli_client)
|
28
28
|
|
29
|
-
suo.lock
|
29
|
+
suo.lock do
|
30
30
|
# critical code here
|
31
31
|
@puppies.pet!
|
32
32
|
end
|
33
33
|
|
34
|
-
# The
|
35
|
-
|
36
|
-
|
37
|
-
Thread.new { suo.lock
|
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" } }
|
38
40
|
|
39
41
|
# will print "One" "Two", but not "Three", as there are only 2 resources
|
40
42
|
|
41
43
|
# custom acquisition timeouts (time to acquire)
|
42
|
-
suo = Suo::Client::Memcached.new(client: some_dalli_client, acquisition_timeout: 1) # in seconds
|
44
|
+
suo = Suo::Client::Memcached.new("protected_key", client: some_dalli_client, acquisition_timeout: 1) # in seconds
|
43
45
|
|
44
46
|
# manually locking/unlocking
|
45
47
|
# the return value from lock without a block is a unique token valid only for the current lock
|
46
48
|
# which must be unlocked manually
|
47
|
-
|
49
|
+
token = suo
|
48
50
|
foo.baz!
|
49
|
-
suo.unlock(
|
51
|
+
suo.unlock(token)
|
50
52
|
|
51
53
|
# custom stale lock expiration (cleaning of dead locks)
|
52
|
-
suo = Suo::Client::Redis.new(client: some_redis_client, stale_lock_expiration: 60*5)
|
54
|
+
suo = Suo::Client::Redis.new("other_key", client: some_redis_client, stale_lock_expiration: 60*5)
|
53
55
|
```
|
54
56
|
|
55
57
|
### Stale locks
|
@@ -59,21 +61,17 @@ suo = Suo::Client::Redis.new(client: some_redis_client, stale_lock_expiration: 6
|
|
59
61
|
To re-acquire a lock in the middle of a block, you can use the refresh method on client.
|
60
62
|
|
61
63
|
```ruby
|
62
|
-
suo = Suo::Client::Redis.new
|
64
|
+
suo = Suo::Client::Redis.new("foo")
|
63
65
|
|
64
66
|
# lock is the same token as seen in the manual example, above
|
65
|
-
suo.lock
|
67
|
+
suo.lock do |token|
|
66
68
|
5.times do
|
67
69
|
baz.bar!
|
68
|
-
suo.refresh(
|
70
|
+
suo.refresh(token)
|
69
71
|
end
|
70
72
|
end
|
71
73
|
```
|
72
74
|
|
73
|
-
## Semaphore
|
74
|
-
|
75
|
-
With multiple resources, Suo acts like a semaphore, but is not strictly a semaphore according to the traditional definition, as the lock acquires ownership.
|
76
|
-
|
77
75
|
## TODO
|
78
76
|
- more race condition tests
|
79
77
|
|
data/lib/suo/client/base.rb
CHANGED
@@ -4,96 +4,101 @@ module Suo
|
|
4
4
|
DEFAULT_OPTIONS = {
|
5
5
|
acquisition_timeout: 0.1,
|
6
6
|
acquisition_delay: 0.01,
|
7
|
-
stale_lock_expiration: 3600
|
7
|
+
stale_lock_expiration: 3600,
|
8
|
+
resources: 1
|
8
9
|
}.freeze
|
9
10
|
|
10
|
-
attr_accessor :client
|
11
|
+
attr_accessor :client, :key, :resources, :options
|
11
12
|
|
12
13
|
include MonitorMixin
|
13
14
|
|
14
|
-
def initialize(options = {})
|
15
|
+
def initialize(key, options = {})
|
15
16
|
fail "Client required" unless options[:client]
|
16
17
|
@options = DEFAULT_OPTIONS.merge(options)
|
17
18
|
@retry_count = (@options[:acquisition_timeout] / @options[:acquisition_delay].to_f).ceil
|
18
19
|
@client = @options[:client]
|
19
|
-
|
20
|
+
@resources = @options[:resources].to_i
|
21
|
+
@key = key
|
22
|
+
super() # initialize Monitor mixin for thread safety
|
20
23
|
end
|
21
24
|
|
22
|
-
def lock
|
23
|
-
token = acquire_lock
|
25
|
+
def lock
|
26
|
+
token = acquire_lock
|
24
27
|
|
25
28
|
if block_given? && token
|
26
29
|
begin
|
27
|
-
yield
|
30
|
+
yield
|
28
31
|
ensure
|
29
|
-
unlock(
|
32
|
+
unlock(token)
|
30
33
|
end
|
31
34
|
else
|
32
35
|
token
|
33
36
|
end
|
34
37
|
end
|
35
38
|
|
36
|
-
def locked?
|
37
|
-
locks
|
39
|
+
def locked?
|
40
|
+
locks.size >= resources
|
38
41
|
end
|
39
42
|
|
40
|
-
def locks
|
41
|
-
val, _ = get
|
43
|
+
def locks
|
44
|
+
val, _ = get
|
42
45
|
cleared_locks = deserialize_and_clear_locks(val)
|
43
46
|
|
44
47
|
cleared_locks
|
45
48
|
end
|
46
49
|
|
47
|
-
def refresh(
|
48
|
-
retry_with_timeout
|
49
|
-
val, cas = get
|
50
|
+
def refresh(token)
|
51
|
+
retry_with_timeout do
|
52
|
+
val, cas = get
|
50
53
|
|
51
54
|
if val.nil?
|
52
|
-
initial_set
|
55
|
+
initial_set
|
53
56
|
next
|
54
57
|
end
|
55
58
|
|
56
59
|
cleared_locks = deserialize_and_clear_locks(val)
|
57
60
|
|
58
|
-
refresh_lock(cleared_locks,
|
61
|
+
refresh_lock(cleared_locks, token)
|
59
62
|
|
60
|
-
break if set(
|
63
|
+
break if set(serialize_locks(cleared_locks), cas)
|
61
64
|
end
|
62
65
|
end
|
63
66
|
|
64
|
-
def unlock(
|
65
|
-
return unless
|
67
|
+
def unlock(token)
|
68
|
+
return unless token
|
66
69
|
|
67
|
-
retry_with_timeout
|
68
|
-
val, cas = get
|
70
|
+
retry_with_timeout do
|
71
|
+
val, cas = get
|
69
72
|
|
70
73
|
break if val.nil?
|
71
74
|
|
72
75
|
cleared_locks = deserialize_and_clear_locks(val)
|
73
76
|
|
74
|
-
acquisition_lock = remove_lock(cleared_locks,
|
77
|
+
acquisition_lock = remove_lock(cleared_locks, token)
|
75
78
|
|
76
79
|
break unless acquisition_lock
|
77
|
-
break if set(
|
80
|
+
break if set(serialize_locks(cleared_locks), cas)
|
78
81
|
end
|
79
82
|
rescue LockClientError => _ # rubocop:disable Lint/HandleExceptions
|
80
83
|
# ignore - assume success due to optimistic locking
|
81
84
|
end
|
82
85
|
|
83
|
-
def clear
|
86
|
+
def clear
|
84
87
|
fail NotImplementedError
|
85
88
|
end
|
86
89
|
|
87
90
|
private
|
88
91
|
|
89
|
-
|
92
|
+
attr_accessor :retry_count
|
93
|
+
|
94
|
+
def acquire_lock
|
90
95
|
token = SecureRandom.base64(16)
|
91
96
|
|
92
|
-
retry_with_timeout
|
93
|
-
val, cas = get
|
97
|
+
retry_with_timeout do
|
98
|
+
val, cas = get
|
94
99
|
|
95
100
|
if val.nil?
|
96
|
-
initial_set
|
101
|
+
initial_set
|
97
102
|
next
|
98
103
|
end
|
99
104
|
|
@@ -104,41 +109,41 @@ module Suo
|
|
104
109
|
|
105
110
|
newval = serialize_locks(cleared_locks)
|
106
111
|
|
107
|
-
return token if set(
|
112
|
+
return token if set(newval, cas)
|
108
113
|
end
|
109
114
|
end
|
110
115
|
|
111
116
|
nil
|
112
117
|
end
|
113
118
|
|
114
|
-
def get
|
119
|
+
def get
|
115
120
|
fail NotImplementedError
|
116
121
|
end
|
117
122
|
|
118
|
-
def set(
|
123
|
+
def set(newval, cas) # rubocop:disable Lint/UnusedMethodArgument
|
119
124
|
fail NotImplementedError
|
120
125
|
end
|
121
126
|
|
122
|
-
def initial_set(
|
127
|
+
def initial_set(val = "") # rubocop:disable Lint/UnusedMethodArgument
|
123
128
|
fail NotImplementedError
|
124
129
|
end
|
125
130
|
|
126
|
-
def synchronize
|
131
|
+
def synchronize
|
127
132
|
mon_synchronize { yield }
|
128
133
|
end
|
129
134
|
|
130
|
-
def retry_with_timeout
|
135
|
+
def retry_with_timeout
|
131
136
|
start = Time.now.to_f
|
132
137
|
|
133
|
-
|
138
|
+
retry_count.times do
|
134
139
|
elapsed = Time.now.to_f - start
|
135
|
-
break if elapsed >=
|
140
|
+
break if elapsed >= options[:acquisition_timeout]
|
136
141
|
|
137
|
-
synchronize
|
142
|
+
synchronize do
|
138
143
|
yield
|
139
144
|
end
|
140
145
|
|
141
|
-
sleep(rand(
|
146
|
+
sleep(rand(options[:acquisition_delay] * 1000).to_f / 1000)
|
142
147
|
end
|
143
148
|
rescue => _
|
144
149
|
raise LockClientError
|
@@ -163,7 +168,7 @@ module Suo
|
|
163
168
|
end
|
164
169
|
|
165
170
|
def clear_expired_locks(locks)
|
166
|
-
expired = Time.now -
|
171
|
+
expired = Time.now - options[:stale_lock_expiration]
|
167
172
|
locks.reject { |time, _| time < expired }
|
168
173
|
end
|
169
174
|
|
data/lib/suo/client/memcached.rb
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
module Suo
|
2
2
|
module Client
|
3
3
|
class Memcached < Base
|
4
|
-
def initialize(options = {})
|
4
|
+
def initialize(key, options = {})
|
5
5
|
options[:client] ||= Dalli::Client.new(options[:connection] || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
|
6
6
|
super
|
7
7
|
end
|
8
8
|
|
9
|
-
def clear
|
10
|
-
@client.delete(key)
|
9
|
+
def clear
|
10
|
+
@client.delete(@key)
|
11
11
|
end
|
12
12
|
|
13
13
|
private
|
14
14
|
|
15
|
-
def get
|
16
|
-
@client.get_cas(key)
|
15
|
+
def get
|
16
|
+
@client.get_cas(@key)
|
17
17
|
end
|
18
18
|
|
19
|
-
def set(
|
20
|
-
@client.set_cas(key, newval, cas)
|
19
|
+
def set(newval, cas)
|
20
|
+
@client.set_cas(@key, newval, cas)
|
21
21
|
end
|
22
22
|
|
23
|
-
def initial_set(
|
24
|
-
@client.set(key, val)
|
23
|
+
def initial_set(val = "")
|
24
|
+
@client.set(@key, val)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
data/lib/suo/client/redis.rb
CHANGED
@@ -1,39 +1,39 @@
|
|
1
1
|
module Suo
|
2
2
|
module Client
|
3
3
|
class Redis < Base
|
4
|
-
def initialize(options = {})
|
4
|
+
def initialize(key, options = {})
|
5
5
|
options[:client] ||= ::Redis.new(options[:connection] || {})
|
6
6
|
super
|
7
7
|
end
|
8
8
|
|
9
|
-
def clear
|
10
|
-
@client.del(key)
|
9
|
+
def clear
|
10
|
+
@client.del(@key)
|
11
11
|
end
|
12
12
|
|
13
13
|
private
|
14
14
|
|
15
|
-
def get
|
16
|
-
[@client.get(key), nil]
|
15
|
+
def get
|
16
|
+
[@client.get(@key), nil]
|
17
17
|
end
|
18
18
|
|
19
|
-
def set(
|
19
|
+
def set(newval, _)
|
20
20
|
ret = @client.multi do |multi|
|
21
|
-
multi.set(key, newval)
|
21
|
+
multi.set(@key, newval)
|
22
22
|
end
|
23
23
|
|
24
24
|
ret && ret[0] == "OK"
|
25
25
|
end
|
26
26
|
|
27
|
-
def synchronize
|
28
|
-
@client.watch(key) do
|
27
|
+
def synchronize
|
28
|
+
@client.watch(@key) do
|
29
29
|
yield
|
30
30
|
end
|
31
31
|
ensure
|
32
32
|
@client.unwatch
|
33
33
|
end
|
34
34
|
|
35
|
-
def initial_set(
|
36
|
-
@client.set(key, val)
|
35
|
+
def initial_set(val = "")
|
36
|
+
@client.set(@key, val)
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
data/lib/suo/version.rb
CHANGED
data/suo.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Nick Elser"]
|
10
10
|
spec.email = ["nick.elser@gmail.com"]
|
11
11
|
|
12
|
-
spec.summary = %q(Distributed locks using Memcached or Redis.)
|
13
|
-
spec.description = %q(Distributed locks using Memcached or Redis.)
|
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
14
|
spec.homepage = "https://github.com/nickelser/suo"
|
15
15
|
spec.license = "MIT"
|
16
16
|
|
data/test/client_test.rb
CHANGED
@@ -4,123 +4,121 @@ TEST_KEY = "suo_test_key".freeze
|
|
4
4
|
|
5
5
|
module ClientTests
|
6
6
|
def client(options = {})
|
7
|
-
@client.class.new(options.merge(client: @client.client))
|
7
|
+
@client.class.new(options[:key] || TEST_KEY, options.merge(client: @client.client))
|
8
8
|
end
|
9
9
|
|
10
10
|
def test_throws_failed_error_on_bad_client
|
11
11
|
assert_raises(Suo::LockClientError) do
|
12
|
-
client = @client.class.new(client: {})
|
13
|
-
client.lock
|
12
|
+
client = @client.class.new(TEST_KEY, client: {})
|
13
|
+
client.lock
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
17
|
def test_single_resource_locking
|
18
|
-
lock1 = @client.lock
|
18
|
+
lock1 = @client.lock
|
19
19
|
refute_nil lock1
|
20
20
|
|
21
|
-
locked = @client.locked?
|
21
|
+
locked = @client.locked?
|
22
22
|
assert_equal true, locked
|
23
23
|
|
24
|
-
lock2 = @client.lock
|
24
|
+
lock2 = @client.lock
|
25
25
|
assert_nil lock2
|
26
26
|
|
27
|
-
@client.unlock(
|
27
|
+
@client.unlock(lock1)
|
28
28
|
|
29
|
-
locked = @client.locked?
|
29
|
+
locked = @client.locked?
|
30
30
|
|
31
31
|
assert_equal false, locked
|
32
32
|
end
|
33
33
|
|
34
34
|
def test_empty_lock_on_invalid_data
|
35
|
-
@client.send(:initial_set,
|
36
|
-
|
37
|
-
assert_equal false, locked
|
35
|
+
@client.send(:initial_set, "bad value")
|
36
|
+
assert_equal false, @client.locked?
|
38
37
|
end
|
39
38
|
|
40
39
|
def test_clear
|
41
|
-
lock1 = @client.lock
|
40
|
+
lock1 = @client.lock
|
42
41
|
refute_nil lock1
|
43
42
|
|
44
|
-
@client.clear
|
45
|
-
|
46
|
-
locked = @client.locked?(TEST_KEY, 1)
|
43
|
+
@client.clear
|
47
44
|
|
48
|
-
assert_equal false, locked
|
45
|
+
assert_equal false, @client.locked?
|
49
46
|
end
|
50
47
|
|
51
48
|
def test_multiple_resource_locking
|
52
|
-
|
49
|
+
@client = client(resources: 2)
|
50
|
+
|
51
|
+
lock1 = @client.lock
|
53
52
|
refute_nil lock1
|
54
53
|
|
55
|
-
|
56
|
-
assert_equal false, locked
|
54
|
+
assert_equal false, @client.locked?
|
57
55
|
|
58
|
-
lock2 = @client.lock
|
56
|
+
lock2 = @client.lock
|
59
57
|
refute_nil lock2
|
60
58
|
|
61
|
-
|
62
|
-
assert_equal true, locked
|
59
|
+
assert_equal true, @client.locked?
|
63
60
|
|
64
|
-
@client.unlock(
|
61
|
+
@client.unlock(lock1)
|
65
62
|
|
66
|
-
|
67
|
-
assert_equal true, locked
|
63
|
+
assert_equal false, @client.locked?
|
68
64
|
|
69
|
-
@client.
|
65
|
+
assert_equal 1, @client.locks.size
|
70
66
|
|
71
|
-
|
72
|
-
|
67
|
+
@client.unlock(lock2)
|
68
|
+
|
69
|
+
assert_equal false, @client.locked?
|
70
|
+
assert_equal 0, @client.locks.size
|
73
71
|
end
|
74
72
|
|
75
73
|
def test_block_single_resource_locking
|
76
74
|
locked = false
|
77
75
|
|
78
|
-
@client.lock
|
76
|
+
@client.lock { locked = true }
|
79
77
|
|
80
78
|
assert_equal true, locked
|
81
79
|
end
|
82
80
|
|
83
81
|
def test_block_unlocks_on_exception
|
84
82
|
assert_raises(RuntimeError) do
|
85
|
-
@client.lock
|
83
|
+
@client.lock{ fail "Test" }
|
86
84
|
end
|
87
85
|
|
88
|
-
|
89
|
-
assert_equal false, locked
|
86
|
+
assert_equal false, @client.locked?
|
90
87
|
end
|
91
88
|
|
92
89
|
def test_readme_example
|
93
90
|
output = Queue.new
|
91
|
+
@client = client(resources: 2)
|
94
92
|
threads = []
|
95
93
|
|
96
|
-
threads << Thread.new { @client.lock
|
97
|
-
threads << Thread.new { @client.lock
|
94
|
+
threads << Thread.new { @client.lock { output << "One"; sleep 0.5 } }
|
95
|
+
threads << Thread.new { @client.lock { output << "Two"; sleep 0.5 } }
|
98
96
|
sleep 0.1
|
99
|
-
threads << Thread.new { @client.lock
|
97
|
+
threads << Thread.new { @client.lock { output << "Three" } }
|
100
98
|
|
101
99
|
threads.each(&:join)
|
102
100
|
|
103
101
|
ret = []
|
104
102
|
|
105
|
-
ret << output.pop
|
106
|
-
ret << output.pop
|
103
|
+
ret << (output.size > 0 ? output.pop : nil)
|
104
|
+
ret << (output.size > 0 ? output.pop : nil)
|
107
105
|
|
108
106
|
ret.sort!
|
109
107
|
|
110
108
|
assert_equal 0, output.size
|
111
109
|
assert_equal %w(One Two), ret
|
112
|
-
assert_equal false, @client.locked?
|
110
|
+
assert_equal false, @client.locked?
|
113
111
|
end
|
114
112
|
|
115
113
|
def test_block_multiple_resource_locking
|
116
114
|
success_counter = Queue.new
|
117
115
|
failure_counter = Queue.new
|
118
116
|
|
119
|
-
client = client(acquisition_timeout: 0.9)
|
117
|
+
@client = client(acquisition_timeout: 0.9, resources: 50)
|
120
118
|
|
121
119
|
100.times.map do |i|
|
122
120
|
Thread.new do
|
123
|
-
success = client.lock
|
121
|
+
success = @client.lock do
|
124
122
|
sleep(3)
|
125
123
|
success_counter << i
|
126
124
|
end
|
@@ -131,18 +129,18 @@ module ClientTests
|
|
131
129
|
|
132
130
|
assert_equal 50, success_counter.size
|
133
131
|
assert_equal 50, failure_counter.size
|
134
|
-
assert_equal false, client.locked?
|
132
|
+
assert_equal false, @client.locked?
|
135
133
|
end
|
136
134
|
|
137
135
|
def test_block_multiple_resource_locking_longer_timeout
|
138
136
|
success_counter = Queue.new
|
139
137
|
failure_counter = Queue.new
|
140
138
|
|
141
|
-
client = client(acquisition_timeout: 3)
|
139
|
+
@client = client(acquisition_timeout: 3, resources: 50)
|
142
140
|
|
143
141
|
100.times.map do |i|
|
144
142
|
Thread.new do
|
145
|
-
success = client.lock
|
143
|
+
success = @client.lock do
|
146
144
|
sleep(0.5)
|
147
145
|
success_counter << i
|
148
146
|
end
|
@@ -153,19 +151,19 @@ module ClientTests
|
|
153
151
|
|
154
152
|
assert_equal 100, success_counter.size
|
155
153
|
assert_equal 0, failure_counter.size
|
156
|
-
assert_equal false, client.locked?
|
154
|
+
assert_equal false, @client.locked?
|
157
155
|
end
|
158
156
|
|
159
157
|
def test_unstale_lock_acquisition
|
160
158
|
success_counter = Queue.new
|
161
159
|
failure_counter = Queue.new
|
162
160
|
|
163
|
-
client = client(stale_lock_expiration: 0.5)
|
161
|
+
@client = client(stale_lock_expiration: 0.5)
|
164
162
|
|
165
|
-
t1 = Thread.new { client.lock
|
163
|
+
t1 = Thread.new { @client.lock { sleep 0.6; success_counter << 1 } }
|
166
164
|
sleep 0.3
|
167
165
|
t2 = Thread.new do
|
168
|
-
locked = client.lock
|
166
|
+
locked = @client.lock { success_counter << 1 }
|
169
167
|
failure_counter << 1 unless locked
|
170
168
|
end
|
171
169
|
|
@@ -173,19 +171,19 @@ module ClientTests
|
|
173
171
|
|
174
172
|
assert_equal 1, success_counter.size
|
175
173
|
assert_equal 1, failure_counter.size
|
176
|
-
assert_equal false, client.locked?
|
174
|
+
assert_equal false, @client.locked?
|
177
175
|
end
|
178
176
|
|
179
177
|
def test_stale_lock_acquisition
|
180
178
|
success_counter = Queue.new
|
181
179
|
failure_counter = Queue.new
|
182
180
|
|
183
|
-
client = client(stale_lock_expiration: 0.5)
|
181
|
+
@client = client(stale_lock_expiration: 0.5)
|
184
182
|
|
185
|
-
t1 = Thread.new { client.lock
|
183
|
+
t1 = Thread.new { @client.lock { sleep 0.6; success_counter << 1 } }
|
186
184
|
sleep 0.55
|
187
185
|
t2 = Thread.new do
|
188
|
-
locked = client.lock
|
186
|
+
locked = @client.lock { success_counter << 1 }
|
189
187
|
failure_counter << 1 unless locked
|
190
188
|
end
|
191
189
|
|
@@ -193,59 +191,59 @@ module ClientTests
|
|
193
191
|
|
194
192
|
assert_equal 2, success_counter.size
|
195
193
|
assert_equal 0, failure_counter.size
|
196
|
-
assert_equal false, client.locked?
|
194
|
+
assert_equal false, @client.locked?
|
197
195
|
end
|
198
196
|
|
199
197
|
def test_refresh
|
200
|
-
client = client(stale_lock_expiration: 0.5)
|
198
|
+
@client = client(stale_lock_expiration: 0.5)
|
201
199
|
|
202
|
-
lock1 = client.lock
|
200
|
+
lock1 = @client.lock
|
203
201
|
|
204
|
-
assert_equal true, client.locked?
|
202
|
+
assert_equal true, @client.locked?
|
205
203
|
|
206
|
-
client.refresh(
|
204
|
+
@client.refresh(lock1)
|
207
205
|
|
208
|
-
assert_equal true, client.locked?
|
206
|
+
assert_equal true, @client.locked?
|
209
207
|
|
210
208
|
sleep 0.55
|
211
209
|
|
212
|
-
assert_equal false, client.locked?
|
210
|
+
assert_equal false, @client.locked?
|
213
211
|
|
214
|
-
lock2 = client.lock
|
212
|
+
lock2 = @client.lock
|
215
213
|
|
216
|
-
client.refresh(
|
214
|
+
@client.refresh(lock1)
|
217
215
|
|
218
|
-
assert_equal true, client.locked?
|
216
|
+
assert_equal true, @client.locked?
|
219
217
|
|
220
|
-
client.unlock(
|
218
|
+
@client.unlock(lock1)
|
221
219
|
|
222
220
|
# edge case with refresh lock in the middle
|
223
|
-
assert_equal true, client.locked?
|
221
|
+
assert_equal true, @client.locked?
|
224
222
|
|
225
|
-
client.clear
|
223
|
+
@client.clear
|
226
224
|
|
227
|
-
assert_equal false, client.locked?
|
225
|
+
assert_equal false, @client.locked?
|
228
226
|
|
229
|
-
client.refresh(
|
227
|
+
@client.refresh(lock2)
|
230
228
|
|
231
|
-
assert_equal true, client.locked?
|
229
|
+
assert_equal true, @client.locked?
|
232
230
|
|
233
|
-
client.unlock(
|
231
|
+
@client.unlock(lock2)
|
234
232
|
|
235
233
|
# now finally unlocked
|
236
|
-
assert_equal false, client.locked?
|
234
|
+
assert_equal false, @client.locked?
|
237
235
|
end
|
238
236
|
|
239
237
|
def test_block_refresh
|
240
238
|
success_counter = Queue.new
|
241
239
|
failure_counter = Queue.new
|
242
240
|
|
243
|
-
client = client(stale_lock_expiration: 0.5)
|
241
|
+
@client = client(stale_lock_expiration: 0.5)
|
244
242
|
|
245
243
|
t1 = Thread.new do
|
246
|
-
client.lock
|
244
|
+
@client.lock do |token|
|
247
245
|
sleep 0.6
|
248
|
-
client.refresh(
|
246
|
+
@client.refresh(token)
|
249
247
|
sleep 1
|
250
248
|
success_counter << 1
|
251
249
|
end
|
@@ -253,7 +251,7 @@ module ClientTests
|
|
253
251
|
|
254
252
|
t2 = Thread.new do
|
255
253
|
sleep 0.8
|
256
|
-
locked = client.lock
|
254
|
+
locked = @client.lock { success_counter << 1 }
|
257
255
|
failure_counter << 1 unless locked
|
258
256
|
end
|
259
257
|
|
@@ -261,19 +259,19 @@ module ClientTests
|
|
261
259
|
|
262
260
|
assert_equal 1, success_counter.size
|
263
261
|
assert_equal 1, failure_counter.size
|
264
|
-
assert_equal false, client.locked?
|
262
|
+
assert_equal false, @client.locked?
|
265
263
|
end
|
266
264
|
|
267
265
|
def test_refresh_multi
|
268
266
|
success_counter = Queue.new
|
269
267
|
failure_counter = Queue.new
|
270
268
|
|
271
|
-
client = client(stale_lock_expiration: 0.5)
|
269
|
+
@client = client(stale_lock_expiration: 0.5, resources: 2)
|
272
270
|
|
273
271
|
t1 = Thread.new do
|
274
|
-
client.lock
|
272
|
+
@client.lock do |token|
|
275
273
|
sleep 0.4
|
276
|
-
client.refresh(
|
274
|
+
@client.refresh(token)
|
277
275
|
success_counter << 1
|
278
276
|
sleep 0.5
|
279
277
|
end
|
@@ -281,7 +279,7 @@ module ClientTests
|
|
281
279
|
|
282
280
|
t2 = Thread.new do
|
283
281
|
sleep 0.55
|
284
|
-
locked = client.lock
|
282
|
+
locked = @client.lock do
|
285
283
|
success_counter << 1
|
286
284
|
sleep 0.5
|
287
285
|
end
|
@@ -291,7 +289,7 @@ module ClientTests
|
|
291
289
|
|
292
290
|
t3 = Thread.new do
|
293
291
|
sleep 0.75
|
294
|
-
locked = client.lock
|
292
|
+
locked = @client.lock { success_counter << 1 }
|
295
293
|
failure_counter << 1 unless locked
|
296
294
|
end
|
297
295
|
|
@@ -299,7 +297,7 @@ module ClientTests
|
|
299
297
|
|
300
298
|
assert_equal 2, success_counter.size
|
301
299
|
assert_equal 1, failure_counter.size
|
302
|
-
assert_equal false, client.locked?
|
300
|
+
assert_equal false, @client.locked?
|
303
301
|
end
|
304
302
|
|
305
303
|
def test_increment_reused_client
|
@@ -307,14 +305,14 @@ module ClientTests
|
|
307
305
|
|
308
306
|
threads = 2.times.map do
|
309
307
|
Thread.new do
|
310
|
-
@client.lock
|
308
|
+
@client.lock { i += 1 }
|
311
309
|
end
|
312
310
|
end
|
313
311
|
|
314
312
|
threads.each(&:join)
|
315
313
|
|
316
314
|
assert_equal 2, i
|
317
|
-
assert_equal false, client.locked?
|
315
|
+
assert_equal false, @client.locked?
|
318
316
|
end
|
319
317
|
|
320
318
|
def test_increment_new_client
|
@@ -322,37 +320,38 @@ module ClientTests
|
|
322
320
|
|
323
321
|
threads = 2.times.map do
|
324
322
|
Thread.new do
|
325
|
-
|
323
|
+
# note this is the method that generates a *new* client
|
324
|
+
client.lock { i += 1 }
|
326
325
|
end
|
327
326
|
end
|
328
327
|
|
329
328
|
threads.each(&:join)
|
330
329
|
|
331
330
|
assert_equal 2, i
|
332
|
-
assert_equal false, client.locked?
|
331
|
+
assert_equal false, @client.locked?
|
333
332
|
end
|
334
333
|
end
|
335
334
|
|
336
335
|
class TestBaseClient < Minitest::Test
|
337
336
|
def setup
|
338
|
-
@client = Suo::Client::Base.new(client: {})
|
337
|
+
@client = Suo::Client::Base.new(TEST_KEY, client: {})
|
339
338
|
end
|
340
339
|
|
341
340
|
def test_not_implemented
|
342
341
|
assert_raises(NotImplementedError) do
|
343
|
-
@client.send(:get
|
342
|
+
@client.send(:get)
|
344
343
|
end
|
345
344
|
|
346
345
|
assert_raises(NotImplementedError) do
|
347
|
-
@client.send(:set,
|
346
|
+
@client.send(:set, "", "")
|
348
347
|
end
|
349
348
|
|
350
349
|
assert_raises(NotImplementedError) do
|
351
|
-
@client.send(:initial_set
|
350
|
+
@client.send(:initial_set)
|
352
351
|
end
|
353
352
|
|
354
353
|
assert_raises(NotImplementedError) do
|
355
|
-
@client.send(:clear
|
354
|
+
@client.send(:clear)
|
356
355
|
end
|
357
356
|
end
|
358
357
|
end
|
@@ -362,7 +361,7 @@ class TestMemcachedClient < Minitest::Test
|
|
362
361
|
|
363
362
|
def setup
|
364
363
|
@dalli = Dalli::Client.new("127.0.0.1:11211")
|
365
|
-
@client = Suo::Client::Memcached.new
|
364
|
+
@client = Suo::Client::Memcached.new(TEST_KEY)
|
366
365
|
teardown
|
367
366
|
end
|
368
367
|
|
@@ -376,7 +375,7 @@ class TestRedisClient < Minitest::Test
|
|
376
375
|
|
377
376
|
def setup
|
378
377
|
@redis = Redis.new
|
379
|
-
@client = Suo::Client::Redis.new
|
378
|
+
@client = Suo::Client::Redis.new(TEST_KEY)
|
380
379
|
teardown
|
381
380
|
end
|
382
381
|
|
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.
|
4
|
+
version: 0.3.0
|
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-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dalli
|
@@ -122,7 +122,7 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: 0.4.7
|
125
|
-
description: Distributed locks using Memcached or Redis.
|
125
|
+
description: Distributed locks (mutexes & semaphores) using Memcached or Redis.
|
126
126
|
email:
|
127
127
|
- nick.elser@gmail.com
|
128
128
|
executables:
|
@@ -173,7 +173,7 @@ rubyforge_project:
|
|
173
173
|
rubygems_version: 2.4.5
|
174
174
|
signing_key:
|
175
175
|
specification_version: 4
|
176
|
-
summary: Distributed locks using Memcached or Redis.
|
176
|
+
summary: Distributed locks (mutexes & semaphores) using Memcached or Redis.
|
177
177
|
test_files:
|
178
178
|
- test/client_test.rb
|
179
179
|
- test/test_helper.rb
|