robust-redis-lock 0.1.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.
- data/lib/redis-lock.rb +104 -0
- data/lib/robust-redis-lock.rb +1 -0
- data/lib/robust-redis-lock/script.rb +24 -0
- data/lib/robust-redis-lock/version.rb +5 -0
- metadata +65 -0
data/lib/redis-lock.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
class Redis::Lock
|
4
|
+
require 'robust-redis-lock/script'
|
5
|
+
|
6
|
+
attr_reader :key
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :redis
|
10
|
+
attr_accessor :timeout
|
11
|
+
attr_accessor :sleep
|
12
|
+
attr_accessor :expire
|
13
|
+
attr_accessor :namespace
|
14
|
+
end
|
15
|
+
|
16
|
+
self.timeout = 60
|
17
|
+
self.expire = 60
|
18
|
+
self.sleep = 0.1
|
19
|
+
self.namespace = 'redis:lock'
|
20
|
+
|
21
|
+
def initialize(key, options={})
|
22
|
+
raise "key cannot be nil" if key.nil?
|
23
|
+
@key = (options[:namespace] || self.class.namespace) + ":" + key
|
24
|
+
|
25
|
+
@redis = options[:redis] || self.class.redis
|
26
|
+
raise "redis cannot be nil" if @redis.nil?
|
27
|
+
|
28
|
+
@timeout = options[:timeout] || self.class.timeout
|
29
|
+
@expire = options[:expire] || self.class.expire
|
30
|
+
@sleep = options[:sleep] || self.class.sleep
|
31
|
+
end
|
32
|
+
|
33
|
+
def lock
|
34
|
+
result = false
|
35
|
+
start_at = Time.now
|
36
|
+
while Time.now - start_at < @timeout
|
37
|
+
break if result = try_lock
|
38
|
+
sleep @sleep.to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
yield if block_given?
|
42
|
+
|
43
|
+
result
|
44
|
+
ensure
|
45
|
+
unlock if block_given?
|
46
|
+
end
|
47
|
+
|
48
|
+
def try_lock
|
49
|
+
now = Time.now.to_i
|
50
|
+
|
51
|
+
# This script loading is not thread safe (touching a class variable), but
|
52
|
+
# that's okay, because the race is harmless.
|
53
|
+
@@lock_script ||= Script.new <<-LUA
|
54
|
+
local key = KEYS[1]
|
55
|
+
local now = tonumber(ARGV[1])
|
56
|
+
local expires_at = tonumber(ARGV[2])
|
57
|
+
local token_key = 'redis:lock:token'
|
58
|
+
|
59
|
+
local prev_expires_at = tonumber(redis.call('hget', key, 'expires_at'))
|
60
|
+
if prev_expires_at and prev_expires_at > now then
|
61
|
+
return {'locked', nil}
|
62
|
+
end
|
63
|
+
|
64
|
+
local next_token = redis.call('incr', token_key)
|
65
|
+
|
66
|
+
redis.call('hset', key, 'expires_at', expires_at)
|
67
|
+
redis.call('hset', key, 'token', next_token)
|
68
|
+
|
69
|
+
if prev_expires_at then
|
70
|
+
return {'recovered', next_token}
|
71
|
+
else
|
72
|
+
return {'acquired', next_token}
|
73
|
+
end
|
74
|
+
LUA
|
75
|
+
result, token = @@lock_script.eval(@redis, :keys => [@key], :argv => [now, now + @expire])
|
76
|
+
|
77
|
+
@token = token if token
|
78
|
+
|
79
|
+
case result
|
80
|
+
when 'locked' then return false
|
81
|
+
when 'recovered' then return :recovered
|
82
|
+
when 'acquired' then return true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def unlock
|
87
|
+
# Since it's possible that the operations in the critical section took a long time,
|
88
|
+
# we can't just simply release the lock. The unlock method checks if @expire_at
|
89
|
+
# remains the same, and do not release when the lock timestamp was overwritten.
|
90
|
+
@@unlock_script ||= Script.new <<-LUA
|
91
|
+
local key = KEYS[1]
|
92
|
+
local token = ARGV[1]
|
93
|
+
|
94
|
+
if redis.call('hget', key, 'token') == token then
|
95
|
+
redis.call('del', key)
|
96
|
+
return true
|
97
|
+
else
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
LUA
|
101
|
+
result = @@unlock_script.eval(@redis, :keys => [@key], :argv => [@token])
|
102
|
+
!!result
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'redis-lock'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Redis::Lock::Script
|
2
|
+
def initialize(script)
|
3
|
+
@script = script
|
4
|
+
@sha = Digest::SHA1.hexdigest(@script)
|
5
|
+
end
|
6
|
+
|
7
|
+
def eval(redis, options={})
|
8
|
+
redis.evalsha(@sha, options)
|
9
|
+
rescue ::Redis::CommandError => e
|
10
|
+
if e.message =~ /^NOSCRIPT/
|
11
|
+
redis.script(:load, @script)
|
12
|
+
retry
|
13
|
+
end
|
14
|
+
if e.message =~ /^ERR unknown command/
|
15
|
+
raise "You are using a version of Redis that does not support LUA scripting. Please use Redis 2.6.0 or greater"
|
16
|
+
end
|
17
|
+
raise e
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
@script
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: robust-redis-lock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kareem Kouddous
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-06-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.0
|
30
|
+
description: Robust redis lock
|
31
|
+
email:
|
32
|
+
- kareeknyc@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- lib/redis-lock.rb
|
38
|
+
- lib/robust-redis-lock/script.rb
|
39
|
+
- lib/robust-redis-lock/version.rb
|
40
|
+
- lib/robust-redis-lock.rb
|
41
|
+
homepage: http://github.com/crowdtap/robust-redis-lock
|
42
|
+
licenses: []
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.8.25
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Robust redis lock
|
65
|
+
test_files: []
|