Class: SafeMemoize::Stores::Redis

Inherits:
Base
  • Object
show all
Defined in:
lib/safe_memoize/stores/redis.rb

Overview

Cache store adapter backed by Redis.

Not auto-required. Add to your application: require "safe_memoize/stores/redis"

Requires a Redis-compatible client that responds to +#get+, +#set+, +#del+, and +#scan_each+. Compatible with the +redis+ gem (v4+) and any drop-in replacement.

Values and keys are serialized with +Marshal+ (Base64-encoded via +Array#pack("m0")+) so that any Ruby object, including +nil+ and +false+, can be stored and retrieved faithfully. TTL is forwarded to Redis as the +PX+ option (milliseconds, rounded up to the nearest millisecond; minimum 1 ms) to preserve sub-second precision.

Examples:

Basic setup

require "redis"
require "safe_memoize/stores/redis"

REDIS_STORE = SafeMemoize::Stores::Redis.new(::Redis.new)

class MyService
  prepend SafeMemoize
  def fetch(id) = http_get(id)
  memoize :fetch, store: REDIS_STORE, ttl: 300
end

With a custom namespace

STORE = SafeMemoize::Stores::Redis.new(::Redis.new, namespace: "myapp:memo")

Constant Summary

Constants inherited from Base

Base::MISS

Instance Method Summary collapse

Methods inherited from Base

#exist?

Constructor Details

#initialize(client, namespace: "safe_memoize") ⇒ Redis

Returns a new instance of Redis.

Parameters:

  • client (Object)

    a Redis-compatible client responding to +#get+, +#set+, +#del+, and +#scan_each+

  • namespace (String) (defaults to: "safe_memoize")

    key prefix used to scope all entries in Redis; defaults to +"safe_memoize"+



41
42
43
44
# File 'lib/safe_memoize/stores/redis.rb', line 41

def initialize(client, namespace: "safe_memoize")
  @client = client
  @namespace = namespace
end

Instance Method Details

#clearvoid

This method returns an undefined value.

Removes all entries written by this adapter (scoped to the namespace). Uses +SCAN+ internally to avoid blocking Redis.



80
81
82
83
84
# File 'lib/safe_memoize/stores/redis.rb', line 80

def clear
  to_delete = []
  @client.scan_each(match: "#{@namespace}:*") { |k| to_delete << k }
  @client.del(*to_delete) unless to_delete.empty?
end

#delete(key) ⇒ void

This method returns an undefined value.

Parameters:

  • key (Object)


73
74
75
# File 'lib/safe_memoize/stores/redis.rb', line 73

def delete(key)
  @client.del(redis_key(key))
end

#keysArray<Object>

Returns all live keys in the namespace, deserialized back to their original Ruby form. Entries that cannot be deserialized are silently skipped. Because Redis handles TTL natively, every key returned by +SCAN+ is live.

Returns:

  • (Array<Object>)


92
93
94
95
96
97
98
99
100
101
102
# File 'lib/safe_memoize/stores/redis.rb', line 92

def keys
  prefix = "#{@namespace}:"
  result = []
  @client.scan_each(match: "#{@namespace}:*") do |rk|
    encoded = rk.delete_prefix(prefix)
    result << Marshal.load(encoded.unpack1("m0")) # rubocop:disable Security/MarshalLoad
  rescue ArgumentError, TypeError
    # skip keys that cannot be deserialized (e.g. written by another serializer)
  end
  result
end

#read(key) ⇒ Object

Returns the stored value, or Base::MISS if absent.

Parameters:

  • key (Object)

    cache key (serialized with Marshal + Base64)

Returns:

  • (Object)

    the stored value, or Base::MISS if absent



48
49
50
51
52
53
54
55
# File 'lib/safe_memoize/stores/redis.rb', line 48

def read(key)
  raw = @client.get(redis_key(key))
  return MISS if raw.nil?

  Marshal.load(raw) # rubocop:disable Security/MarshalLoad
rescue TypeError, ArgumentError
  MISS
end

#write(key, value, expires_in: nil) ⇒ void

This method returns an undefined value.

Parameters:

  • key (Object)

    cache key

  • value (Object)

    value to store (may be +nil+ or +false+)

  • expires_in (Numeric, nil) (defaults to: nil)

    TTL in seconds forwarded to Redis as +PX+ (milliseconds, rounded up; minimum 1 ms); +nil+ means no expiry



62
63
64
65
66
67
68
69
# File 'lib/safe_memoize/stores/redis.rb', line 62

def write(key, value, expires_in: nil)
  raw = Marshal.dump(value)
  if expires_in
    @client.set(redis_key(key), raw, px: [(expires_in * 1000).ceil, 1].max)
  else
    @client.set(redis_key(key), raw)
  end
end