familia 2.0.0.pre5 → 2.0.0.pre7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -0,0 +1,596 @@
1
+ # Expiration Feature Guide
2
+
3
+ ## Overview
4
+
5
+ The Expiration feature provides automatic Time-To-Live (TTL) management for Familia objects with support for class-level defaults, instance-level overrides, and cascading expiration to related fields. This feature ensures data retention policies are consistently enforced across your application.
6
+
7
+ ## Core Concepts
8
+
9
+ ### TTL Hierarchy
10
+
11
+ Familia uses a three-tier expiration system:
12
+
13
+ 1. **Instance-level expiration** - Set on individual objects
14
+ 2. **Class-level default expiration** - Inherited by all instances of the class
15
+ 3. **Global default expiration** - Familia-wide fallback (`Familia.default_expiration`)
16
+
17
+ ### Cascading Expiration
18
+
19
+ When a Horreum object has related fields (DataTypes), the expiration feature automatically cascades TTL updates to all related objects, ensuring consistent data lifecycle management.
20
+
21
+ ## Basic Usage
22
+
23
+ ### Enabling Expiration
24
+
25
+ ```ruby
26
+ class Session < Familia::Horreum
27
+ feature :expiration
28
+ default_expiration 1.hour # Class-level default
29
+
30
+ identifier_field :session_id
31
+ field :session_id, :user_id, :data
32
+ list :activity_log
33
+ end
34
+ ```
35
+
36
+ ### Setting Class Defaults
37
+
38
+ ```ruby
39
+ class UserSession < Familia::Horreum
40
+ feature :expiration
41
+
42
+ # Set default expiration for all instances
43
+ default_expiration 30.minutes
44
+
45
+ field :user_id, :ip_address, :csrf_token
46
+ end
47
+
48
+ # Can also set or update programmatically
49
+ UserSession.default_expiration(1.hour)
50
+ UserSession.default_expiration # => 3600.0
51
+ ```
52
+
53
+ ### Instance-Level TTL Management
54
+
55
+ ```ruby
56
+ session = UserSession.new(user_id: 123)
57
+
58
+ # Uses class default (1 hour)
59
+ session.default_expiration # => 3600.0
60
+
61
+ # Set custom expiration for this instance
62
+ session.default_expiration = 15.minutes
63
+ session.default_expiration # => 900.0
64
+
65
+ # Apply expiration to database
66
+ session.update_expiration # Uses instance expiration (15 minutes)
67
+
68
+ # Or specify expiration inline
69
+ session.update_expiration(default_expiration: 5.minutes)
70
+ ```
71
+
72
+ ## Advanced Usage
73
+
74
+ ### Inheritance and Parent Defaults
75
+
76
+ ```ruby
77
+ class BaseSession < Familia::Horreum
78
+ feature :expiration
79
+ default_expiration 2.hours # Parent default
80
+ end
81
+
82
+ class GuestSession < BaseSession
83
+ # Inherits parent's 2-hour default
84
+ field :temporary_data
85
+ end
86
+
87
+ class AdminSession < BaseSession
88
+ # Override parent default
89
+ default_expiration 8.hours
90
+ field :admin_permissions
91
+ end
92
+
93
+ GuestSession.default_expiration # => 7200.0 (2 hours from parent)
94
+ AdminSession.default_expiration # => 28800.0 (8 hours, overridden)
95
+ ```
96
+
97
+ ### Cascading to Related Fields
98
+
99
+ ```ruby
100
+ class Customer < Familia::Horreum
101
+ feature :expiration
102
+ default_expiration 24.hours
103
+
104
+ identifier_field :customer_id
105
+ field :customer_id, :name, :email
106
+ list :recent_orders # Will get same TTL
107
+ set :favorite_categories # Will get same TTL
108
+ hashkey :preferences # Will get same TTL
109
+ end
110
+
111
+ customer = Customer.new(customer_id: 'cust_123')
112
+ customer.save
113
+
114
+ # This will set TTL on the main object AND all related fields
115
+ customer.update_expiration(default_expiration: 12.hours)
116
+ # Sets expiration on:
117
+ # - customer:cust_123 (main hash)
118
+ # - customer:cust_123:recent_orders (list)
119
+ # - customer:cust_123:favorite_categories (set)
120
+ # - customer:cust_123:preferences (hashkey)
121
+ ```
122
+
123
+ ### Conditional Expiration
124
+
125
+ ```ruby
126
+ class AnalyticsEvent < Familia::Horreum
127
+ feature :expiration
128
+
129
+ identifier_field :event_id
130
+ field :event_id, :event_type, :user_id, :timestamp, :data
131
+
132
+ def should_expire?
133
+ event_type == 'temporary' || timestamp < 1.day.ago
134
+ end
135
+
136
+ def save_with_conditional_expiration
137
+ save
138
+
139
+ if should_expire?
140
+ update_expiration(default_expiration: 1.hour)
141
+ else
142
+ update_expiration(default_expiration: 30.days)
143
+ end
144
+ end
145
+ end
146
+ ```
147
+
148
+ ### Zero Expiration (Persistent Data)
149
+
150
+ ```ruby
151
+ class PermanentRecord < Familia::Horreum
152
+ feature :expiration
153
+ default_expiration 0 # Never expires
154
+
155
+ field :permanent_data
156
+ end
157
+
158
+ # Zero expiration means data persists indefinitely
159
+ record = PermanentRecord.new
160
+ record.update_expiration # No-op, data won't expire
161
+ ```
162
+
163
+ ## Integration Patterns
164
+
165
+ ### Rails Integration
166
+
167
+ ```ruby
168
+ # app/models/user_session.rb
169
+ class UserSession < Familia::Horreum
170
+ feature :expiration
171
+
172
+ # Different TTLs based on Rails environment
173
+ default_expiration case Rails.env
174
+ when 'development' then 8.hours # Long for debugging
175
+ when 'test' then 1.minute # Quick cleanup
176
+ when 'production' then 30.minutes # Security-focused
177
+ end
178
+
179
+ identifier_field :session_token
180
+ field :session_token, :user_id, :ip_address, :user_agent
181
+ hashkey :flash_messages
182
+
183
+ after_save :apply_expiration
184
+
185
+ private
186
+
187
+ def apply_expiration
188
+ update_expiration
189
+ end
190
+ end
191
+ ```
192
+
193
+ ### Background Job Integration
194
+
195
+ ```ruby
196
+ class SessionCleanupJob
197
+ include Sidekiq::Worker
198
+
199
+ def perform
200
+ # Extend expiration for active sessions
201
+ UserSession.all.each do |session|
202
+ if session.recently_active?
203
+ session.update_expiration(default_expiration: 30.minutes)
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ # Schedule cleanup
210
+ SessionCleanupJob.perform_in(5.minutes)
211
+ ```
212
+
213
+ ### Middleware Integration
214
+
215
+ ```ruby
216
+ class SessionExpirationMiddleware
217
+ def initialize(app)
218
+ @app = app
219
+ end
220
+
221
+ def call(env)
222
+ session_token = extract_session_token(env)
223
+
224
+ if session_token
225
+ session = UserSession.find(session_token)
226
+
227
+ # Extend session TTL on each request
228
+ session&.update_expiration(default_expiration: 30.minutes)
229
+ end
230
+
231
+ @app.call(env)
232
+ end
233
+
234
+ private
235
+
236
+ def extract_session_token(env)
237
+ # Extract from cookies, headers, etc.
238
+ end
239
+ end
240
+ ```
241
+
242
+ ## TTL Monitoring and Management
243
+
244
+ ### Checking Current TTL
245
+
246
+ ```ruby
247
+ session = UserSession.find('session_123')
248
+
249
+ # Check TTL using Redis TTL command (returns seconds remaining)
250
+ ttl_seconds = session.ttl # e.g., 1800 (30 minutes left)
251
+
252
+ # Convert to more readable format
253
+ case ttl_seconds
254
+ when -1
255
+ puts "Session never expires"
256
+ when -2
257
+ puts "Session key doesn't exist"
258
+ when 0..3600
259
+ puts "Session expires in #{ttl_seconds / 60} minutes"
260
+ else
261
+ puts "Session expires in #{ttl_seconds / 3600} hours"
262
+ end
263
+ ```
264
+
265
+ ### Batch TTL Updates
266
+
267
+ ```ruby
268
+ class SessionManager
269
+ def self.extend_all_sessions(new_ttl)
270
+ UserSession.all.each do |session|
271
+ session.update_expiration(default_expiration: new_ttl)
272
+ end
273
+ end
274
+
275
+ def self.expire_inactive_sessions
276
+ UserSession.all.select(&:inactive?).each do |session|
277
+ # Set very short TTL for inactive sessions
278
+ session.update_expiration(default_expiration: 5.minutes)
279
+ end
280
+ end
281
+
282
+ def self.make_sessions_permanent
283
+ # Remove expiration from all sessions
284
+ UserSession.all.each do |session|
285
+ session.persist # Remove TTL entirely
286
+ end
287
+ end
288
+ end
289
+ ```
290
+
291
+ ### TTL-Based Data Lifecycle
292
+
293
+ ```ruby
294
+ class DataRetentionService
295
+ TTL_POLICIES = {
296
+ guest_session: 30.minutes,
297
+ user_session: 2.hours,
298
+ admin_session: 8.hours,
299
+ analytics_event: 30.days,
300
+ audit_log: 1.year,
301
+ temporary_upload: 1.hour
302
+ }.freeze
303
+
304
+ def self.apply_retention_policies
305
+ TTL_POLICIES.each do |data_type, ttl|
306
+ model_class = data_type.to_s.camelize.constantize
307
+
308
+ model_class.all.each do |record|
309
+ record.update_expiration(default_expiration: ttl)
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ # Run as scheduled job
316
+ DataRetentionService.apply_retention_policies
317
+ ```
318
+
319
+ ## Performance Considerations
320
+
321
+ ### Efficient TTL Updates
322
+
323
+ ```ruby
324
+ # ❌ Inefficient: Multiple round trips
325
+ sessions.each do |session|
326
+ session.update_expiration(default_expiration: 1.hour)
327
+ end
328
+
329
+ # ✅ Efficient: Batch operations
330
+ redis = Familia.dbclient
331
+ pipeline = redis.pipelined do |pipe|
332
+ sessions.each do |session|
333
+ pipe.expire(session.dbkey, 3600)
334
+
335
+ # Also expire related fields if needed
336
+ session.class.related_fields.each do |name, _|
337
+ related_key = "#{session.dbkey}:#{name}"
338
+ pipe.expire(related_key, 3600)
339
+ end
340
+ end
341
+ end
342
+ ```
343
+
344
+ ### Avoiding Expiration Conflicts
345
+
346
+ ```ruby
347
+ class ResilientSession < Familia::Horreum
348
+ feature :expiration
349
+ default_expiration 30.minutes
350
+
351
+ field :user_id, :data
352
+
353
+ def safe_update_expiration(new_ttl = nil)
354
+ new_ttl ||= default_expiration
355
+
356
+ # Only update if key exists
357
+ return unless exists?
358
+
359
+ begin
360
+ update_expiration(default_expiration: new_ttl)
361
+ rescue => e
362
+ # Log error but don't crash the application
363
+ Familia.logger.warn "Failed to update expiration for #{dbkey}: #{e.message}"
364
+ false
365
+ end
366
+ end
367
+ end
368
+ ```
369
+
370
+ ## Debugging and Troubleshooting
371
+
372
+ ### Debug Expiration Issues
373
+
374
+ ```ruby
375
+ # Enable debug logging to see expiration operations
376
+ Familia.debug = true
377
+
378
+ session = UserSession.new(session_token: 'debug_session')
379
+ session.save
380
+ session.update_expiration(default_expiration: 5.minutes)
381
+ # Logs will show:
382
+ # [update_expiration] Expires session:debug_session in 300.0 seconds
383
+ ```
384
+
385
+ ### Common Issues
386
+
387
+ **1. Expiration Not Applied**
388
+ ```ruby
389
+ session = UserSession.new
390
+ # ❌ Won't work - object must be saved first
391
+ session.update_expiration(default_expiration: 1.hour)
392
+
393
+ # ✅ Correct - save first, then expire
394
+ session.save
395
+ session.update_expiration(default_expiration: 1.hour)
396
+ ```
397
+
398
+ **2. Related Fields Not Expiring**
399
+ ```ruby
400
+ class Customer < Familia::Horreum
401
+ feature :expiration
402
+
403
+ field :name
404
+ list :orders # ❌ Won't cascade without proper relation definition
405
+ end
406
+
407
+ # ✅ Fix: Ensure relations are properly tracked
408
+ class Customer < Familia::Horreum
409
+ feature :expiration
410
+
411
+ field :name
412
+ list :orders
413
+
414
+ # Explicitly track relation if needed
415
+ def update_expiration(**opts)
416
+ super(**opts)
417
+
418
+ # Manually cascade to specific fields if needed
419
+ orders.expire(opts[:default_expiration] || default_expiration)
420
+ end
421
+ end
422
+ ```
423
+
424
+ **3. Inheritance Issues**
425
+ ```ruby
426
+ class BaseModel < Familia::Horreum
427
+ # ❌ Parent doesn't have expiration feature
428
+ default_expiration 1.hour
429
+ end
430
+
431
+ class DerivedModel < BaseModel
432
+ feature :expiration # ❌ Child has feature but parent doesn't
433
+ end
434
+
435
+ # ✅ Fix: Enable feature on parent class
436
+ class BaseModel < Familia::Horreum
437
+ feature :expiration
438
+ default_expiration 1.hour
439
+ end
440
+ ```
441
+
442
+ ## Testing TTL Behavior
443
+
444
+ ### RSpec Testing
445
+
446
+ ```ruby
447
+ RSpec.describe UserSession do
448
+ describe "expiration behavior" do
449
+ let(:session) { described_class.new(session_token: 'test_session') }
450
+
451
+ it "inherits class default expiration" do
452
+ expect(session.default_expiration).to eq(described_class.default_expiration)
453
+ end
454
+
455
+ it "allows instance-level expiration override" do
456
+ session.default_expiration = 15.minutes
457
+ expect(session.default_expiration).to eq(900.0)
458
+ end
459
+
460
+ it "applies TTL to database key" do
461
+ session.save
462
+ session.update_expiration(default_expiration: 10.minutes)
463
+
464
+ ttl = session.ttl
465
+ expect(ttl).to be > 500 # Should be close to 600 seconds
466
+ expect(ttl).to be <= 600
467
+ end
468
+
469
+ it "cascades expiration to related fields" do
470
+ session.save
471
+ session.activity_log.push('login') # Assume activity_log is a list
472
+
473
+ session.update_expiration(default_expiration: 5.minutes)
474
+
475
+ # Both main object and related fields should have TTL
476
+ expect(session.ttl).to be > 250
477
+ expect(session.activity_log.ttl).to be > 250
478
+ end
479
+ end
480
+ end
481
+ ```
482
+
483
+ ### Integration Testing
484
+
485
+ ```ruby
486
+ # test/integration/session_expiration_test.rb
487
+ class SessionExpirationTest < ActionDispatch::IntegrationTest
488
+ test "session extends TTL on activity" do
489
+ # Login and get session
490
+ post '/login', params: { username: 'test', password: 'password' }
491
+ session_token = response.cookies['session_token']
492
+
493
+ session = UserSession.find(session_token)
494
+ initial_ttl = session.ttl
495
+
496
+ # Wait a bit
497
+ sleep 2
498
+
499
+ # Make another request
500
+ get '/dashboard'
501
+
502
+ # TTL should be refreshed
503
+ refreshed_ttl = session.ttl
504
+ expect(refreshed_ttl).to be > initial_ttl
505
+ end
506
+ end
507
+ ```
508
+
509
+ ## Best Practices
510
+
511
+ ### 1. Set Appropriate Defaults
512
+
513
+ ```ruby
514
+ class SessionStore < Familia::Horreum
515
+ feature :expiration
516
+
517
+ # Choose TTL based on security requirements
518
+ case Rails.env
519
+ when 'development'
520
+ default_expiration 8.hours # Convenience for debugging
521
+ when 'test'
522
+ default_expiration 1.minute # Fast cleanup in tests
523
+ when 'production'
524
+ default_expiration 30.minutes # Security-focused
525
+ end
526
+ end
527
+ ```
528
+
529
+ ### 2. Monitor TTL Health
530
+
531
+ ```ruby
532
+ class TTLHealthCheck
533
+ def self.check_session_health
534
+ expired_count = 0
535
+ total_count = 0
536
+
537
+ UserSession.all.each do |session|
538
+ total_count += 1
539
+
540
+ ttl = session.ttl
541
+ if ttl == -2 # Key doesn't exist
542
+ expired_count += 1
543
+ elsif ttl < 300 # Less than 5 minutes remaining
544
+ # Extend TTL for active sessions
545
+ session.update_expiration(default_expiration: 30.minutes) if session.active?
546
+ end
547
+ end
548
+
549
+ {
550
+ total_sessions: total_count,
551
+ expired_sessions: expired_count,
552
+ expiration_rate: expired_count.to_f / total_count
553
+ }
554
+ end
555
+ end
556
+ ```
557
+
558
+ ### 3. Graceful Degradation
559
+
560
+ ```ruby
561
+ class RobustSessionManager
562
+ def self.get_or_create_session(session_token)
563
+ session = UserSession.find(session_token)
564
+
565
+ # Check if session exists and hasn't expired
566
+ if session&.ttl&.positive?
567
+ # Extend TTL on access
568
+ session.update_expiration(default_expiration: 30.minutes)
569
+ session
570
+ else
571
+ # Create new session if old one expired
572
+ create_new_session
573
+ end
574
+ rescue => e
575
+ # Fallback: create new session on any error
576
+ Rails.logger.warn "Session retrieval failed: #{e.message}"
577
+ create_new_session
578
+ end
579
+ end
580
+ ```
581
+
582
+ ### 4. Environment-Specific Configuration
583
+
584
+ ```ruby
585
+ # config/initializers/familia_expiration.rb
586
+ Familia.configure do |config|
587
+ # Set global default based on environment
588
+ config.default_expiration = case Rails.env
589
+ when 'development' then 0 # No expiration for debugging
590
+ when 'test' then 1.minute # Quick cleanup
591
+ when 'production' then 1.hour # Reasonable default
592
+ end
593
+ end
594
+ ```
595
+
596
+ The Expiration feature provides a robust foundation for managing data lifecycle in Familia applications, with flexible configuration options and automatic cascading to related objects.