redlock 0.1.2 → 0.1.3

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,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e2d0a189eba4a2f362d02f034cfa0957eba666eb
4
- data.tar.gz: 18f9e90023c218eb9b5d65a71946b39fb98a8617
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MWU3MmRhNjM0OWFkYTMyOTMzNzBiNDFjYjFmZDkwOGRhNGFkMzgzMQ==
5
+ data.tar.gz: !binary |-
6
+ M2EyYWE0YTk5NTg3MThiZWIzNjA2OGRhMjE3MzY1ZjBmMmU2OTI2Zg==
5
7
  SHA512:
6
- metadata.gz: 65cd0b7c60b9756eba2f0fcce27677b9f26e32db74f3cbea7f036744ae52bfd0e50f8ce72cddb538fafc49a409c0f7a6257976d9234be8305d8a77151958980d
7
- data.tar.gz: 0531fe15435ad4601ad83c8aa107ac6e3073f4f2be49682c74ff379f9b9c20f603e140b51c604b5baffdfb7650a3e4762cdea9495870fa1b684f99bcbab6930d
8
+ metadata.gz: !binary |-
9
+ MDI3YjUyYjQxOTJmNzAyZTE5MGY3MjlhNWYyMjVmZDE4YWQ1YTRkMDA0ODcw
10
+ M2RlYmQ0NTA3NGUwZjdlYWM0MjM1MjFiNjY4N2YwMzkwM2I5M2MxZjM5OWVi
11
+ YzdlYzVmOTBlOTRkNzRhNjNhYjNhYzU0OGU1ZTJhNzg0NjUxMjQ=
12
+ data.tar.gz: !binary |-
13
+ MDFmYTU0MjRhZWFhOGY2NTM5YmM4NmQ2ZTA2MDI5NzQ1NjVlZjc2ODhhM2Nm
14
+ N2JkNmJlZTI5ODU3NzM3MmIxZjUxY2M5ZmZhOTNiOThiNzdmOGI1Zjc2ZTI3
15
+ ZDkxZGExNTBkY2Y3MGMyMmU0M2U4MmM4OWE5MWZhNTM5Mjg5ODQ=
data/README.md CHANGED
@@ -79,11 +79,23 @@ lock_manager.lock("resource_key", 2000) do |locked|
79
79
  end
