Class: Redlock::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/redlock/client.rb,
lib/redlock/testing.rb

Defined Under Namespace

Classes: RedisInstance

Constant Summary collapse

DEFAULT_REDIS_HOST =
ENV["DEFAULT_REDIS_HOST"] || "localhost"
DEFAULT_REDIS_PORT =
ENV["DEFAULT_REDIS_PORT"] || "6379"
DEFAULT_REDIS_URLS =
["redis://#{DEFAULT_REDIS_HOST}:#{DEFAULT_REDIS_PORT}"]
DEFAULT_REDIS_TIMEOUT =
0.1
DEFAULT_RETRY_COUNT =
3
DEFAULT_RETRY_DELAY =
200
DEFAULT_RETRY_JITTER =
50
CLOCK_DRIFT_FACTOR =
0.01

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(servers = DEFAULT_REDIS_URLS, options = {}) ⇒ Client

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)
    


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 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

Class Attribute Details

.testing_modeObject

Returns the value of attribute testing_mode.



6
7
8
# File 'lib/redlock/testing.rb', line 6

def testing_mode
  @testing_mode
end

Class Method Details

.default_time_sourceObject

Returns default time source function depending on CLOCK_MONOTONIC availability.



30
31
32
33
34
35
36
# 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

Instance Method Details

#get_remaining_ttl_for_lock(lock_info) ⇒ Object

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



125
126
127
128
129
# 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

#get_remaining_ttl_for_resource(resource) ⇒ Object

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



135
136
137
138
139
# 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

#lock(resource, ttl, options = {}, &block) ⇒ Object

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.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 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

#lock!(resource, *args) ⇒ Object

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.



111
112
113
114
115
116
117
118
# 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

#locked?(resource) ⇒ Boolean

Checks if a resource is locked Params:

lock_info

the lock that has been acquired when you locked the resource

Returns:

  • (Boolean)


144
145
146
147
# File 'lib/redlock/client.rb', line 144

def locked?(resource)
  ttl = get_remaining_ttl_for_resource(resource)
  !(ttl.nil? || ttl.zero?)
end

#testing_mode=(mode) ⇒ Object



9
10
11
12
13
14
15
# 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

#try_lock_instances(resource, ttl, options) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# 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

#try_lock_instances_without_testingObject



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/redlock/testing.rb', line 17

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

#unlock(lock_info) ⇒ Object

Unlocks a resource. Params:

lock_info

the lock that has been acquired when you locked the resource.



104
105
106
# File 'lib/redlock/client.rb', line 104

def unlock(lock_info)
  @servers.each { |s| s.unlock(lock_info[:resource], lock_info[:value]) }
end

#unlock_without_testingObject

Unlocks a resource. Params:

lock_info

the lock that has been acquired when you locked the resource.



33
34
35
# File 'lib/redlock/testing.rb', line 33

def unlock(lock_info)
  @servers.each { |s| s.unlock(lock_info[:resource], lock_info[:value]) }
end

#valid_lock?(lock_info) ⇒ Boolean

Checks if a lock is still valid Params:

lock_info

the lock that has been acquired when you locked the resource

Returns:

  • (Boolean)


152
153
154
155
# 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