redlock 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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: []