familia 1.2.0 → 2.0.0.pre.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +68 -0
- data/.github/workflows/docs.yml +64 -0
- data/.gitignore +4 -0
- data/.pre-commit-config.yaml +3 -1
- data/.rubocop.yml +16 -9
- data/.rubocop_todo.yml +177 -31
- data/.yardopts +9 -0
- data/CLAUDE.md +141 -0
- data/Gemfile +15 -2
- data/Gemfile.lock +76 -34
- data/README.md +39 -23
- data/bin/irb +3 -0
- data/docs/connection_pooling.md +317 -0
- data/familia.gemspec +9 -5
- data/lib/familia/base.rb +19 -9
- data/lib/familia/connection.rb +232 -65
- data/lib/familia/core_ext.rb +1 -1
- data/lib/familia/datatype/commands.rb +59 -0
- data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
- data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
- data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
- data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
- data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
- data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
- data/lib/familia/datatype.rb +243 -0
- data/lib/familia/errors.rb +5 -2
- data/lib/familia/features/expiration.rb +33 -34
- data/lib/familia/features/quantization.rb +9 -3
- data/lib/familia/features/safe_dump.rb +2 -3
- data/lib/familia/features.rb +2 -2
- data/lib/familia/horreum/class_methods.rb +97 -110
- data/lib/familia/horreum/commands.rb +46 -51
- data/lib/familia/horreum/connection.rb +82 -0
- data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
- data/lib/familia/horreum/serialization.rb +61 -198
- data/lib/familia/horreum/settings.rb +6 -17
- data/lib/familia/horreum/utils.rb +11 -10
- data/lib/familia/horreum.rb +69 -60
- data/lib/familia/logging.rb +12 -12
- data/lib/familia/multi_result.rb +72 -0
- data/lib/familia/refinements.rb +7 -44
- data/lib/familia/settings.rb +11 -11
- data/lib/familia/utils.rb +123 -90
- data/lib/familia/version.rb +4 -21
- data/lib/familia.rb +17 -12
- data/lib/middleware/database_middleware.rb +150 -0
- data/try/configuration/scenarios_try.rb +65 -0
- data/try/core/connection_try.rb +58 -0
- data/try/core/errors_try.rb +93 -0
- data/try/core/extensions_try.rb +26 -0
- data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
- data/try/{00_familia_try.rb → core/familia_try.rb} +5 -3
- data/try/core/middleware_try.rb +68 -0
- data/try/core/refinements_try.rb +39 -0
- data/try/core/settings_try.rb +76 -0
- data/try/core/tools_try.rb +54 -0
- data/try/core/utils_try.rb +189 -0
- data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
- data/try/datatypes/datatype_base_try.rb +69 -0
- data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
- data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
- data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
- data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
- data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
- data/try/edge_cases/empty_identifiers_try.rb +48 -0
- data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -7
- data/try/edge_cases/json_serialization_try.rb +85 -0
- data/try/edge_cases/race_conditions_try.rb +60 -0
- data/try/edge_cases/reserved_keywords_try.rb +59 -0
- data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
- data/try/edge_cases/ttl_side_effects_try.rb +51 -0
- data/try/features/expiration_try.rb +86 -0
- data/try/features/quantization_try.rb +90 -0
- data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
- data/try/features/safe_dump_try.rb +137 -0
- data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
- data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
- data/try/horreum/class_methods_try.rb +41 -0
- data/try/horreum/commands_try.rb +49 -0
- data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
- data/try/horreum/relations_try.rb +146 -0
- data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
- data/try/horreum/settings_try.rb +43 -0
- data/try/integration/cross_component_try.rb +46 -0
- data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
- data/try/{40_customer_try.rb → models/customer_try.rb} +20 -17
- data/try/models/datatype_base_try.rb +101 -0
- data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
- data/try/performance/benchmarks_try.rb +55 -0
- data/try/pooling/README.md +20 -0
- data/try/pooling/configurable_stress_test_try.rb +435 -0
- data/try/pooling/connection_pool_test_try.rb +273 -0
- data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
- data/try/pooling/lib/connection_pool_metrics.rb +372 -0
- data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
- data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
- data/try/pooling/lib/visualize_stress_results.rb +434 -0
- data/try/pooling/pool_siege_try.rb +509 -0
- data/try/pooling/run_stress_tests_try.rb +482 -0
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
- data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
- data/try/prototypes/atomic_saves_v4.rb +105 -0
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
- metadata +140 -43
- data/.github/workflows/ruby.yml +0 -71
- data/VERSION.yml +0 -4
- data/lib/familia/redistype/commands.rb +0 -59
- data/lib/familia/redistype.rb +0 -228
- data/lib/familia/tools.rb +0 -68
- data/lib/redis_middleware.rb +0 -109
- data/try/20_redis_type_try.rb +0 -70
- data/try/91_json_bug_try.rb +0 -86
data/lib/familia/horreum.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/horreum.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
#
|
@@ -6,8 +6,8 @@ module Familia
|
|
6
6
|
#
|
7
7
|
# Key features:
|
8
8
|
# * Provides instance-level access to a single hash in Redis
|
9
|
-
# * Includes Familia for class/module level access to
|
10
|
-
# * Uses 'hashkey' to define a
|
9
|
+
# * Includes Familia for class/module level access to Database types and operations
|
10
|
+
# * Uses 'hashkey' to define a Database hash referred to as "object"
|
11
11
|
# * Applies a default expiry (5 years) to all keys
|
12
12
|
#
|
13
13
|
# Metaprogramming:
|
@@ -55,13 +55,15 @@ module Familia
|
|
55
55
|
#
|
56
56
|
class << self
|
57
57
|
attr_accessor :parent
|
58
|
-
|
58
|
+
# TODO: Where are we calling dbclient= from now with connection pool?
|
59
|
+
attr_writer :dbclient, :dump_method, :load_method
|
59
60
|
attr_reader :has_relations
|
60
61
|
|
61
62
|
# Extends ClassMethods to subclasses and tracks Familia members
|
62
63
|
def inherited(member)
|
63
64
|
Familia.trace :HORREUM, nil, "Welcome #{member} to the family", caller(1..1) if Familia.debug?
|
64
65
|
member.extend(ClassMethods)
|
66
|
+
member.extend(Connection)
|
65
67
|
member.extend(Features)
|
66
68
|
|
67
69
|
# Tracks all the classes/modules that include Familia. It's
|
@@ -84,17 +86,8 @@ module Familia
|
|
84
86
|
Familia.ld "[Horreum] Initializing #{self.class}"
|
85
87
|
initialize_relatives
|
86
88
|
|
87
|
-
#
|
88
|
-
#
|
89
|
-
# this logic would live somewhere else b/c we only need to call it once
|
90
|
-
# per class definition. Here it gets called every time an instance is
|
91
|
-
# instantiated.
|
92
|
-
unless self.class.fields.include?(:key)
|
93
|
-
# Define the 'key' field for this class
|
94
|
-
# This approach allows flexibility in how identifiers are generated
|
95
|
-
# while ensuring each object has a consistent way to be referenced
|
96
|
-
self.class.field :key
|
97
|
-
end
|
89
|
+
# No longer auto-create a key field - the identifier method will
|
90
|
+
# directly use the field specified by identifier_field
|
98
91
|
|
99
92
|
# Detect if first argument is a hash (legacy support)
|
100
93
|
if args.size == 1 && args.first.is_a?(Hash) && kwargs.empty?
|
@@ -131,7 +124,7 @@ module Familia
|
|
131
124
|
else
|
132
125
|
Familia.ld "[Horreum] #{self.class} initialized with no arguments"
|
133
126
|
# Default values are intentionally NOT set here to:
|
134
|
-
# - Maintain
|
127
|
+
# - Maintain Database memory efficiency (only store non-nil values)
|
135
128
|
# - Avoid conflicts with nil-skipping serialization logic
|
136
129
|
# - Preserve consistent exists? behavior (empty vs default-filled objects)
|
137
130
|
# - Keep initialization lightweight for unused fields
|
@@ -143,50 +136,49 @@ module Familia
|
|
143
136
|
init if respond_to?(:init)
|
144
137
|
end
|
145
138
|
|
146
|
-
# Sets up related
|
139
|
+
# Sets up related Database objects for the instance
|
147
140
|
# This method is crucial for establishing Redis-based relationships
|
148
141
|
#
|
149
142
|
# This needs to be called in the initialize method.
|
150
143
|
#
|
151
144
|
def initialize_relatives
|
152
|
-
# Generate instances of each
|
145
|
+
# Generate instances of each DataType. These need to be
|
153
146
|
# unique for each instance of this class so they can piggyback
|
154
147
|
# on the specifc index of this instance.
|
155
148
|
#
|
156
149
|
# i.e.
|
157
|
-
# familia_object.
|
158
|
-
# familia_object.
|
150
|
+
# familia_object.dbkey == v1:bone:INDEXVALUE:object
|
151
|
+
# familia_object.related_object.dbkey == v1:bone:INDEXVALUE:name
|
159
152
|
#
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
opts = redis_type_definition.opts
|
153
|
+
self.class.related_fields.each_pair do |name, datatype_definition|
|
154
|
+
klass = datatype_definition.klass
|
155
|
+
opts = datatype_definition.opts
|
164
156
|
Familia.ld "[#{self.class}] initialize_relatives #{name} => #{klass} #{opts.keys}"
|
165
157
|
|
166
158
|
# As a subclass of Familia::Horreum, we add ourselves as the parent
|
167
|
-
# automatically. This is what determines the
|
168
|
-
# instance and which
|
159
|
+
# automatically. This is what determines the dbkey for DataType
|
160
|
+
# instance and which database connection.
|
169
161
|
#
|
170
|
-
# e.g. If the parent's
|
171
|
-
# then the
|
162
|
+
# e.g. If the parent's dbkey is `customer:customer_id:object`
|
163
|
+
# then the dbkey for this DataType instance will be
|
172
164
|
# `customer:customer_id:name`.
|
173
165
|
#
|
174
166
|
opts[:parent] = self # unless opts.key(:parent)
|
175
167
|
|
176
168
|
suffix_override = opts.fetch(:suffix, name)
|
177
169
|
|
178
|
-
# Instantiate the
|
170
|
+
# Instantiate the DataType object and below we store it in
|
179
171
|
# an instance variable.
|
180
|
-
|
172
|
+
related_object = klass.new suffix_override, opts
|
181
173
|
|
182
|
-
# Freezes the
|
174
|
+
# Freezes the related_object, making it immutable.
|
183
175
|
# This ensures the object's state remains consistent and prevents any modifications,
|
184
176
|
# safeguarding its integrity and making it thread-safe.
|
185
177
|
# Any attempts to change the object after this will raise a FrozenError.
|
186
|
-
|
178
|
+
related_object.freeze
|
187
179
|
|
188
180
|
# e.g. customer.name #=> `#<Familia::HashKey:0x0000...>`
|
189
|
-
instance_variable_set :"@#{name}",
|
181
|
+
instance_variable_set :"@#{name}", related_object
|
190
182
|
end
|
191
183
|
end
|
192
184
|
|
@@ -198,7 +190,7 @@ module Familia
|
|
198
190
|
# (i.e., had non-nil values assigned)
|
199
191
|
# @private
|
200
192
|
def initialize_with_positional_args(*args)
|
201
|
-
Familia.trace :INITIALIZE_ARGS,
|
193
|
+
Familia.trace :INITIALIZE_ARGS, dbclient, args, caller(1..1) if Familia.debug?
|
202
194
|
self.class.fields.zip(args).filter_map do |field, value|
|
203
195
|
if value
|
204
196
|
send(:"#{field}=", value)
|
@@ -217,9 +209,9 @@ module Familia
|
|
217
209
|
# (i.e., had non-nil values assigned)
|
218
210
|
# @private
|
219
211
|
def initialize_with_keyword_args(**fields)
|
220
|
-
Familia.trace :INITIALIZE_KWARGS,
|
212
|
+
Familia.trace :INITIALIZE_KWARGS, dbclient, fields.keys, caller(1..1) if Familia.debug?
|
221
213
|
self.class.fields.filter_map do |field|
|
222
|
-
#
|
214
|
+
# Database will give us field names as strings back, but internally
|
223
215
|
# we use symbols. So we check for both.
|
224
216
|
value = fields[field.to_sym] || fields[field.to_s]
|
225
217
|
if value
|
@@ -230,8 +222,8 @@ module Familia
|
|
230
222
|
end
|
231
223
|
private :initialize_with_keyword_args
|
232
224
|
|
233
|
-
def
|
234
|
-
# Deserialize
|
225
|
+
def initialize_with_keyword_args_deserialize_value(**fields)
|
226
|
+
# Deserialize Database string values back to their original types
|
235
227
|
deserialized_fields = fields.transform_values { |value| deserialize_value(value) }
|
236
228
|
initialize_with_keyword_args(**deserialized_fields)
|
237
229
|
end
|
@@ -240,7 +232,7 @@ module Familia
|
|
240
232
|
# hash and refreshes the existing object.
|
241
233
|
#
|
242
234
|
# This method is part of horreum.rb rather than serialization.rb because it
|
243
|
-
# operates solely on the provided values and doesn't query
|
235
|
+
# operates solely on the provided values and doesn't query Database or other
|
244
236
|
# external sources. That's why it's called "optimistic" refresh: it assumes
|
245
237
|
# the provided values are correct and updates the object accordingly.
|
246
238
|
#
|
@@ -250,54 +242,71 @@ module Familia
|
|
250
242
|
# the object with.
|
251
243
|
# @return [Array] The list of field names that were updated.
|
252
244
|
def optimistic_refresh(**fields)
|
253
|
-
Familia.ld "[optimistic_refresh] #{self.class} #{
|
254
|
-
|
245
|
+
Familia.ld "[optimistic_refresh] #{self.class} #{dbkey} #{fields.keys}"
|
246
|
+
initialize_with_keyword_args_deserialize_value(**fields)
|
255
247
|
end
|
256
248
|
|
257
249
|
# Determines the unique identifier for the instance
|
258
|
-
# This method is used to generate
|
250
|
+
# This method is used to generate dbkeys for the object
|
251
|
+
# Returns nil for unsaved objects (following standard ORM patterns)
|
259
252
|
def identifier
|
260
|
-
definition = self.class.
|
261
|
-
|
262
|
-
|
263
|
-
#
|
264
|
-
# call each method in turn and join the results. When it's nil, raise
|
265
|
-
# an error
|
253
|
+
definition = self.class.identifier_field
|
254
|
+
return nil if definition.nil?
|
255
|
+
|
256
|
+
# Call the identifier field or proc (validation already done at class definition time)
|
266
257
|
unique_id = case definition
|
267
258
|
when Symbol, String
|
268
259
|
send(definition)
|
269
260
|
when Proc
|
270
261
|
definition.call(self)
|
271
|
-
when Array
|
272
|
-
Familia.join(definition.map { |method| send(method) })
|
273
|
-
else
|
274
|
-
raise Problem, "Invalid identifier definition: #{definition.inspect}"
|
275
262
|
end
|
276
263
|
|
277
|
-
#
|
278
|
-
raise
|
279
|
-
|
264
|
+
# Return nil for unpopulated identifiers (like unsaved ActiveRecord objects)
|
265
|
+
# Only raise errors when the identifier is actually needed for Redis operations
|
266
|
+
return nil if unique_id.nil? || unique_id.to_s.empty?
|
280
267
|
|
281
268
|
unique_id
|
282
269
|
end
|
283
270
|
|
271
|
+
attr_writer :dbclient
|
272
|
+
|
273
|
+
# Summon the mystical Database connection from the depths of instance or class.
|
274
|
+
#
|
275
|
+
# This method is like a magical divining rod, always pointing to the nearest
|
276
|
+
# source of Database goodness. It first checks if we have a personal Redis
|
277
|
+
# connection (@dbclient), and if not, it borrows the class's connection.
|
278
|
+
#
|
279
|
+
# @return [Redis] A shimmering Database connection, ready for your bidding.
|
280
|
+
#
|
281
|
+
# @example Finding your Database way
|
282
|
+
# puts object.dbclient
|
283
|
+
# # => #<Redis client v5.4.1 for redis://localhost:6379/0>
|
284
|
+
#
|
285
|
+
def dbclient
|
286
|
+
conn = Fiber[:familia_transaction] || @dbclient || self.class.dbclient
|
287
|
+
# conn.select(self.class.logical_database)
|
288
|
+
conn
|
289
|
+
end
|
290
|
+
|
291
|
+
def generate_id
|
292
|
+
@objid ||= Familia.generate_id # rubocop:disable Naming/MemoizedInstanceVariableName
|
293
|
+
end
|
294
|
+
|
284
295
|
# The principle is: **If Familia objects have `to_s`, then they should work
|
285
|
-
# everywhere strings are expected**, including as
|
296
|
+
# everywhere strings are expected**, including as Database hash field names.
|
286
297
|
def to_s
|
287
298
|
# Enable polymorphic string usage for Familia objects
|
288
299
|
# This allows passing Familia objects directly where strings are expected
|
289
300
|
# without requiring explicit .identifier calls
|
301
|
+
return super if identifier.to_s.empty?
|
290
302
|
identifier.to_s
|
291
|
-
rescue => e
|
292
|
-
# Fallback for cases where identifier might fail
|
293
|
-
Familia.ld "[#{self.class}#to_s] Failed to get identifier: #{e.message}"
|
294
|
-
"#<#{self.class}:0x#{object_id.to_s(16)}>"
|
295
303
|
end
|
296
304
|
end
|
297
305
|
end
|
298
306
|
|
299
307
|
require_relative 'horreum/class_methods'
|
300
308
|
require_relative 'horreum/commands'
|
309
|
+
require_relative 'horreum/connection'
|
301
310
|
require_relative 'horreum/serialization'
|
302
311
|
require_relative 'horreum/settings'
|
303
312
|
require_relative 'horreum/utils'
|
data/lib/familia/logging.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/logging.rb
|
2
2
|
|
3
3
|
require 'pathname'
|
4
4
|
require 'logger'
|
@@ -126,7 +126,7 @@ module Familia
|
|
126
126
|
#
|
127
127
|
# @param label [Symbol] A label for the trace message (e.g., :EXPAND,
|
128
128
|
# :FROMREDIS, :LOAD, :EXISTS).
|
129
|
-
# @param
|
129
|
+
# @param dbclient [Redis, Redis::Future, nil] The Database instance or
|
130
130
|
# Future being used.
|
131
131
|
# @param ident [String] An identifier or key related to the operation being
|
132
132
|
# traced.
|
@@ -134,31 +134,31 @@ module Familia
|
|
134
134
|
# obtained from `caller` or `caller.first`. Default is nil.
|
135
135
|
#
|
136
136
|
# @example
|
137
|
-
# Familia.trace :LOAD, Familia.
|
137
|
+
# Familia.trace :LOAD, Familia.dbclient(uri), objkey, caller(1..1) if
|
138
138
|
# Familia.debug?
|
139
139
|
#
|
140
140
|
# @return [nil]
|
141
141
|
#
|
142
142
|
# @note This method only executes if LoggerTraceRefinement::ENABLED is true.
|
143
|
-
# @note The
|
144
|
-
# pipelined and multi blocks), or nil (when the
|
143
|
+
# @note The dbclient can be a Database object, Redis::Future (used in
|
144
|
+
# pipelined and multi blocks), or nil (when the database connection isn't
|
145
145
|
# relevant).
|
146
146
|
#
|
147
|
-
def trace(label,
|
147
|
+
def trace(label, dbclient, ident, context = nil)
|
148
148
|
return unless LoggerTraceRefinement::ENABLED
|
149
149
|
|
150
|
-
# Usually
|
150
|
+
# Usually dbclient is a Database object, but it could be
|
151
151
|
# a Redis::Future which is what is used inside of pipelined
|
152
152
|
# and multi blocks. In some contexts it's nil where the
|
153
|
-
#
|
154
|
-
instance_id = if
|
155
|
-
case
|
153
|
+
# database connection isn't relevant.
|
154
|
+
instance_id = if dbclient
|
155
|
+
case dbclient
|
156
156
|
when Redis
|
157
|
-
|
157
|
+
dbclient.id.respond_to?(:to_s) ? dbclient.id.to_s : dbclient.class.name
|
158
158
|
when Redis::Future
|
159
159
|
"Redis::Future"
|
160
160
|
else
|
161
|
-
|
161
|
+
dbclient.class.name
|
162
162
|
end
|
163
163
|
end
|
164
164
|
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# lib/familia/multi_result.rb
|
2
|
+
|
3
|
+
# The magical MultiResult, keeper of Redis's deepest secrets!
|
4
|
+
#
|
5
|
+
# This quirky little class wraps up the outcome of a Database "transaction"
|
6
|
+
# (or as I like to call it, a "Database dance party") with a bow made of
|
7
|
+
# pure Ruby delight. It knows if your commands were successful and
|
8
|
+
# keeps the results safe in its pocket dimension.
|
9
|
+
#
|
10
|
+
# @attr_reader success [Boolean] The golden ticket! True if all your
|
11
|
+
# Database wishes came true in the transaction.
|
12
|
+
# @attr_reader results [Array<String>] A mystical array of return values,
|
13
|
+
# each one a whisper from the Database gods.
|
14
|
+
#
|
15
|
+
# @example Summoning a MultiResult from the void
|
16
|
+
# result = MultiResult.new(true, ["OK", "OK"])
|
17
|
+
#
|
18
|
+
# @example Divining the success of your Database ritual
|
19
|
+
# if result.successful?
|
20
|
+
# puts "Huzzah! The Database spirits smile upon you!"
|
21
|
+
# else
|
22
|
+
# puts "Alas! The Database gremlins have conspired against us!"
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @example Peering into the raw essence of results
|
26
|
+
# result.results.each_with_index do |value, index|
|
27
|
+
# puts "Command #{index + 1} whispered back: #{value}"
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
class MultiResult
|
31
|
+
# @return [Boolean] true if all commands in the transaction succeeded,
|
32
|
+
# false otherwise
|
33
|
+
attr_reader :success
|
34
|
+
|
35
|
+
# @return [Array<String>] The raw return values from the Database commands
|
36
|
+
attr_reader :results
|
37
|
+
|
38
|
+
# Creates a new MultiResult instance.
|
39
|
+
#
|
40
|
+
# @param success [Boolean] Whether all commands succeeded
|
41
|
+
# @param results [Array<String>] The raw results from Database commands
|
42
|
+
def initialize(success, results)
|
43
|
+
@success = success
|
44
|
+
@results = results
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a tuple representing the result of the transaction.
|
48
|
+
#
|
49
|
+
# @return [Array] A tuple containing the success status and the raw results.
|
50
|
+
# The success status is a boolean indicating if all commands succeeded.
|
51
|
+
# The raw results is an array of return values from the Database commands.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# [true, ["OK", true, 1]]
|
55
|
+
#
|
56
|
+
def tuple
|
57
|
+
[successful?, results]
|
58
|
+
end
|
59
|
+
alias to_a tuple
|
60
|
+
|
61
|
+
def to_h
|
62
|
+
{ success: successful?, results: results }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Convenient method to check if the commit was successful.
|
66
|
+
#
|
67
|
+
# @return [Boolean] true if all commands succeeded, false otherwise
|
68
|
+
def successful?
|
69
|
+
@success
|
70
|
+
end
|
71
|
+
alias success? successful?
|
72
|
+
end
|
data/lib/familia/refinements.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/refinements.rb
|
2
2
|
|
3
3
|
require 'pathname'
|
4
4
|
require 'logger'
|
@@ -6,44 +6,6 @@ require 'logger'
|
|
6
6
|
# Controls whether tracing is enabled via an environment variable
|
7
7
|
FAMILIA_TRACE = ENV.fetch('FAMILIA_TRACE', 'false').downcase
|
8
8
|
|
9
|
-
# FlexibleHashAccess
|
10
|
-
#
|
11
|
-
# This module provides a refinement for the Hash class to allow flexible access
|
12
|
-
# to hash keys using either strings or symbols interchangeably for reading values.
|
13
|
-
#
|
14
|
-
# Note: This refinement only affects reading from the hash. Writing to the hash
|
15
|
-
# maintains the original key type.
|
16
|
-
#
|
17
|
-
# @example Using the refinement
|
18
|
-
# using FlexibleHashAccess
|
19
|
-
#
|
20
|
-
# h = { name: "Alice", "age" => 30 }
|
21
|
-
# h[:name] # => "Alice"
|
22
|
-
# h["name"] # => "Alice"
|
23
|
-
# h[:age] # => 30
|
24
|
-
# h["age"] # => 30
|
25
|
-
#
|
26
|
-
# h["job"] = "Developer"
|
27
|
-
# h[:job] # => "Developer"
|
28
|
-
# h["job"] # => "Developer"
|
29
|
-
#
|
30
|
-
# h[:salary] = 75000
|
31
|
-
# h[:salary] # => 75000
|
32
|
-
# h["salary"] # => nil (original key type is preserved)
|
33
|
-
#
|
34
|
-
module FlexibleHashAccess
|
35
|
-
refine Hash do
|
36
|
-
##
|
37
|
-
# Retrieves a value from the hash using either a string or symbol key.
|
38
|
-
#
|
39
|
-
# @param key [String, Symbol] The key to look up
|
40
|
-
# @return [Object, nil] The value associated with the key, or nil if not found
|
41
|
-
def [](key)
|
42
|
-
super(key.to_s) || super(key.to_sym)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
9
|
# LoggerTraceRefinement
|
48
10
|
#
|
49
11
|
# This module adds a 'trace' log level to the Ruby Logger class.
|
@@ -62,11 +24,12 @@ end
|
|
62
24
|
# logger.trace("This is a trace message")
|
63
25
|
#
|
64
26
|
module LoggerTraceRefinement
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
27
|
+
unless defined?(ENABLED)
|
28
|
+
# Indicates whether trace logging is enabled
|
29
|
+
ENABLED = %w[1 true yes].include?(FAMILIA_TRACE).freeze
|
30
|
+
# The numeric level for trace logging (same as DEBUG)
|
31
|
+
TRACE = 0
|
32
|
+
end
|
70
33
|
|
71
34
|
refine Logger do
|
72
35
|
##
|
data/lib/familia/settings.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/settings.rb
|
2
2
|
|
3
3
|
module Familia
|
4
4
|
|
5
5
|
@delim = ':'
|
6
6
|
@prefix = nil
|
7
7
|
@suffix = :object
|
8
|
-
@
|
9
|
-
@
|
8
|
+
@default_expiration = 0 # see update_expiration. Zero is skip. nil is an exception.
|
9
|
+
@logical_database = nil
|
10
10
|
|
11
11
|
module Settings
|
12
12
|
|
13
|
-
attr_writer :delim, :suffix, :
|
13
|
+
attr_writer :delim, :suffix, :default_expiration, :logical_database, :prefix
|
14
14
|
|
15
15
|
def delim(val = nil)
|
16
16
|
@delim = val if val
|
@@ -27,14 +27,15 @@ module Familia
|
|
27
27
|
@suffix
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
31
|
-
@
|
32
|
-
@
|
30
|
+
def default_expiration(v = nil)
|
31
|
+
@default_expiration = v unless v.nil?
|
32
|
+
@default_expiration
|
33
33
|
end
|
34
34
|
|
35
|
-
def
|
36
|
-
@
|
37
|
-
@
|
35
|
+
def logical_database(v = nil)
|
36
|
+
Familia.trace :DB, dbclient, "#{@logical_database} #{v}", caller(1..1) if Familia.debug?
|
37
|
+
@logical_database = v unless v.nil?
|
38
|
+
@logical_database
|
38
39
|
end
|
39
40
|
|
40
41
|
# We define this do-nothing method because it reads better
|
@@ -44,5 +45,4 @@ module Familia
|
|
44
45
|
end
|
45
46
|
|
46
47
|
end
|
47
|
-
|
48
48
|
end
|