Skip to content

Commit

Permalink
Added extend semantics that fails when the lock isn't owned
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Phillips committed May 19, 2016
1 parent 6e4adf0 commit df3cb94
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 0 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ rescue Redlock::LockError
end
```

To extend the life of the lock, provided that you didn't let it expire:

```ruby
begin
block_result = lock_manager.lock!("resource_key", 2000) do |lock_info|
# critical code
lock_manager.extend_life!(lock_info, 3000)
# more critical code
end
rescue Redlock::LockError
# error handling
end
```
There's also a non-bang version that returns true when the lock was
extended

## Run tests

Make sure you have at least 1 redis instances up.
Expand Down
42 changes: 42 additions & 0 deletions lib/redlock/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,30 @@ def lock(resource, ttl, extend: nil, &block)
end
end

def extend_life(to_extend, ttl)
value = to_extend.fetch(:value)
resource = to_extend.fetch(:resource)


extended, time_elapsed = timed do
@servers.all? { |s| s.extend_life(resource, value, ttl) }
end

validity = ttl - time_elapsed - drift(ttl)

if extended
{ validity: validity, resource: resource, value: value }
else
@servers.each { |s| s.unlock(resource, value) }
false
end
end

def extend_life!(to_extend, ttl)
new_lock_info = self.extend_life(to_extend, ttl)
raise LockError, 'failed to extend lock' unless new_lock_info
end

# Unlocks a resource.
# Params:
# +lock_info+:: the lock that has been acquired when you locked the resource.
Expand Down Expand Up @@ -91,6 +115,16 @@ class RedisInstance
end
eos

EXTEND_LOCK_SCRIPT = <<-eos
if redis.call("get", KEYS[1]) == ARGV[1] then
redis.call("expire", KEYS[1], ARGV[2])
return 0
else
return 1
end
eos


def initialize(connection)
if connection.respond_to?(:client)
@redis = connection
Expand All @@ -107,6 +141,13 @@ def lock(resource, val, ttl)
end
end

def extend_life(resource, val, ttl)
recover_from_script_flush do
rc = @redis.evalsha @extend_lock_script_sha, keys: [resource], argv: [val, ttl]
rc == 0
end
end

def unlock(resource, val)
recover_from_script_flush do
@redis.evalsha @unlock_script_sha, keys: [resource], argv: [val]
Expand All @@ -120,6 +161,7 @@ def unlock(resource, val)
def load_scripts
@unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
@lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
@extend_lock_script_sha = @redis.script(:load, EXTEND_LOCK_SCRIPT)
end

def recover_from_script_flush
Expand Down
34 changes: 34 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,40 @@
end
end

describe "extend" do
context 'when lock is available' do
before { @lock_info = lock_manager.lock(resource_key, ttl) }
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }

it 'can extend its own lock' do
lock_info = lock_manager.extend_life(@lock_info, ttl)
expect(lock_info).to be_lock_info_for(resource_key)
end

it "can't extend a nonexistent lock" do
lock_manager.unlock(@lock_info)
lock_info = lock_manager.extend_life(@lock_info, ttl)
expect(lock_info).to eq(false)
end
end
end

describe "extend!" do
context 'when lock is available' do
before { @lock_info = lock_manager.lock(resource_key, ttl) }
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }

it 'can extend its own lock' do
expect{ lock_manager.extend_life!(@lock_info, ttl) }.to_not raise_error
end

it "can't extend a nonexistent lock" do
lock_manager.unlock(@lock_info)
expect{ lock_manager.extend_life!(@lock_info, ttl) }.to raise_error(Redlock::LockError)
end
end
end

describe 'lock!' do
context 'when lock is available' do
it 'locks' do
Expand Down

0 comments on commit df3cb94

Please sign in to comment.