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