familia 1.0.0.pre.rc3 → 1.0.0.pre.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +198 -48
  4. data/VERSION.yml +1 -1
  5. data/lib/familia/base.rb +29 -1
  6. data/lib/familia/features/expiration.rb +90 -0
  7. data/lib/familia/features/quantization.rb +56 -0
  8. data/lib/familia/features/safe_dump.rb +2 -33
  9. data/lib/familia/features.rb +5 -4
  10. data/lib/familia/horreum/class_methods.rb +112 -46
  11. data/lib/familia/horreum/commands.rb +9 -3
  12. data/lib/familia/horreum/relations_management.rb +2 -2
  13. data/lib/familia/horreum/serialization.rb +23 -42
  14. data/lib/familia/horreum/settings.rb +0 -8
  15. data/lib/familia/horreum/utils.rb +0 -1
  16. data/lib/familia/horreum.rb +1 -1
  17. data/lib/familia/logging.rb +26 -4
  18. data/lib/familia/redistype/serialization.rb +60 -38
  19. data/lib/familia/redistype.rb +45 -17
  20. data/lib/familia/settings.rb +11 -1
  21. data/lib/familia/tools.rb +68 -0
  22. data/lib/familia/types/hashkey.rb +5 -5
  23. data/lib/familia/types/list.rb +2 -2
  24. data/lib/familia/types/sorted_set.rb +12 -12
  25. data/lib/familia/types/string.rb +1 -1
  26. data/lib/familia/types/unsorted_set.rb +2 -2
  27. data/lib/familia/utils.rb +106 -51
  28. data/lib/familia/version.rb +2 -2
  29. data/try/10_familia_try.rb +4 -4
  30. data/try/20_redis_type_try.rb +9 -6
  31. data/try/26_redis_bool_try.rb +1 -1
  32. data/try/27_redis_horreum_try.rb +1 -1
  33. data/try/30_familia_object_try.rb +3 -2
  34. data/try/40_customer_try.rb +3 -3
  35. data/try/test_helpers.rb +9 -2
  36. metadata +5 -5
  37. data/lib/familia/features/api_version.rb +0 -19
  38. data/lib/familia/features/atomic_saves.rb +0 -8
  39. data/lib/familia/features/quantizer.rb +0 -35
@@ -95,15 +95,13 @@ module Familia
95
95
  def fast_writer!(name)
96
96
  define_method :"#{name}!" do |*args|
97
97
  # Check if the correct number of arguments is provided (exactly one).
98
- if args.size != 1
99
- raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
100
- end
98
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)" if args.size != 1
101
99
 
102
100
  value = args.first
103
101
 
104
102
  begin
105
103
  # Trace the operation if debugging is enabled.
106
- Familia.trace :FAST_WRITER, redis, "#{name}: #{value.inspect}", caller if Familia.debug?
104
+ Familia.trace :FAST_WRITER, redis, "#{name}: #{value.inspect}", caller(1..1) if Familia.debug?
107
105
 
108
106
  # Convert the provided value to a format suitable for Redis storage.
109
107
  prepared = to_redis(value)
@@ -146,11 +144,6 @@ module Familia
146
144
  @redis_types
147
145
  end
148
146
 
149
- def ttl(v = nil)
150
- @ttl = v unless v.nil?
151
- @ttl || parent&.ttl
152
- end
153
-
154
147
  def db(v = nil)
155
148
  @db = v unless v.nil?
156
149
  @db || parent&.db
@@ -161,9 +154,10 @@ module Familia
161
154
  @uri || parent&.uri
162
155
  end
163
156
 
164
- def all(suffix = :object)
157
+ def all(suffix = nil)
158
+ suffix ||= self.suffix
165
159
  # objects that could not be parsed will be nil
166
- keys(suffix).filter_map { |k| from_key(k) }
160
+ keys(suffix).filter_map { |k| from_rediskey(k) }
167
161
  end
168
162
 
169
163
  def any?(filter = '*')
