Class: Ask::State::Providers::Redis
- Inherits:
-
Adapter
- Object
- Adapter
- Ask::State::Providers::Redis
- Defined in:
- lib/ask/state/providers/redis.rb
Overview
A distributed key-value store backed by Redis.
Implements the full Adapter contract: key-value storage, distributed locking, message queues, and ordered lists. Best suited for multi-process or multi-host deployments where shared state is required.
Constant Summary collapse
- NAMESPACE =
"ask:state"
Instance Method Summary collapse
-
#acquire_lock(key, ttl: 10) ⇒ Object
-- distributed locking --.
- #clear ⇒ Object
-
#close ⇒ Object
-- lifecycle --.
- #delete(key) ⇒ Object
- #dequeue(queue) ⇒ Object
-
#enqueue(queue, value) ⇒ Object
-- message queues --.
- #exists?(key) ⇒ Boolean
-
#get(key) ⇒ Object
-- key-value --.
-
#initialize(url: ENV.fetch("REDIS_URL", "redis://localhost:6379")) ⇒ Redis
constructor
Creates a new Redis-backed state store.
- #keys(pattern: nil) ⇒ Object
-
#list_append(key, value, max_length: nil) ⇒ Object
-- ordered lists --.
- #list_range(key, start = 0, stop = -1)) ⇒ Object
- #list_remove(key, value) ⇒ Object
- #queue_depth(queue) ⇒ Object
- #release_lock(key, lock) ⇒ Object
- #set(key, value, ttl: nil) ⇒ Object
- #set_if_not_exists(key, value, ttl: nil) ⇒ Object
Constructor Details
#initialize(url: ENV.fetch("REDIS_URL", "redis://localhost:6379")) ⇒ Redis
Creates a new Redis-backed state store.
27 28 29 30 31 |
# File 'lib/ask/state/providers/redis.rb', line 27 def initialize(url: ENV.fetch("REDIS_URL", "redis://localhost:6379")) require "redis" @redis = ::Redis.new(url: url) end |
Instance Method Details
#acquire_lock(key, ttl: 10) ⇒ Object
-- distributed locking --
74 75 76 77 78 79 80 81 |
# File 'lib/ask/state/providers/redis.rb', line 74 def acquire_lock(key, ttl: 10) prefixed_key = prefixed("lock:#{key}") token = SecureRandom.hex(16) expires_at = Time.now + ttl acquired = @redis.call("SET", prefixed_key, token, "NX", "EX", ttl) == "OK" acquired ? Lock.new(id: key, token: token, expires_at: expires_at) : nil end |
#clear ⇒ Object
56 57 58 59 |
# File 'lib/ask/state/providers/redis.rb', line 56 def clear keys = @redis.call("KEYS", "#{NAMESPACE}:*") @redis.call("DEL", *keys) if keys.any? end |
#close ⇒ Object
-- lifecycle --
146 147 148 |
# File 'lib/ask/state/providers/redis.rb', line 146 def close @redis&.close end |
#delete(key) ⇒ Object
46 47 48 |
# File 'lib/ask/state/providers/redis.rb', line 46 def delete(key) @redis.call("DEL", prefixed(key)) end |
#dequeue(queue) ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/ask/state/providers/redis.rb', line 109 def dequeue(queue) val = @redis.call("LPOP", prefixed("queue:#{queue}")) return nil unless val entry = JSON.parse(val) QueueEntry.new( id: entry["id"], value: entry["value"], enqueued_at: Time.parse(entry["enqueued_at"]) ) end |
#enqueue(queue, value) ⇒ Object
-- message queues --
100 101 102 103 104 105 106 107 |
# File 'lib/ask/state/providers/redis.rb', line 100 def enqueue(queue, value) id = SecureRandom.uuid entry = { id: id, value: value, enqueued_at: Time.now.iso8601 } @redis.call("RPUSH", prefixed("queue:#{queue}"), JSON.generate(entry)) QueueEntry.new(id: id, value: value, enqueued_at: Time.now) end |
#exists?(key) ⇒ Boolean
61 62 63 |
# File 'lib/ask/state/providers/redis.rb', line 61 def exists?(key) @redis.call("EXISTS", prefixed(key)) == 1 end |
#get(key) ⇒ Object
-- key-value --
35 36 37 38 |
# File 'lib/ask/state/providers/redis.rb', line 35 def get(key) val = @redis.call("GET", prefixed(key)) val ? JSON.parse(val) : nil end |
#keys(pattern: nil) ⇒ Object
65 66 67 68 69 70 |
# File 'lib/ask/state/providers/redis.rb', line 65 def keys(pattern: nil) glob = pattern ? "#{NAMESPACE}:#{pattern}" : "#{NAMESPACE}:*" raw = @redis.call("KEYS", glob) prefix_len = NAMESPACE.length + 1 raw.map { |k| k[prefix_len..] } end |
#list_append(key, value, max_length: nil) ⇒ Object
-- ordered lists --
127 128 129 130 131 132 |
# File 'lib/ask/state/providers/redis.rb', line 127 def list_append(key, value, max_length: nil) @redis.call("RPUSH", prefixed("list:#{key}"), JSON.generate(value)) return unless max_length @redis.call("LTRIM", prefixed("list:#{key}"), -max_length, -1) end |
#list_range(key, start = 0, stop = -1)) ⇒ Object
134 135 136 137 |
# File 'lib/ask/state/providers/redis.rb', line 134 def list_range(key, start = 0, stop = -1) items = @redis.call("LRANGE", prefixed("list:#{key}"), start, stop) items.map { |item| JSON.parse(item) } end |
#list_remove(key, value) ⇒ Object
139 140 141 142 |
# File 'lib/ask/state/providers/redis.rb', line 139 def list_remove(key, value) serialized = JSON.generate(value) @redis.call("LREM", prefixed("list:#{key}"), 0, serialized) end |
#queue_depth(queue) ⇒ Object
121 122 123 |
# File 'lib/ask/state/providers/redis.rb', line 121 def queue_depth(queue) @redis.call("LLEN", prefixed("queue:#{queue}")) end |
#release_lock(key, lock) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/ask/state/providers/redis.rb', line 83 def release_lock(key, lock) prefixed_key = prefixed("lock:#{key}") # Atomic release: only delete if token matches (Lua script) script = <<~LUA if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end LUA @redis.call("EVAL", script, [1], [prefixed_key, lock.token]) == 1 end |
#set(key, value, ttl: nil) ⇒ Object
40 41 42 43 44 |
# File 'lib/ask/state/providers/redis.rb', line 40 def set(key, value, ttl: nil) args = [prefixed(key), JSON.generate(value)] args.concat(["EX", ttl.to_i]) if ttl @redis.call("SET", *args) end |
#set_if_not_exists(key, value, ttl: nil) ⇒ Object
50 51 52 53 54 |
# File 'lib/ask/state/providers/redis.rb', line 50 def set_if_not_exists(key, value, ttl: nil) args = [prefixed(key), JSON.generate(value)] args.concat(["EX", ttl.to_i]) if ttl @redis.call("SET", *args, "NX") == "OK" end |