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.1'

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



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

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

#dataObject



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

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

#extendObject



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/redis-lock.rb', line 144

def extend
  @@extend_script ||= Script.new <<-LUA
      local key = KEYS[1]
      local key_group = KEYS[2]
      local expires_at = tonumber(ARGV[1])
      local token = ARGV[2]

      if redis.call('hget', key, 'token') == token then
        redis.call('hset', key, 'expires_at', expires_at)
        redis.call('zadd', key_group, expires_at, key)
        return true
      else
        return false
      end
  LUA
  !!@@extend_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [now.to_i + @expire, @token])
end

#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



162
163
164
# File 'lib/redis-lock.rb', line 162

def now
  Time.now
end

#try_lockObject



83
84
85
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
# File 'lib/redis-lock.rb', line 83

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 <<-LUA
      local key = KEYS[1]
      local key_group = KEYS[2]
      local now = tonumber(ARGV[1])
      local expires_at = tonumber(ARGV[2])
      local data = ARGV[3]
      local token_key = 'redis:lock:token'

      local prev_expires_at = tonumber(redis.call('hget', key, 'expires_at'))
      if prev_expires_at and prev_expires_at > now then
        return {'locked', nil}
      end

      local next_token = redis.call('incr', token_key)

      redis.call('hset', key, 'expires_at', expires_at)
      redis.call('hset', key, 'token', next_token)
      redis.call('hset', key, 'data', data)
      redis.call('zadd', key_group, expires_at, key)

      if prev_expires_at then
        return {'recovered', next_token}
      else
        return {'acquired', next_token}
      end
  LUA
  result, token = @@lock_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [now.to_i, now.to_i + @expire, @serializer.dump(@data)])

  @token = token if token

  case result
  when 'locked'    then return false
  when 'recovered' then return :recovered
  when 'acquired'  then return true
  end
end

#unlockObject



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/redis-lock.rb', line 123

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 <<-LUA
      local key = KEYS[1]
      local key_group = KEYS[2]
      local token = ARGV[1]

      if redis.call('hget', key, 'token') == token then
        redis.call('del', key)
        redis.call('zrem', key_group, key)
        return true
      else
        return false
      end
  LUA
  result = @@unlock_script.eval(@redis, :keys => [@key, @key_group_key], :argv => [@token])
  !!result
end