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 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: []