Class: Idempo::RedisBackend

Inherits:
Object
  • Object
show all
Defined in:
lib/idempo/redis_backend.rb

Defined Under Namespace

Classes: NullPool, Store

Constant Summary collapse

LOCK_TTL_SECONDS =

The TTL value for the lock, which is used if the process holding the lock crashes or gets killed

5 * 60
DELETE_BY_KEY_AND_VALUE_SCRIPT =
<<~EOL
  redis.replicate_commands()
  if redis.call("get",KEYS[1]) == ARGV[1] then
    -- we are still holding the lock, release it
    redis.call("del",KEYS[1])
    return "ok"
  else
    -- someone else holds the lock or it has expired
    return "stale"
  end
EOL
SET_WITH_TTL_IF_LOCK_STILL_HELD_SCRIPT =
<<~EOL
  redis.replicate_commands()
  if redis.call("get", KEYS[1]) == ARGV[1] then
    -- we are still holding the lock, we can go ahead and set it
    redis.call("set", KEYS[2], ARGV[2], "px", ARGV[3])
    return "ok"
  else
    return "stale"
  end
EOL

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(redis_or_connection_pool = Redis.new) ⇒ RedisBackend

Returns a new instance of RedisBackend.



67
68
69
70
71
# File 'lib/idempo/redis_backend.rb', line 67

def initialize(redis_or_connection_pool = Redis.new)
  require "redis"
  require "securerandom"
  @redis_pool = redis_or_connection_pool.respond_to?(:with) ? redis_or_connection_pool : NullPool.new(redis_or_connection_pool)
end

Class Method Details

.eval_or_evalsha(redis, script_code, keys:, argv:) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/idempo/redis_backend.rb', line 95

def self.eval_or_evalsha(redis, script_code, keys:, argv:)
  script_sha = Digest::SHA1.hexdigest(script_code)
  redis.evalsha(script_sha, keys: keys, argv: argv)
rescue Redis::CommandError => e
  if e.message.include? "NOSCRIPT"
    # The Redis server has never seen this script before. Needs to run only once in the entire lifetime
    # of the Redis server, until the script changes - in which case it will be loaded under a different SHA
    redis.script(:load, script_code)
    retry
  else
    raise e
  end
end

Instance Method Details

#prune!Object



91
92
93
# File 'lib/idempo/redis_backend.rb', line 91

def prune!
  # Do nothing
end

#with_idempotency_key(request_key) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/idempo/redis_backend.rb', line 73

def with_idempotency_key(request_key)
  lock_key = "idempo:lock:#{request_key}"
  token = SecureRandom.bytes(32)
  did_acquire = @redis_pool.with { |r| r.set(lock_key, token, nx: true, ex: LOCK_TTL_SECONDS) }

  raise Idempo::ConcurrentRequest unless did_acquire

  begin
    store = Store.new(redis_pool: @redis_pool, lock_redis_key: lock_key, lock_token: token, key: request_key)
    yield(store)
  ensure
    outcome_of_del = @redis_pool.with do |r|
      Idempo::RedisBackend.eval_or_evalsha(r, DELETE_BY_KEY_AND_VALUE_SCRIPT, keys: [lock_key], argv: [token])
    end
    Measurometer.increment_counter("idempo.redis_lock_state_when_releasing_lock", 1, state: outcome_of_del)
  end
end