redlock 0.2.2 → 1.0.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
2
  SHA1:
3
- metadata.gz: 70744bd95881adacc0fc6623a3ba75ee0840339e
4
- data.tar.gz: 3b5b971c30395079d645eab57504ba002b7bed61
3
+ metadata.gz: 12311731641f60b7873ced7fcb039d073fc21525
4
+ data.tar.gz: 58bc941bc7bbebc10b3fcc32a32e970c3abcefe8
5
5
  SHA512:
6
- metadata.gz: 3e468fb7a3a98f4cb896a8d6ac6bed4ed25574ae20b6bd9f66c478674f8a3346404dcfc063bc82e74c1669e3e5402f8b1423e381c6e520073baf652b38ef69b1
7
- data.tar.gz: 91506cfd3c6913f84cb50ce243ad13e4b9c4593a093872f83ac9518227ca477416f022b564f4aaa9004dab6c6f083614934283f58219b0970aa0cb33140d27f7
6
+ metadata.gz: 719b17fb2701e48d3f574f198bafd078fd0f9b6ec91f686e4ece547e222c1b4bf09f5ee94868f1d1417c2e6c87d5c3b4742a185c13bb005fc83d868b2b5d0bc1
7
+ data.tar.gz: cc759db7fe3ba5f8cf09f1f1427564d1c338a97dc1959304e7fdc561f2817e5f133a54b381382ca1b96712e3109f41247ffa5da4a40bf6fd05a8bfb2d875832b
@@ -1,23 +1,23 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redlock (0.2.2)
4
+ redlock (1.0.0)
5
5
  redis (>= 3.0.0, < 5.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- coveralls (0.8.19)
10
+ coveralls (0.8.22)
11
11
  json (>= 1.8, < 3)
12
- simplecov (~> 0.12.0)
12
+ simplecov (~> 0.16.1)
13
13
  term-ansicolor (~> 1.3)
14
- thor (~> 0.19.1)
14
+ thor (~> 0.19.4)
15
15
  tins (~> 1.6)
16
16
  diff-lcs (1.3)
17
- docile (1.1.5)
18
- json (2.0.3)
17
+ docile (1.3.1)
18
+ json (2.1.0)
19
19
  rake (11.3.0)
20
- redis (4.0.1)
20
+ redis (4.0.3)
21
21
  rspec (3.5.0)
22
22
  rspec-core (~> 3.5.0)
23
23
  rspec-expectations (~> 3.5.0)
@@ -31,24 +31,24 @@ GEM
31
31
  diff-lcs (>= 1.2.0, < 2.0)
32
32
  rspec-support (~> 3.5.0)
33
33
  rspec-support (3.5.0)
34
- simplecov (0.12.0)
35
- docile (~> 1.1.0)
34
+ simplecov (0.16.1)
35
+ docile (~> 1.1)
36
36
  json (>= 1.8, < 3)
37
37
  simplecov-html (~> 0.10.0)
38
- simplecov-html (0.10.0)
39
- term-ansicolor (1.4.0)
38
+ simplecov-html (0.10.2)
39
+ term-ansicolor (1.6.0)
40
40
  tins (~> 1.0)
41
41
  thor (0.19.4)
42
- tins (1.13.0)
42
+ tins (1.16.3)
43
43
 
44
44
  PLATFORMS
45
45
  ruby
46
46
 
47
47
  DEPENDENCIES
48
- coveralls (~> 0.8.13)
48
+ coveralls (~> 0.8)
49
49
  rake (~> 11.1, >= 11.1.2)
50
50
  redlock!
51
51
  rspec (~> 3, >= 3.0.0)
52
52
 
53
53
  BUNDLED WITH
54
- 1.16.0
54
+ 1.17.2
data/README.md CHANGED
@@ -43,30 +43,27 @@ Or install it yourself as:
43
43
 
44
44
  ## Usage example
45
45
 
46
+ ### Acquiring a lock
47
+
46
48
  ```ruby
47
49
  # Locking
48
50
  lock_manager = Redlock::Client.new([ "redis://127.0.0.1:7777", "redis://127.0.0.1:7778", "redis://127.0.0.1:7779" ])
49
51
  first_try_lock_info = lock_manager.lock("resource_key", 2000)
50
52
  second_try_lock_info = lock_manager.lock("resource_key", 2000)
51
53
 
52
- # it prints lock info {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
53
54
  p first_try_lock_info
54
- # it prints false
55
+ # => {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
56
+
55
57
  p second_try_lock_info
58
+ # => false
56
59
 
57
60
  # Unlocking
58
61
  lock_manager.unlock(first_try_lock_info)
62
+
59
63
  second_try_lock_info = lock_manager.lock("resource_key", 2000)
60
64
 
61
- # now it prints lock info
62
65
  p second_try_lock_info
63
- ```
64
-
65
- Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+. It also allows clients to set any other arbitrary options on the Redis connection, e.g. password, driver, and more.
66
-
67
- ```ruby
68
- servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
69
- redlock = Redlock::Client.new(servers)
66
+ # => {validity: 1962, resource: "resource_key", value: "generated_uuid5"}
70
67
  ```
71
68
 
72
69
  There's also a block version that automatically unlocks the lock:
@@ -81,7 +78,7 @@ lock_manager.lock("resource_key", 2000) do |locked|
81
78
  end
82
79
  ```
83
80
 
84
- There's also a bang version that only executes the block if the lock is successfully acquired, returning the block's value as a result, or raising an exception otherwise:
81
+ There's also a bang version that only executes the block if the lock is successfully acquired, returning the block's value as a result, or raising an exception otherwise. Passing a block is mandatory.
85
82
 
86
83
  ```ruby
87
84
  begin
@@ -93,34 +90,43 @@ rescue Redlock::LockError
93
90
  end
94
91
  ```
95
92
 
93
+ ### Extending a lock
94
+
96
95
  To extend the life of the lock:
97
96
 
98
97
  ```ruby
99
98
  begin
100
- block_result = lock_manager.lock!("resource_key", 2000) do |lock_info|
101
- # critical code
102
- lock_manager.lock("resource key", 3000, extend: lock_info)
103
- # more critical code
99
+ lock_info = lock_manager.lock("resource_key", 2000)
100
+ while lock_info
101
+ # Critical code
102
+
103
+ # Time up and more work to do? Extend the lock.
104
+ lock_info = lock_manager.lock("resource key", 3000, extend: lock_info)
104
105
  end
105
106
  rescue Redlock::LockError
106
107
  # error handling
107
108
  end
108
109
  ```
109
110
 
110
- The above code will also acquire the lock if the previous lock has expired and the lock is currently free. Keep in mind that this means the lock could have been acquired by someone else in the meantime. To only extend the life of the lock if currently locked by yourself, use `extend_life` parameter:
111
+ The above code will also acquire the lock if the previous lock has expired and the lock is currently free. Keep in mind that this means the lock could have been acquired by someone else in the meantime. To only extend the life of the lock if currently locked by yourself, use the `extend_life` parameter:
111
112
 
112
113
  ```ruby
113
- begin
114
- block_result = lock_manager.lock!("resource_key", 2000) do |lock_info|
115
- # critical code
116
- lock_manager.lock("resource key", 3000, extend: lock_info, extend_life: true)
117
- # more critical code, only if lock was still hold
118
- end
119
- rescue Redlock::LockError
120
- # error handling
121
- end
114
+ lock_manager.lock("resource key", 3000, extend: lock_info, extend_life: true)
115
+ ```
116
+
117
+ ## Redis client configuration
118
+
119
+ `Redlock::Client` expects URLs or Redis objects on initialization. Redis objects should be used for configuring the connection in more detail, i.e. setting username and password.
120
+
121
+ ```ruby
122
+ servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
123
+ redlock = Redlock::Client.new(servers)
122
124
  ```
123
125
 
126
+ Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+.
127
+
128
+ ## Redlock configuration
129
+
124
130
  It's possible to customize the retry logic providing the following options:
125
131
 
126
132
  ```ruby
@@ -135,7 +141,6 @@ It's possible to customize the retry logic providing the following options:
135
141
 
136
142
  For more information you can check [documentation](http://www.rubydoc.info/gems/redlock/Redlock%2FClient:initialize).
137
143
 
138
-
139
144
  ## Run tests
140
145
 
141
146
  Make sure you have [docker installed](https://docs.docker.com/engine/installation/).
@@ -12,14 +12,27 @@ module Redlock
12
12
  DEFAULT_RETRY_JITTER = 50
13
13
  CLOCK_DRIFT_FACTOR = 0.01
14
14
 
15
+ ##
16
+ # Returns default time source function depending on CLOCK_MONOTONIC availability.
17
+ #
18
+ def self.default_time_source
19
+ if defined?(Process::CLOCK_MONOTONIC)
20
+ proc { (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i }
21
+ else
22
+ proc { (Time.now.to_f * 1000).to_i }
23
+ end
24
+ end
25
+
15
26
  # Create a distributed lock manager implementing redlock algorithm.
16
27
  # Params:
17
28
  # +servers+:: The array of redis connection URLs or Redis connection instances. Or a mix of both.
18
- # +options+:: You can override the default value for `retry_count`, `retry_delay` and `retry_gitter`.
29
+ # +options+::
19
30
  # * `retry_count` being how many times it'll try to lock a resource (default: 3)
20
31
  # * `retry_delay` being how many ms to sleep before try to lock again (default: 200)
21
32
  # * `retry_jitter` being how many ms to jitter retry delay (default: 50)
22
33
  # * `redis_timeout` being how the Redis timeout will be set in seconds (default: 0.1)
34
+ # * `time_source` being a callable object returning a monotonic time in milliseconds
35
+ # (default: see #default_time_source)
23
36
  def initialize(servers = DEFAULT_REDIS_URLS, options = {})
24
37
  redis_timeout = options[:redis_timeout] || DEFAULT_REDIS_TIMEOUT
25
38
  @servers = servers.map do |server|
@@ -33,6 +46,7 @@ module Redlock
33
46
  @retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT
34
47
  @retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
35
48
  @retry_jitter = options[:retry_jitter] || DEFAULT_RETRY_JITTER
49
+ @time_source = options[:time_source] || self.class.default_time_source
36
50
  end
37
51
 
38
52
  # Locks a resource for a given time.
@@ -112,6 +126,8 @@ module Redlock
112
126
  recover_from_script_flush do
113
127
  @redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock]
114
128
  end
129
+ rescue Redis::CannotConnectError
130
+ false
115
131
  end
116
132
 
117
133
  def unlock(resource, val)
@@ -149,7 +165,7 @@ module Redlock
149
165
  end
150
166
 
151
167
  def try_lock_instances(resource, ttl, options)
152
- tries = options[:extend] ? 1 : @retry_count
168
+ tries = options[:extend] ? 1 : (@retry_count + 1)
153
169
 
154
170
  tries.times do |attempt_number|
155
171
  # Wait a random delay before retrying.
@@ -188,8 +204,8 @@ module Redlock
188
204
  end
189
205
 
190
206
  def timed
191
- start_time = (Time.now.to_f * 1000).to_i
192
- [yield, (Time.now.to_f * 1000).to_i - start_time]
207
+ start_time = @time_source.call()
208
+ [yield, @time_source.call() - start_time]
193
209
  end
194
210
  end
195
211
  end
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = '0.2.2'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
22
22
 
23
- spec.add_development_dependency "coveralls", "~> 0.8.13"
23
+ spec.add_development_dependency "coveralls", "~> 0.8"
24
24
  spec.add_development_dependency 'rake', '~> 11.1', '>= 11.1.2'
25
25
  spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
26
26
  end
@@ -1,10 +1,12 @@
1
1
  require 'spec_helper'
2
2
  require 'securerandom'
3
+ require 'redis'
3
4
 
4
5
  RSpec.describe Redlock::Client do
5
6
  # It is recommended to have at least 3 servers in production
6
7
  let(:lock_manager_opts) { { retry_count: 3 } }
7
8
  let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
9
+ let(:redis_client) { Redis.new }
8
10
  let(:resource_key) { SecureRandom.hex(3) }
9
11
  let(:ttl) { 1000 }
10
12
  let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
@@ -14,7 +16,6 @@ RSpec.describe Redlock::Client do
14
16
 
15
17
  describe 'initialize' do
16
18
  it 'accepts both redis URLs and Redis objects' do
17
- print redis1_host
18
19
  servers = [ "redis://#{redis1_host}:#{redis1_port}", Redis.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
19
20
  redlock = Redlock::Client.new(servers)
20
21
 
@@ -42,6 +43,12 @@ RSpec.describe Redlock::Client do
42
43
  expect(@lock_info).to be_lock_info_for(resource_key)
43
44
  end
44
45
 
46
+ it 'interprets lock time as milliseconds' do
47
+ ttl = 20000
48
+ @lock_info = lock_manager.lock(resource_key, ttl)
49
+ expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
50
+ end
51
+
45
52
  it 'can extend its own lock' do
46
53
  my_lock_info = lock_manager.lock(resource_key, ttl)
47
54
  @lock_info = lock_manager.lock(resource_key, ttl, extend: my_lock_info)
@@ -64,6 +71,16 @@ RSpec.describe Redlock::Client do
64
71
  end
65
72
  end
66
73
 
74
+ it '(when extending) resets the TTL, rather than adding extra time to it' do
75
+ ttl = 20000
76
+ lock_info = lock_manager.lock(resource_key, ttl)
77
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
78
+
79
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_life: true)
80
+ expect(lock_info).not_to be_nil
81
+ expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
82
+ end
83
+
67
84
  context 'when extend_only_if_life flag is not given' do
68
85
  it "sets the given value when trying to extend a non-existent lock" do
69
86
  @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_life: false)
@@ -95,14 +112,14 @@ RSpec.describe Redlock::Client do
95
112
  expect(lock_info).to eql(false)
96
113
  end
97
114
 
98
- it 'retries up to \'retry_count\' times' do
115
+ it 'tries up to \'retry_count\' + 1 times' do
99
116
  expect(lock_manager).to receive(:lock_instances).exactly(
100
- lock_manager_opts[:retry_count]).times.and_return(false)
117
+ lock_manager_opts[:retry_count] + 1).times.and_return(false)
101
118
  lock_manager.lock(resource_key, ttl)
102
119
  end
103
120
 
104
121
  it 'sleeps in between retries' do
105
- expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count] - 1).times
122
+ expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count]).times
106
123
  lock_manager.lock(resource_key, ttl)
