Module: Familia::Features::Relationships::RedisOperations
- Defined in:
- lib/familia/features/relationships/redis_operations.rb
Overview
Redis operations module providing atomic multi-collection operations and native Redis set operations for relationships
Instance Method Summary collapse
-
#atomic_operation(redis = nil) {|Redis| ... } ⇒ Array
Execute multiple Redis operations atomically using MULTI/EXEC.
-
#batch_zadd(redis_key, items, mode: :normal) ⇒ Object
Batch add multiple items to a sorted set.
-
#cleanup_temp_keys(pattern = 'temp:*', batch_size = 100) ⇒ Object
Clean up expired temporary keys.
-
#create_temp_key(base_name, ttl = 300) ⇒ String
Create temporary Redis key with automatic cleanup.
-
#query_by_score(redis_key, start_score = '-inf', end_score = '+inf', offset: 0, count: -1,, with_scores: false, min_permission: nil) ⇒ Array
Query sorted set with score filtering and permission checking.
-
#redis_connection ⇒ Object
Get Redis connection for the current class or instance.
-
#set_operation(operation, destination, source_keys, weights: nil, aggregate: :sum, ttl: nil) ⇒ Integer
Perform Redis set operations (union, intersection, difference) on sorted sets.
-
#update_multiple_presence(collections, action, identifier, default_score = nil) ⇒ Object
Update object presence in multiple collections atomically.
Instance Method Details
#atomic_operation(redis = nil) {|Redis| ... } ⇒ Array
Execute multiple Redis operations atomically using MULTI/EXEC
21 22 23 24 25 26 27 |
# File 'lib/familia/features/relationships/redis_operations.rb', line 21 def atomic_operation(redis = nil) redis ||= redis_connection redis.multi do |tx| yield tx if block_given? end end |
#batch_zadd(redis_key, items, mode: :normal) ⇒ Object
Batch add multiple items to a sorted set
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/familia/features/relationships/redis_operations.rb', line 146 def batch_zadd(redis_key, items, mode: :normal) return 0 if items.empty? redis = redis_connection zadd_args = items.flat_map { |item| [item[:score], item[:member]] } case mode when :nx redis.zadd(redis_key, zadd_args, nx: true) when :xx redis.zadd(redis_key, zadd_args, xx: true) when :lt redis.zadd(redis_key, zadd_args, lt: true) when :gt redis.zadd(redis_key, zadd_args, gt: true) else redis.zadd(redis_key, zadd_args) end end |
#cleanup_temp_keys(pattern = 'temp:*', batch_size = 100) ⇒ Object
Clean up expired temporary keys
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/familia/features/relationships/redis_operations.rb', line 228 def cleanup_temp_keys(pattern = 'temp:*', batch_size = 100) self.class.dbclient cursor = 0 loop do cursor, keys = dbclient.scan(cursor, match: pattern, count: batch_size) if keys.any? # Check TTL and remove keys that should have expired keys.each_slice(batch_size) do |key_batch| dbclient.pipelined do |pipeline| key_batch.each do |key| ttl = dbclient.ttl(key) pipeline.del(key) if ttl == -1 # Key exists but has no TTL end end end end break if cursor.zero? end end |
#create_temp_key(base_name, ttl = 300) ⇒ String
Create temporary Redis key with automatic cleanup
124 125 126 127 128 129 130 131 132 133 |
# File 'lib/familia/features/relationships/redis_operations.rb', line 124 def create_temp_key(base_name, ttl = 300) = Time.now.to_i random_suffix = SecureRandom.hex(3) temp_key = "temp:#{base_name}:#{}:#{random_suffix}" # Set immediate expiry to ensure cleanup even if operation fails redis_connection.expire(temp_key, ttl) temp_key end |
#query_by_score(redis_key, start_score = '-inf', end_score = '+inf', offset: 0, count: -1,, with_scores: false, min_permission: nil) ⇒ Array
Query sorted set with score filtering and permission checking
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/familia/features/relationships/redis_operations.rb', line 182 def query_by_score(redis_key, start_score = '-inf', end_score = '+inf', offset: 0, count: -1, with_scores: false, min_permission: nil) self.class.dbclient # Adjust score range for permission filtering if = ScoreEncoding.() # Ensure minimum score includes required permission level if start_score.is_a?(Numeric) decoded = decode_score(start_score) if decoded[:permissions] < start_score = encode_score(decoded[:timestamp], ) end else start_score = encode_score(0, ) end end = { limit: (count.positive? ? [offset, count] : nil), with_scores: with_scores }.compact results = dbclient.zrangebyscore(redis_key, start_score, end_score, **) # Filter results by permission if needed using correct bitwise operations if && with_scores = ScoreEncoding.() results = results.select do |_member, score| decoded = decode_score(score) # Use bitwise AND to check if permission mask is satisfied decoded[:permissions].allbits?() end end results end |
#redis_connection ⇒ Object
Get Redis connection for the current class or instance
252 253 254 255 256 257 258 259 260 |
# File 'lib/familia/features/relationships/redis_operations.rb', line 252 def redis_connection if self.class.respond_to?(:dbclient) self.class.dbclient elsif respond_to?(:dbclient) dbclient else Familia.dbclient end end |
#set_operation(operation, destination, source_keys, weights: nil, aggregate: :sum, ttl: nil) ⇒ Integer
Perform Redis set operations (union, intersection, difference) on sorted sets
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/familia/features/relationships/redis_operations.rb', line 79 def set_operation(operation, destination, source_keys, weights: nil, aggregate: :sum, ttl: nil) return 0 if source_keys.empty? redis = redis_connection atomic_operation(redis) do |tx| case operation when :union if weights tx.zunionstore(destination, source_keys.zip(weights).to_h, aggregate: aggregate) else tx.zunionstore(destination, source_keys, aggregate: aggregate) end when :intersection if weights tx.zinterstore(destination, source_keys.zip(weights).to_h, aggregate: aggregate) else tx.zinterstore(destination, source_keys, aggregate: aggregate) end when :difference first_key = source_keys.first other_keys = source_keys[1..] || [] tx.zunionstore(destination, [first_key]) other_keys.each do |key| members = redis.zrange(key, 0, -1) tx.zrem(destination, members) if members.any? end end tx.expire(destination, ttl) if ttl end redis.zcard(destination) end |
#update_multiple_presence(collections, action, identifier, default_score = nil) ⇒ Object
Update object presence in multiple collections atomically
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/familia/features/relationships/redis_operations.rb', line 42 def update_multiple_presence(collections, action, identifier, default_score = nil) return unless collections&.any? redis = self.class.dbclient atomic_operation(redis) do |tx| collections.each do |collection_config| redis_key = collection_config[:key] score = collection_config[:score] || default_score || current_score case action when :add tx.zadd(redis_key, score, identifier) when :remove tx.zrem(redis_key, identifier) when :update # Use ZADD with XX flag to only update existing members tx.zadd(redis_key, score, identifier, xx: true) end end end end |