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 Redis/Valkey data structures. It enables automatic data cleanup and supports cascading expiration across related objects.
This feature allows you to: - Set 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
# Set custom expiration for new objects session.update_expiration(default_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(default_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(default_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(default_expiration: “invalid”) # => Familia::Problem: Default expiration must be a number
session.update_expiration(default_expiration: -1) # => Familia::Problem: Default expiration must be non-negative
Performance Considerations:
- TTL operations are performed on Redis/Valkey side with minimal overhead
- Cascading expiration uses pipelining for efficiency when possible
- Zero expiration values skip Redis EXPIRE calls entirely
- TTL queries are direct Redis operations (very fast)
Defined Under Namespace
Modules: ClassMethods
Instance Method Summary collapse
-
#default_expiration ⇒ Float
Get the default expiration time for this instance.
-
#default_expiration=(num) ⇒ Object
Set 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(default_expiration: nil) ⇒ Boolean
Sets an expiration time for the Redis/Valkey 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.
209 210 211 |
# File 'lib/familia/features/expiration.rb', line 209 def default_expiration @default_expiration || self.class.default_expiration end |
#default_expiration=(num) ⇒ Object
Set the default expiration time for this instance
198 199 200 |
# File 'lib/familia/features/expiration.rb', line 198 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
316 317 318 319 320 321 |
# File 'lib/familia/features/expiration.rb', line 316 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
301 302 303 |
# File 'lib/familia/features/expiration.rb', line 301 def expires? ttl > 0 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.
334 335 336 337 338 339 340 |
# File 'lib/familia/features/expiration.rb', line 334 def extend_expiration(duration) current_ttl = ttl return false if current_ttl < 0 # No current expiration set new_ttl = current_ttl + duration.to_f expire(new_ttl) end |
#persist! ⇒ Boolean
Remove expiration, making the object persist indefinitely
349 350 351 |
# File 'lib/familia/features/expiration.rb', line 349 def persist! redis.persist(dbkey) end |
#ttl ⇒ Integer
Get the remaining time to live for this object’s data
293 294 295 |
# File 'lib/familia/features/expiration.rb', line 293 def ttl redis.ttl(dbkey) end |
#update_expiration(default_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 Redis/Valkey data associated with this object
This method allows setting a Time To Live (TTL) for the data in Redis, after which it will be automatically removed. The method also handles cascading expiration to related data structures when applicable.
240 241 242 243 244 245 246 247 248 249 250 251 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 |
# File 'lib/familia/features/expiration.rb', line 240 def update_expiration(default_expiration: nil) default_expiration ||= self.default_expiration # Handle cascading expiration to related data structures if self.class.relations? Familia.ld "[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.ld "[update_expiration] Updating expiration for #{name} (#{obj.dbkey}) to #{default_expiration}" obj.update_expiration(default_expiration: default_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 default_expiration.is_a?(Numeric) raise Familia::Problem, "Default expiration must be a number (#{default_expiration.class} given for #{self.class})" end unless default_expiration >= 0 raise Familia::Problem, "Default expiration must be non-negative (#{default_expiration} given for #{self.class})" end # If zero, simply skip setting an expiry for this key. If we were to set # 0, Redis would drop the key immediately. if default_expiration.zero? Familia.ld "[update_expiration] No expiration for #{self.class} (#{dbkey})" return true end Familia.ld "[update_expiration] Expires #{dbkey} in #{default_expiration} seconds" # 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(default_expiration) end |