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

Instance Method Details

#default_expirationFloat

Get the default expiration time for this instance

Returns the instance-specific default expiration, falling back to class default expiration if not set.

Returns:

  • (Float)

    The default expiration in seconds



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

Parameters:

  • num (Numeric)

    Expiration time in seconds



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

Examples:

Check if expired

session.expired?  # => true if TTL <= 0

Check if expiring within 5 minutes

session.expired?(5.minutes)  # => true if TTL <= 300

Parameters:

  • threshold (Numeric) (defaults to: 0)

    Consider expired if TTL is below this threshold (default: 0)

Returns:

  • (Boolean)

    true if expired or expiring 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

Returns:

  • (Boolean)

    true if TTL is set, false if data persists indefinitely



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.

Examples:

Extend session by 1 hour

session.extend_expiration(1.hour)

Parameters:

  • duration (Numeric)

    Additional time in seconds

Returns:

  • (Boolean)

    Success of the operation



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

Examples:

Make session persistent

session.persist!

Returns:

  • (Boolean)

    Success of the operation



349
350
351
# File 'lib/familia/features/expiration.rb', line 349

def persist!
  redis.persist(dbkey)
end

#ttlInteger

Get the remaining time to live for this object’s data

Examples:

Check remaining TTL

session.ttl  # => 3599 (expires in ~1 hour)
session.ttl.zero?  # => false

Check if expired or no TTL

expired_session.ttl  # => -1

Returns:

  • (Integer)

    Seconds remaining before expiration, or -1 if no TTL is set



293
294
295
# File 'lib/familia/features/expiration.rb', line 293

def ttl
  redis.ttl(dbkey)
end

#update_expiration(default_expiration: nil) ⇒ Boolean

Note:

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.

Examples:

Setting an expiration of one day

object.update_expiration(default_expiration: 86400)

Using default expiration

object.update_expiration  # Uses class default_expiration

Removing expiration (persist indefinitely)

object.update_expiration(default_expiration: 0)

Parameters:

  • default_expiration (Numeric, nil) (defaults to: nil)

    The Time To Live in seconds. If nil, the default TTL will be used.

Returns:

  • (Boolean)

    Returns true if the expiration was set successfully, false otherwise.

Raises:

  • (Familia::Problem)

    Raises an error if the default expiration is not a non-negative number.



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.related_fields.keys}"
    self.class.related_fields.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