@@ -184,12 +178,48 @@ module Familia
184
178
  @prefix || name.downcase.gsub('::', Familia.delim).to_sym
185
179
  end
186
180
 
187
- def create *args
188
- me = from_array(*args)
189
- raise "#{self} exists: #{me.rediskey}" if me.exists?
181
+ # Creates and persists a new instance of the class.
182
+ #
183
+ # @param *args [Array] Variable number of positional arguments to be passed
184
+ # to the constructor.
185
+ # @param **kwargs [Hash] Keyword arguments to be passed to the constructor.
186
+ # @return [Object] The newly created and persisted instance.
187
+ # @raise [Familia::Problem] If an instance with the same identifier already
188
+ # exists.
189
+ #
190
+ # This method serves as a factory method for creating and persisting new
191
+ # instances of the class. It combines object instantiation, existence
192
+ # checking, and persistence in a single operation.
193
+ #
194
+ # The method is flexible in accepting both positional and keyword arguments:
195
+ # - Positional arguments (*args) are passed directly to the constructor.
196
+ # - Keyword arguments (**kwargs) are passed as a hash to the constructor.
197
+ #
198
+ # After instantiation, the method checks if an object with the same
199
+ # identifier already exists. If it does, a Familia::Problem exception is
200
+ # raised to prevent overwriting existing data.
201
+ #
202
+ # Finally, the method saves the new instance returns it.
203
+ #
204
+ # @example Creating an object with keyword arguments
205
+ # User.create(name: "John", age: 30)
206
+ #
207
+ # @example Creating an object with positional and keyword arguments (not recommended)
208
+ # Product.create("SKU123", name: "Widget", price: 9.99)
209
+ #
210
+ # @note The behavior of this method depends on the implementation of #new,
211
+ # #exists?, and #save in the class and its superclasses.
212
+ #
213
+ # @see #new
214
+ # @see #exists?
215
+ # @see #save
216
+ #
217
+ def create *args, **kwargs
218
+ fobj = new(*args, **kwargs)
219
+ raise Familia::Problem, "#{self} already exists: #{fobj.rediskey}" if fobj.exists?
190
220
 
191
- me.save
192
- me
221
+ fobj.save
222
+ fobj
193
223
  end
194
224
 
195
225
  def multiget(*ids)
@@ -201,7 +231,7 @@ module Familia
201
231
  ids.collect! { |objid| rediskey(objid) }
202
232
  return [] if ids.compact.empty?
203
233
 
204
- Familia.trace :MULTIGET, redis, "#{ids.size}: #{ids}", caller if Familia.debug?
234
+ Familia.trace :MULTIGET, redis, "#{ids.size}: #{ids}", caller(1..1) if Familia.debug?
205
235
  redis.mget(*ids)
206
236
  end
207
237
 
@@ -230,18 +260,18 @@ module Familia
230
260
  # debugging.
231
261
  #
232
262
  # @example
233
- # User.from_key("user:123") # Returns a User instance if it exists,
263
+ # User.from_rediskey("user:123") # Returns a User instance if it exists,
234
264
  # nil otherwise
235
265
  #
236
- def from_key(objkey)
266
+ def from_rediskey(objkey)
237
267
  raise ArgumentError, 'Empty key' if objkey.to_s.empty?
238
268
 
239
269
  # We use a lower-level method here b/c we're working with the
240
270
  # full key and not just the identifier.
241
271
  does_exist = redis.exists(objkey).positive?
242
272
 
243
- Familia.ld "[.from_key] #{self} from key #{objkey} (exists: #{does_exist})"
244
- Familia.trace :FROM_KEY, redis, objkey, caller if Familia.debug?
273
+ Familia.ld "[.from_rediskey] #{self} from key #{objkey} (exists: #{does_exist})"
274
+ Familia.trace :FROM_KEY, redis, objkey, caller(1..1) if Familia.debug?
245
275
 
246
276
  # This is the reason for calling exists first. We want to definitively
247
277
  # and without any ambiguity know if the object exists in Redis. If it
