Class: Idempotency::Cache

Inherits:
Object
  • Object
show all
Defined in:
lib/idempotency/cache.rb

Defined Under Namespace

Classes: LockConflict

Constant Summary collapse

DEFAULT_CACHE_EXPIRY =

seconds = 1 hour

86_400
COMPARE_AND_DEL_SCRIPT =
"    local value = ARGV[1]\n    local cached_value = redis.call('GET', KEYS[1])\n\n    if( value == cached_value )\n    then\n        redis.call('DEL', KEYS[1])\n        return value\n    end\n\n    return cached_value\n"
COMPARE_AND_DEL_SCRIPT_SHA =
Digest::SHA1.hexdigest(COMPARE_AND_DEL_SCRIPT)

Instance Method Summary collapse

Constructor Details

#initialize(config: Idempotency.config) ⇒ Cache

Returns a new instance of Cache.



27
28
29
30
# File 'lib/idempotency/cache.rb', line 27

def initialize(config: Idempotency.config)
  @logger = config.logger
  @redis_pool = config.redis_pool
end

Instance Method Details

#get(fingerprint) ⇒ Object



32
33
34
35
36
37
38
39
40
# File 'lib/idempotency/cache.rb', line 32

def get(fingerprint)
  key = response_cache_key(fingerprint)

  cached_response = with_redis do |r|
    r.get(key)
  end

  deserialize(cached_response) if cached_response
end

#lock(fingerprint, duration) ⇒ Object

Raises:



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/idempotency/cache.rb', line 57

def lock(fingerprint, duration)
  random_value = SecureRandom.hex
  key = lock_key(fingerprint)

  lock_acquired = with_redis do |r|
    r.set(key, random_value, nx: true, ex: duration || Idempotency.config.default_lock_expiry)
  end

  raise LockConflict unless lock_acquired

  random_value
end

#release_lock(fingerprint, acquired_lock) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/idempotency/cache.rb', line 70

def release_lock(fingerprint, acquired_lock)
  with_redis do |r|
    lock_released = r.evalsha(COMPARE_AND_DEL_SCRIPT_SHA, keys: [lock_key(fingerprint)], argv: [acquired_lock])
    raise LockConflict if lock_released != acquired_lock
  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
      r.script(:load, COMPARE_AND_DEL_SCRIPT)
      retry
    else
      raise e
    end
  end
end

#set(fingerprint, response_status, response_headers, response_body) ⇒ Object



42
43
44
45
46
47
48
# File 'lib/idempotency/cache.rb', line 42

def set(fingerprint, response_status, response_headers, response_body)
  key = response_cache_key(fingerprint)

  with_redis do |r|
    r.set(key, serialize(response_status, response_headers, response_body), ex: DEFAULT_CACHE_EXPIRY)
  end
end

#with_lock(fingerprint, duration) ⇒ Object



50
51
52
53
54
55
# File 'lib/idempotency/cache.rb', line 50

def with_lock(fingerprint, duration)
  acquired_lock = lock(fingerprint, duration)
  yield
ensure
  release_lock(fingerprint, acquired_lock) if acquired_lock
end