Module: Familia::Features::Expiration
- Defined in:
- lib/familia/features/expiration.rb
Overview
Expiration is a feature that provides Time To Live (TTL) management for Familia objects and their associated Valkey/Redis data structures. It enables automatic data cleanup and supports cascading expiration across related objects.
This feature allows you to:
- UnsortedSet default expiration times at the class level
- Update expiration times for individual objects
- Cascade expiration settings to related data structures
- Query remaining TTL for objects
- Handle expiration inheritance in class hierarchies
Example:
class Session < Familia::Horreum feature :expiration default_expiration 1.hour
field :user_id, :data, :created_at
list :activity_log
end
session = Session.new(user_id: 123, data: { role: 'admin' }) session.save
# Automatically expires in 1 hour (default_expiration) session.ttl # => 3599 (seconds remaining)
# Update expiration to 30 minutes session.update_expiration(30.minutes) session.ttl # => 1799
# UnsortedSet custom expiration for new objects session.update_expiration(expiration: 2.hours)
Class-Level Configuration:
Default expiration can be set at the class level and will be inherited by subclasses unless overridden:
class BaseModel < Familia::Horreum feature :expiration default_expiration 1.day end
class ShortLivedModel < BaseModel default_expiration 5.minutes # Overrides parent end
class InheritedModel < BaseModel # Inherits 1.day from BaseModel end
Cascading Expiration:
When an object has related data structures (lists, sets, etc.), the expiration feature automatically applies TTL to all related structures:
class User < Familia::Horreum feature :expiration default_expiration 30.days
field :email, :name
list :sessions # Will also expire in 30 days
set :permissions # Will also expire in 30 days
hashkey :preferences # Will also expire in 30 days
end
Fine-Grained Control:
Related structures can have their own expiration settings:
class Analytics < Familia::Horreum feature :expiration default_expiration 1.year
field :metric_name
list :hourly_data, default_expiration: 1.week # Shorter TTL
list :daily_data, default_expiration: 1.month # Medium TTL
list :monthly_data # Uses class default (1.year)
end
Zero Expiration:
Setting expiration to 0 (zero) disables TTL, making data persist indefinitely:
session.update_expiration(expiration: 0) # No expiration
TTL Querying:
Check remaining time before expiration:
session.ttl # => 3599 (seconds remaining) session.ttl.zero? # => false (still has time) expired_session.ttl # => -1 (already expired or no TTL set)
Integration Patterns:
# Conditional expiration based on user type class UserSession < Familia::Horreum feature :expiration
field :user_id, :user_type
def save
super
case user_type
when 'premium'
update_expiration(7.days)
when 'free'
update_expiration(1.hour)
else
update_expiration(expiration)
end
end
end
# Background job cleanup class DataCleanupJob def perform # Extend expiration for active users active_sessions = Session.where(active: true) active_sessions.each do |session| session.update_expiration(session.default_expiration) end end end
Error Handling:
The feature validates expiration values and raises descriptive errors:
session.update_expiration(expiration: "invalid") # => Familia::Problem: Default expiration must be a number
session.update_expiration(expiration: -1) # => Familia::Problem: Default expiration must be non-negative
Performance Considerations:
- TTL operations are performed on Valkey/Redis side with minimal overhead
- Cascading expiration uses pipelining for efficiency when possible
- Zero expiration values skip Valkey/Redis EXPIRE calls entirely
- TTL queries are direct db operations (very fast)
Defined Under Namespace
Modules: ModelClassMethods
Instance Method Summary collapse
-
#default_expiration ⇒ Float
Get the default expiration time for this instance.
-
#default_expiration=(num) ⇒ Object
UnsortedSet the default expiration time for this instance.
-
#expired?(threshold = 0) ⇒ Boolean
Check if this object's data has expired or will expire soon.
-
#expires? ⇒ Boolean
Check if this object's data will expire.
-
#extend_expiration(duration) ⇒ Boolean
Extend the expiration time by the specified duration.
-
#persist! ⇒ Boolean
Remove expiration, making the object persist indefinitely.
-
#ttl ⇒ Integer
Get the remaining time to live for this object's data.
-
#update_expiration(expiration: nil) ⇒ Boolean
Sets an expiration time for the Valkey/Redis data associated with this object.
Instance Method Details
#default_expiration ⇒ Float
Get the default expiration time for this instance
Returns the instance-specific default expiration, falling back to class default expiration if not set.
221 222 223 |
# File 'lib/familia/features/expiration.rb', line 221 def default_expiration @default_expiration || self.class.default_expiration end |
#default_expiration=(num) ⇒ Object
UnsortedSet the default expiration time for this instance
210 211 212 |
# File 'lib/familia/features/expiration.rb', line 210 def default_expiration=(num) @default_expiration = num.to_f end |
#expired?(threshold = 0) ⇒ Boolean
Check if this object's data has expired or will expire soon
337 338 339 340 341 342 343 |
# File 'lib/familia/features/expiration.rb', line 337 def expired?(threshold = 0) current_ttl = ttl return false if current_ttl == -1 # no expiration set return true if current_ttl == -2 # key does not exist current_ttl <= threshold end |
#expires? ⇒ Boolean
Check if this object's data will expire
322 323 324 |
# File 'lib/familia/features/expiration.rb', line 322 def expires? ttl.positive? end |
#extend_expiration(duration) ⇒ Boolean
Extend the expiration time by the specified duration
This adds the given duration to the current TTL, effectively extending the object's lifetime without changing the default expiration setting.
356 357 358 359 360 361 362 |
# File 'lib/familia/features/expiration.rb', line 356 def extend_expiration(duration) current_ttl = ttl return false unless current_ttl.positive? # no current expiration set new_ttl = current_ttl + duration.to_f expire(new_ttl) end |
#persist! ⇒ Boolean
Remove expiration, making the object persist indefinitely
371 372 373 |
# File 'lib/familia/features/expiration.rb', line 371 def persist! dbclient.persist(dbkey) end |
#ttl ⇒ Integer
Get the remaining time to live for this object's data
314 315 316 |
# File 'lib/familia/features/expiration.rb', line 314 def ttl dbclient.ttl(dbkey) end |
#update_expiration(expiration: nil) ⇒ Boolean
If default expiration is set to zero, the expiration will be removed, making the data persist indefinitely.
Sets an expiration time for the Valkey/Redis data associated with this object
This method allows setting a Time To Live (TTL) for the data in Valkey/Redis, after which it will be automatically removed. The method also handles cascading expiration to related data structures when applicable.
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/familia/features/expiration.rb', line 252 def update_expiration(expiration: nil) expiration ||= default_expiration # Handle cascading expiration to related data structures if self.class.relations? Familia.debug "[update_expiration] #{self.class} has relations: #{self.class..keys}" self.class..each do |name, definition| # Skip relations that don't have their own expiration settings next if definition.opts[:default_expiration].nil? obj = send(name) Familia.debug "[update_expiration] Updating expiration for #{name} (#{obj.dbkey}) to #{expiration}" obj.update_expiration(expiration: expiration) end end # Validate expiration value # It's important to raise exceptions here and not just log warnings. We # don't want to silently fail at setting expirations and cause data # retention issues (e.g. not removed in a timely fashion). unless expiration.is_a?(Numeric) raise Familia::Problem, "Default expiration must be a number (#{expiration.class} given for #{self.class})" end unless expiration >= 0 raise Familia::Problem, "Default expiration must be non-negative (#{expiration} given for #{self.class})" end # If zero, simply skip setting an expiry for this key. If we were to set # 0, Valkey/Redis would drop the key immediately. if expiration.zero? Familia.debug "[update_expiration] No expiration for #{self.class} (#{dbkey})" return true end # Structured TTL operation logging Familia.debug "TTL updated", operation: :expire, key: dbkey, ttl_seconds: expiration, class: self.class.name, identifier: (identifier rescue nil) # The Valkey/Redis' EXPIRE command returns 1 if the timeout was set, 0 # if key does not exist or the timeout could not be set. Via redis-rb, # it's a boolean. expire(expiration) end |