suo 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a30ddf4504c61a3c6e3896fea1300c8100140902
4
- data.tar.gz: ec8b97cf374d959037c8825e9f0a6131f487dcff
3
+ metadata.gz: 6334c57e0648ae8fefdeb1113664c3e6631dd03c
4
+ data.tar.gz: 0f35accaea812d463d911b1602a30372952ca19b
5
5
  SHA512:
6
- metadata.gz: e630516db130906d81fc7dafcb8c8532711710cfe6dbcd2932d2245a8294af716ac65b4d2551876acac7815a8d08593009489329c1f305f8f4927e64cb19b2e0
7
- data.tar.gz: 135a570b6781da6ffb9232845a6ef7a9c096edb63025588b77173b6b93f69e63ceeb8de41a8818aa2d4cbf3c9d15b4d7ed2f04ff6bf2973c3cea47d74bcd77f7
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 [![Build Status](https://travis-ci.org/nickelser/suo.png?branch=master)](https://travis-ci.org/nickelser/suo)
1
+ # Suo [![Build Status](https://travis-ci.org/nickelser/suo.svg?branch=master)](https://travis-ci.org/nickelser/suo) [![Gem Version](https://badge.fury.io/rb/suo.svg)](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
 
@@ -2,13 +2,13 @@ module Suo
2
2
  module Client
3
3
  class Base
4
4
  DEFAULT_OPTIONS = {
5
- retry_count: 3,
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).merge(_initialized: true)
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 = {}) # rubocop:disable Lint/UnusedMethodArgument
37
- fail NotImplementedError
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 = merge_defaults(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
- client = options[:client]
51
- locks = deserialize_locks(client.get(key))
72
+ val, _ = get(key, options)
73
+ locks = deserialize_locks(val)
52
74
 
53
- locks.size
75
+ locks
54
76
  end
55
77
 
56
- def refresh(key, acquisition_token, options = {}) # rubocop:disable Lint/UnusedMethodArgument
57
- fail NotImplementedError
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 = {}) # rubocop:disable Lint/UnusedMethodArgument
61
- fail NotImplementedError
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
- unless options[:_initialized]
70
- options = self::DEFAULT_OPTIONS.merge(options)
123
+ options = self::DEFAULT_OPTIONS.merge(options)
71
124
 
72
- fail "Client required" unless options[:client]
73
- end
125
+ fail "Client required" unless options[:client]
74
126
 
75
- if options[:retry_timeout]
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).map do |time, token|
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 => _
@@ -7,129 +7,23 @@ module Suo
7
7
  end
8
8
 
9
9
  class << self
10
- def lock(key, resources = 1, options = {})
10
+ def clear(key, options = {})
11
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
- 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
- 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
- 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
- newval = serialize_locks(locks)
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 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
- 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 clear(key, options = {})
131
- options = merge_defaults(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
@@ -7,158 +7,35 @@ module Suo
7
7
  end
8
8
 
9
9
  class << self
10
- def lock(key, resources = 1, options = {})
10
+ def clear(key, options = {})
11
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
- 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
- def refresh(key, acquisition_token, options = {})
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
- refreshed = ret[0] == "OK"
87
- ensure
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 unlock(key, acquisition_token, options = {})
102
- options = merge_defaults(options)
103
- client = options[:client]
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
- break if cleared
26
+ ret[0] == "OK"
27
+ end
150
28
 
151
- sleep(rand(options[:retry_delay] * 1000).to_f / 1000)
152
- end
153
- rescue => boom # rubocop:disable Lint/HandleExceptions
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 clear(key, options = {})
160
- options = merge_defaults(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
@@ -1,3 +1,3 @@
1
1
  module Suo
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
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.9) do
78
- sleep(1)
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(1)
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.lock(TEST_KEY, 1)
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.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-12 00:00:00.000000000 Z
11
+ date: 2015-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dalli