Class: Smith::PersistenceAdapters::RedisStore

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

Constant Summary collapse

TRANSIENT_ERROR_NAMES =

Redis transient errors — narrow list; non-transient errors (CommandError, etc.) propagate up immediately. Pattern matches Redis::BaseConnectionError if loaded (covers Connection/Timeout) via class-name guard so Smith doesn't require redis at load time.

%w[
  Redis::BaseConnectionError
  Redis::TimeoutError
  Redis::CannotConnectError
  Redis::ConnectionError
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(redis:, namespace: "smith") ⇒ RedisStore

Returns a new instance of RedisStore.



27
28
29
30
# File 'lib/smith/persistence_adapters/redis_store.rb', line 27

def initialize(redis:, namespace: "smith")
  @redis_source = redis
  @namespace = namespace.nil? ? nil : namespace.to_s.dup.freeze
end

Class Method Details

.transient_errorsObject



19
20
21
22
23
24
25
# File 'lib/smith/persistence_adapters/redis_store.rb', line 19

def self.transient_errors
  TRANSIENT_ERROR_NAMES.filter_map do |name|
    Object.const_get(name)
  rescue NameError
    nil
  end + [Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EPIPE]
end

Instance Method Details

#delete(key) ⇒ Object



48
49
50
51
52
# File 'lib/smith/persistence_adapters/redis_store.rb', line 48

def delete(key)
  Retry.with_retries(operation: :delete, transient: self.class.transient_errors) do
    client.del(namespaced(key), namespaced_heartbeat(key))
  end
end

#fetch(key) ⇒ Object



42
43
44
45
46
# File 'lib/smith/persistence_adapters/redis_store.rb', line 42

def fetch(key)
  Retry.with_retries(operation: :fetch, transient: self.class.transient_errors) do
    client.get(namespaced(key))
  end
end

#last_heartbeat(key) ⇒ Object



67
68
69
70
71
72
73
74
75
76
# File 'lib/smith/persistence_adapters/redis_store.rb', line 67

def last_heartbeat(key)
  Retry.with_retries(operation: :last_heartbeat, transient: self.class.transient_errors) do
    raw = client.get(namespaced_heartbeat(key))
    next nil if raw.nil?

    Time.parse(raw).utc
  rescue ArgumentError
    nil
  end
end

#record_heartbeat(key, ttl: Smith.config.persistence_ttl) ⇒ Object



56
57
58
59
60
61
62
63
64
65
# File 'lib/smith/persistence_adapters/redis_store.rb', line 56

def record_heartbeat(key, ttl: Smith.config.persistence_ttl)
  Retry.with_retries(operation: :record_heartbeat, transient: self.class.transient_errors) do
    iso = Time.now.utc.iso8601
    if ttl
      client.set(namespaced_heartbeat(key), iso, ex: ttl)
    else
      client.set(namespaced_heartbeat(key), iso)
    end
  end
end

#store(key, payload, ttl: Smith.config.persistence_ttl) ⇒ Object



32
33
34
35
36
37
38
39
40
# File 'lib/smith/persistence_adapters/redis_store.rb', line 32

def store(key, payload, ttl: Smith.config.persistence_ttl)
  Retry.with_retries(operation: :store, transient: self.class.transient_errors) do
    if ttl
      client.set(namespaced(key), payload, ex: ttl)
    else
      client.set(namespaced(key), payload)
    end
  end
end

#store_versioned(key, payload, expected_version:, ttl: Smith.config.persistence_ttl) ⇒ Object

Optimistic locking via Redis WATCH/MULTI/EXEC. Raises Smith::PersistenceVersionConflict on a stale expected_version OR on EXEC failure (WATCH detected concurrent write).



81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/smith/persistence_adapters/redis_store.rb', line 81

def store_versioned(key, payload, expected_version:, ttl: Smith.config.persistence_ttl)
  Retry.with_retries(operation: :store_versioned, transient: self.class.transient_errors) do
    RedisVersionedWrite.new(
      client: client,
      key: key,
      storage_key: namespaced(key),
      payload: payload,
      expected_version: expected_version,
      ttl: ttl
    ).call
  end
end

#transaction_open?Boolean

Returns:

  • (Boolean)


54
# File 'lib/smith/persistence_adapters/redis_store.rb', line 54

def transaction_open? = false