Class: Gitlab::ExclusiveLease
- Inherits:
-
Object
- Object
- Gitlab::ExclusiveLease
- Includes:
- Utils::StrongMemoize
- Defined in:
- lib/gitlab/exclusive_lease.rb
Overview
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 servers. It is a cheap alternative to using SQL queries and updates: you do not need to change the SQL schema to start using ExclusiveLease.
Constant Summary collapse
- PREFIX =
'gitlab:exclusive_lease'
- NoKey =
Class.new(ArgumentError)
- LUA_CANCEL_SCRIPT =
<<~EOS local key, uuid = KEYS[1], ARGV[1] if redis.call("get", key) == uuid then redis.call("del", key) end EOS
- LUA_RENEW_SCRIPT =
<<~EOS 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
Class Method Summary collapse
- .cancel(key, uuid) ⇒ Object
- .ensure_prefixed_key(key) ⇒ Object
- .get_uuid(key) ⇒ Object
- .redis_shared_state_key(key) ⇒ Object
-
.reset_all!(scope = '*') ⇒ Object
Removes any existing exclusive_lease from redis Don’t run this in a live system without making sure no one is using the leases.
-
.throttle(key, group: nil, period: 1.hour, count: 1, &block) ⇒ Object
yield to the block at most count times per period.
- .use_cluster_shared_state? ⇒ Boolean
- .use_double_lock? ⇒ Boolean
- .with_read_redis(&blk) ⇒ Object
-
.with_write_redis(&blk) ⇒ Object
rubocop:disable CodeReuse/ActiveRecord.
Instance Method Summary collapse
-
#cancel ⇒ Object
Gives up this lease, allowing it to be obtained by others.
-
#exists? ⇒ Boolean
Returns true if the key for this lease is set.
-
#initialize(key, uuid: nil, timeout:) ⇒ ExclusiveLease
constructor
A new instance of ExclusiveLease.
-
#renew ⇒ Object
Try to renew an existing lease.
-
#try_obtain ⇒ Object
Try to obtain the lease.
-
#ttl ⇒ Object
Returns the TTL of the Redis key.
-
#waiting? ⇒ Boolean
This lease is waiting to obtain.
Constructor Details
#initialize(key, uuid: nil, timeout:) ⇒ ExclusiveLease
Returns a new instance of ExclusiveLease.
106 107 108 109 110 |
# File 'lib/gitlab/exclusive_lease.rb', line 106 def initialize(key, uuid: nil, timeout:) @redis_shared_state_key = self.class.redis_shared_state_key(key) @timeout = timeout @uuid = uuid || SecureRandom.uuid end |
Class Method Details
.cancel(key, uuid) ⇒ Object
63 64 65 66 67 68 69 |
# File 'lib/gitlab/exclusive_lease.rb', line 63 def self.cancel(key, uuid) return unless key.present? with_write_redis do |redis| redis.eval(LUA_CANCEL_SCRIPT, keys: [ensure_prefixed_key(key)], argv: [uuid]) end end |
.ensure_prefixed_key(key) ⇒ Object
75 76 77 78 79 |
# File 'lib/gitlab/exclusive_lease.rb', line 75 def self.ensure_prefixed_key(key) raise NoKey unless key.present? key.start_with?(PREFIX) ? key : redis_shared_state_key(key) end |
.get_uuid(key) ⇒ Object
35 36 37 38 39 |
# File 'lib/gitlab/exclusive_lease.rb', line 35 def self.get_uuid(key) with_read_redis do |redis| redis.get(redis_shared_state_key(key)) || false end end |
.redis_shared_state_key(key) ⇒ Object
71 72 73 |
# File 'lib/gitlab/exclusive_lease.rb', line 71 def self.redis_shared_state_key(key) "#{PREFIX}:#{key}" end |
.reset_all!(scope = '*') ⇒ Object
Removes any existing exclusive_lease from redis Don’t run this in a live system without making sure no one is using the leases
83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/gitlab/exclusive_lease.rb', line 83 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 Gitlab::Redis::ClusterSharedState.with do |redis| redis.scan_each(match: redis_shared_state_key(scope)).each do |key| redis.del(key) end end end |
.throttle(key, group: nil, period: 1.hour, count: 1, &block) ⇒ Object
yield to the block at most count times per period
Defaults to once per hour.
For example:
# toot the train horn at most every 20min:
throttle(locomotive.id, count: 3, period: 1.hour) { toot_train_horn }
# Brake suddenly at most once every minute:
throttle(locomotive.id, period: 1.minute) { brake_suddenly }
# Specify a uniqueness group:
throttle(locomotive.id, group: :locomotive_brake) { brake_suddenly }
If a group is not specified, each block will get a separate group to itself.
55 56 57 58 59 60 61 |
# File 'lib/gitlab/exclusive_lease.rb', line 55 def self.throttle(key, group: nil, period: 1.hour, count: 1, &block) group ||= block.source_location.join(':') return if new("el:throttle:#{group}:#{key}", timeout: period.to_i / count).waiting? yield end |
.use_cluster_shared_state? ⇒ Boolean
97 98 99 100 |
# File 'lib/gitlab/exclusive_lease.rb', line 97 def self.use_cluster_shared_state? Gitlab::SafeRequestStore[:use_cluster_shared_state] ||= Feature.enabled?(:use_cluster_shared_state_for_exclusive_lease) end |
.use_double_lock? ⇒ Boolean
102 103 104 |
# File 'lib/gitlab/exclusive_lease.rb', line 102 def self.use_double_lock? Gitlab::SafeRequestStore[:use_double_lock] ||= Feature.enabled?(:enable_exclusive_lease_double_lock_rw) end |
.with_read_redis(&blk) ⇒ Object
183 184 185 186 187 188 189 190 191 |
# File 'lib/gitlab/exclusive_lease.rb', line 183 def self.with_read_redis(&blk) if use_cluster_shared_state? Gitlab::Redis::ClusterSharedState.with(&blk) elsif use_double_lock? Gitlab::Redis::SharedState.with(&blk) || Gitlab::Redis::ClusterSharedState.with(&blk) else Gitlab::Redis::SharedState.with(&blk) end end |
.with_write_redis(&blk) ⇒ Object
rubocop:disable CodeReuse/ActiveRecord
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/gitlab/exclusive_lease.rb', line 167 def self.with_write_redis(&blk) if use_cluster_shared_state? result = Gitlab::Redis::ClusterSharedState.with(&blk) Gitlab::Redis::SharedState.with(&blk) result elsif use_double_lock? result = Gitlab::Redis::SharedState.with(&blk) Gitlab::Redis::ClusterSharedState.with(&blk) result else Gitlab::Redis::SharedState.with(&blk) end end |
Instance Method Details
#cancel ⇒ Object
Gives up this lease, allowing it to be obtained by others.
195 196 197 |
# File 'lib/gitlab/exclusive_lease.rb', line 195 def cancel self.class.cancel(@redis_shared_state_key, @uuid) end |
#exists? ⇒ Boolean
Returns true if the key for this lease is set.
149 150 151 152 153 |
# File 'lib/gitlab/exclusive_lease.rb', line 149 def exists? self.class.with_read_redis do |redis| redis.exists?(@redis_shared_state_key) # rubocop:disable CodeReuse/ActiveRecord end end |
#renew ⇒ Object
Try to renew an existing lease. Return lease UUID on success, false if the lease is taken by a different UUID or inexistent.
141 142 143 144 145 146 |
# File 'lib/gitlab/exclusive_lease.rb', line 141 def renew self.class.with_write_redis do |redis| result = redis.eval(LUA_RENEW_SCRIPT, keys: [@redis_shared_state_key], argv: [@uuid, @timeout]) result == @uuid end end |
#try_obtain ⇒ Object
Try to obtain the lease. Return lease UUID on success, false if the lease is already taken.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/gitlab/exclusive_lease.rb', line 114 def try_obtain return try_obtain_with_new_lock if self.class.use_cluster_shared_state? # Performing a single SET is atomic obtained = set_lease(Gitlab::Redis::SharedState) && @uuid # traffic to new store is minimal since only the first lock holder can run SETNX in ClusterSharedState return false unless obtained return obtained unless self.class.use_double_lock? return obtained if same_store # 2nd setnx will surely fail if store are the same second_lock_obtained = set_lease(Gitlab::Redis::ClusterSharedState) && @uuid # cancel is safe since it deletes key only if value matches uuid # i.e. it will not delete the held lock on ClusterSharedState cancel unless second_lock_obtained second_lock_obtained end |
#ttl ⇒ Object
Returns the TTL of the Redis key.
This method will return ‘nil` if no TTL could be obtained.
158 159 160 161 162 163 164 |
# File 'lib/gitlab/exclusive_lease.rb', line 158 def ttl self.class.with_read_redis do |redis| ttl = redis.ttl(@redis_shared_state_key) ttl if ttl > 0 end end |
#waiting? ⇒ Boolean
This lease is waiting to obtain
135 136 137 |
# File 'lib/gitlab/exclusive_lease.rb', line 135 def waiting? !try_obtain end |