forked from nateware/redis-objects
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlock.rb
84 lines (75 loc) · 3.04 KB
/
lock.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
require File.dirname(__FILE__) + '/base_object'
class Redis
#
# Class representing a lock. This functions like a proxy class, in
# that you can say @object.lock_name { block } to use the lock and also
# @object.counter_name.clear to reset on it. You can use this
# directly, but it is better to use the lock :foo class method in your
# class to define a lock.
#
class Lock < BaseObject
class LockTimeout < StandardError; end #:nodoc:
attr_reader :key, :options, :redis
def initialize(key, *args)
super(key, *args)
@options[:timeout] ||= 5
@options[:init] = false if @options[:init].nil? # default :init to false
@redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
end
# Clear the lock. Should only be needed if there's a server crash
# or some other event that gets locks in a stuck state.
def clear
redis.del(key)
end
alias_method :delete, :clear
# Get the lock and execute the code block. Any other code that needs the lock
# (on any server) will spin waiting for the lock up to the :timeout
# that was specified when the lock was defined.
def lock(&block)
start = Time.now
gotit = false
expiration = nil
while Time.now - start < @options[:timeout]
expiration = generate_expiration
# Use the expiration as the value of the lock.
gotit = redis.setnx(key, expiration)
break if gotit
# Lock is being held. Now check to see if it's expired (if we're using
# lock expiration).
# See "Handling Deadlocks" section on http://code.google.com/p/redis/wiki/SetnxCommand
if !@options[:expiration].nil?
old_expiration = redis.get(key).to_f
if old_expiration < Time.now.to_f
# If it's expired, use GETSET to update it.
expiration = generate_expiration
old_expiration = redis.getset(key, expiration).to_f
# Since GETSET returns the old value of the lock, if the old expiration
# is still in the past, we know no one else has expired the locked
# and we now have it.
if old_expiration < Time.now.to_f
gotit = true
break
end
end
end
sleep 0.1
end
raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec" unless gotit
begin
yield
ensure
# We need to be careful when cleaning up the lock key. If we took a really long
# time for some reason, and the lock expired, someone else may have it, and
# it's not safe for us to remove it. Check how much time has passed since we
# wrote the lock key and only delete it if it hasn't expired (or we're not using
# lock expiration)
if @options[:expiration].nil? || expiration > Time.now.to_f
redis.del(key)
end
end
end
def generate_expiration
@options[:expiration].nil? ? 1 : (Time.now + @options[:expiration].to_f + 1).to_f
end
end
end