robust-redis-lock 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+
@@ -0,0 +1,5 @@
1
+ class Redis
2
+ class Lock
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
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: []