Class: Familia::DataType Abstract

Inherits:
Object
  • Object
show all
Extended by:
ClassMethods, Features
Includes:
Base, Connection, DatabaseCommands, Serialization, Settings
Defined in:
lib/familia/data_type.rb,
lib/familia/data_type/settings.rb,
lib/familia/data_type/connection.rb,
lib/familia/data_type/class_methods.rb,
lib/familia/data_type/serialization.rb,
lib/familia/data_type/database_commands.rb

Overview

This class is abstract.

Subclass and implement Database data type specific methods

DataType - Base class for Database data type wrappers

This class provides common functionality for various Database data types such as String, JsonStringKey, List, UnsortedSet, SortedSet, and HashKey.

== Write Method Transaction Safety Audit (2026-02-25)

All write methods use dbclient which is transaction-aware: inside a Horreum#transaction block, Fiber[:familia_transaction] routes commands through the transaction connection. Outside a transaction, each write is a standalone command followed by a separate EXPIRE (2 round trips, no atomicity guarantee between them).

Methods marked read-then-write are NOT atomic outside of transactions.

Type Method Redis Cmd update_exp Read-then-write
UnsortedSet add SADD yes no
UnsortedSet remove_element SREM yes no
UnsortedSet pop SPOP yes no
UnsortedSet move SMOVE yes no
SortedSet add ZADD yes no
SortedSet remove_element ZREM yes no
SortedSet increment ZINCRBY yes no
SortedSet decrement ZINCRBY (neg) yes (via increment) no
SortedSet remrangebyrank ZREMRANGEBYRANK yes no
SortedSet remrangebyscore ZREMRANGEBYSCORE yes no
HashKey []= HSET yes no
HashKey hsetnx HSETNX conditional no
HashKey remove_field HDEL yes no
HashKey increment HINCRBY yes no
HashKey decrement HINCRBY (neg) yes (via increment) no
HashKey update HMSET yes no
ListKey push RPUSH(+LTRIM) yes no
ListKey unshift LPUSH(+LTRIM) yes no
ListKey pop RPOP yes no
ListKey shift LPOP yes no
ListKey remove_element LREM yes no
StringKey value= SET yes no
StringKey setnx SETNX yes no
StringKey increment INCR yes no
StringKey incrementby INCRBY yes no
StringKey decrement DECR yes no
StringKey decrementby DECRBY yes no
StringKey append APPEND yes no
StringKey setbit SETBIT yes no
StringKey setrange SETRANGE yes no
StringKey getset GETSET yes no
StringKey del DEL no no
Counter reset SET (via set) yes (via value=) no
Counter incr_if_lt EVAL (Lua) yes no (atomic Lua)
Lock acquire SETNX(+EXPIRE) yes (via setnx) no
Lock release EVAL (Lua) no (deletes key) no (atomic Lua)
Lock force_unlock! DEL no (deletes key) no

Notes:

  • Counter#increment_if_less_than uses a Lua script (EVAL) for atomic threshold check + increment. Previously used GET then conditional INCRBY which was not atomic outside of a transaction.
  • Lock#release uses a Lua script (EVAL) which IS atomic on the server.
  • StringKey#del and Lock methods that delete the key do not call update_expiration because the key no longer exists.
  • HashKey#hsetnx only calls update_expiration when the field was actually set (ret == 1), which is correct conditional behavior.

Defined Under Namespace

Modules: ClassMethods, Connection, DatabaseCommands, Serialization, Settings

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes included from Settings

#current_key_version, #delim, #encryption_keys, #encryption_personalization, #logical_database, #prefix, #schema_path, #schema_validator, #schemas, #strict_write_order, #suffix, #transaction_mode

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Features::Autoloader

autoload_files, included, normalize_to_config_name

Methods included from Serialization

#deserialize_value, #deserialize_values, #deserialize_values_with_nil, #serialize_value

Methods included from DatabaseCommands

#current_expiration, #delete!, #echo, #exists?, #expire, #expireat, #move, #persist, #rename, #renamenx, #type

Methods included from Connection

#dbclient, #dbkey, #direct_access, #uri

Methods included from Connection::Behavior

#connect, #create_dbclient, #multi, #normalize_uri, #pipeline, #pipelined, #transaction, #uri=, #url, #url=

Methods included from Settings

#configure, #default_suffix, #pipelined_mode, #pipelined_mode=

Methods included from Base

add_feature, #as_json, #expired?, #expires?, find_feature, #generate_id, #to_json, #to_s, #ttl, #update_expiration, #uuid

Constructor Details

#initialize(keystring, opts = {}) ⇒ DataType

+keystring+: If parent is set, this will be used as the suffix for dbkey. Otherwise this becomes the value of the key. If this is an Array, the elements will be joined.

Options:

:class => A class that responds to from_json. This will be used when loading data from the database to unmarshal the class. JSON serialization is used for all data storage.

:parent => The Familia object that this datatype object belongs to. This can be a class that includes Familia or an instance.

:default_expiration => the time to live in seconds. When not nil, this will set the default expiration for this dbkey whenever #save is called. You can also call it explicitly via #update_expiration.

:default => the default value (String-only)

:dbkey => a hardcoded key to use instead of the deriving the from the name and parent (e.g. a derived key: customer:custid:secret_counter).

:suffix => the suffix to use for the key (e.g. 'scores' in customer:custid:scores). :prefix => the prefix to use for the key (e.g. 'customer' in customer:custid:scores).

Connection precendence: uses the database connection of the parent or the value of opts[:dbclient] or Familia.dbclient (in that order).



