Class: GCRA::RedisStore

Inherits:
Object
  • Object
show all
Defined in:
lib/gcra/redis_store.rb

Overview

Redis store, expects all timestamps and durations to be integers with nanoseconds since epoch.

Constant Summary collapse

CAS_SCRIPT =
"local v = redis.call('get', KEYS[1])\nif v == false then\n  return redis.error_reply(\"key does not exist\")\nend\nif v ~= ARGV[1] then\n  return 0\nend\nredis.call('psetex', KEYS[1], ARGV[3], ARGV[2])\nreturn 1\n".freeze
CAS_SHA =

Digest::SHA1.hexdigest(CAS_SCRIPT)

"89118e702230c0d65969c5fc557a6e942a2f4d31".freeze
CAS_SCRIPT_MISSING_KEY_RESPONSE_PATTERN =
Regexp.new('^key does not exist')
SCRIPT_NOT_IN_CACHE_RESPONSE_PATTERN =
Regexp.new(
  '^NOSCRIPT No matching script(?:\. Please use EVAL\.)?'
)

Instance Method Summary collapse

Constructor Details

#initialize(redis, key_prefix, options = {}) ⇒ RedisStore

Returns a new instance of RedisStore.



23
24
25
26
27
28
# File 'lib/gcra/redis_store.rb', line 23

def initialize(redis, key_prefix, options = {})
  @redis = redis
  @key_prefix = key_prefix

  @reconnect_on_readonly = options[:reconnect_on_readonly] || false
end

Instance Method Details

#compare_and_set_with_ttl(key, old_value, new_value, ttl_nano) ⇒ Object

Atomically compare the value at key to the old value. If it matches, set it to the new value and return true. Otherwise, return false. If the key does not exist in the store, return false with no error. If the swap succeeds, update the ttl for the key atomically.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/gcra/redis_store.rb', line 67

def compare_and_set_with_ttl(key, old_value, new_value, ttl_nano)
  full_key = @key_prefix + key
  retried = false
  begin
    ttl_milli = calculate_ttl_milli(ttl_nano)
    swapped = @redis.evalsha(CAS_SHA, keys: [full_key], argv: [old_value, new_value, ttl_milli])
  rescue Redis::ReadOnlyError => e
    if @reconnect_on_readonly && !retried
      @redis.close
      retried = true
      retry
    end
    raise
  rescue Redis::CommandError => e
    if e.message =~ CAS_SCRIPT_MISSING_KEY_RESPONSE_PATTERN
      return false
    elsif e.message =~ SCRIPT_NOT_IN_CACHE_RESPONSE_PATTERN && !retried
      @redis.script('load', CAS_SCRIPT)
      retried = true
      retry
    end
    raise
  end

  return swapped == 1
end

#get_with_time(key) ⇒ Object

Returns the value of the key or nil, if it isn’t in the store. Also returns the time from the Redis server, with microsecond precision.



32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/gcra/redis_store.rb', line 32

def get_with_time(key)
  time_response, value = @redis.pipelined do |pipeline|
    pipeline.time # returns tuple (seconds since epoch, microseconds)
    pipeline.get(@key_prefix + key)
  end
  # Convert tuple to nanoseconds
  time = (time_response[0] * 1_000_000 + time_response[1]) * 1_000
  if value != nil
    value = value.to_i
  end

  return value, time
end

#set_if_not_exists_with_ttl(key, value, ttl_nano) ⇒ Object

Set the value of key only if it is not already set. Return whether the value was set. Also set the key’s expiration (ttl, in seconds).



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/gcra/redis_store.rb', line 48

def set_if_not_exists_with_ttl(key, value, ttl_nano)
  full_key = @key_prefix + key
  retried = false
  begin
    ttl_milli = calculate_ttl_milli(ttl_nano)
    @redis.set(full_key, value, nx: true, px: ttl_milli)
  rescue Redis::ReadOnlyError => e
    if @reconnect_on_readonly && !retried
      @redis.close
      retried = true
      retry
    end
    raise
  end
end