Class: Redis::Lock

Inherits:
Object
  • Object
show all
Defined in:
lib/robust-redis-lock/version.rb,
lib/redis-lock.rb

Defined Under Namespace

Classes: Script

Constant Summary collapse

VERSION =
'0.4.2'

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Lock

Returns a new instance of Lock.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/redis-lock.rb', line 39

def initialize(*args)
  raise "invalid number of args: expected 1..3, got #{args.length}" if args.length < 1 || args.length > 3

  key = args.shift
  raise "key cannot be nil" if key.nil?

  if args.length == 2
    @data = args.shift
  end

  @options = args.shift || {}

  namespace_prefix = self.class.namespace_prefix(@options) unless key.start_with?(self.class.namespace_prefix(@options))
  @key = [namespace_prefix, key].compact.join(':')
  @key_group_key = self.class.key_group_key(@options)

  @redis    = @options[:redis] || self.class.redis
  raise "redis cannot be nil" if @redis.nil?

  @timeout    = @options[:timeout]    || self.class.timeout
  @expire     = @options[:expire]     || self.class.expire
  @sleep      = @options[:sleep]      || self.class.sleep
  @serializer = @options[:serializer] || self.class.serializer
end

Class Attribute Details

.expireObject

Returns the value of attribute expire.



13
14
15
# File 'lib/redis-lock.rb', line 13

def expire
  @expire
end

.key_groupObject

Returns the value of attribute key_group.



15
16
17
# File 'lib/redis-lock.rb', line 15

def key_group
  @key_group
end

.namespaceObject

Returns the value of attribute namespace.



14
15
16
# File 'lib/redis-lock.rb', line 14

def namespace
  @namespace
end

.redisObject

Returns the value of attribute redis.



10
11
12
# File 'lib/redis-lock.rb', line 10

def redis
  @redis
end

.serializerObject

Returns the value of attribute serializer.



16
17
18
# File 'lib/redis-lock.rb', line 16

def serializer
  @serializer
end

.sleepObject

Returns the value of attribute sleep.



12
13
14
# File 'lib/redis-lock.rb', line 12

def sleep
  @sleep
end

.timeoutObject

Returns the value of attribute timeout.



11
12
13
# File 'lib/redis-lock.rb', line 11

def timeout
  @timeout
end

Instance Attribute Details

#keyObject (readonly)

Returns the value of attribute key.



7
8
9
# File 'lib/redis-lock.rb', line 7

def key
  @key
end

Class Method Details

.expired(options = {}) ⇒ Object



18
19
20
21
# File 'lib/redis-lock.rb', line 18

def expired(options={})
  redis = options[:redis] || self.redis
  redis.zrangebyscore(key_group_key(options), 0, Time.now.to_i).to_a.map { |key| self.new(key, options) }
end

.key_group_key(options) ⇒ Object



23
24
25
# File 'lib/redis-lock.rb', line 23

def key_group_key(options)
  [namespace_prefix(options), (options[:key_group] || self.key_group), 'group'].join(':')
end

.namespace_prefix(options) ⇒ Object



27
28
29
# File 'lib/redis-lock.rb', line 27

def namespace_prefix(options)
  (options[:namespace] || self.namespace)
end

Instance Method Details

#==(other) ⇒ Object



171
172
173
# File 'lib/redis-lock.rb', line 171

def ==(other)
  self.key == other.key
end

#dataObject



79
80
81
82
83
84
# File 'lib/redis-lock.rb', line 79

def data
  @data ||= begin
              raw = @redis.hget(key, 'data')
              @serializer.load(raw) if raw
            end
end

#extendObject



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/redis-lock.rb', line 149

def extend
  @@extend_script ||= Script.new "      local key = KEYS[1]\n      local key_group = KEYS[2]\n      local expires_at = tonumber(ARGV[1])\n      local token = ARGV[2]\n\n      if redis.call('hget', key, 'token') == token then\n        redis.call('hset', key, 'expires_at', expires_at)\n        redis.call('zadd', key_group, expires_at, key)\n        return true\n      else\n        return false\n      end\n  LUA\n  !!@@extend_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [now.to_i + @expire, @token])\nend\n"

#lockObject



64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/redis-lock.rb', line 64

def lock
  result = false
  start_at = now
  while now - start_at < @timeout
    break if result = try_lock
    sleep @sleep.to_f
  end

  yield if block_given? && result

  result
ensure
  unlock if block_given?
end

#nowObject



167
168
169
# File 'lib/redis-lock.rb', line 167

def now
  Time.now
end

#try_lockObject



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/redis-lock.rb', line 86

def try_lock
  # This script loading is not thread safe (touching a class variable), but
  # that's okay, because the race is harmless.
  @@lock_script ||= Script.new "      local key = KEYS[1]\n      local key_group = KEYS[2]\n      local now = tonumber(ARGV[1])\n      local expires_at = tonumber(ARGV[2])\n      local data = ARGV[3]\n      local token_key = 'redis:lock:token'\n\n      local prev_expires_at = tonumber(redis.call('hget', key, 'expires_at'))\n      if prev_expires_at and prev_expires_at > now then\n        return {'locked', nil}\n      end\n\n      local next_token = redis.call('incr', token_key)\n\n      redis.call('hset', key, 'expires_at', expires_at)\n      redis.call('hset', key, 'token', next_token)\n      redis.call('zadd', key_group, expires_at, key)\n      if data then\n        redis.call('hset', key, 'data', data)\n      end\n\n      if prev_expires_at then\n        return {'recovered', next_token}\n      else\n        return {'acquired', next_token}\n      end\n  LUA\n  result, token = @@lock_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [now.to_i, now.to_i + @expire, @serializer.dump(data)])\n\n  @token = token if token\n\n  case result\n  when 'locked'    then return false\n  when 'recovered' then return :recovered\n  when 'acquired'  then return true\n  end\nend\n"

#unlockObject



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/redis-lock.rb', line 128

def unlock
  # Since it's possible that the operations in the critical section took a long time,
  # we can't just simply release the lock. The unlock method checks if @expire_at
  # remains the same, and do not release when the lock timestamp was overwritten.
  @@unlock_script ||= Script.new "      local key = KEYS[1]\n      local key_group = KEYS[2]\n      local token = ARGV[1]\n\n      if redis.call('hget', key, 'token') == token then\n        redis.call('del', key)\n        redis.call('zrem', key_group, key)\n        return true\n      else\n        return false\n      end\n  LUA\n  result = @@unlock_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [@token])\n  !!result\nend\n"