suo 0.3.0 → 0.4.0

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
- SHA1:
3
- metadata.gz: 32aa3b3d87eea1a5332765503443960363b69d96
4
- data.tar.gz: 57feb579d0f4c168e811ba46d14712fc4cd41159
2
+ SHA256:
3
+ metadata.gz: 76af80589128e43f0f5b0664f6604f657dd6735df18e18bf6134e49aae2656cb
4
+ data.tar.gz: '0982702d597aa9d91da01f3921cabfdbdceb2e2e61961c4c7d5e2438d76883db'
5
5
  SHA512:
6
- metadata.gz: 0742cbe948509ebc83ece59c59c9179dbfc900a620a9d4beccf8e784f1dfe1ed5ab037b2686f18b988899634d23ac018c5741c823c840848c33fb5af87bc4e55
7
- data.tar.gz: 7864bf86c8197c73d8016fb76a77ba9c8f506d2e1d8aef7c59798d2c0d9c368e5fa6ab556233e788c1ff307103ab242d0cb081596d69b40b38e7ff2b819b7c82
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
@@ -74,7 +74,7 @@ Style/SpaceInsideBrackets:
74
74
  Style/AndOr:
75
75
  Enabled: false
76
76
 
77
- Style/TrailingComma:
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/SpaceAfterControlKeyword:
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: 8
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
@@ -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
 
@@ -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 = SecureRandom.base64(16)
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 = "") # rubocop:disable Lint/UnusedMethodArgument
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 = Time.now.to_f
135
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
137
136
 
138
137
  retry_count.times do
139
- elapsed = Time.now.to_f - start
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 == "") ? [] : MessagePack.unpack(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]
@@ -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
- @client.set_cas(@key, newval, cas)
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.set(@key, val)
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
@@ -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
- @client.del(@key)
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
- [@client.get(@key), nil]
26
+ [with { |r| r.get(@key) }, nil]
17
27
  end
18
28
 
19
- def set(newval, _)
20
- ret = @client.multi do |multi|
21
- multi.set(@key, newval)
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] == "OK"
40
+ ret && ret[0] == OK_STR
25
41
  end
26
42
 
27
43
  def synchronize
28
- @client.watch(@key) do
29
- yield
30
- end
44
+ with { |r| r.watch(@key) { yield } }
31
45
  ensure
32
- @client.unwatch
46
+ with { |r| r.unwatch }
33
47
  end
34
48
 
35
- def initial_set(val = "")
36
- @client.set(@key, val)
49
+ def initial_set(val = BLANK_STR)
50
+ set(val, nil)
51
+ nil
37
52
  end
38
53
  end
39
54
  end
@@ -1,3 +1,3 @@
1
1
  module Suo
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0".freeze
3
3
  end
@@ -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 = "~> 2.0"
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", "~> 1.5"
30
- spec.add_development_dependency "rake", "~> 10.0"
31
- spec.add_development_dependency "rubocop", "~> 0.30.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
@@ -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.3.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: 2015-04-16 00:00:00.000000000 Z
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: '1.5'
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: '1.5'
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: '10.0'
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: '10.0'
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.30.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.30.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.0'
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
- rubyforge_project:
173
- rubygems_version: 2.4.5
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:
@@ -1,6 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.2.0
4
- services:
5
- - memcached
6
- - redis-server