107
124
  end
108
125
 
@@ -123,6 +140,29 @@ RSpec.describe Redlock::Client do
123
140
  end
124
141
  end
125
142
 
143
+ context 'when a server goes away' do
144
+ it 'does not raise an error on connection issues' do
145
+ # We re-route the lock manager to a (hopefully) non-existent Redis URL.
146
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
147
+ redis_instance.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
148
+
149
+ expect {
150
+ expect(lock_manager.lock(resource_key, ttl)).to be_falsey
151
+ }.to_not raise_error
152
+ end
153
+ end
154
+
155
+ context 'when a server comes back' do
156
+ it 'recovers from connection issues' do
157
+ # Same as above.
158
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
159
+ redis_instance.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
160
+ expect(lock_manager.lock(resource_key, ttl)).to be_falsey
161
+ redis_instance.instance_variable_set(:@redis, Redis.new(url: "redis://#{redis1_host}:#{redis1_port}"))
162
+ expect(lock_manager.lock(resource_key, ttl)).to be_truthy
163
+ end
164
+ end
165
+
126
166
  context 'when script cache has been flushed' do
127
167
  before(:each) do
128
168
  @manipulated_instance = lock_manager.instance_variable_get(:@servers).first
@@ -267,4 +307,23 @@ RSpec.describe Redlock::Client do
267
307
  end
