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.
- checksums.yaml +4 -4
- data/.github/workflows/claude-code-review.yml +57 -0
- data/.github/workflows/claude.yml +71 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +3 -0
- data/CLAUDE.md +32 -10
- data/Gemfile +2 -2
- data/Gemfile.lock +4 -3
- data/docs/wiki/API-Reference.md +95 -18
- data/docs/wiki/Connection-Pooling-Guide.md +437 -0
- data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
- data/docs/wiki/Expiration-Feature-Guide.md +596 -0
- data/docs/wiki/Feature-System-Guide.md +631 -0
- data/docs/wiki/Features-System-Developer-Guide.md +892 -0
- data/docs/wiki/Field-System-Guide.md +784 -0
- data/docs/wiki/Home.md +82 -15
- data/docs/wiki/Implementation-Guide.md +126 -33
- data/docs/wiki/Quantization-Feature-Guide.md +721 -0
- data/docs/wiki/Relationships-Guide.md +684 -0
- data/docs/wiki/Security-Model.md +65 -25
- data/docs/wiki/Transient-Fields-Guide.md +280 -0
- data/examples/bit_encoding_integration.rb +237 -0
- data/examples/redis_command_validation_example.rb +231 -0
- data/examples/relationships_basic.rb +273 -0
- data/lib/familia/base.rb +1 -1
- data/lib/familia/connection.rb +3 -3
- data/lib/familia/data_type/types/counter.rb +38 -0
- data/lib/familia/data_type/types/hashkey.rb +18 -0
- data/lib/familia/data_type/types/lock.rb +43 -0
- data/lib/familia/data_type/types/string.rb +9 -2
- data/lib/familia/data_type.rb +9 -6
- data/lib/familia/encryption/encrypted_data.rb +137 -0
- data/lib/familia/encryption/manager.rb +21 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
- data/lib/familia/encryption.rb +1 -1
- data/lib/familia/errors.rb +17 -3
- data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
- data/lib/familia/features/encrypted_fields.rb +413 -4
- data/lib/familia/features/expiration.rb +319 -33
- data/lib/familia/features/quantization.rb +385 -44
- data/lib/familia/features/relationships/cascading.rb +438 -0
- data/lib/familia/features/relationships/indexing.rb +370 -0
- data/lib/familia/features/relationships/membership.rb +503 -0
- data/lib/familia/features/relationships/permission_management.rb +264 -0
- data/lib/familia/features/relationships/querying.rb +620 -0
- data/lib/familia/features/relationships/redis_operations.rb +274 -0
- data/lib/familia/features/relationships/score_encoding.rb +442 -0
- data/lib/familia/features/relationships/tracking.rb +379 -0
- data/lib/familia/features/relationships.rb +466 -0
- data/lib/familia/features/safe_dump.rb +1 -1
- data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
- data/lib/familia/features/transient_fields.rb +192 -10
- data/lib/familia/features.rb +2 -1
- data/lib/familia/field_type.rb +5 -2
- data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
- data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
- data/lib/familia/horreum/core/serialization.rb +535 -0
- data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
- data/lib/familia/horreum/core.rb +21 -0
- data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
- data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
- data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
- data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
- data/lib/familia/horreum.rb +17 -17
- data/lib/familia/validation/command_recorder.rb +336 -0
- data/lib/familia/validation/expectations.rb +519 -0
- data/lib/familia/validation/test_helpers.rb +443 -0
- data/lib/familia/validation/validator.rb +412 -0
- data/lib/familia/validation.rb +140 -0
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -1
- data/try/core/create_method_try.rb +240 -0
- data/try/core/database_consistency_try.rb +299 -0
- data/try/core/errors_try.rb +25 -4
- data/try/core/familia_try.rb +1 -1
- data/try/core/persistence_operations_try.rb +297 -0
- data/try/data_types/counter_try.rb +93 -0
- data/try/data_types/lock_try.rb +133 -0
- data/try/debugging/debug_aad_process.rb +82 -0
- data/try/debugging/debug_concealed_internal.rb +59 -0
- data/try/debugging/debug_concealed_reveal.rb +61 -0
- data/try/debugging/debug_context_aad.rb +68 -0
- data/try/debugging/debug_context_simple.rb +80 -0
- data/try/debugging/debug_cross_context.rb +62 -0
- data/try/debugging/debug_database_load.rb +64 -0
- data/try/debugging/debug_encrypted_json_check.rb +53 -0
- data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
- data/try/debugging/debug_exists_lifecycle.rb +54 -0
- data/try/debugging/debug_field_decrypt.rb +74 -0
- data/try/debugging/debug_fresh_cross_context.rb +73 -0
- data/try/debugging/debug_load_path.rb +66 -0
- data/try/debugging/debug_method_definition.rb +46 -0
- data/try/debugging/debug_method_resolution.rb +41 -0
- data/try/debugging/debug_minimal.rb +24 -0
- data/try/debugging/debug_provider.rb +68 -0
- data/try/debugging/debug_secure_behavior.rb +73 -0
- data/try/debugging/debug_string_class.rb +46 -0
- data/try/debugging/debug_test.rb +46 -0
- data/try/debugging/debug_test_design.rb +80 -0
- data/try/edge_cases/hash_symbolization_try.rb +1 -0
- data/try/edge_cases/reserved_keywords_try.rb +1 -0
- data/try/edge_cases/string_coercion_try.rb +2 -0
- data/try/encryption/encryption_core_try.rb +6 -4
- data/try/features/categorical_permissions_try.rb +515 -0
- data/try/features/encrypted_fields_core_try.rb +19 -11
- data/try/features/encrypted_fields_integration_try.rb +66 -70
- data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
- data/try/features/encrypted_fields_security_try.rb +151 -144
- data/try/features/encryption_fields/aad_protection_try.rb +108 -23
- data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
- data/try/features/encryption_fields/context_isolation_try.rb +30 -8
- data/try/features/encryption_fields/error_conditions_try.rb +6 -6
- data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
- data/try/features/encryption_fields/fresh_key_try.rb +27 -22
- data/try/features/encryption_fields/key_rotation_try.rb +16 -10
- data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
- data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
- data/try/features/encryption_fields/thread_safety_try.rb +6 -6
- data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/relationships_edge_cases_try.rb +145 -0
- data/try/features/relationships_performance_minimal_try.rb +132 -0
- data/try/features/relationships_performance_simple_try.rb +155 -0
- data/try/features/relationships_performance_try.rb +420 -0
- data/try/features/relationships_performance_working_try.rb +144 -0
- data/try/features/relationships_try.rb +237 -0
- data/try/features/safe_dump_try.rb +3 -0
- data/try/features/transient_fields/redacted_string_try.rb +2 -0
- data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
- data/try/features/transient_fields_core_try.rb +1 -1
- data/try/features/transient_fields_integration_try.rb +1 -1
- data/try/helpers/test_helpers.rb +26 -1
- data/try/horreum/base_try.rb +14 -8
- data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
- data/try/horreum/initialization_try.rb +1 -1
- data/try/horreum/relations_try.rb +2 -2
- data/try/horreum/serialization_persistent_fields_try.rb +8 -8
- data/try/horreum/serialization_try.rb +39 -4
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +1 -1
- data/try/validation/atomic_operations_try.rb.disabled +320 -0
- data/try/validation/command_validation_try.rb.disabled +207 -0
- data/try/validation/performance_validation_try.rb.disabled +324 -0
- data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
- metadata +81 -12
- data/TEST_COVERAGE.md +0 -40
- data/lib/familia/features/relatable_objects.rb +0 -125
- data/lib/familia/horreum/serialization.rb +0 -473
- 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.
|