gitlab-org--gitlab-foss/lib/gitlab/exclusive_lease.rb

99 lines
2.9 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2016-10-27 12:59:52 +00:00
require 'securerandom'
2016-03-10 09:41:16 +00:00
module Gitlab
# This class implements an 'exclusive lease'. We call it a 'lease'
# because it has a set expiry time. We call it 'exclusive' because only
# one caller may obtain a lease for a given key at a time. The
# implementation is intended to work across GitLab processes and across
2016-10-27 12:59:52 +00:00
# servers. It is a cheap alternative to using SQL queries and updates:
2016-03-10 09:41:16 +00:00
# you do not need to change the SQL schema to start using
# ExclusiveLease.
2016-03-10 11:37:14 +00:00
#
2016-03-10 09:41:16 +00:00
class ExclusiveLease
2017-06-23 05:36:19 +00:00
LUA_CANCEL_SCRIPT = <<~EOS.freeze
2016-10-27 12:59:52 +00:00
local key, uuid = KEYS[1], ARGV[1]
if redis.call("get", key) == uuid then
redis.call("del", key)
end
EOS
2017-06-23 05:36:19 +00:00
LUA_RENEW_SCRIPT = <<~EOS.freeze
local key, uuid, ttl = KEYS[1], ARGV[1], ARGV[2]
if redis.call("get", key) == uuid then
redis.call("expire", key, ttl)
return uuid
end
EOS
def self.get_uuid(key)
Gitlab::Redis::SharedState.with do |redis|
redis.get(redis_shared_state_key(key)) || false
end
end
2016-10-27 12:59:52 +00:00
def self.cancel(key, uuid)
Gitlab::Redis::SharedState.with do |redis|
redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_shared_state_key(key)], argv: [uuid])
2016-10-27 12:59:52 +00:00
end
end
def self.redis_shared_state_key(key)
2016-10-27 12:59:52 +00:00
"gitlab:exclusive_lease:#{key}"
end
# Removes any existing exclusive_lease from redis
# Don't run this in a live system without making sure no one is using the leases
def self.reset_all!(scope = '*')
Gitlab::Redis::SharedState.with do |redis|
redis.scan_each(match: redis_shared_state_key(scope)).each do |key|
redis.del(key)
end
end
end
def initialize(key, uuid: nil, timeout:)
@redis_shared_state_key = self.class.redis_shared_state_key(key)
2016-10-27 12:59:52 +00:00
@timeout = timeout
@uuid = uuid || SecureRandom.uuid
2016-03-10 09:41:16 +00:00
end
2016-10-27 12:59:52 +00:00
# Try to obtain the lease. Return lease UUID on success,
2016-03-10 09:41:16 +00:00
# false if the lease is already taken.
def try_obtain
2016-03-10 17:39:50 +00:00
# Performing a single SET is atomic
Gitlab::Redis::SharedState.with do |redis|
redis.set(@redis_shared_state_key, @uuid, nx: true, ex: @timeout) && @uuid
2016-03-16 17:10:03 +00:00
end
2016-03-10 09:41:16 +00:00
end
2017-06-23 05:36:19 +00:00
# Try to renew an existing lease. Return lease UUID on success,
# false if the lease is taken by a different UUID or inexistent.
def renew
Gitlab::Redis::SharedState.with do |redis|
result = redis.eval(LUA_RENEW_SCRIPT, keys: [@redis_shared_state_key], argv: [@uuid, @timeout])
2017-06-23 05:36:19 +00:00
result == @uuid
end
end
# Returns true if the key for this lease is set.
def exists?
Gitlab::Redis::SharedState.with do |redis|
redis.exists(@redis_shared_state_key)
end
end
# Returns the TTL of the Redis key.
#
# This method will return `nil` if no TTL could be obtained.
def ttl
Gitlab::Redis::SharedState.with do |redis|
ttl = redis.ttl(@redis_shared_state_key)
ttl if ttl.positive?
end
end
2016-03-10 09:41:16 +00:00
end
end