Class: Magick::Adapters::Registry
- Inherits:
-
Object
- Object
- Magick::Adapters::Registry
- Defined in:
- lib/magick/adapters/registry.rb
Constant Summary collapse
- CACHE_INVALIDATION_CHANNEL =
'magick:cache:invalidate'- MAX_INVALIDATION_PAYLOAD_BYTES =
An invalidation message is a small JSON object; anything larger than this did not come from us and is not worth parsing.
512- FEATURE_NAME_PATTERN =
Accept only conservative feature identifiers coming off the wire. Anything outside this alphabet (newlines, spaces, Unicode punctuation, 200-char garbage) is a sign of a malformed or malicious publisher and must not be fed back into Magick.features / feature.reload.
/\A[a-zA-Z0-9_\-.:]{1,120}\z/.freeze
- DEFAULT_REFRESH_INTERVAL =
How long a process may go without re-reading the shared backend. Pub/Sub is the fast path; this is the bound on staleness when that path is broken or bypassed — a write made outside a gem process, a Redis user without pub/sub permission, a proxy that drops SUBSCRIBE, a subscriber connection silently killed by a NAT/LB. Before it existed, a registered feature that missed one invalidation stayed stale until restart, and nothing said so. See ADR-0002.
30.0- SUBSCRIBER_FAILURE_REPORT_INTERVAL =
A subscriber that cannot subscribe retries every 5s. The first failure is reported at once; while it keeps failing, at most one report per this many seconds, so a permanently broken Redis does not flood the log.
300.0- SUBSCRIBER_RETRY_DELAY =
Seconds between attempts to (re)subscribe after a failure.
5
Instance Attribute Summary collapse
-
#active_record_adapter ⇒ Object
readonly
Public so Versioning can apply tiered retention: hot window written to memory/Redis, unlimited archive written to ActiveRecord only.
-
#async_writer ⇒ Object
readonly
The serialized writer draining async Redis writes, or nil when this registry has not made one yet (sync mode, or no write has happened).
-
#memory_adapter ⇒ Object
readonly
Public so Versioning can apply tiered retention: hot window written to memory/Redis, unlimited archive written to ActiveRecord only.
-
#redis_adapter ⇒ Object
Public so Versioning can apply tiered retention: hot window written to memory/Redis, unlimited archive written to ActiveRecord only.
-
#refresh_interval ⇒ Object
Seconds between periodic re-reads of the shared backend, or nil when the periodic refresh is off and Pub/Sub is the only thing that updates a registered feature (the behavior before the periodic refresh existed).
Instance Method Summary collapse
- #all_features ⇒ Object
-
#authoritative_get_all_data(feature_name) ⇒ Object
Read a feature's complete data straight from the shared, authoritative backend — ActiveRecord first (it is written synchronously on every set/set_all_data), then Redis — bypassing this process's local memory cache, and refresh memory with the result.
- #delete(feature_name) ⇒ Object
-
#delete_key(feature_name, key) ⇒ Object
Remove one key from a feature across every configured layer.
-
#ensure_subscriber! ⇒ Object
Restart the Pub/Sub subscriber after a fork.
-
#exists?(feature_name) ⇒ Boolean
Fail-safe by contract: an unreachable backend means "not found", never an exception.
- #get(feature_name, key) ⇒ Object
-
#get_all_data(feature_name) ⇒ Object
Load all keys for a single feature in one call instead of N separate get() calls.
-
#health ⇒ Object
A snapshot for host health checks: is this process listening for invalidations, when did it last confirm its view against the shared backend, and is anything queued.
-
#initialize(memory_adapter, redis_adapter = nil, active_record_adapter: nil, circuit_breaker: nil, async: false, primary: nil, async_queue_limit: nil, async_enqueue_timeout: nil, refresh_interval: DEFAULT_REFRESH_INTERVAL) ⇒ Registry
constructor
A new instance of Registry.
-
#invalidate_cache(feature_name) ⇒ Object
Explicitly trigger cache invalidation for a feature This is useful for targeting updates that need immediate cache invalidation Invalidates memory cache in current process AND publishes to Redis for other processes.
-
#next_sequence(feature_name, key, floor: 0) ⇒ Object
Allocate a number from a shared counter.
-
#pending_async_writes ⇒ Object
Writes accepted by the async writer but not yet sent to Redis.
-
#preload! ⇒ Object
Bulk load ALL features into memory cache in minimal queries.
-
#publish_cache_invalidation(feature_name) ⇒ Object
Publish cache invalidation message to Redis Pub/Sub (without deleting local memory cache) This is useful when you've just updated the cache and want to notify other processes but keep the local memory cache intact.
-
#publisher_id ⇒ Object
This registry's identity on the invalidation channel.
-
#redis_available? ⇒ Boolean
Check if Redis adapter is available.
-
#redis_client ⇒ Object
Get Redis client (public method for use by other classes).
-
#refresh_all_from_source ⇒ Object
Bulk variant of #authoritative_get_all_data: refresh the local memory cache for EVERY feature from the shared backend in 1-2 queries.
-
#refresh_from_source! ⇒ Object
Re-read every feature from the shared backend and bring this process in line with it: features whose stored data changed since the last read are written into memory and, when registered here, reloaded.
-
#refresh_if_stale! ⇒ Object
Hot-path entry point, called on every feature evaluation.
- #set(feature_name, key, value) ⇒ Object
-
#set_all_data(feature_name, data_hash) ⇒ Object
Bulk set multiple keys for a feature in one call (1 query instead of N).
-
#shutdown(timeout: 5) ⇒ Object
Gracefully terminate the Pub/Sub subscriber thread and its Redis connection.
- #stopping? ⇒ Boolean
-
#subscriber_running? ⇒ Boolean
True while this process holds a live subscription on the invalidation channel — the thread is alive AND Redis has acknowledged the SUBSCRIBE.
Constructor Details
#initialize(memory_adapter, redis_adapter = nil, active_record_adapter: nil, circuit_breaker: nil, async: false, primary: nil, async_queue_limit: nil, async_enqueue_timeout: nil, refresh_interval: DEFAULT_REFRESH_INTERVAL) ⇒ Registry
Returns a new instance of Registry.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/magick/adapters/registry.rb', line 38 def initialize(memory_adapter, redis_adapter = nil, active_record_adapter: nil, circuit_breaker: nil, async: false, primary: nil, async_queue_limit: nil, async_enqueue_timeout: nil, refresh_interval: DEFAULT_REFRESH_INTERVAL) @memory_adapter = memory_adapter @redis_adapter = redis_adapter @active_record_adapter = active_record_adapter @circuit_breaker = circuit_breaker || Magick::CircuitBreaker.new @async = async @async_queue_limit = async_queue_limit @async_enqueue_timeout = async_enqueue_timeout @async_writer = nil @write_order_mutex = Mutex.new @primary = primary || :memory # :memory, :redis, or :active_record @subscriber_thread = nil @subscriber = nil @subscriber_pid = nil # process that opened @subscriber; a fork must not touch its parent's @subscriber_generation = 0 # bumped to retire a running subscriber (reconfigure, shutdown) @subscribed = false @subscriber_last_error = nil @subscriber_failure_reported_at = nil @refresh_interval = normalize_refresh_interval(refresh_interval) @refresh_gate = Mutex.new # claims the once-per-interval slot @source_read_mutex = Mutex.new # serializes the bulk read + apply @last_refresh_at = nil # monotonic; gates the next refresh @last_refresh_time = nil # wall clock; reported by #health @source_snapshot = nil # feature => data as last read from the shared backend @stopping = false @shutdown_mutex = Mutex.new @owner_pid = Process.pid @identity_mutex = Mutex.new @publisher_id_pid = Process.pid @publisher_id = generate_publisher_id # Only start Pub/Sub subscriber if Redis is available # In memory-only mode, each process has isolated cache (no cross-process invalidation) start_cache_invalidation_subscriber if redis_adapter end |
Instance Attribute Details
#active_record_adapter ⇒ Object (readonly)
Public so Versioning can apply tiered retention: hot window written to memory/Redis, unlimited archive written to ActiveRecord only.
526 527 528 |
# File 'lib/magick/adapters/registry.rb', line 526 def active_record_adapter @active_record_adapter end |
#async_writer ⇒ Object (readonly)
The serialized writer draining async Redis writes, or nil when this registry has not made one yet (sync mode, or no write has happened).
530 531 532 |
# File 'lib/magick/adapters/registry.rb', line 530 def async_writer @async_writer end |
#memory_adapter ⇒ Object (readonly)
Public so Versioning can apply tiered retention: hot window written to memory/Redis, unlimited archive written to ActiveRecord only.
526 527 528 |
# File 'lib/magick/adapters/registry.rb', line 526 def memory_adapter @memory_adapter end |
#redis_adapter ⇒ Object
Public so Versioning can apply tiered retention: hot window written to memory/Redis, unlimited archive written to ActiveRecord only.
526 527 528 |
# File 'lib/magick/adapters/registry.rb', line 526 def redis_adapter @redis_adapter end |
#refresh_interval ⇒ Object
Seconds between periodic re-reads of the shared backend, or nil when the periodic refresh is off and Pub/Sub is the only thing that updates a registered feature (the behavior before the periodic refresh existed).
372 373 374 |
# File 'lib/magick/adapters/registry.rb', line 372 def refresh_interval @refresh_interval end |
Instance Method Details
#all_features ⇒ Object
197 198 199 200 201 202 203 204 205 |
# File 'lib/magick/adapters/registry.rb', line 197 def all_features features = [] features += safely([]) { memory_adapter&.all_features } || [] features += redis_read { |redis| redis.all_features } || [] features += safely([]) { active_record_adapter&.all_features } || [] # Version history and audit history are stored under reserved # pseudo-feature namespaces; they are bookkeeping, not features. features.uniq.reject { |f| reserved_store_name?(f) } end |
#authoritative_get_all_data(feature_name) ⇒ Object
Read a feature's complete data straight from the shared, authoritative backend — ActiveRecord first (it is written synchronously on every set/set_all_data), then Redis — bypassing this process's local memory cache, and refresh memory with the result.
The Admin UI uses this so a toggle is reflected immediately on whichever process/container serves the (load-balanced) request after the write, instead of rendering this process's possibly-stale memory cache while it waits for Pub/Sub invalidation to arrive.
247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/magick/adapters/registry.rb', line 247 def (feature_name) data = read_from_source(feature_name) if data && !data.empty? memory_adapter&.set_all_data(feature_name, data) return data end # Source unavailable (Redis/AR down, or feature absent there) — fall back # to whatever this process already has rather than wiping a usable cache. memory_adapter ? memory_adapter.get_all_data(feature_name) : {} end |
#delete(feature_name) ⇒ Object
171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/magick/adapters/registry.rb', line 171 def delete(feature_name) memory_adapter&.delete(feature_name) if redis_adapter # Behind the breaker like every other Redis write, and the peers are # only told to reload once Redis has actually forgotten the feature. deleted = write_to_redis(:delete, feature_name) { redis_adapter.delete(feature_name) } publish_cache_invalidation(feature_name) if deleted end return unless active_record_adapter write_to_active_record(:delete, feature_name) { active_record_adapter.delete(feature_name) } end |
#delete_key(feature_name, key) ⇒ Object
Remove one key from a feature across every configured layer.
No local-write bookkeeping is needed: self-invalidation is keyed on the publisher of each message, so this process's own echo is recognised by identity — and #delete_key does not publish at all.
333 334 335 336 337 338 339 |
# File 'lib/magick/adapters/registry.rb', line 333 def delete_key(feature_name, key) deleted = false [memory_adapter, redis_adapter, active_record_adapter].compact.each do |adapter| deleted = true if safely_delete_key(adapter, feature_name, key) end deleted end |
#ensure_subscriber! ⇒ Object
Restart the Pub/Sub subscriber after a fork. The subscriber thread is not carried into child processes, so a worker inheriting a stale reference must re-create its own subscription. Safe to call on every request; it only does work when Process.pid changes.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/magick/adapters/registry.rb', line 79 def ensure_subscriber! return if @owner_pid == Process.pid @shutdown_mutex.synchronize do return if @owner_pid == Process.pid # The thread did not survive the fork and the connection object is the # parent's: dropped, never closed (see #close_subscriber_connection). @subscriber_thread = nil @subscriber = nil @subscribed = false @owner_pid = Process.pid @stopping = false end start_cache_invalidation_subscriber if redis_adapter end |
#exists?(feature_name) ⇒ Boolean
Fail-safe by contract: an unreachable backend means "not found", never an exception. Callers reach this from outside the fail-safe evaluation path, where a raise would surface as a 500 rather than a disabled flag.
189 190 191 192 193 194 195 |
# File 'lib/magick/adapters/registry.rb', line 189 def exists?(feature_name) return true if safely(false) { memory_adapter&.exists?(feature_name) } return true if redis_read { |redis| redis.exists?(feature_name) } == true return true if safely(false) { active_record_adapter&.exists?(feature_name) } == true false end |
#get(feature_name, key) ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/magick/adapters/registry.rb', line 128 def get(feature_name, key) # Try memory first (fastest) - no Redis calls needed thanks to Pub/Sub invalidation value = memory_adapter.get(feature_name, key) if memory_adapter return value unless value.nil? # Fall back to Redis if available value = redis_read { |redis| redis.get(feature_name, key) } if !value.nil? && memory_adapter memory_adapter.set(feature_name, key, value) return value end # Fall back to Active Record if available if active_record_adapter begin value = active_record_adapter.get(feature_name, key) memory_adapter.set(feature_name, key, value) if !value.nil? && memory_adapter return value rescue StandardError, AdapterError nil end end nil end |
#get_all_data(feature_name) ⇒ Object
Load all keys for a single feature in one call instead of N separate get() calls
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/magick/adapters/registry.rb', line 208 def get_all_data(feature_name) # Try memory first if memory_adapter data = memory_adapter.get_all_data(feature_name) return data unless data.nil? || data.empty? end # Fall back to Redis data = redis_read { |redis| redis.get_all_data(feature_name) } if data && !data.empty? memory_adapter.set_all_data(feature_name, data) if memory_adapter return data end # Fall back to Active Record if active_record_adapter begin data = active_record_adapter.get_all_data(feature_name) if data && !data.empty? memory_adapter.set_all_data(feature_name, data) if memory_adapter return data end rescue StandardError, AdapterError # AR failed end end {} end |
#health ⇒ Object
A snapshot for host health checks: is this process listening for invalidations, when did it last confirm its view against the shared backend, and is anything queued. Plain values, safe to render as JSON.
452 453 454 455 456 457 458 459 460 461 462 |
# File 'lib/magick/adapters/registry.rb', line 452 def health { redis: redis_available?, active_record: !active_record_adapter.nil?, subscriber_running: subscriber_running?, subscriber_last_error: @subscriber_last_error, refresh_interval: @refresh_interval, last_source_refresh_at: @last_refresh_time, pending_async_writes: pending_async_writes } end |
#invalidate_cache(feature_name) ⇒ Object
Explicitly trigger cache invalidation for a feature This is useful for targeting updates that need immediate cache invalidation Invalidates memory cache in current process AND publishes to Redis for other processes
361 362 363 364 365 366 367 |
# File 'lib/magick/adapters/registry.rb', line 361 def invalidate_cache(feature_name) # Invalidate memory cache in current process immediately memory_adapter&.delete(feature_name) # Publish to Redis Pub/Sub to invalidate cache in other processes publish_cache_invalidation(feature_name) end |
#next_sequence(feature_name, key, floor: 0) ⇒ Object
Allocate a number from a shared counter. Unlike #set this deliberately does NOT fan out to every layer: a counter must have exactly one authority, or two processes reading different layers would be handed the same number. ActiveRecord is preferred (durable, row-locked), then Redis (atomic HINCRBY), then this process's memory as a last resort.
346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/magick/adapters/registry.rb', line 346 def next_sequence(feature_name, key, floor: 0) [active_record_adapter, redis_adapter, memory_adapter].compact.each do |adapter| value = begin adapter.next_sequence(feature_name, key, floor: floor) rescue StandardError, AdapterError, NotImplementedError nil end return value if value end nil end |
#pending_async_writes ⇒ Object
Writes accepted by the async writer but not yet sent to Redis.
533 534 535 |
# File 'lib/magick/adapters/registry.rb', line 533 def pending_async_writes @async_writer ? @async_writer.pending : 0 end |
#preload! ⇒ Object
Bulk load ALL features into memory cache in minimal queries. Call this after configuration to warm the cache.
Version snapshots and audit history are skipped, exactly as #all_features skips them: both are unbounded bookkeeping, and pulling the version archive into every worker's memory cache at boot cost tens of megabytes per worker on installs with a few thousand versions.
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/magick/adapters/registry.rb', line 278 def preload! all_data = {} # Load from ActiveRecord first (source of truth for persistence) if active_record_adapter begin all_data = load_features_only(active_record_adapter) rescue StandardError, AdapterError # AR failed, try Redis end end # Merge/override with Redis data (more up-to-date than AR in most setups) redis_data = redis_read { |redis| load_features_only(redis) } redis_data&.each do |feature_name, data| all_data[feature_name] ||= {} all_data[feature_name].merge!(data) end # Reserved bookkeeping namespaces (version snapshots, audit history) # are not features: keep their blobs out of the feature cache. all_data = all_data.reject { |feature_name, _| reserved_store_name?(feature_name) } # Populate memory cache in bulk if memory_adapter && !all_data.empty? all_data.each do |feature_name, data| memory_adapter.set_all_data(feature_name, data) end end all_data end |
#publish_cache_invalidation(feature_name) ⇒ Object
Publish cache invalidation message to Redis Pub/Sub (without deleting local memory cache) This is useful when you've just updated the cache and want to notify other processes but keep the local memory cache intact
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 |
# File 'lib/magick/adapters/registry.rb', line 479 def publish_cache_invalidation(feature_name) return unless redis_adapter begin # Through the breaker for two reasons: PUBLISH is a Redis round-trip # like any other and must not block a request thread on a dead # server, and an open circuit means this process cannot have written # the new value — inviting peers to reload would hand them the # pre-toggle state. circuit_breaker.call do redis_client = redis_adapter.client redis_client&.publish(CACHE_INVALIDATION_CHANNEL, (feature_name)) end rescue CircuitOpenError AdapterFailure.report(backend: :redis, operation: :publish_cache_invalidation, feature_name: feature_name, reason: 'circuit breaker open') rescue StandardError => e # Best effort for this process, but a dropped invalidation leaves every # OTHER process holding a stale value, so it is reported like any other # failed write rather than swallowed. AdapterFailure.report(backend: :redis, operation: :publish_cache_invalidation, feature_name: feature_name, error: e) end end |
#publisher_id ⇒ Object
This registry's identity on the invalidation channel. Every message it publishes carries it, and it is the ONLY thing the subscriber suppresses on: a message is ignored when we published it, and acted on otherwise.
Per-registry rather than per-process, so two registries sharing a process still see each other's writes; random, because PIDs collide across containers.
511 512 513 514 515 516 517 518 519 520 521 522 |
# File 'lib/magick/adapters/registry.rb', line 511 def publisher_id @identity_mutex.synchronize do # A forked child inherits the parent's identity along with the rest of # its memory. Re-mint it so parent and child do not each mistake the # other's invalidations for their own echo and ignore them. if @publisher_id_pid != Process.pid @publisher_id_pid = Process.pid @publisher_id = generate_publisher_id end @publisher_id end end |
#redis_available? ⇒ Boolean
Check if Redis adapter is available
465 466 467 |
# File 'lib/magick/adapters/registry.rb', line 465 def redis_available? !redis_adapter.nil? end |
#redis_client ⇒ Object
Get Redis client (public method for use by other classes)
470 471 472 473 474 |
# File 'lib/magick/adapters/registry.rb', line 470 def redis_client return nil unless redis_adapter redis_adapter.client end |
#refresh_all_from_source ⇒ Object
Bulk variant of #authoritative_get_all_data: refresh the local memory cache for EVERY feature from the shared backend in 1-2 queries. Returns the loaded data. Used by the Admin UI index so the full list reflects authoritative state regardless of which container serves it.
263 264 265 266 267 268 269 |
# File 'lib/magick/adapters/registry.rb', line 263 def refresh_all_from_source data = load_all_from_source if memory_adapter && !data.empty? data.each { |feature_name, feature_data| memory_adapter.set_all_data(feature_name, feature_data) } end data end |
#refresh_from_source! ⇒ Object
Re-read every feature from the shared backend and bring this process in line with it: features whose stored data changed since the last read are written into memory and, when registered here, reloaded. ActiveRecord is read first (written synchronously on every set), then Redis — the same source the Admin UI treats as authoritative.
The comparison is against the previous read of the source, not against memory. A local write in flight (memory ahead of the store, e.g. an async Redis write still queued) therefore never gets reverted: the source has not changed, so the feature is left alone, and when the write lands it shows up as a change whose reload is a no-op. The first read, with no previous one to compare to, is measured against memory instead.
Features that vanished from the source are deliberately NOT evicted. A backend that answers with a partial view (one adapter reachable, the other not; ActiveRecord half backfilled) must not be able to strip targeting off live features. Deletion through the gem still publishes an invalidation, which does evict.
Returns the names of the features that changed, or nil when no source answered (nothing is touched in that case). Never raises.
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/magick/adapters/registry.rb', line 416 def refresh_from_source! return nil unless source_adapter? # Serialized so a manual Magick.refresh! racing the scheduled read # cannot interleave two snapshots; the evaluation path never waits here # because #refresh_if_stale! admits one caller per interval. @source_read_mutex.synchronize do data = read_source_features return nil if data.nil? changed = changed_since_last_read(data) changed.each do |feature_name| memory_adapter&.set_all_data(feature_name, data[feature_name]) reload_registered_feature(feature_name) end @source_snapshot = data @last_refresh_time = Time.now changed end rescue StandardError => e AdapterFailure.report(backend: source_backend, operation: :refresh, error: e) nil end |
#refresh_if_stale! ⇒ Object
Hot-path entry point, called on every feature evaluation. Almost always a clock read and a comparison; once per interval, in exactly one thread, it re-reads the shared backend (see #refresh_from_source!). Concurrent callers skip rather than queue, and the slot is claimed before the read so a slow or failing source is probed once per interval, not once per evaluation. Never raises. Returns what #refresh_from_source! returned, or nil when nothing was done.
385 386 387 388 389 390 391 392 393 |
# File 'lib/magick/adapters/registry.rb', line 385 def refresh_if_stale! interval = @refresh_interval return nil unless interval && source_adapter? && refresh_due?(interval) return nil unless claim_refresh_slot(interval) refresh_from_source! rescue StandardError nil end |
#set(feature_name, key, value) ⇒ Object
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/magick/adapters/registry.rb', line 154 def set(feature_name, key, value) update_redis = proc do write_to_redis(:set, feature_name) { redis_adapter.set(feature_name, key, value) } end # Memory is always updated synchronously; Redis follows either inline # or through the serialized async writer. write_through(feature_name, update_redis) do memory_adapter&.set(feature_name, key, value) end # Always update Active Record if available (as fallback/persistence layer) return unless active_record_adapter write_to_active_record(:set, feature_name) { active_record_adapter.set(feature_name, key, value) } end |
#set_all_data(feature_name, data_hash) ⇒ Object
Bulk set multiple keys for a feature in one call (1 query instead of N)
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/magick/adapters/registry.rb', line 312 def set_all_data(feature_name, data_hash) update_redis = proc do write_to_redis(:set_all_data, feature_name) { redis_adapter.set_all_data(feature_name, data_hash) } end write_through(feature_name, update_redis) do memory_adapter&.set_all_data(feature_name, data_hash) end return unless active_record_adapter write_to_active_record(:set_all_data, feature_name) do active_record_adapter.set_all_data(feature_name, data_hash) end end |
#shutdown(timeout: 5) ⇒ Object
Gracefully terminate the Pub/Sub subscriber thread and its Redis connection.
Without this, Ruby/Puma shutdown waits on the blocking subscribe call.
110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/magick/adapters/registry.rb', line 110 def shutdown(timeout: 5) @shutdown_mutex.synchronize do return if @stopping @stopping = true end # Drain queued writes while the Redis connection is still usable. drain_async_writer(timeout) stop_subscriber!(timeout: timeout) true end |
#stopping? ⇒ Boolean
124 125 126 |
# File 'lib/magick/adapters/registry.rb', line 124 def stopping? @stopping == true end |
#subscriber_running? ⇒ Boolean
True while this process holds a live subscription on the invalidation channel — the thread is alive AND Redis has acknowledged the SUBSCRIBE. A thread stuck in the retry loop, or one whose connection was torn down, answers false.
445 446 447 |
# File 'lib/magick/adapters/registry.rb', line 445 def subscriber_running? @subscribed == true && !@subscriber_thread.nil? && @subscriber_thread.alive? end |