class Redlock::Client
Constants
- CLOCK_DRIFT_FACTOR
- DEFAULT_REDIS_HOST
- DEFAULT_REDIS_PORT
- DEFAULT_REDIS_TIMEOUT
- DEFAULT_REDIS_URLS
- DEFAULT_RETRY_COUNT
- DEFAULT_RETRY_DELAY
- DEFAULT_RETRY_JITTER
Attributes
Public Class Methods
Returns default time source function depending on CLOCK_MONOTONIC availability.
# File lib/redlock/client.rb, line 30 def self.default_time_source if defined?(Process::CLOCK_MONOTONIC) proc { (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i } else proc { (Time.now.to_f * 1000).to_i } end end
Create a distributed lock manager implementing redlock algorithm. Params:
servers
-
The array of redis connection URLs or Redis connection instances. Or a mix of both.
options
-
‘retry_count` being how many times it’ll try to lock a resource (default: 3)
-
‘retry_delay` being how many ms to sleep before try to lock again (default: 200)
-
‘retry_jitter` being how many ms to jitter retry delay (default: 50)
-
‘redis_timeout` being how the Redis timeout will be set in seconds (default: 0.1)
-
‘time_source` being a callable object returning a monotonic time in milliseconds
(default: see #default_time_source)
-
# File lib/redlock/client.rb, line 48 def initialize(servers = DEFAULT_REDIS_URLS, options = {}) redis_timeout = options[:redis_timeout] || DEFAULT_REDIS_TIMEOUT @servers = servers.map do |server| if server.is_a?(String) RedisInstance.new(url: server, timeout: redis_timeout) else RedisInstance.new(server) end end @quorum = (servers.length / 2).to_i + 1 @retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT @retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY @retry_jitter = options[:retry_jitter] || DEFAULT_RETRY_JITTER @time_source = options[:time_source] || self.class.default_time_source end
Public Instance Methods
Gets remaining ttl of a resource. The ttl is returned if the holder currently holds the lock and it has not expired, otherwise the method returns nil. Params:
lock_info
-
the lock that has been acquired when you locked the resource
# File lib/redlock/client.rb, line 125 def get_remaining_ttl_for_lock(lock_info) ttl_info = try_get_remaining_ttl(lock_info[:resource]) return nil if ttl_info.nil? || ttl_info[:value] != lock_info[:value] ttl_info[:ttl] end
Gets remaining ttl of a resource. If there is no valid lock, the method returns nil. Params:
resource
-
the name of the resource (string) for which to check the ttl
# File lib/redlock/client.rb, line 135 def get_remaining_ttl_for_resource(resource) ttl_info = try_get_remaining_ttl(resource) return nil if ttl_info.nil? ttl_info[:ttl] end
Locks a resource for a given time. Params:
resource
-
the resource (or key) string to be locked.
ttl
-
The time-to-live in ms for the lock.
options
-
Hash of optional parameters
* +retry_count+: see +initialize+ * +retry_delay+: see +initialize+ * +retry_jitter+: see +initialize+ * +extend+: A lock ("lock_info") to extend. * +extend_only_if_locked+: Boolean, if +extend+ is given, only acquire lock if currently held * +extend_only_if_life+: Deprecated, same as +extend_only_if_locked+ * +extend_life+: Deprecated, same as +extend_only_if_locked+
block
-
an optional block to be executed; after its execution, the lock (if successfully
acquired) is automatically unlocked.
# File lib/redlock/client.rb, line 78 def lock(resource, ttl, options = {}, &block) lock_info = try_lock_instances(resource, ttl, options) if options[:extend_only_if_life] && !Gem::Deprecate.skip warn 'DEPRECATION WARNING: The `extend_only_if_life` option has been renamed `extend_only_if_locked`.' options[:extend_only_if_locked] = options[:extend_only_if_life] end if options[:extend_life] && !Gem::Deprecate.skip warn 'DEPRECATION WARNING: The `extend_life` option has been renamed `extend_only_if_locked`.' options[:extend_only_if_locked] = options[:extend_life] end if block_given? begin yield lock_info !!lock_info ensure unlock(lock_info) if lock_info end else lock_info end end
Locks a resource, executing the received block only after successfully acquiring the lock, and returning its return value as a result. See Redlock::Client#lock
for parameters.
# File lib/redlock/client.rb, line 111 def lock!(resource, *args) fail 'No block passed' unless block_given? lock(resource, *args) do |lock_info| raise LockError, resource unless lock_info return yield end end
Checks if a resource is locked Params:
lock_info
-
the lock that has been acquired when you locked the resource
# File lib/redlock/client.rb, line 144 def locked?(resource) ttl = get_remaining_ttl_for_resource(resource) !(ttl.nil? || ttl.zero?) end
# File lib/redlock/testing.rb, line 9 def testing_mode=(mode) warn 'DEPRECATION WARNING: Instance-level `testing_mode` has been removed, and this ' + 'setter will be removed in the future. Please set the testing mode on the `Redlock::Client` ' + 'instead, e.g. `Redlock::Client.testing_mode = :bypass`.' self.class.testing_mode = mode end
Unlocks a resource. Params:
lock_info
-
the lock that has been acquired when you locked the resource.
# File lib/redlock/client.rb, line 104 def unlock(lock_info) @servers.each { |s| s.unlock(lock_info[:resource], lock_info[:value]) } end
Checks if a lock is still valid Params:
lock_info
-
the lock that has been acquired when you locked the resource
# File lib/redlock/client.rb, line 152 def valid_lock?(lock_info) ttl = get_remaining_ttl_for_lock(lock_info) !(ttl.nil? || ttl.zero?) end
Private Instance Methods
# File lib/redlock/client.rb, line 286 def attempt_retry_delay(attempt_number, options) retry_delay = options[:retry_delay] || @retry_delay retry_jitter = options[:retry_jitter] || @retry_jitter retry_delay = if retry_delay.respond_to?(:call) retry_delay.call(attempt_number) else retry_delay end (retry_delay + rand(retry_jitter)).to_f / 1000 end
# File lib/redlock/client.rb, line 361 def drift(ttl) # Add 2 milliseconds to the drift to account for Redis expires # precision, which is 1 millisecond, plus 1 millisecond min drift # for small TTLs. (ttl * CLOCK_DRIFT_FACTOR).to_i + 2 end
# File lib/redlock/client.rb, line 300 def lock_instances(resource, ttl, options) value = (options[:extend] || { value: SecureRandom.uuid })[:value] allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes' errors = [] locked, time_elapsed = timed do @servers.count do |s| s.lock(resource, value, ttl, allow_new_lock) rescue => e errors << e false end end validity = ttl - time_elapsed - drift(ttl) if locked >= @quorum && validity >= 0 { validity: validity, resource: resource, value: value } else @servers.each { |s| s.unlock(resource, value) } if errors.size >= @quorum err_msgs = errors.map { |e| "#{e.class}: #{e.message}" }.join("\n") raise LockAcquisitionError.new("Too many Redis errors prevented lock acquisition:\n#{err_msgs}", errors) end false end end
# File lib/redlock/client.rb, line 368 def timed start_time = @time_source.call() [yield, @time_source.call() - start_time] end
# File lib/redlock/client.rb, line 330 def try_get_remaining_ttl(resource) # Responses from the servers are a 2 tuple of format [lock_value, ttl]. # The lock_value is nil if it does not exist. Since servers may have # different lock values, the responses are grouped by the lock_value and # transofrmed into a hash: { lock_value1 => [ttl1, ttl2, ttl3], # lock_value2 => [ttl4, tt5] } ttls_by_value, time_elapsed = timed do @servers.map { |s| s.get_remaining_ttl(resource) } .select { |ttl_tuple| ttl_tuple&.first } .group_by(&:first) .transform_values { |ttl_tuples| ttl_tuples.map { |t| t.last } } end # Authoritative lock value is that which is returned by the majority of # servers authoritative_value, ttls = ttls_by_value.max_by { |(lock_value, ttls)| ttls.length } if ttls && ttls.size >= @quorum # Return the minimum TTL of an N/2+1 selection. It will always be # correct (it will guarantee that at least N/2+1 servers have a TTL that # value or longer) min_ttl = ttls.sort.last(@quorum).first min_ttl = min_ttl - time_elapsed - drift(min_ttl) { value: authoritative_value, ttl: min_ttl } else # No lock_value is authoritatively held for the resource nil end end
# File lib/redlock/client.rb, line 266 def try_lock_instances(resource, ttl, options) retry_count = options[:retry_count] || @retry_count tries = options[:extend] ? 1 : (retry_count + 1) last_error = nil tries.times do |attempt_number| # Wait a random delay before retrying. sleep(attempt_retry_delay(attempt_number, options)) if attempt_number > 0 lock_info = lock_instances(resource, ttl, options) return lock_info if lock_info rescue => e last_error = e end raise last_error if last_error false end