80
80
  ```
81
81
 
82
+ 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:
83
+
84
+ ```ruby
85
+ begin
86
+ block_result = lock_manager.lock!("resource_key", 2000) do
87
+ # critical code
88
+ end
89
+ rescue Redlock::LockException
90
+ # error handling
91
+ end
92
+ ```
93
+
82
94
  ## Run tests
83
95
 
84
96
  Make sure you have at least 1 redis instances up.
85
97
 
86
- $ rspec
98
+ $ rspec
87
99
 
88
100
  ## Disclaimer
89
101
 
@@ -2,4 +2,6 @@ require 'redlock/version'
2
2
 
3
3
  module Redlock
4
4
  autoload :Client, 'redlock/client'
5
+
6
+ LockError = Class.new(StandardError)
5
7
  end
@@ -34,9 +34,11 @@ module Redlock
34
34
  # Params:
35
35
  # +resource+:: the resource (or key) string to be locked.
36
36
  # +ttl+:: The time-to-live in ms for the lock.
37
- # +block+:: an optional block that automatically unlocks the lock.
38
- def lock(resource, ttl, &block)
39
- lock_info = try_lock_instances(resource, ttl)
37
+ # +extend+: A lock ("lock_info") to extend.
38
+ # +block+:: an optional block to be executed; after its execution, the lock (if successfully
39
+ # acquired) is automatically unlocked.
40
+ def lock(resource, ttl, extend: nil, &block)
41
+ lock_info = try_lock_instances(resource, ttl, extend)
40
42
 
41
43
  if block_given?
42
44
  begin
@@ -57,6 +59,18 @@ module Redlock
57
59
  @servers.each { |s| s.unlock(lock_info[:resource], lock_info[:value]) }
58
60
  end
59
61
 
62
+ # Locks a resource, executing the received block only after successfully acquiring the lock,
63
+ # and returning its return value as a result.
64
+ # See Redlock::Client#lock for parameters.
65
+ def lock!(*args, **keyword_args)
66
+ fail 'No block passed' unless block_given?
67
+
68
+ lock(*args, **keyword_args) do |lock_info|
69
+ raise LockError, 'failed to acquire lock' unless lock_info
70
+ return yield
71
+ end
72
+ end
73
+
60
74
  private
61
75
 
62
76
  class RedisInstance
@@ -68,6 +82,15 @@ module Redlock
68
82
  end
69
83
  eos
70
84
 
85
+ # thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
86
+ # also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
87
+ # and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
88
+ LOCK_SCRIPT = <<-eos
89
+ if redis.call("exists", KEYS[1]) == 0 or redis.call("get", KEYS[1]) == ARGV[1] then
90
+ return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
91
+ end
92
+ eos
93
+
71
94
  def initialize(connection)
72
95
  if connection.respond_to?(:client)
73
96
  @redis = connection
@@ -79,7 +102,7 @@ module Redlock
79
102
  end
80
103
 
81
104
  def lock(resource, val, ttl)
82
- @redis.set(resource, val, nx: true, px: ttl)
105
+ @redis.evalsha(@lock_script_sha, keys: [resource], argv: [val, ttl])
83
106
  end
84
107
 
85
108
  def unlock(resource, val)
@@ -92,12 +115,13 @@ module Redlock
92
115
 
93
116
  def load_scripts
94
117
  @unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
118
+ @lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
95
119
  end
96
120
  end
97
121
 
98
- def try_lock_instances(resource, ttl)
122
+ def try_lock_instances(resource, ttl, extend)
99
123
  @retry_count.times do
100
- lock_info = lock_instances(resource, ttl)
124
+ lock_info = lock_instances(resource, ttl, extend)
101
125
  return lock_info if lock_info
102
126
 
103
127
  # Wait a random delay before retrying
@@ -107,8 +131,8 @@ module Redlock
107
131
  false
108
132
  end
109
133
 
110
- def lock_instances(resource, ttl)
111
- value = SecureRandom.uuid
134
+ def lock_instances(resource, ttl, extend)
135
+ value = extend ? extend.fetch(:value) : SecureRandom.uuid
112
136
 
113
137
  locked, time_elapsed = timed do
114
138
  @servers.select { |s| s.lock(resource, value, ttl) }.size
@@ -128,7 +152,7 @@ module Redlock
128
152
  # Add 2 milliseconds to the drift to account for Redis expires
129
153
  # precision, which is 1 millisecond, plus 1 millisecond min drift
130
154
  # for small TTLs.
131
- drift = (ttl * CLOCK_DRIFT_FACTOR).to_i + 2
155
+ (ttl * CLOCK_DRIFT_FACTOR).to_i + 2
132
156
  end
133
157
 
134
158
  def timed
@@ -4,17 +4,17 @@ module Redlock
4
4
 
5
5
  alias_method :try_lock_instances_without_testing, :try_lock_instances
6
6
 
7
- def try_lock_instances(resource, ttl)
7
+ def try_lock_instances(resource, ttl, extend)
8
8
  if @testing_mode == :bypass
9
9
  {
10
10
  validity: ttl,
11
11
  resource: resource,
12
- value: SecureRandom.uuid
12
+ value: extend ? extend.fetch(:value) : SecureRandom.uuid
13
13
  }
14
14
  elsif @testing_mode == :fail
15
15
  false
16
16
  else
17
- try_lock_instances_without_testing resource, ttl
17
+ try_lock_instances_without_testing resource, ttl, extend
18
18
  end
19
19
  end
20
20
 
@@ -25,12 +25,13 @@ module Redlock
25
25
  end
26
26
 
27
27
  class RedisInstance
28
+ alias_method :load_scripts_without_testing, :load_scripts
29
+
28
30
  def load_scripts
29
- begin
30
- @unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
31
- rescue Redis::CommandError
32
- # ignore
33
- end
31
+ load_scripts_without_testing
32
+ rescue Redis::CommandError
33
+ # FakeRedis doesn't have #script, but doesn't need it either.
34
+ raise unless defined?(::FakeRedis)
34
35
  end
35
36
  end
36
37
  end
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
@@ -35,6 +35,25 @@ RSpec.describe Redlock::Client do
35
35
 
36
36
  expect(@lock_info).to be_lock_info_for(resource_key)
37
37
  end
38
+
39
+ it 'can extend its own lock' do
40
+ my_lock_info = lock_manager.lock(resource_key, ttl)
41
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: my_lock_info)
42
+ expect(@lock_info).to be_lock_info_for(resource_key)
43
+ expect(@lock_info[:value]).to eq(my_lock_info[:value])
44
+ end
45
+
46
+ it "sets the given value when trying to extend a non-existent lock" do
47
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'})
48
+ expect(@lock_info).to be_lock_info_for(resource_key)
49
+ expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
50
+ end
51
+
52
+ it "doesn't extend somebody else's lock" do
53
+ @lock_info = lock_manager.lock(resource_key, ttl)
54
+ second_attempt = lock_manager.lock(resource_key, ttl)
55
+ expect(second_attempt).to eq(false)
56
+ end
38
57
  end
39
58
 
40
59
  context 'when lock is not available' do
@@ -46,6 +65,12 @@ RSpec.describe Redlock::Client do
46
65
 
47
66
  expect(lock_info).to eql(false)
48
67
  end
68
+
69
+ it "can't extend somebody else's lock" do
70
+ yet_another_lock_info = @another_lock_info.merge value: 'gibberish'
71
+ lock_info = lock_manager.lock(resource_key, ttl, extend: yet_another_lock_info)
72
+ expect(lock_info).to eql(false)
73
+ end
49
74
  end
50
75
 
51
76
  describe 'block syntax' do
@@ -107,4 +132,47 @@ RSpec.describe Redlock::Client do
107
132
  expect(resource_key).to be_lockable(lock_manager, ttl)
108
133
  end
109
134
  end
135
+
136
+ describe 'lock!' do
137
+ context 'when lock is available' do
138
+ it 'locks' do
139
+ lock_manager.lock!(resource_key, ttl) do
140
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
141
+ end
142
+ end
143
+
144
+ it "returns the received block's return value" do
145
+ rv = lock_manager.lock!(resource_key, ttl) { :success }
146
+ expect(rv).to eql(:success)
147
+ end
148
+
149
+ it 'automatically unlocks' do
150
+ lock_manager.lock!(resource_key, ttl) {}
151
+ expect(resource_key).to be_lockable(lock_manager, ttl)
152
+ end
153
+
154
+ it 'automatically unlocks when block raises exception' do
155
+ lock_manager.lock!(resource_key, ttl) { fail } rescue nil
156
+ expect(resource_key).to be_lockable(lock_manager, ttl)
157
+ end
158
+ end
159
+
160
+ context 'when lock is not available' do
161
+ before { @another_lock_info = lock_manager.lock(resource_key, ttl) }
162
+ after { lock_manager.unlock(@another_lock_info) }
163
+
164
+ it 'raises a LockError' do
165
+ expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(Redlock::LockError)
166
+ end
167
+
168
+ it 'does not execute the block' do
169
+ expect do
170
+ begin
171
+ lock_manager.lock!(resource_key, ttl) { fail }
172
+ rescue Redlock::LockError
173
+ end
174
+ end.to_not raise_error
175
+ end
176
+ end
177
+ end
110
178
  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.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leandro Moreira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-09-13 00:00:00.000000000 Z
11
+ date: 2015-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -17,7 +17,7 @@ dependencies:
17
17
  - - ~>
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3'
20
- - - '>='
20
+ - - ! '>='
21
21
  - !ruby/object:Gem::Version
22
22
  version: 3.0.5
23
23
  type: :runtime
@@ -27,7 +27,7 @@ dependencies:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
29
  version: '3'
30
- - - '>='
30
+ - - ! '>='
31
31
  - !ruby/object:Gem::Version
32
32
  version: 3.0.5
33
33
  - !ruby/object:Gem::Dependency
@@ -48,14 +48,14 @@ dependencies:
48
48
  name: coveralls
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - '>='
51
+ - - ! '>='
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - '>='
58
+ - - ! '>='
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0'
61
61
  - !ruby/object:Gem::Dependency
@@ -120,12 +120,12 @@ require_paths:
120
120
  - lib
121
121
  required_ruby_version: !ruby/object:Gem::Requirement
122
122
  requirements:
123
- - - '>='
123
+ - - ! '>='
124
124
  - !ruby/object:Gem::Version
125
125
  version: '0'
126
126
  required_rubygems_version: !ruby/object:Gem::Requirement
127
127
  requirements:
128
- - - '>='
128
+ - - ! '>='
129
129
  - !ruby/object:Gem::Version
130
130
  version: '0'
131
131
  requirements: []