268
308
  end
269
309
  end
310
+
311
+ describe '#default_time_source' do
312
+ context 'when CLOCK_MONOTONIC is available (MRI, JRuby)' do
313
+ it 'returns a callable using Process.clock_gettime()' do
314
+ skip 'CLOCK_MONOTONIC not defined' unless defined?(Process::CLOCK_MONOTONIC)
315
+ expect(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC).and_call_original
316
+ Redlock::Client.default_time_source.call()
317
+ end
318
+ end
319
+
320
+ context 'when CLOCK_MONOTONIC is not available' do
321
+ it 'returns a callable using Time.now()' do
322
+ cm = Process.send(:remove_const, :CLOCK_MONOTONIC)
323
+ expect(Time).to receive(:now).and_call_original
324
+ Redlock::Client.default_time_source.call()
325
+ Process.const_set(:CLOCK_MONOTONIC, cm) if cm
326
+ end
327
+ end
328
+ end
270
329
  end
@@ -41,3 +41,11 @@ RSpec::Matchers.define :be_lockable do |lock_manager, ttl|
41
41
  "expected that #{resource_key} would be lockable"
42
42
  end
43
43
  end
44
+
45
+ RSpec.configure do |c|
46
+ # NOTE: this protects against erroneous "focus: true" commits
47
+ unless ENV['CI'] == 'true'
48
+ c.filter_run focus: true
49
+ c.run_all_when_everything_filtered = true
50
+ end
51
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redlock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leandro Moreira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-12-22 00:00:00.000000000 Z
11
+ date: 2018-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.8.13
39
+ version: '0.8'
40
40
  type: :development
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.8.13
46
+ version: '0.8'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rake
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -131,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
131
  version: '0'
132
132
  requirements: []
133
133
  rubyforge_project:
134
- rubygems_version: 2.5.2.1
134
+ rubygems_version: 2.5.2.3
135
135
  signing_key:
136
136
  specification_version: 4
137
137
  summary: Distributed lock using Redis written in Ruby.