You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

171 lines
4.4 KiB
Lua

--- Simple locking module using redis and ngx_lua
-- @module resty.redis.lock
local ngx = require 'ngx'
local setmetatable = setmetatable
-- I disdain module(), but that's the way the lua-resty stuff seems to always be done
-- so why fight it
module(...)
_VERSION = '0.1.2'
local mt = { __index = _M }
local scripts = {
touch = {
script = [[
local val = redis.call('get', KEYS[1])
if not val then
return 0
end
if val == ARGV[1] then
redis.call('expire', KEYS[1], ARGV[2])
return 1
else
return 0
end
]],
sha1 = nil
},
unlock = {
script = [[
local val = redis.call('get', KEYS[1])
if not val then
return 0
end
if val == ARGV[1] then
redis.call('del', KEYS[1])
return 1
else
return 0
end
]],
sha1 = nil
},
lock = {
script = [[
local val = redis.call('setnx', KEYS[1], ARGV[1])
if val == 0 then
return 0
end
redis.call('expire', KEYS[1], ARGV[2])
return 1
]],
sha1 = nil
}
}
local function call_script(self, script_name, ...)
local redis = self.redis
local script = scripts[script_name]
if not script then
return nil, "invalid script"
end
local sha1 = script.sha1
if not sha1 then
-- we could do the sha ourselves, but just be 100% sure that
-- it matches redis by letting redis tell us. Also, this will work
-- when LuaJit is not availible
local ans, err = redis:script("LOAD", script.script)
if not ans then
return nil, err
end
sha1 = ans
script.sha1 = sha1
end
local ans, err = redis:evalsha(sha1, 1, self.key, self.id, ...)
if not ans then
return nil, err
end
return ans
end
--- create a new redis lock.
-- @tparam resty.redis redis a resty.redis object
-- @tparam string key key to use for locking. The actual redis key will be this string prepended with "LOCK:"
-- @tparam number ttl expiry time for the lock. default is 60 seconds
-- @treturn resty.redis.lock a lock object
function new(redis, key, ttl)
return setmetatable( { redis = redis, key = "LOCK:" .. key, ttl = ttl or 60 }, mt)
end
-- try to obtain the lock
-- @tparam resty.redis.lock self
-- @treturn boolean lock results. Technically it returns a string on success and a nil on failure, but should be treated as a boolean
-- @treturn string error, if applicable
function try_lock(self)
self.id = ngx.now() + self.ttl + 1
local ans, err = call_script(self, "lock", self.ttl)
if 1 ~= ans then
self.id = nil
end
return self.id
end
--- try to obtain the lock. Retry until lock is obtained or after a certain number of retries
-- @tparam resty.redis.lock self
-- @tparam number retries how many time to attempt to obtain the lock. default: 100
-- @tparam number sleep how long to sleep between retries. default: 0.010 (10ms). With the defaults, the lock will be retried for 10 seconds
-- @treturn boolean lock results. @see try_lock
-- @treturn string error, if applicable
function lock(self, retries, sleep)
retries = retries or 100
if retries < 1 then retries = 1 end
sleep = sleep or 0.010
local locked, err = nil
repeat
locked, err = self:try_lock()
retries = retries - 1
ngx.sleep(sleep)
until locked or retries == 0
return locked, err
end
--- "touch" the lock. If the lock is held, extend the expire time
-- @tparam resty.redis.lock self
-- @tparam number ttl how long to extend the lock for. default: value of ttl passed to `new`
-- @treturn boolean success
-- @treturn string error, if applicable
function touch(self, ttl)
ttl = ttl or self.ttl
if not self.id then
return nil, "not locked"
end
local ans, err = call_script(self, "touch", ttl)
if not ans then
return nil, err
end
return (ans == 1)
end
--- unock the lock
-- @tparam resty.redis.lock self
-- @treturn boolean if lock was successfully unlocked
-- @treturn string error, if applicable
function unlock(self)
if not self.id then
return nil, "not locked"
end
local ans, err = call_script(self, "unlock")
if not ans then
return nil, err
end
self.id = nil
return (ans == 1)
end
local class_mt = {
-- to prevent use of casual module global variables
__newindex = function (table, key, val)
error('attempt to write to undeclared variable "' .. key .. '"')
end
}
setmetatable(_M, class_mt)