Module: SimpleLock

Extended by:
With
Defined in:
lib/simple_lock.rb,
lib/initializers/with.rb,
lib/simple_lock/redis.rb,
lib/simple_lock/config.rb,
lib/simple_lock/scripts.rb,
lib/simple_lock/version.rb,
lib/initializers/delegation.rb

Defined Under Namespace

Modules: Delegation, With Classes: Config, Error, Redis, Script

Constant Summary collapse

NOSCRIPT_MAX_RETRIES =
1
LOCK_VALUE =
"1"
SCRIPTS =
{
  lock: Script.new("return redis.call('set', KEYS[1], #{LOCK_VALUE}, 'NX', 'PX', ARGV[1])"),
  unlock: Script.new("redis.call('del', KEYS[1])")
}.freeze
VERSION =
"1.0.0"

Class Method Summary collapse

Methods included from With

with

Class Method Details

.backoff_for_attempt(attempt) ⇒ Object



83
84
85
86
87
# File 'lib/simple_lock.rb', line 83

def self.backoff_for_attempt(attempt)
  delay = config.retry_proc.respond_to?(:call) ? config.retry_proc.call(attempt) : config.retry_delay

  (delay + rand(config.retry_jitter)).fdiv(1000)
end

.client ⇒ Object



15
16
17
# File 'lib/simple_lock.rb', line 15

def self.client
  @client ||= SimpleLock::Redis.new(url: nil)
end

.client=(client_or_url) ⇒ Object



19
20
21
22
23
24
25
26
27
28
# File 'lib/simple_lock.rb', line 19

def self.client=(client_or_url)
  case client_or_url
  when SimpleLock::Redis
    @client = client
  when String
    @client = SimpleLock::Redis.new(url: client_or_url)
  else
    raise ArgumentError, "client must be an instance of SimpleLock::Redis or a String"
  end
end

.config ⇒ Object



30
31
32
# File 'lib/simple_lock.rb', line 30

def self.config
  @config ||= Config.new
end

.load_scripts ⇒ Object



62
63
64
65
66
# File 'lib/simple_lock.rb', line 62

def self.load_scripts
  SCRIPTS.each_value do |script|
    client.script("load", script.raw)
  end
end

.lock(key, ttl) ⇒ Object

rubocop:disable Metrics/MethodLength



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/simple_lock.rb', line 35

def self.lock(key, ttl)
  key = "#{config.key_prefix}#{key}"

  locked = (config.retry_count + 1).times.any? do |attempt|
    sleep(backoff_for_attempt(attempt)) unless attempt.zero?

    safe_exec_script(SCRIPTS[:lock], [key], [ttl]) == "OK"
  end

  return locked unless block_given?

  begin
    yield(locked)
  ensure
    unlock(key) if locked
  end
end

.safe_exec_script(script) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/simple_lock.rb', line 68

def self.safe_exec_script(script, ...)
  retries = 0

  begin
    client.evalsha(script.sha, ...)
  rescue ::Redis::CommandError => e
    if e.message.include?("NOSCRIPT") && (retries += 1) <= NOSCRIPT_MAX_RETRIES
      load_scripts
      retry
    end

    raise Error, e.message
  end
end

.unlock(key) ⇒ Object

rubocop:enable Metrics/MethodLength



54
55
56
57
58
59
60
# File 'lib/simple_lock.rb', line 54

def self.unlock(key)
  key = "#{config.key_prefix}#{key}"

  safe_exec_script(SCRIPTS[:unlock], [key])
rescue StandardError
  # Nothing to do, this is just a best-effort attempt.
end