@@ -251,7 +281,7 @@ module Familia
251
281
  return unless does_exist
252
282
 
253
283
  obj = redis.hgetall(objkey) # horreum objects are persisted as redis hashes
254
- Familia.trace :FROM_KEY2, redis, "#{objkey}: #{obj.inspect}", caller if Familia.debug?
284
+ Familia.trace :FROM_KEY2, redis, "#{objkey}: #{obj.inspect}", caller(1..1) if Familia.debug?
255
285
 
256
286
  new(**obj)
257
287
  end
@@ -265,7 +295,7 @@ module Familia
265
295
  # @return [Object, nil] An instance of the class if found, nil otherwise.
266
296
  #
267
297
  # This method constructs the full Redis key using the provided identifier
268
- # and suffix, then delegates to `from_key` for the actual retrieval and
298
+ # and suffix, then delegates to `from_rediskey` for the actual retrieval and
269
299
  # instantiation.
270
300
  #
271
301
  # It's a higher-level method that abstracts away the key construction,
@@ -273,59 +303,95 @@ module Familia
273
303
  # identifier.
274
304
  #
275
305
  # @example
276
- # User.from_redis(123) # Equivalent to User.from_key("user:123:object")
306
+ # User.from_identifier(123) # Equivalent to User.from_rediskey("user:123:object")
277
307
  #
278
- def from_redis(identifier, suffix = :object)
308
+ def from_identifier(identifier, suffix = nil)
309
+ suffix ||= self.suffix
279
310
  return nil if identifier.to_s.empty?
280
311
 
281
312
  objkey = rediskey(identifier, suffix)
282
- Familia.ld "[.from_redis] #{self} from key #{objkey})"
283
- Familia.trace :FROM_REDIS, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
284
- from_key objkey
313
+
314
+ Familia.ld "[.from_identifier] #{self} from key #{objkey})"
315
+ Familia.trace :FROM_IDENTIFIER, Familia.redis(uri), objkey, caller(1..1).first if Familia.debug?
316
+ from_rediskey objkey
285
317
  end
318
+ alias load from_identifier
286
319
 
287
- def exists?(identifier, suffix = :object)
320
+ # Checks if an object with the given identifier exists in Redis.
321
+ #
322
+ # @param identifier [String, Integer] The unique identifier for the object.
323
+ # @param suffix [Symbol, nil] The suffix to use in the Redis key (default: class suffix).
324
+ # @return [Boolean] true if the object exists, false otherwise.
325
+ #
326
+ # This method constructs the full Redis key using the provided identifier and suffix,
327
+ # then checks if the key exists in Redis.
328
+ #
329
+ # @example
330
+ # User.exists?(123) # Returns true if user:123:object exists in Redis
331
+ #
332
+ def exists?(identifier, suffix = nil)
333
+ suffix ||= self.suffix
288
334
  return false if identifier.to_s.empty?
289
335
 
290
336
  objkey = rediskey identifier, suffix
291
337
 
292
338
  ret = redis.exists objkey
293
- Familia.trace :EXISTS, redis, "#{objkey} #{ret.inspect}", caller if Familia.debug?
294
- ret.positive?
339
+ Familia.trace :EXISTS, redis, "#{objkey} #{ret.inspect}", caller(1..1) if Familia.debug?
340
+
341
+ ret.positive? # differs from redis API but I think it's okay bc `exists?` is a predicate method.
295
342
  end
296
343
 
297
- def destroy!(identifier, suffix = :object)
344
+ # Destroys an object in Redis with the given identifier.
345
+ #
346
+ # @param identifier [String, Integer] The unique identifier for the object to destroy.
347
+ # @param suffix [Symbol, nil] The suffix to use in the Redis key (default: class suffix).
348
+ # @return [Boolean] true if the object was successfully destroyed, false otherwise.
349
+ #
350
+ # This method constructs the full Redis key using the provided identifier and suffix,
351
+ # then removes the corresponding key from Redis.
352
+ #
353
+ # @example
354
+ # User.destroy!(123) # Removes user:123:object from Redis
355
+ #
356
+ def destroy!(identifier, suffix = nil)
357
+ suffix ||= self.suffix
298
358
  return false if identifier.to_s.empty?
