Class: Magick::Adapters::Redis
- Defined in:
- lib/magick/adapters/redis.rb
Constant Summary collapse
- DEFAULT_TIMEOUTS =
redis-rb defaults every timeout to 5s. A feature-flag lookup is on the hot path of a request, and a Redis that black-holes packets (a dropped route, a hung box) does not refuse the connection — it just never answers — so an untimed client pins the request thread for those 5s while the circuit breaker never gets an error to count. One second is already an eternity for a HGET against a local Redis; past that we would rather fall through to the next adapter.
{ connect_timeout: 1.0, read_timeout: 1.0, write_timeout: 1.0 }.freeze
Instance Method Summary collapse
- #all_features ⇒ Object
-
#client ⇒ Object
Public accessor for the underlying Redis client.
- #delete(feature_name) ⇒ Object
- #delete_key(feature_name, key) ⇒ Object
- #exists?(feature_name) ⇒ Boolean
- #get(feature_name, key) ⇒ Object
- #get_all_data(feature_name) ⇒ Object
-
#initialize(redis_client = nil) ⇒ Redis
constructor
A new instance of Redis.
- #load_all_features_data ⇒ Object
-
#load_features_data_with_prefix(prefix) ⇒ Object
SCAN does the narrowing server-side; the Ruby check that follows guards against a glob metacharacter in the prefix widening the match.
-
#load_features_data_without_prefixes(prefixes) ⇒ Object
SCAN has no negative MATCH, so the keys are filtered by name before the pipeline runs.
-
#next_sequence(feature_name, key, floor: 0) ⇒ Object
Server-side allocation: HSETNX seeds the counter only if it is missing and HINCRBY is atomic, so every process sharing this Redis is handed a different number no matter how their calls interleave.
- #set(feature_name, key, value) ⇒ Object
- #set_all_data(feature_name, data_hash) ⇒ Object
Constructor Details
#initialize(redis_client = nil) ⇒ Redis
Returns a new instance of Redis.
21 22 23 24 25 26 27 |
# File 'lib/magick/adapters/redis.rb', line 21 def initialize(redis_client = nil) @redis = redis_client || default_redis_client @namespace = 'magick:features' raise AdapterError, 'Redis client is required' unless @redis rescue StandardError => e raise AdapterError, "Failed to initialize Redis adapter: #{e.}" end |
Instance Method Details
#all_features ⇒ Object
54 55 56 57 58 59 |
# File 'lib/magick/adapters/redis.rb', line 54 def all_features keys = scan_keys keys.map { |key| key.sub("#{namespace}:", '') } rescue StandardError => e raise AdapterError, "Failed to get all features from Redis: #{e.}" end |
#client ⇒ Object
Public accessor for the underlying Redis client
143 144 145 |
# File 'lib/magick/adapters/redis.rb', line 143 def client @redis end |
#delete(feature_name) ⇒ Object
42 43 44 45 46 |
# File 'lib/magick/adapters/redis.rb', line 42 def delete(feature_name) redis.del(key_for(feature_name)) rescue StandardError => e raise AdapterError, "Failed to delete from Redis: #{e.}" end |
#delete_key(feature_name, key) ⇒ Object
115 116 117 118 119 |
# File 'lib/magick/adapters/redis.rb', line 115 def delete_key(feature_name, key) redis.hdel(key_for(feature_name), key.to_s).to_i.positive? rescue StandardError => e raise AdapterError, "Failed to delete key from Redis: #{e.}" end |
#exists?(feature_name) ⇒ Boolean
48 49 50 51 52 |
# File 'lib/magick/adapters/redis.rb', line 48 def exists?(feature_name) redis.exists?(key_for(feature_name)) rescue StandardError => e raise AdapterError, "Failed to check existence in Redis: #{e.}" end |
#get(feature_name, key) ⇒ Object
29 30 31 32 33 34 |
# File 'lib/magick/adapters/redis.rb', line 29 def get(feature_name, key) value = redis.hget(key_for(feature_name), key.to_s) deserialize_value(value) rescue StandardError => e raise AdapterError, "Failed to get from Redis: #{e.}" end |
#get_all_data(feature_name) ⇒ Object
61 62 63 64 65 66 67 68 69 70 |
# File 'lib/magick/adapters/redis.rb', line 61 def get_all_data(feature_name) raw = redis.hgetall(key_for(feature_name)) return {} if raw.nil? || raw.empty? raw.each_with_object({}) do |(k, v), result| result[k.to_s] = deserialize_value(v) end rescue StandardError => e raise AdapterError, "Failed to get all data from Redis: #{e.}" end |
#load_all_features_data ⇒ Object
72 73 74 75 76 |
# File 'lib/magick/adapters/redis.rb', line 72 def load_all_features_data fetch_features_data(scan_keys) rescue StandardError => e raise AdapterError, "Failed to load all features from Redis: #{e.}" end |
#load_features_data_with_prefix(prefix) ⇒ Object
SCAN does the narrowing server-side; the Ruby check that follows guards against a glob metacharacter in the prefix widening the match.
80 81 82 83 84 85 86 |
# File 'lib/magick/adapters/redis.rb', line 80 def load_features_data_with_prefix(prefix) prefix = prefix.to_s keys = scan_keys(match: "#{glob_escape(prefix)}*") fetch_features_data(keys.select { |key| feature_name_from(key).start_with?(prefix) }) rescue StandardError => e raise AdapterError, "Failed to load prefixed features from Redis: #{e.}" end |
#load_features_data_without_prefixes(prefixes) ⇒ Object
SCAN has no negative MATCH, so the keys are filtered by name before the pipeline runs. Only the names travel; the excluded hashes are never fetched, which is the whole point — the version archive is the bulk of the payload a preload would otherwise pull down.
92 93 94 95 96 97 98 99 100 101 |
# File 'lib/magick/adapters/redis.rb', line 92 def load_features_data_without_prefixes(prefixes) prefixes = Array(prefixes).map(&:to_s) keys = scan_keys.reject do |key| name = feature_name_from(key) prefixes.any? { |prefix| name.start_with?(prefix) } end fetch_features_data(keys) rescue StandardError => e raise AdapterError, "Failed to load unprefixed features from Redis: #{e.}" end |
#next_sequence(feature_name, key, floor: 0) ⇒ Object
Server-side allocation: HSETNX seeds the counter only if it is missing and HINCRBY is atomic, so every process sharing this Redis is handed a different number no matter how their calls interleave.
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/magick/adapters/redis.rb', line 124 def next_sequence(feature_name, key, floor: 0) redis_key = key_for(feature_name) field = key.to_s floor = floor.to_i redis.hsetnx(redis_key, field, floor) value = redis.hincrby(redis_key, field, 1).to_i return value if value > floor # The counter trailed the history the caller can see (a dump restored # without it, or entries written before there was one). Jump it past the # floor with another atomic increment: concurrent callers doing the same # still come away with distinct numbers. redis.hincrby(redis_key, field, (floor - value) + 1).to_i rescue StandardError => e raise AdapterError, "Failed to allocate sequence in Redis: #{e.}" end |
#set(feature_name, key, value) ⇒ Object
36 37 38 39 40 |
# File 'lib/magick/adapters/redis.rb', line 36 def set(feature_name, key, value) redis.hset(key_for(feature_name), key.to_s, serialize_value(value)) rescue StandardError => e raise AdapterError, "Failed to set in Redis: #{e.}" end |
#set_all_data(feature_name, data_hash) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/magick/adapters/redis.rb', line 103 def set_all_data(feature_name, data_hash) return if data_hash.nil? || data_hash.empty? serialized = {} data_hash.each do |key, value| serialized[key.to_s] = serialize_value(value) end redis.mapped_hmset(key_for(feature_name), serialized) rescue StandardError => e raise AdapterError, "Failed to set all data in Redis: #{e.}" end |