Class: Magick::Adapters::Redis

Inherits:
Base
  • Object
show all
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

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.message}"
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.message}"
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.message}"
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.message}"
end

#exists?(feature_name) ⇒ Boolean

Returns:

  • (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.message}"
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.message}"
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.message}"
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.message}"
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.message}"
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.message}"
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.message}"
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.message}"
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.message}"
end