299
359
 
300
360
  objkey = rediskey identifier, suffix
301
361
 
302
362
  ret = redis.del objkey
303
- if Familia.debug?
304
- Familia.trace :DELETED, redis, "#{objkey}: #{ret.inspect}",
305
- caller
306
- end
363
+ Familia.trace :DELETED, redis, "#{objkey}: #{ret.inspect}", caller(1..1) if Familia.debug?
307
364
  ret.positive?
308
365
  end
309
366
 
367
+ # Finds all keys in Redis matching the given suffix pattern.
368
+ #
369
+ # @param suffix [String] The suffix pattern to match (default: '*').
370
+ # @return [Array<String>] An array of matching Redis keys.
371
+ #
372
+ # This method searches for all Redis keys that match the given suffix pattern.
373
+ # It uses the class's rediskey method to construct the search pattern.
374
+ #
375
+ # @example
376
+ # User.find # Returns all keys matching user:*:object
377
+ # User.find('active') # Returns all keys matching user:*:active
378
+ #
310
379
  def find(suffix = '*')
311
380
  redis.keys(rediskey('*', suffix)) || []
312
381
  end
313
382
 
314
- def qstamp(quantum = nil, pattern = nil, now = Familia.now)
315
- quantum ||= ttl || 10.minutes
316
- pattern ||= '%H%M'
317
- rounded = now - (now % quantum)
318
- Time.at(rounded).utc.strftime(pattern)
319
- end
320
-
321
383
  # +identifier+ can be a value or an Array of values used to create the index.
322
384
  # We don't enforce a default suffix; that's left up to the instance.
323
385
  # The suffix is used to differentiate between different types of objects.
324
386
  #
325
- #
326
- # A nil +suffix+ will not be included in the key.
387
+ # +suffix+ If a nil value is explicitly passed in, it won't appear in the redis
388
+ # key that's returned. If no suffix is passed in, the class' suffix is used
389
+ # as the default (via the class method self.suffix). It's an important
390
+ # distinction b/c passing in an explicitly nil is how RedisType objects
391
+ # at the class level are created without the global default 'object'
392
+ # suffix. See RedisType#rediskey "parent_class?" for more details.
327
393
  def rediskey(identifier, suffix = self.suffix)
328
- Familia.ld "[.rediskey] #{identifier} for #{self} (suffix:#{suffix})"
394
+ # Familia.ld "[.rediskey] #{identifier} for #{self} (suffix:#{suffix})"
329
395
  raise NoIdentifier, self if identifier.to_s.empty?
330
396
 
331
397
  identifier &&= identifier.to_s
@@ -19,8 +19,8 @@ module Familia
19
19
  module Commands
20
20
 
21
21
  def exists?
22
- ret = redis.exists rediskey
23
- ret.positive? # differs from redis API but I think it's okay bc `exists?` is a predicate method.
22
+ # Trace output comes from the class method
23
+ self.class.exists? identifier, suffix
24
24
  end
25
25
 
26
26
  # Sets a timeout on key. After the timeout has expired, the key will automatically be deleted.
@@ -28,10 +28,12 @@ module Familia
28
28
  #
29
29
  def expire(ttl = nil)
30
30
  ttl ||= self.class.ttl
31
+ Familia.trace :EXPIRE, redis, ttl, caller(1..1) if Familia.debug?
31
32
  redis.expire rediskey, ttl.to_i
32
33
  end
33
34
 
34
35
  def realttl
36
+ Familia.trace :REALTTL, redis, redisuri, caller(1..1) if Familia.debug?
35
37
  redis.ttl rediskey
36
38
  end
37
39
 
@@ -41,15 +43,18 @@ module Familia
41
43
  # @return [Integer] The number of fields that were removed from the hash (0 or 1).
42
44
  # @note This method is destructive, as indicated by the bang (!).
