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 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