125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/familia/data_type.rb', line 125

def initialize(keystring, opts = {})
  @keystring = keystring
  @keystring = @keystring.join(Familia.delim) if @keystring.is_a?(Array)

  # Remove all keys from the opts that are not in the allowed list
  @opts = DataType.valid_keys_only(opts || {})

  # Apply the options to instance method setters of the same name
  @opts.each do |k, v|
    send(:"#{k}=", v) if respond_to? :"#{k}="
  end

  init if respond_to? :init
end

Class Attribute Details

Returns the value of attribute has_related_fields.



95
96
97
# File 'lib/familia/data_type.rb', line 95

def has_related_fields
  @has_related_fields
end

.registered_typesObject (readonly)

Returns the value of attribute registered_types.



95
96
97
# File 'lib/familia/data_type.rb', line 95

def registered_types
  @registered_types
end

.valid_optionsObject (readonly)

Returns the value of attribute valid_options.



95
96
97
# File 'lib/familia/data_type.rb', line 95

def valid_options
  @valid_options
end

Instance Attribute Details

#features_enabledObject (readonly) Originally defined in module Features

Returns the value of attribute features_enabled.

#logical_database(val = nil) ⇒ Object Originally defined in module ClassMethods

#parentObject Originally defined in module ClassMethods

Returns the value of attribute parent.

#prefixObject Originally defined in module ClassMethods

Returns the value of attribute prefix.

#suffixObject Originally defined in module ClassMethods

Returns the value of attribute suffix.

#uri(val = nil) ⇒ Object Originally defined in module ClassMethods

Returns the value of attribute uri.

Class Method Details

.feature(feature_name = nil, **options) ⇒ Array? Originally defined in module Features

Enables a feature for the current class with optional configuration.

Features are modular capabilities that can be mixed into Familia::Horreum classes. Each feature can be configured with options that are stored per-class, ensuring complete isolation between different models.

Examples:

Enable feature without options

class User < Familia::Horreum
  feature :expiration
end

Enable feature with options (per-class storage)

class User < Familia::Horreum
  feature :object_identifier, generator: :uuid_v4
end

class Session < Familia::Horreum
  feature :object_identifier, generator: :hex  # Different options
end

# Each class maintains separate options:
User.feature_options(:object_identifier)    #=> {generator: :uuid_v4}
Session.feature_options(:object_identifier) #=> {generator: :hex}

Parameters:

  • feature_name (Symbol, String, nil) (defaults to: nil)

    the name of the feature to enable. If nil, returns the list of currently enabled features.

  • options (Hash)

    configuration options for the feature. These are stored per-class and do not interfere with other models' configurations.

Returns:

  • (Array, nil)

    the list of enabled features if feature_name is nil, otherwise nil

Raises:

.inherited(obj) ⇒ Object Originally defined in module ClassMethods

.register(klass, methname) ⇒ Object Originally defined in module ClassMethods

To be called inside every class that inherits DataType +methname+ is the term used for the class and instance methods that are created for the given +klass+ (e.g. set, list, etc)

.registered_type(methname) ⇒ Object Originally defined in module ClassMethods

Get the registered type class from a given method name +methname+ is the method name used to register the class (e.g. :set, :list, etc) Returns the registered class or nil if not found

.relations?Boolean Originally defined in module ClassMethods

Returns:

  • (Boolean)

.valid_keys_only(opts) ⇒ Object Originally defined in module ClassMethods

Instance Method Details

#default_expirationNumeric

Override the default_expiration instance method to inherit from the parent Horreum when this DataType doesn't have its own explicit default_expiration option. This enables TTL cascade: when a Horreum class has default_expiration 1.hour and a relation like set :tags doesn't specify its own, the tags set will use the parent's TTL.

Precedence:

  1. Instance-level @default_expiration (set directly)
  2. Explicit opts:default_expiration
  3. Parent Horreum's default_expiration (cascade)
  4. Class-level default (Familia.default_expiration, typically 0)

Relations with no_expiration: true are excluded from cascade and always return 0 (no TTL).

Returns:

  • (Numeric)

    The expiration in seconds



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/familia/data_type.rb', line 183

def default_expiration
  return 0 if @opts && @opts[:no_expiration]

  # Check instance-level override first
  return @default_expiration if @default_expiration

  # Check explicit opts from relation declaration
  return @opts[:default_expiration] if @opts && @opts[:default_expiration]

  # Inherit from parent Horreum if available
  if @parent_ref.respond_to?(:default_expiration)
    parent_exp = @parent_ref.default_expiration
    return parent_exp if parent_exp && parent_exp > 0
  end

  # Fall back to class-level default
  self.class.default_expiration
end

#warn_if_dirty!void

This method returns an undefined value.

Checks if the parent Horreum object has unsaved scalar field changes and emits a warning (or raises) before a collection write.

This guards against a subtle issue where collection operations (SADD, RPUSH, ZADD, HSET) write to Redis immediately while scalar field changes remain only in memory. If the process crashes before the scalar fields are saved, the collection data is persisted but the scalar data is lost, creating an inconsistent state.

Raises:



152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/familia/data_type.rb', line 152

def warn_if_dirty!
  return unless @parent_ref.respond_to?(:dirty?) && @parent_ref.dirty?

  dirty = @parent_ref.dirty_fields
  message = "Writing to #{self.class.name} #{dbkey} while parent " \
            "#{@parent_ref.class.name} has unsaved scalar fields: #{dirty.join(', ')}"

  if Familia.strict_write_order
    raise Familia::Problem, message
  else
    Familia.warn message
  end
end