43
45
  def hdel!(field)
46
+ Familia.trace :HDEL, redis, field, caller(1..1) if Familia.debug?
44
47
  redis.hdel rediskey, field
45
48
  end
46
49
 
47
50
  def redistype
51
+ Familia.trace :REDISTYPE, redis, redisuri, caller(1..1) if Familia.debug?
48
52
  redis.type rediskey(suffix)
49
53
  end
50
54
 
51
55
  # Parity with RedisType#rename
52
56
  def rename(newkey)
57
+ Familia.trace :RENAME, redis, "#{rediskey} -> #{newkey}", caller(1..1) if Familia.debug?
53
58
  redis.rename rediskey, newkey
54
59
  end
55
60
 
@@ -61,13 +66,14 @@ module Familia
61
66
  alias all hgetall
62
67
 
63
68
  def hget(field)
69
+ Familia.trace :HGET, redis, field, caller(1..1) if Familia.debug?
64
70
  redis.hget rediskey(suffix), field
65
71
  end
66
72
 
67
73
  # @return The number of fields that were added to the hash. If the
68
74
  # field already exists, this will return 0.
69
75
  def hset(field, value)
70
- Familia.trace :HSET, redis, redisuri, caller(1..1) if Familia.debug?
76
+ Familia.trace :HSET, redis, field, caller(1..1) if Familia.debug?
71
77
  redis.hset rediskey, field, value
72
78
  end
73
79
 
@@ -81,7 +81,7 @@ module Familia
81
81
 
82
82
  # Creates an instance-level relation
83
83
  def attach_instance_redis_object_relation(name, klass, opts)
84
- Familia.ld "[Attaching instance-level #{name}] #{klass} => (#{self}) #{opts}"
84
+ Familia.ld "[#{self}##{name}] Attaching instance-level #{klass} #{opts}"
85
85
  raise ArgumentError, "Name is blank (#{klass})" if name.to_s.empty?
86
86
 
87
87
  name = name.to_s.to_sym
@@ -106,7 +106,7 @@ module Familia
106
106
 
107
107
  # Creates a class-level relation
108
108
  def attach_class_redis_object_relation(name, klass, opts)
109
- Familia.ld "[#{self}] Attaching class-level #{name} #{klass} => #{opts}"
109
+ Familia.ld "[#{self}.#{name}] Attaching class-level #{klass} #{opts}"
110
110
  raise ArgumentError, 'Name is blank (klass)' if name.to_s.empty?
111
111
 
112
112
  name = name.to_s.to_sym
@@ -113,19 +113,23 @@ module Familia
113
113
  Familia.ld "[save] #{self.class} #{rediskey} #{ret}"
114
114
 
115
115
  # Did Redis accept our offering?
116
- ret.uniq.all? { |value| value == "OK" }
116
+ ret.uniq.all? { |value| ["OK", true].include?(value) }
117
117
  end
118
118
 
119
119
  # Apply a smattering of fields to this object like fairy dust.
120
120
  #
121
- # @param fields [Hash] A magical bag of named attributes to sprinkle onto this instance.
122
- # Each key-value pair is like a tiny spell, ready to enchant our object's properties.
121
+ # @param fields [Hash] A magical bag of named attributes to sprinkle onto
122
+ # this instance. Each key-value pair is like a tiny spell, ready to
123
+ # enchant our object's properties.
123
124
  #
124
- # @return [self] Returns the newly bejeweled instance, now sparkling with fresh attributes.
125
+ # @return [self] Returns the newly bejeweled instance, now sparkling with
126
+ # fresh attributes.
125
127
  #
126
128
  # @example Giving your object a makeover
127
- # dragon.apply_fields(name: "Puff", breathes: "fire", loves: "little boys named Jackie")
128
- # # => #<Dragon:0x007f8a1c8b0a28 @name="Puff", @breathes="fire", @loves="little boys named Jackie">
129
+ # dragon.apply_fields(name: "Puff", breathes: "fire", loves: "little boys
130
+ # named Jackie")
131
+ # # => #<Dragon:0x007f8a1c8b0a28 @name="Puff", @breathes="fire",
132
+ # @loves="little boys named Jackie">
129
133
  #
