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(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 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

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



219
220
221
# File 'lib/familia/features/expiration.rb', line 219

def default_expiration
  @default_expiration || self.class.default_expiration
end

#default_expiration=(num) ⇒ Object

UnsortedSet the default expiration time for this instance

Parameters:

  • num (Numeric)

    Expiration time in seconds



208
209
210
# File 'lib/familia/features/expiration.rb', line 208

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



329
330
331
332
333
334
335
# File 'lib/familia/features/expiration.rb', line 329

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



314
315
316
# File 'lib/familia/features/expiration.rb', line 314

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.

Examples:

Extend session by 1 hour

session.extend_expiration(1.hour)

Parameters:

  • duration (Numeric)

    Additional time in seconds

Returns:

  • (Boolean)

    Success of the operation



348
349
350
351
352
353
354
# File 'lib/familia/features/expiration.rb', line 348

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

Examples:

Make session persistent

session.persist!

Returns:

  • (Boolean)

    Success of the operation



363
364
365
# File 'lib/familia/features/expiration.rb', line 363

def persist!
  dbclient.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



306
307
308
# File 'lib/familia/features/expiration.rb', line 306

def ttl
  dbclient.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 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.

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.



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
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/familia/features/expiration.rb', line 250

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, Valkey/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"

  # 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(default_expiration)
end