suo 0.3.0 → 0.4.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 +5 -5
- data/.github/workflows/CI.yml +38 -0
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +24 -0
- data/README.md +12 -3
- data/lib/suo/client/base.rb +18 -19
- data/lib/suo/client/memcached.rb +14 -6
- data/lib/suo/client/redis.rb +27 -12
- data/lib/suo/version.rb +1 -1
- data/suo.gemspec +4 -5
- data/test/client_test.rb +6 -0
- metadata +18 -21
- data/.travis.yml +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 76af80589128e43f0f5b0664f6604f657dd6735df18e18bf6134e49aae2656cb
|
4
|
+
data.tar.gz: '0982702d597aa9d91da01f3921cabfdbdceb2e2e61961c4c7d5e2438d76883db'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ce36f6537803bdd00962f79e8774b779afdd91546c6b5b7fc090372d7d2bdc0c9e00b34863a1fbbb9d8f5aa2a240af6907f96ff3a415304709132e19f084be3
|
7
|
+
data.tar.gz: 3b8324c772750b34cc5d3268573d10abdce23e2026000f35117f894673e2616f5c3dc2c41d627978ae848072e20e2072c70183b81161bba884b69d61473e8ef2
|
@@ -0,0 +1,38 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- master
|
7
|
+
pull_request:
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
build:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
fail-fast: false
|
14
|
+
matrix:
|
15
|
+
ruby:
|
16
|
+
- '2.5'
|
17
|
+
- '2.6'
|
18
|
+
- '2.7'
|
19
|
+
- '3.0'
|
20
|
+
- ruby-head
|
21
|
+
continue-on-error: ${{ matrix.ruby == 'ruby-head' }}
|
22
|
+
services:
|
23
|
+
memcached:
|
24
|
+
image: memcached
|
25
|
+
ports:
|
26
|
+
- 11211:11211
|
27
|
+
redis:
|
28
|
+
image: redis
|
29
|
+
ports:
|
30
|
+
- 6379:6379
|
31
|
+
steps:
|
32
|
+
- uses: actions/checkout@v2
|
33
|
+
- uses: ruby/setup-ruby@v1
|
34
|
+
with:
|
35
|
+
ruby-version: ${{ matrix.ruby }}
|
36
|
+
bundler-cache: true
|
37
|
+
- run: |
|
38
|
+
bundle exec rake
|
data/.rubocop.yml
CHANGED
@@ -74,7 +74,7 @@ Style/SpaceInsideBrackets:
|
|
74
74
|
Style/AndOr:
|
75
75
|
Enabled: false
|
76
76
|
|
77
|
-
Style/
|
77
|
+
Style/TrailingCommaInLiteral:
|
78
78
|
Enabled: true
|
79
79
|
|
80
80
|
Style/SpaceBeforeComma:
|
@@ -98,7 +98,7 @@ Style/SpaceAfterColon:
|
|
98
98
|
Style/SpaceAfterComma:
|
99
99
|
Enabled: true
|
100
100
|
|
101
|
-
Style/
|
101
|
+
Style/SpaceAroundKeyword:
|
102
102
|
Enabled: true
|
103
103
|
|
104
104
|
Style/SpaceAfterNot:
|
@@ -163,7 +163,7 @@ Style/StringLiterals:
|
|
163
163
|
EnforcedStyle: double_quotes
|
164
164
|
|
165
165
|
Metrics/CyclomaticComplexity:
|
166
|
-
Max:
|
166
|
+
Max: 10
|
167
167
|
|
168
168
|
Metrics/LineLength:
|
169
169
|
Max: 128
|
@@ -214,3 +214,6 @@ Metrics/ParameterLists:
|
|
214
214
|
|
215
215
|
Metrics/PerceivedComplexity:
|
216
216
|
Enabled: false
|
217
|
+
|
218
|
+
Style/Documentation:
|
219
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
## 0.4.0
|
2
|
+
|
3
|
+
- Monotonic clock for locks, avoiding issues with DST (thanks @doits)
|
4
|
+
- Pooled connection support (thanks @mlarraz)
|
5
|
+
- Switch to Github actions for tests (thanks @mlarraz)
|
6
|
+
- Update supported Ruby versions (thanks @mlarraz & @pat)
|
7
|
+
|
8
|
+
## 0.3.4
|
9
|
+
|
10
|
+
- Support for connection pooling when using memcached locks, via `with` blocks using Dalli (thanks to Lev).
|
11
|
+
|
12
|
+
## 0.3.3
|
13
|
+
|
14
|
+
- Default TTL for keys to allow for short-lived locking keys (thanks to Ian Remillard) without leaking memory.
|
15
|
+
- Vastly improve initial lock acquisition, especially on Redis (thanks to Jeremy Wadscak).
|
16
|
+
|
17
|
+
## 0.3.2
|
18
|
+
|
19
|
+
- Custom lock tokens (thanks to avokhmin).
|
20
|
+
|
21
|
+
## 0.3.1
|
22
|
+
|
23
|
+
- Slight memory leak fix.
|
24
|
+
|
1
25
|
## 0.3.0
|
2
26
|
|
3
27
|
- Dramatically simplify the interface by forcing clients to specify the key & resources at lock initialization instead of every method call.
|
data/README.md
CHANGED
@@ -34,7 +34,7 @@ end
|
|
34
34
|
# The resources argument is the number of resources the semaphore will allow to lock (defaulting to one - a mutex)
|
35
35
|
suo = Suo::Client::Memcached.new("bar_resource", client: some_dalli_client, resources: 2)
|
36
36
|
|
37
|
-
Thread.new { suo.lock{ puts "One"; sleep 2 } }
|
37
|
+
Thread.new { suo.lock { puts "One"; sleep 2 } }
|
38
38
|
Thread.new { suo.lock { puts "Two"; sleep 2 } }
|
39
39
|
Thread.new { suo.lock { puts "Three" } }
|
40
40
|
|
@@ -46,7 +46,7 @@ suo = Suo::Client::Memcached.new("protected_key", client: some_dalli_client, acq
|
|
46
46
|
# manually locking/unlocking
|
47
47
|
# the return value from lock without a block is a unique token valid only for the current lock
|
48
48
|
# which must be unlocked manually
|
49
|
-
token = suo
|
49
|
+
token = suo.lock
|
50
50
|
foo.baz!
|
51
51
|
suo.unlock(token)
|
52
52
|
|
@@ -72,12 +72,21 @@ suo.lock do |token|
|
|
72
72
|
end
|
73
73
|
```
|
74
74
|
|
75
|
+
### Time To Live
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
Suo::Client::Redis.new("bar_resource", ttl: 60) #ttl in seconds
|
79
|
+
```
|
80
|
+
|
81
|
+
A key representing a set of lockable resources is removed once the last resource lock is released and the `ttl` time runs out. When another lock is acquired and the key has been removed the key has to be recreated.
|
82
|
+
|
83
|
+
|
75
84
|
## TODO
|
76
85
|
- more race condition tests
|
77
86
|
|
78
87
|
## History
|
79
88
|
|
80
|
-
View the [changelog](https://github.com/nickelser/suo/blob/master/CHANGELOG.md)
|
89
|
+
View the [changelog](https://github.com/nickelser/suo/blob/master/CHANGELOG.md).
|
81
90
|
|
82
91
|
## Contributing
|
83
92
|
|
data/lib/suo/client/base.rb
CHANGED
@@ -5,25 +5,30 @@ module Suo
|
|
5
5
|
acquisition_timeout: 0.1,
|
6
6
|
acquisition_delay: 0.01,
|
7
7
|
stale_lock_expiration: 3600,
|
8
|
-
resources: 1
|
8
|
+
resources: 1,
|
9
|
+
ttl: 60,
|
9
10
|
}.freeze
|
10
11
|
|
12
|
+
BLANK_STR = "".freeze
|
13
|
+
|
11
14
|
attr_accessor :client, :key, :resources, :options
|
12
15
|
|
13
16
|
include MonitorMixin
|
14
17
|
|
15
18
|
def initialize(key, options = {})
|
16
19
|
fail "Client required" unless options[:client]
|
20
|
+
|
17
21
|
@options = DEFAULT_OPTIONS.merge(options)
|
18
22
|
@retry_count = (@options[:acquisition_timeout] / @options[:acquisition_delay].to_f).ceil
|
19
23
|
@client = @options[:client]
|
20
24
|
@resources = @options[:resources].to_i
|
21
25
|
@key = key
|
26
|
+
|
22
27
|
super() # initialize Monitor mixin for thread safety
|
23
28
|
end
|
24
29
|
|
25
|
-
def lock
|
26
|
-
token = acquire_lock
|
30
|
+
def lock(custom_token = nil)
|
31
|
+
token = acquire_lock(custom_token)
|
27
32
|
|
28
33
|
if block_given? && token
|
29
34
|
begin
|
@@ -51,16 +56,13 @@ module Suo
|
|
51
56
|
retry_with_timeout do
|
52
57
|
val, cas = get
|
53
58
|
|
54
|
-
if val.nil?
|
55
|
-
initial_set
|
56
|
-
next
|
57
|
-
end
|
59
|
+
cas = initial_set if val.nil?
|
58
60
|
|
59
61
|
cleared_locks = deserialize_and_clear_locks(val)
|
60
62
|
|
61
63
|
refresh_lock(cleared_locks, token)
|
62
64
|
|
63
|
-
break if set(serialize_locks(cleared_locks), cas)
|
65
|
+
break if set(serialize_locks(cleared_locks), cas, expire: cleared_locks.empty?)
|
64
66
|
end
|
65
67
|
end
|
66
68
|
|
@@ -77,7 +79,7 @@ module Suo
|
|
77
79
|
acquisition_lock = remove_lock(cleared_locks, token)
|
78
80
|
|
79
81
|
break unless acquisition_lock
|
80
|
-
break if set(serialize_locks(cleared_locks), cas)
|
82
|
+
break if set(serialize_locks(cleared_locks), cas, expire: cleared_locks.empty?)
|
81
83
|
end
|
82
84
|
rescue LockClientError => _ # rubocop:disable Lint/HandleExceptions
|
83
85
|
# ignore - assume success due to optimistic locking
|
@@ -91,16 +93,13 @@ module Suo
|
|
91
93
|
|
92
94
|
attr_accessor :retry_count
|
93
95
|
|
94
|
-
def acquire_lock
|
95
|
-
token
|
96
|
+
def acquire_lock(token = nil)
|
97
|
+
token ||= SecureRandom.base64(16)
|
96
98
|
|
97
99
|
retry_with_timeout do
|
98
100
|
val, cas = get
|
99
101
|
|
100
|
-
if val.nil?
|
101
|
-
initial_set
|
102
|
-
next
|
103
|
-
end
|
102
|
+
cas = initial_set if val.nil?
|
104
103
|
|
105
104
|
cleared_locks = deserialize_and_clear_locks(val)
|
106
105
|
|
@@ -124,7 +123,7 @@ module Suo
|
|
124
123
|
fail NotImplementedError
|
125
124
|
end
|
126
125
|
|
127
|
-
def initial_set(val =
|
126
|
+
def initial_set(val = BLANK_STR) # rubocop:disable Lint/UnusedMethodArgument
|
128
127
|
fail NotImplementedError
|
129
128
|
end
|
130
129
|
|
@@ -133,10 +132,10 @@ module Suo
|
|
133
132
|
end
|
134
133
|
|
135
134
|
def retry_with_timeout
|
136
|
-
start =
|
135
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
137
136
|
|
138
137
|
retry_count.times do
|
139
|
-
elapsed =
|
138
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
140
139
|
break if elapsed >= options[:acquisition_timeout]
|
141
140
|
|
142
141
|
synchronize do
|
@@ -158,7 +157,7 @@ module Suo
|
|
158
157
|
end
|
159
158
|
|
160
159
|
def deserialize_locks(val)
|
161
|
-
unpacked = (val.nil? || val ==
|
160
|
+
unpacked = (val.nil? || val == BLANK_STR) ? [] : MessagePack.unpack(val)
|
162
161
|
|
163
162
|
unpacked.map do |time, token|
|
164
163
|
[Time.at(time), token]
|
data/lib/suo/client/memcached.rb
CHANGED
@@ -7,21 +7,29 @@ module Suo
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def clear
|
10
|
-
@client.delete(@key)
|
10
|
+
@client.with { |client| client.delete(@key) }
|
11
11
|
end
|
12
12
|
|
13
13
|
private
|
14
14
|
|
15
15
|
def get
|
16
|
-
@client.get_cas(@key)
|
16
|
+
@client.with { |client| client.get_cas(@key) }
|
17
17
|
end
|
18
18
|
|
19
|
-
def set(newval, cas)
|
20
|
-
|
19
|
+
def set(newval, cas, expire: false)
|
20
|
+
if expire
|
21
|
+
@client.with { |client| client.set_cas(@key, newval, cas, @options[:ttl]) }
|
22
|
+
else
|
23
|
+
@client.with { |client| client.set_cas(@key, newval, cas) }
|
24
|
+
end
|
21
25
|
end
|
22
26
|
|
23
|
-
def initial_set(val =
|
24
|
-
@client.
|
27
|
+
def initial_set(val = BLANK_STR)
|
28
|
+
@client.with do |client|
|
29
|
+
client.set(@key, val)
|
30
|
+
_val, cas = client.get_cas(@key)
|
31
|
+
cas
|
32
|
+
end
|
25
33
|
end
|
26
34
|
end
|
27
35
|
end
|
data/lib/suo/client/redis.rb
CHANGED
@@ -1,39 +1,54 @@
|
|
1
1
|
module Suo
|
2
2
|
module Client
|
3
3
|
class Redis < Base
|
4
|
+
OK_STR = "OK".freeze
|
5
|
+
|
4
6
|
def initialize(key, options = {})
|
5
7
|
options[:client] ||= ::Redis.new(options[:connection] || {})
|
6
8
|
super
|
7
9
|
end
|
8
10
|
|
9
11
|
def clear
|
10
|
-
|
12
|
+
with { |r| r.del(@key) }
|
11
13
|
end
|
12
14
|
|
13
15
|
private
|
14
16
|
|
17
|
+
def with(&block)
|
18
|
+
if @client.respond_to?(:with)
|
19
|
+
@client.with(&block)
|
20
|
+
else
|
21
|
+
yield @client
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
15
25
|
def get
|
16
|
-
[
|
26
|
+
[with { |r| r.get(@key) }, nil]
|
17
27
|
end
|
18
28
|
|
19
|
-
def set(newval, _)
|
20
|
-
ret =
|
21
|
-
multi
|
29
|
+
def set(newval, _, expire: false)
|
30
|
+
ret = with do |r|
|
31
|
+
r.multi do |rr|
|
32
|
+
if expire
|
33
|
+
rr.setex(@key, @options[:ttl], newval)
|
34
|
+
else
|
35
|
+
rr.set(@key, newval)
|
36
|
+
end
|
37
|
+
end
|
22
38
|
end
|
23
39
|
|
24
|
-
ret && ret[0] ==
|
40
|
+
ret && ret[0] == OK_STR
|
25
41
|
end
|
26
42
|
|
27
43
|
def synchronize
|
28
|
-
|
29
|
-
yield
|
30
|
-
end
|
44
|
+
with { |r| r.watch(@key) { yield } }
|
31
45
|
ensure
|
32
|
-
|
46
|
+
with { |r| r.unwatch }
|
33
47
|
end
|
34
48
|
|
35
|
-
def initial_set(val =
|
36
|
-
|
49
|
+
def initial_set(val = BLANK_STR)
|
50
|
+
set(val, nil)
|
51
|
+
nil
|
37
52
|
end
|
38
53
|
end
|
39
54
|
end
|
data/lib/suo/version.rb
CHANGED
data/suo.gemspec
CHANGED
@@ -16,19 +16,18 @@ Gem::Specification.new do |spec|
|
|
16
16
|
|
17
17
|
spec.files = `git ls-files -z`.split("\x0")
|
18
18
|
spec.bindir = "bin"
|
19
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
20
|
spec.require_paths = ["lib"]
|
22
21
|
|
23
|
-
spec.required_ruby_version = "
|
22
|
+
spec.required_ruby_version = ">= 2.5"
|
24
23
|
|
25
24
|
spec.add_dependency "dalli"
|
26
25
|
spec.add_dependency "redis"
|
27
26
|
spec.add_dependency "msgpack"
|
28
27
|
|
29
|
-
spec.add_development_dependency "bundler"
|
30
|
-
spec.add_development_dependency "rake", "~>
|
31
|
-
spec.add_development_dependency "rubocop", "~> 0.
|
28
|
+
spec.add_development_dependency "bundler"
|
29
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
30
|
+
spec.add_development_dependency "rubocop", "~> 0.49.0"
|
32
31
|
spec.add_development_dependency "minitest", "~> 5.5.0"
|
33
32
|
spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4.7"
|
34
33
|
end
|
data/test/client_test.rb
CHANGED
@@ -31,6 +31,12 @@ module ClientTests
|
|
31
31
|
assert_equal false, locked
|
32
32
|
end
|
33
33
|
|
34
|
+
def test_lock_with_custom_token
|
35
|
+
token = 'foo-bar'
|
36
|
+
lock = @client.lock token
|
37
|
+
assert_equal lock, token
|
38
|
+
end
|
39
|
+
|
34
40
|
def test_empty_lock_on_invalid_data
|
35
41
|
@client.send(:initial_set, "bad value")
|
36
42
|
assert_equal false, @client.locked?
|
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.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Elser
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dalli
|
@@ -56,44 +56,44 @@ dependencies:
|
|
56
56
|
name: bundler
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rake
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '13.0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '13.0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rubocop
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: 0.
|
89
|
+
version: 0.49.0
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: 0.
|
96
|
+
version: 0.49.0
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: minitest
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -125,15 +125,13 @@ dependencies:
|
|
125
125
|
description: Distributed locks (mutexes & semaphores) using Memcached or Redis.
|
126
126
|
email:
|
127
127
|
- nick.elser@gmail.com
|
128
|
-
executables:
|
129
|
-
- console
|
130
|
-
- setup
|
128
|
+
executables: []
|
131
129
|
extensions: []
|
132
130
|
extra_rdoc_files: []
|
133
131
|
files:
|
132
|
+
- ".github/workflows/CI.yml"
|
134
133
|
- ".gitignore"
|
135
134
|
- ".rubocop.yml"
|
136
|
-
- ".travis.yml"
|
137
135
|
- CHANGELOG.md
|
138
136
|
- Gemfile
|
139
137
|
- LICENSE.txt
|
@@ -154,24 +152,23 @@ homepage: https://github.com/nickelser/suo
|
|
154
152
|
licenses:
|
155
153
|
- MIT
|
156
154
|
metadata: {}
|
157
|
-
post_install_message:
|
155
|
+
post_install_message:
|
158
156
|
rdoc_options: []
|
159
157
|
require_paths:
|
160
158
|
- lib
|
161
159
|
required_ruby_version: !ruby/object:Gem::Requirement
|
162
160
|
requirements:
|
163
|
-
- - "
|
161
|
+
- - ">="
|
164
162
|
- !ruby/object:Gem::Version
|
165
|
-
version: '2.
|
163
|
+
version: '2.5'
|
166
164
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
165
|
requirements:
|
168
166
|
- - ">="
|
169
167
|
- !ruby/object:Gem::Version
|
170
168
|
version: '0'
|
171
169
|
requirements: []
|
172
|
-
|
173
|
-
|
174
|
-
signing_key:
|
170
|
+
rubygems_version: 3.1.2
|
171
|
+
signing_key:
|
175
172
|
specification_version: 4
|
176
173
|
summary: Distributed locks (mutexes & semaphores) using Memcached or Redis.
|
177
174
|
test_files:
|