130
134
  def apply_fields(**fields)
131
135
  fields.each do |field, value|
@@ -140,10 +144,12 @@ module Familia
140
144
  # This method performs a sacred ritual, sending our cherished attributes
141
145
  # on a journey through the ethernet to find their resting place in Redis.
142
146
  #
143
- # @return [Array<String>] A mystical array of strings, cryptic messages from the Redis gods.
147
+ # @return [Array<String>] A mystical array of strings, cryptic messages
148
+ # from the Redis gods.
144
149
  #
145
- # @note Be warned, young programmer! This method dabbles in the arcane art of transactions.
146
- # Side effects may include data persistence and a slight tingling sensation.
150
+ # @note Be warned, young programmer! This method dabbles in the arcane
151
+ # art of transactions. Side effects may include data persistence and a
152
+ # slight tingling sensation.
147
153
  #
148
154
  # @example Offering your changes to the Redis deities
149
155
  # unicorn.name = "Charlie"
@@ -155,6 +161,10 @@ module Familia
155
161
  Familia.ld "[commit_fields] #{self.class} #{rediskey} #{to_h}"
156
162
  transaction do |conn|
157
163
  hmset
164
+
165
+ # Only classes that have the expiration ferature enabled will
166
+ # actually set an expiration time on their keys. Otherwise
167
+ # this will be a no-op.
158
168
  update_expiration
159
169
  end
160
170
  end
@@ -183,7 +193,6 @@ module Familia
183
193
  delete!
184
194
  end
185
195
 
186
-
187
196
  # Refreshes the object's state by querying Redis and overwriting the
188
197
  # current field values. This method performs a destructive update on the
189
198
  # object, regardless of unsaved changes.
@@ -217,7 +226,8 @@ module Familia
217
226
  # into a more plebeian hash. But fear not, for in this form, it can slip through
218
227
  # the cracks of the universe (or at least, into Redis) with ease.
219
228
  #
220
- # @return [Hash] A glittering hash, each key a field name, each value a Redis-ready treasure.
229
+ # @return [Hash] A glittering hash, each key a field name, each value a
230
+ # Redis-ready treasure.
221
231
  #
222
232
  # @example Turning your dragon into a hash
223
233
  # dragon.to_h
@@ -247,7 +257,8 @@ module Familia
247
257
  # unicorn.to_a
248
258
  # # => ["Charlie", "magnificent", 5]
249
259
  #
250
- # @note Each value is carefully disguised in its Redis costume before joining the parade.
260
+ # @note Each value is carefully disguised in its Redis costume
261
+ # before joining the parade.
251
262
  #
252
263
  def to_a
253
264
  self.class.fields.map do |field|
@@ -299,36 +310,6 @@ module Familia
299
310
  prepared
300
311
  end
301
312
 
302
- # Set an expiration date for our data, like a "best before" sticker for Redis!
303
- #
304
- # This method gives our data a lifespan in Redis. It's like telling Redis,
305
- # "Hey, this data is fresh now, but it might get stale after a while!"
306
- #
307
- # @param ttl [Integer, nil] The Time To Live in seconds. If nil, we'll check
308
- # our options for a default expiration time.
309
- #
310
- # @return [Boolean] true if the expiration was set successfully, false otherwise.
311
- # It's like asking Redis, "Did you stick that expiration label on properly?"
312
- #
313
- # @example Making your pet rock data mortal
314
- # rocky.update_expiration(86400) # Dwayne will live in Redis for one day
315
- #
316
- # @note If the TTL is zero, we assume our data wants to live forever.
317
- # Immortality in Redis! Who wouldn't want that?
318
- #
319
- def update_expiration(ttl = nil)
320
- ttl ||= opts[:ttl]
321
- ttl = ttl.to_i
322
-
323
- return if ttl.zero?
324
-
325
- Familia.ld "Setting expiration for #{rediskey} to #{ttl} seconds"
326
-
327
- # EXPIRE command returns 1 if the timeout was set, 0 if key does not
328
- # exist or the timeout could not be set.
329
- expire(ttl).positive?
330
- end
331
-
332
313
  end
