Module: Familia::AtomicOperations
- Defined in:
- lib/familia/atomic_operations.rb
Overview
AtomicOperations provides Redis utilities for atomic, zero-downtime data replacement. These primitives are datastore-level building blocks shared across index rebuilds, audit/repair routines, and any other code that needs to swap a key's contents without exposing a transient empty state.
The canonical pattern:
- Build replacement contents in a temporary key (see AtomicOperations.build_temp_key).
- Atomically swap it into place with AtomicOperations.atomic_swap.
All methods are module_function-style; call them directly on the module.
Class Method Summary collapse
-
.atomic_swap(temp_key, final_key, redis) ⇒ Object
Performs atomic swap of temp key to final key.
-
.build_temp_key(base_key) ⇒ String
Builds a temporary key name for atomic swaps.
Class Method Details
.atomic_swap(temp_key, final_key, redis) ⇒ Object
Performs atomic swap of temp key to final key.
Non-empty rebuilds use Redis RENAME (>= 2.6), which atomically replaces final_key if it exists. Readers observe either the old index or the new one; there is no window in which final_key is absent. This avoids the partial-update, race-condition, and stale-visibility problems of a two-step DEL+RENAME sequence.
Empty rebuilds (no temp key) intentionally DEL final_key so the live index reflects the empty result set. In that branch readers can observe final_key as absent -- this is the correct outcome for an index with zero members, not a transient gap.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/familia/atomic_operations.rb', line 54 def self.atomic_swap(temp_key, final_key, redis) # Check if temp key exists first - RENAME fails on non-existent keys. # redis.exists returns Integer across all supported redis-rb versions; # using > 0 also tolerates a future boolean return without breaking. unless redis.exists(temp_key) > 0 Familia.info "[AtomicOp] No temp key to swap (empty result set)" # Empty rebuild: remove the live index so reads reflect zero members. # This is the one path where readers can legitimately see final_key # as absent -- the index genuinely has no entries. redis.del(final_key) return end # RENAME atomically replaces final_key if it exists (Redis >= 2.6), # so readers never observe a missing final_key during a non-empty # swap. A preceding DEL would open a gap where concurrent HGETs # return nil. redis.rename(temp_key, final_key) Familia.info "[AtomicOp] Atomic swap completed: #{temp_key} -> #{final_key}" rescue Redis::CommandError => e # If temp key doesn't exist, just log and return (already handled above) if e..include?("no such key") Familia.info "[AtomicOp] Temp key vanished during swap (concurrent operation?)" return end # For other errors, preserve temp key for debugging Familia.warn "[AtomicOp] Atomic swap failed: #{e.}" Familia.warn "[AtomicOp] Temp key preserved for debugging: #{temp_key}" raise end |