Class: Familia::HashKey
- Includes:
- DataType::CollectionBase
- Defined in:
- lib/familia/data_type/types/hashkey.rb
Constant Summary collapse
- CLAIM_FIELD_SCRIPT =
Server-side compare-and-set: HSET the field only if it is unclaimed or already holds a value this caller owns.
KEYS[1] is the hash; ARGV[1] the field; ARGV[2] the canonical value to write; ARGV[3..] additional values that count as "already ours" (see #claim_field for the legacy-encoding case). A stored value matching a non-canonical variant is rewritten to the canonical form, so a claim doubles as an in-place normalization.
<<~LUA local current = redis.call('HGET', KEYS[1], ARGV[1]) if current == false then redis.call('HSET', KEYS[1], ARGV[1], ARGV[2]) return 0 end for i = 2, #ARGV do if current == ARGV[i] then if current ~= ARGV[2] then redis.call('HSET', KEYS[1], ARGV[1], ARGV[2]) end return 1 end end return current LUA
- RELEASE_FIELD_SCRIPT =
Server-side compare-and-delete: HDEL the field only if it holds a value this caller owns. Same KEYS/ARGV shape as CLAIM_FIELD_SCRIPT minus the normalization (there is nothing to normalize on the way out).
<<~LUA local current = redis.call('HGET', KEYS[1], ARGV[1]) if current == false then return 0 end for i = 2, #ARGV do if current == ARGV[i] then return redis.call('HDEL', KEYS[1], ARGV[1]) end end return 0 LUA
Instance Attribute Summary collapse
-
#features_enabled ⇒ Object
included
from Features
readonly
Returns the value of attribute features_enabled.
- #logical_database(val = nil) ⇒ Object included from DataType::ClassMethods
-
#parent ⇒ Object
included
from DataType::ClassMethods
Returns the value of attribute parent.
-
#prefix ⇒ Object
included
from DataType::ClassMethods
Returns the value of attribute prefix.
-
#suffix ⇒ Object
included
from DataType::ClassMethods
Returns the value of attribute suffix.
-
#uri(val = nil) ⇒ Object
included
from DataType::ClassMethods
Returns the value of attribute uri.
Attributes included from Settings
#current_key_version, #default_expiration, #delim, #encryption_hkdf_salt, #encryption_hkdf_salt_history, #encryption_keys, #encryption_personalization, #encryption_personalization_history, #logical_database, #prefix, #raise_on_unsaved_parent_write, #schema_path, #schema_validator, #schemas, #strict_write_order, #suffix, #transaction_mode
Instance Method Summary collapse
- #[](field) ⇒ Object (also: #get)
-
#[]=(field, val) ⇒ Object
(also: #put, #store, #add)
+return+ [Integer] Returns 1 if the field is new and added, 0 if the field already existed and the value was updated.
-
#claim_field(field, val) ⇒ Symbol, Object
Claims +field+ for +val+, atomically, server-side.
- #decrement(field, by = 1) ⇒ Object (also: #decr, #decrby)
-
#each(matching: nil, batch_size: 100) {|field, value| ... } ⇒ Enumerator, self
Iterates over field-value pairs in the hash.
- #empty? ⇒ Boolean
-
#expire_fields(seconds, *fields) ⇒ Array<Integer>
(also: #hexpire)
Sets expiration time in seconds on one or more hash fields.
-
#expireat_fields(unix_time, *fields) ⇒ Array<Integer>
(also: #hexpireat)
Sets absolute expiration time (Unix timestamp in seconds) on hash fields.
-
#expiretime_fields(*fields) ⇒ Array<Integer>
(also: #hexpiretime)
Returns the absolute Unix expiration timestamp in seconds for hash fields.
- #fetch(field, default = nil) ⇒ Object
-
#field_count ⇒ Integer
(also: #size, #length, #count)
Returns the number of fields in the hash.
- #hgetall ⇒ Object (also: #all)
-
#hsetnx(field, val) ⇒ Integer
Sets field in the hash stored at key to value, only if field does not yet exist.
-
#incrbyfloat(field, by) ⇒ Float
(also: #incrfloat)
Increments the float value of a hash field by the given amount.
- #increment(field, by = 1) ⇒ Object (also: #incr, #incrby)
- #key?(field) ⇒ Boolean (also: #has_key?, #include?, #member?)
- #keys ⇒ Object
-
#persist_fields(*fields) ⇒ Array<Integer>
(also: #hpersist)
Removes expiration from one or more hash fields.
-
#pexpire_fields(milliseconds, *fields) ⇒ Array<Integer>
(also: #hpexpire)
Sets expiration time in milliseconds on one or more hash fields.
-
#pexpireat_fields(unix_time_ms, *fields) ⇒ Array<Integer>
(also: #hpexpireat)
Sets absolute expiration time (Unix timestamp in milliseconds) on hash fields.
-
#pexpiretime_fields(*fields) ⇒ Array<Integer>
(also: #hpexpiretime)
Returns the absolute Unix expiration timestamp in milliseconds for hash fields.
-
#pttl_fields(*fields) ⇒ Array<Integer>
(also: #hpttl)
Returns the remaining TTL in milliseconds for one or more hash fields.
-
#randfield(count = nil, withvalues: false) ⇒ String, ...
(also: #hrandfield)
Returns one or more random fields from the hash.
-
#refresh ⇒ self
The friendly neighborhood refresh method!.
-
#refresh! ⇒ void
The Great Database Refresh-o-matic 3000 for HashKey!.
-
#release_field(field, val) ⇒ Integer, Redis::Future
Deletes +field+ only if it currently holds a value equivalent to +val+.
-
#remove_field(field) ⇒ Integer
(also: #remove, #remove_element)
Removes a field from the hash.
-
#scan(cursor = 0, match: nil, count: nil) ⇒ Array<Integer, Hash>
(also: #hscan)
Incrementally iterates over fields in the hash using cursor-based iteration.
-
#strlen(field) ⇒ Integer
(also: #hstrlen)
Returns the string length of the value associated with field.
-
#ttl_fields(*fields) ⇒ Array<Integer>
(also: #httl)
Returns the remaining TTL in seconds for one or more hash fields.
- #update(hsh = {}) ⇒ Object (also: #merge!)
- #values ⇒ Object
- #values_at(*fields) ⇒ Object
Methods included from DataType::CollectionBase
#collection_type?, #each_record
Methods included from Features::Autoloader
autoload_files, included, normalize_to_config_name
Methods included from DataType::Serialization
#deserialize_value, #deserialize_values, #deserialize_values_with_nil, #serialize_value, #strip_legacy_json_encoding
Methods included from DataType::DatabaseCommands
#current_expiration, #delete!, #echo, #exists?, #expire, #expireat, #move, #persist, #rename, #renamenx, #type
Methods included from DataType::Connection
Methods included from Connection::Behavior
#connect, #create_dbclient, #multi, #normalize_uri, #pipeline, #pipelined, #transaction, #uri=, #url, #url=
Methods included from Settings
#configure, #default_suffix, #dirty_write_warnings, #dirty_write_warnings=, #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
This class inherits a constructor from Familia::DataType
Instance Attribute Details
#features_enabled ⇒ Object (readonly) Originally defined in module Features
Returns the value of attribute features_enabled.
#logical_database(val = nil) ⇒ Object Originally defined in module DataType::ClassMethods
#parent ⇒ Object Originally defined in module DataType::ClassMethods
Returns the value of attribute parent.
#prefix ⇒ Object Originally defined in module DataType::ClassMethods
Returns the value of attribute prefix.
#suffix ⇒ Object Originally defined in module DataType::ClassMethods
Returns the value of attribute suffix.
#uri(val = nil) ⇒ Object Originally defined in module DataType::ClassMethods
Returns the value of attribute uri.
Instance Method Details
#[](field) ⇒ Object Also known as: get
161 162 163 |
# File 'lib/familia/data_type/types/hashkey.rb', line 161 def [](field) deserialize_value dbclient.hget(dbkey, field.to_s) end |
#[]=(field, val) ⇒ Object Also known as: put, store, add
This method executes a Redis HSET immediately, unlike scalar field setters which are deferred until save. If the parent object has unsaved scalar field changes, consider calling save first to avoid split-brain state.
+return+ [Integer] Returns 1 if the field is new and added, 0 if the field already existed and the value was updated.
144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/familia/data_type/types/hashkey.rb', line 144 def []=(field, val) warn_if_dirty! ret = dbclient.hset dbkey, field.to_s, serialize_value(val) update_expiration ret rescue TypeError => e Familia.error "[hset]= #{e.}" Familia.debug "[hset]= #{dbkey} #{field}=#{val}" echo :hset, Familia.pretty_stack(limit: 1) if Familia.debug # logs via echo to the db and back klass = val.class msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}" raise e.class, msg end |
#claim_field(field, val) ⇒ Symbol, Object
Claims +field+ for +val+, atomically, server-side.
This is the enforcement half of the unique-index invariant (ADR-0002). A read-then-HSET pair cannot enforce uniqueness on a key many writers share -- two savers both read "unclaimed" and the second HSET silently overwrites the first. WATCH does not help either: its granularity is the whole key, so every concurrent save of the class aborts every other one. A single EVAL checks and writes at field granularity, conflicting only on a genuine collision, and touches exactly one key (so it is Redis Cluster safe).
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/familia/data_type/types/hashkey.rb', line 78 def claim_field(field, val) if Fiber[:familia_transaction] || Fiber[:familia_pipeline] raise Familia::OperationModeError, <<~ERROR_MESSAGE Cannot claim_field inside a transaction or pipeline: EVAL returns a Future there, so the claim's verdict cannot be checked. Claim before opening the MULTI (see ADR-0002). ERROR_MESSAGE end result = dbclient.eval( CLAIM_FIELD_SCRIPT, keys: [dbkey], argv: [field.to_s, *ownership_variants(val)] ) return deserialize_value(result) unless result.is_a?(Integer) update_expiration result.zero? ? :created : :owned end |
#decrement(field, by = 1) ⇒ Object Also known as: decr, decrby
238 239 240 |
# File 'lib/familia/data_type/types/hashkey.rb', line 238 def decrement(field, by = 1) increment field, -by end |
#each(matching: nil, batch_size: 100) {|field, value| ... } ⇒ Enumerator, self
Pattern matches field names only (plain strings). To filter on values, use Enumerable#select instead.
Iterates over field-value pairs in the hash.
Uses HSCAN for memory-efficient iteration. Optionally filters by field name pattern using Redis MATCH.
283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/familia/data_type/types/hashkey.rb', line 283 def each(matching: nil, batch_size: 100, &block) return to_enum(:each, matching: matching, batch_size: batch_size) unless block cursor = 0 loop do new_cursor, pairs = scan(cursor, match: matching, count: batch_size) pairs.each(&block) cursor = new_cursor break if cursor.zero? end self end |
#empty? ⇒ Boolean
134 135 136 |
# File 'lib/familia/data_type/types/hashkey.rb', line 134 def empty? field_count.zero? end |
#expire_fields(seconds, *fields) ⇒ Array<Integer> Also known as: hexpire
Requires Redis 7.4+
Sets expiration time in seconds on one or more hash fields.
409 410 411 |
# File 'lib/familia/data_type/types/hashkey.rb', line 409 def expire_fields(seconds, *fields) call_hash_field_command('HEXPIRE', seconds, fields: fields) end |
#expireat_fields(unix_time, *fields) ⇒ Array<Integer> Also known as: hexpireat
Requires Redis 7.4+
Sets absolute expiration time (Unix timestamp in seconds) on hash fields.
438 439 440 |
# File 'lib/familia/data_type/types/hashkey.rb', line 438 def expireat_fields(unix_time, *fields) call_hash_field_command('HEXPIREAT', unix_time, fields: fields) end |
#expiretime_fields(*fields) ⇒ Array<Integer> Also known as: hexpiretime
Requires Redis 7.4+
Returns the absolute Unix expiration timestamp in seconds for hash fields.
510 511 512 |
# File 'lib/familia/data_type/types/hashkey.rb', line 510 def expiretime_fields(*fields) call_hash_field_command('HEXPIRETIME', fields: fields) end |
#fetch(field, default = nil) ⇒ Object
166 167 168 169 170 171 172 173 174 175 |
# File 'lib/familia/data_type/types/hashkey.rb', line 166 def fetch(field, default = nil) ret = self[field.to_s] if ret.nil? raise IndexError, "No such index for: #{field}" if default.nil? default else ret end end |
#field_count ⇒ Integer Also known as: size, length, count
Returns the number of fields in the hash
127 128 129 |
# File 'lib/familia/data_type/types/hashkey.rb', line 127 def field_count dbclient.hlen dbkey end |
#hgetall ⇒ Object Also known as: all
185 186 187 188 189 |
# File 'lib/familia/data_type/types/hashkey.rb', line 185 def hgetall dbclient.hgetall(dbkey).transform_values do |v| deserialize_value v end end |
#hsetnx(field, val) ⇒ Integer
Sets field in the hash stored at key to value, only if field does not yet exist. If field already exists, this operation has no effect.
197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/familia/data_type/types/hashkey.rb', line 197 def hsetnx(field, val) warn_if_dirty! ret = dbclient.hsetnx dbkey, field.to_s, serialize_value(val) update_expiration if ret == 1 ret rescue TypeError => e Familia.error "[hsetnx] #{e.}" Familia.debug "[hsetnx] #{dbkey} #{field}=#{val}" echo :hsetnx, Familia.pretty_stack(limit: 1) if Familia.debug # logs via echo to the db and back klass = val.class msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}" raise e.class, msg end |
#incrbyfloat(field, by) ⇒ Float Also known as: incrfloat
Increments the float value of a hash field by the given amount.
339 340 341 342 343 |
# File 'lib/familia/data_type/types/hashkey.rb', line 339 def incrbyfloat(field, by) ret = dbclient.hincrbyfloat(dbkey, field.to_s, by).to_f update_expiration ret end |
#increment(field, by = 1) ⇒ Object Also known as: incr, incrby
230 231 232 233 234 |
# File 'lib/familia/data_type/types/hashkey.rb', line 230 def increment(field, by = 1) ret = dbclient.hincrby(dbkey, field.to_s, by).to_i update_expiration ret end |
#key?(field) ⇒ Boolean Also known as: has_key?, include?, member?
211 212 213 |
# File 'lib/familia/data_type/types/hashkey.rb', line 211 def key?(field) dbclient.hexists dbkey, field.to_s end |
#keys ⇒ Object
177 178 179 |
# File 'lib/familia/data_type/types/hashkey.rb', line 177 def keys dbclient.hkeys dbkey end |
#persist_fields(*fields) ⇒ Array<Integer> Also known as: hpersist
Requires Redis 7.4+
Removes expiration from one or more hash fields.
495 496 497 |
# File 'lib/familia/data_type/types/hashkey.rb', line 495 def persist_fields(*fields) call_hash_field_command('HPERSIST', fields: fields) end |
#pexpire_fields(milliseconds, *fields) ⇒ Array<Integer> Also known as: hpexpire
Requires Redis 7.4+
Sets expiration time in milliseconds on one or more hash fields.
423 424 425 |
# File 'lib/familia/data_type/types/hashkey.rb', line 423 def pexpire_fields(milliseconds, *fields) call_hash_field_command('HPEXPIRE', milliseconds, fields: fields) end |
#pexpireat_fields(unix_time_ms, *fields) ⇒ Array<Integer> Also known as: hpexpireat
Requires Redis 7.4+
Sets absolute expiration time (Unix timestamp in milliseconds) on hash fields.
452 453 454 |
# File 'lib/familia/data_type/types/hashkey.rb', line 452 def pexpireat_fields(unix_time_ms, *fields) call_hash_field_command('HPEXPIREAT', unix_time_ms, fields: fields) end |
#pexpiretime_fields(*fields) ⇒ Array<Integer> Also known as: hpexpiretime
Requires Redis 7.4+
Returns the absolute Unix expiration timestamp in milliseconds for hash fields.
523 524 525 |
# File 'lib/familia/data_type/types/hashkey.rb', line 523 def pexpiretime_fields(*fields) call_hash_field_command('HPEXPIRETIME', fields: fields) end |
#pttl_fields(*fields) ⇒ Array<Integer> Also known as: hpttl
Requires Redis 7.4+
Returns the remaining TTL in milliseconds for one or more hash fields.
480 481 482 |
# File 'lib/familia/data_type/types/hashkey.rb', line 480 def pttl_fields(*fields) call_hash_field_command('HPTTL', fields: fields) end |
#randfield(count = nil, withvalues: false) ⇒ String, ... Also known as: hrandfield
Returns one or more random fields from the hash.
377 378 379 380 381 382 383 384 385 386 387 388 |
# File 'lib/familia/data_type/types/hashkey.rb', line 377 def randfield(count = nil, withvalues: false) if count.nil? dbclient.hrandfield(dbkey) elsif withvalues pairs = dbclient.hrandfield(dbkey, count, withvalues: true) # pairs is array of [field, value, field, value, ...] # Convert to array of [field, deserialized_value] pairs pairs.each_slice(2).map { |field, val| [field, deserialize_value(val)] } else dbclient.hrandfield(dbkey, count) end end |
#refresh ⇒ self
The friendly neighborhood refresh method!
This method is like refresh! but with better manners - it returns self so you can chain it with other methods. It's perfect for when you want to refresh your hash and immediately do something with it.
629 630 631 632 |
# File 'lib/familia/data_type/types/hashkey.rb', line 629 def refresh refresh! self end |
#refresh! ⇒ void
This operation is atomic - it either succeeds completely or fails safely. Any unsaved changes to the hash will be overwritten.
This method returns an undefined value.
The Great Database Refresh-o-matic 3000 for HashKey!
This method performs a complete refresh of the hash's state from the database. It's like giving your hash a memory transfusion - out with the old state, in with the fresh data straight from Valkey/Redis!
602 603 604 605 606 607 608 609 610 611 |
# File 'lib/familia/data_type/types/hashkey.rb', line 602 def refresh! Familia.trace :REFRESH, nil, self.class.uri if Familia.debug? raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey) fields = hgetall Familia.debug "[refresh!] #{self.class} #{dbkey} #{fields.keys}" # For HashKey, we update by merging the fresh data update(fields) end |
#release_field(field, val) ⇒ Integer, Redis::Future
Deletes +field+ only if it currently holds a value equivalent to +val+.
The release half of the claim protocol. An unconditional HDEL lets a stale in-memory value delete a claim another record now legitimately owns -- the same shared-key overwrite #claim_field closes on the way in.
Unlike #claim_field this is safe to queue inside a MULTI: it needs no return value to decide anything, so the script's atomicity still holds at EXEC time.
111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/familia/data_type/types/hashkey.rb', line 111 def release_field(field, val) ret = dbclient.eval( RELEASE_FIELD_SCRIPT, keys: [dbkey], argv: [field.to_s, *ownership_variants(val)] ) # Refreshed unconditionally, matching #[]= -- a release that deleted # nothing still touched the index key's TTL policy, and `ret` cannot gate # it anyway: inside a MULTI (where this method is explicitly safe to # queue) `ret` is a Future with no value to test. On an index hash with # no default_expiration -- the usual case -- this returns early without # issuing a command. update_expiration ret end |
#remove_field(field) ⇒ Integer Also known as: remove, remove_element
Removes a field from the hash
221 222 223 224 225 226 |
# File 'lib/familia/data_type/types/hashkey.rb', line 221 def remove_field(field) warn_if_dirty! ret = dbclient.hdel dbkey, field.to_s update_expiration ret end |
#scan(cursor = 0, match: nil, count: nil) ⇒ Array<Integer, Hash> Also known as: hscan
Incrementally iterates over fields in the hash using cursor-based iteration.
This is more memory-efficient than hgetall for large hashes.
316 317 318 319 320 321 322 323 324 325 326 327 |
# File 'lib/familia/data_type/types/hashkey.rb', line 316 def scan(cursor = 0, match: nil, count: nil) opts = {} opts[:match] = match if match opts[:count] = count if count new_cursor, pairs = dbclient.hscan(dbkey, cursor, **opts) # pairs is an array of [field, value] pairs, convert to hash with deserialization result_hash = pairs.to_h.transform_values { |v| deserialize_value(v) } [new_cursor.to_i, result_hash] end |
#strlen(field) ⇒ Integer Also known as: hstrlen
Returns the string length of the value associated with field.
354 355 356 |
# File 'lib/familia/data_type/types/hashkey.rb', line 354 def strlen(field) dbclient.hstrlen(dbkey, field.to_s) end |
#ttl_fields(*fields) ⇒ Array<Integer> Also known as: httl
Requires Redis 7.4+
Returns the remaining TTL in seconds for one or more hash fields.
467 468 469 |
# File 'lib/familia/data_type/types/hashkey.rb', line 467 def ttl_fields(*fields) call_hash_field_command('HTTL', fields: fields) end |
#update(hsh = {}) ⇒ Object Also known as: merge!
244 245 246 247 248 249 250 251 252 253 |
# File 'lib/familia/data_type/types/hashkey.rb', line 244 def update(hsh = {}) warn_if_dirty! raise ArgumentError, 'Argument to bulk_set must be a hash' unless hsh.is_a?(Hash) data = hsh.inject([]) { |ret, pair| ret << [pair[0], serialize_value(pair[1])] }.flatten ret = dbclient.hmset(dbkey, *data) update_expiration ret end |
#values ⇒ Object
181 182 183 |
# File 'lib/familia/data_type/types/hashkey.rb', line 181 def values dbclient.hvals(dbkey).map { |v| deserialize_value v } end |
#values_at(*fields) ⇒ Object
256 257 258 259 260 |
# File 'lib/familia/data_type/types/hashkey.rb', line 256 def values_at *fields string_fields = fields.flatten.compact.map(&:to_s) elements = dbclient.hmget(dbkey, *string_fields) deserialize_values(*elements) end |