333
314
  # End of Serialization module
334
315
 
@@ -29,14 +29,6 @@ module Familia
29
29
  }
30
30
  end
31
31
 
32
- def ttl=(v)
33
- @ttl = v.to_i
34
- end
35
-
36
- def ttl
37
- @ttl || self.class.ttl
38
- end
39
-
40
32
  def db=(v)
41
33
  @db = v.to_i
42
34
  end
@@ -28,7 +28,6 @@ module Familia
28
28
  # from the `identifier` method).
29
29
  #
30
30
  def rediskey(suffix = nil, ignored = nil)
31
- Familia.ld "[#rediskey] #{identifier} for #{self.class}"
32
31
  raise Familia::NoIdentifier, "No identifier for #{self.class}" if identifier.to_s.empty?
33
32
  suffix ||= self.suffix # use the instance method to get the default suffix
34
33
  self.class.rediskey identifier, suffix
@@ -56,7 +56,7 @@ module Familia
56
56
  class << self
57
57
  # Extends ClassMethods to subclasses and tracks Familia members
58
58
  def inherited(member)
59
- Familia.trace :INHERITED, nil, "Inherited by #{member}", caller if Familia.debug?
59
+ Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
60
60
  member.extend(ClassMethods)
61
61
  member.extend(Features)
62
62
 
@@ -126,26 +126,48 @@ module Familia
126
126
  #
127
127
  # @param label [Symbol] A label for the trace message (e.g., :EXPAND,
128
128
  # :FROMREDIS, :LOAD, :EXISTS).
129
- # @param redis_instance [Object] The Redis instance being used.
129
+ # @param redis_instance [Redis, Redis::Future, nil] The Redis instance or
130
+ # Future being used.
130
131
  # @param ident [String] An identifier or key related to the operation being
131
132
  # traced.
132
133
  # @param context [Array<String>, String, nil] The calling context, typically
133
134
  # obtained from `caller` or `caller.first`. Default is nil.
134
135
  #
135
136
  # @example
136
- # Familia.trace :LOAD, Familia.redis(uri), objkey, caller if Familia.debug?
137
- #
137
+ # Familia.trace :LOAD, Familia.redis(uri), objkey, caller(1..1) if
138
+ # Familia.debug?
138
139
  #
139
140
  # @return [nil]
140
141
  #
142
+ # @note This method only executes if LoggerTraceRefinement::ENABLED is true.
143
+ # @note The redis_instance can be a Redis object, Redis::Future (used in
144
+ # pipelined and multi blocks), or nil (when the redis connection isn't
145
+ # relevant).
146
+ #
141
147
  def trace(label, redis_instance, ident, context = nil)
142
148
  return unless LoggerTraceRefinement::ENABLED
143
- instance_id = redis_instance&.id
149
+
150
+ # Usually redis_instance is a Redis object, but it could be
151
+ # a Redis::Future which is what is used inside of pipelined
152
+ # and multi blocks. In some contexts it's nil where the
153
+ # redis connection isn't relevant.
154
+ instance_id = if redis_instance
155
+ case redis_instance
156
+ when Redis
157
+ redis_instance.id.respond_to?(:to_s) ? redis_instance.id.to_s : redis_instance.class.name
158
+ when Redis::Future
159
+ "Redis::Future"
160
+ else
161
+ redis_instance.class.name
162
+ end
163
+ end
164
+
144
165
  codeline = if context
145
166
  context = [context].flatten
146
167
  context.reject! { |line| line =~ %r{lib/familia} }
147
168
  context.first
148
169
  end
170
+
149
171
  @logger.trace format('[%s] %s -> %s <- at %s', label, instance_id, ident, codeline)
150
172
  end
151
173