familia 2.0.0.pre18 → 2.0.0.pre19
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/CHANGELOG.rst +58 -6
- data/CLAUDE.md +34 -9
- data/Gemfile +2 -2
- data/Gemfile.lock +9 -47
- data/README.md +39 -0
- data/changelog.d/20251011_012003_delano_159_datatype_transaction_pipeline_support.rst +91 -0
- data/changelog.d/20251011_203905_delano_next.rst +30 -0
- data/changelog.d/20251011_212633_delano_next.rst +13 -0
- data/changelog.d/20251011_221253_delano_next.rst +26 -0
- data/docs/guides/feature-expiration.md +18 -18
- data/docs/migrating/v2.0.0-pre19.md +197 -0
- data/examples/datatype_standalone.rb +281 -0
- data/lib/familia/connection/behavior.rb +252 -0
- data/lib/familia/connection/handlers.rb +95 -0
- data/lib/familia/connection/operation_core.rb +1 -1
- data/lib/familia/connection/{pipeline_core.rb → pipelined_core.rb} +2 -2
- data/lib/familia/connection/transaction_core.rb +7 -9
- data/lib/familia/connection.rb +3 -2
- data/lib/familia/data_type/connection.rb +151 -7
- data/lib/familia/data_type/database_commands.rb +7 -4
- data/lib/familia/data_type/serialization.rb +4 -0
- data/lib/familia/data_type/types/hashkey.rb +1 -1
- data/lib/familia/errors.rb +51 -14
- data/lib/familia/features/expiration/extensions.rb +8 -10
- data/lib/familia/features/expiration.rb +19 -19
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +39 -38
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +115 -43
- data/lib/familia/features/relationships/indexing.rb +37 -42
- data/lib/familia/features/relationships/indexing_relationship.rb +14 -4
- data/lib/familia/field_type.rb +2 -1
- data/lib/familia/horreum/connection.rb +11 -35
- data/lib/familia/horreum/database_commands.rb +129 -10
- data/lib/familia/horreum/definition.rb +2 -1
- data/lib/familia/horreum/management.rb +21 -15
- data/lib/familia/horreum/persistence.rb +190 -66
- data/lib/familia/horreum/serialization.rb +3 -0
- data/lib/familia/horreum/utils.rb +0 -8
- data/lib/familia/horreum.rb +31 -12
- data/lib/familia/logging.rb +2 -5
- data/lib/familia/settings.rb +7 -7
- data/lib/familia/version.rb +1 -1
- data/lib/middleware/database_logger.rb +76 -5
- data/try/edge_cases/string_coercion_try.rb +4 -4
- data/try/features/expiration/expiration_try.rb +1 -1
- data/try/features/relationships/indexing_try.rb +28 -4
- data/try/features/relationships/relationships_api_changes_try.rb +4 -4
- data/try/integration/connection/fiber_context_preservation_try.rb +3 -3
- data/try/integration/connection/operation_mode_guards_try.rb +1 -1
- data/try/integration/connection/pipeline_fallback_integration_try.rb +12 -12
- data/try/integration/create_method_try.rb +22 -22
- data/try/integration/data_types/datatype_pipelines_try.rb +104 -0
- data/try/integration/data_types/datatype_transactions_try.rb +247 -0
- data/try/integration/models/customer_safe_dump_try.rb +5 -1
- data/try/integration/models/familia_object_try.rb +1 -1
- data/try/integration/persistence_operations_try.rb +162 -10
- data/try/unit/data_types/boolean_try.rb +1 -1
- data/try/unit/data_types/string_try.rb +1 -1
- data/try/unit/horreum/auto_indexing_on_save_try.rb +32 -16
- data/try/unit/horreum/automatic_index_validation_try.rb +253 -0
- data/try/unit/horreum/base_try.rb +1 -1
- data/try/unit/horreum/class_methods_try.rb +2 -2
- data/try/unit/horreum/initialization_try.rb +1 -1
- data/try/unit/horreum/relations_try.rb +4 -4
- data/try/unit/horreum/serialization_try.rb +2 -2
- data/try/unit/horreum/unique_index_edge_cases_try.rb +376 -0
- data/try/unit/horreum/unique_index_guard_validation_try.rb +281 -0
- metadata +14 -2
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/familia/connection/behavior.rb
|
4
|
+
|
5
|
+
module Familia
|
6
|
+
module Connection
|
7
|
+
# Shared connection behavior for both Horreum and DataType classes
|
8
|
+
#
|
9
|
+
# This module extracts common connection management functionality that was
|
10
|
+
# previously duplicated between Horreum::Connection and DataType::Connection.
|
11
|
+
# It provides:
|
12
|
+
#
|
13
|
+
# * URI normalization with logical_database support
|
14
|
+
# * Connection creation methods
|
15
|
+
# * Transaction and pipeline execution methods
|
16
|
+
# * Consistent connection API across object types
|
17
|
+
#
|
18
|
+
# Classes including this module must implement:
|
19
|
+
# * `dbclient(uri = nil)` - Connection resolution method
|
20
|
+
# * `build_connection_chain` (private) - Chain of Responsibility setup
|
21
|
+
#
|
22
|
+
# @example Basic usage in a class
|
23
|
+
# class MyDataStore
|
24
|
+
# include Familia::Connection::Behavior
|
25
|
+
#
|
26
|
+
# def dbclient(uri = nil)
|
27
|
+
# @connection_chain ||= build_connection_chain
|
28
|
+
# @connection_chain.handle(uri)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# private
|
32
|
+
#
|
33
|
+
# def build_connection_chain
|
34
|
+
# # ... handler setup ...
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
module Behavior
|
39
|
+
def self.included(base)
|
40
|
+
base.class_eval do
|
41
|
+
attr_writer :dbclient
|
42
|
+
attr_reader :uri
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Normalizes various URI formats to a consistent URI object
|
47
|
+
#
|
48
|
+
# Handles multiple input types and considers the logical_database setting
|
49
|
+
# when uri is nil or Integer. This method is public so connection handlers
|
50
|
+
# can use it for consistent URI processing.
|
51
|
+
#
|
52
|
+
# @param uri [Integer, String, URI, nil] The URI to normalize
|
53
|
+
# @return [URI] Normalized URI object
|
54
|
+
# @raise [ArgumentError] If URI type is invalid
|
55
|
+
#
|
56
|
+
# @example Integer database number
|
57
|
+
# normalize_uri(2) # => URI with db=2 on default server
|
58
|
+
#
|
59
|
+
# @example String URI
|
60
|
+
# normalize_uri('redis://localhost:6379/1')
|
61
|
+
#
|
62
|
+
# @example nil with logical_database
|
63
|
+
# class MyModel
|
64
|
+
# include Familia::Connection::Behavior
|
65
|
+
# attr_accessor :logical_database
|
66
|
+
# end
|
67
|
+
# model = MyModel.new
|
68
|
+
# model.logical_database = 3
|
69
|
+
# model.normalize_uri(nil) # => URI with db=3
|
70
|
+
#
|
71
|
+
def normalize_uri(uri)
|
72
|
+
case uri
|
73
|
+
when Integer
|
74
|
+
new_uri = Familia.uri.dup
|
75
|
+
new_uri.db = uri
|
76
|
+
new_uri
|
77
|
+
when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
|
78
|
+
URI.parse(uri)
|
79
|
+
when URI
|
80
|
+
uri
|
81
|
+
when nil
|
82
|
+
# Use logical_database if available, otherwise fall back to Familia.uri
|
83
|
+
if respond_to?(:logical_database) && logical_database
|
84
|
+
new_uri = Familia.uri.dup
|
85
|
+
new_uri.db = logical_database
|
86
|
+
new_uri
|
87
|
+
else
|
88
|
+
Familia.uri
|
89
|
+
end
|
90
|
+
else
|
91
|
+
raise ArgumentError, "Invalid URI type: #{uri.class.name}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Creates a new Database connection instance
|
96
|
+
#
|
97
|
+
# This method always creates a fresh connection and does not use caching.
|
98
|
+
# Each call returns a new Redis client instance that you are responsible
|
99
|
+
# for managing and closing when done.
|
100
|
+
#
|
101
|
+
# @param uri [String, URI, Integer, nil] The URI of the Database server
|
102
|
+
# @return [Redis] A new Database client connection
|
103
|
+
#
|
104
|
+
# @example Creating a new connection
|
105
|
+
# client = create_dbclient('redis://localhost:6379/1')
|
106
|
+
# client.ping
|
107
|
+
# client.close
|
108
|
+
#
|
109
|
+
def create_dbclient(uri = nil)
|
110
|
+
parsed_uri = normalize_uri(uri)
|
111
|
+
Familia.create_dbclient(parsed_uri)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Alias for create_dbclient (backward compatibility)
|
115
|
+
def connect(*)
|
116
|
+
create_dbclient(*)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Sets the URI for this object's database connection
|
120
|
+
#
|
121
|
+
# @param uri [String, URI, Integer] The new URI
|
122
|
+
# @return [URI] The normalized URI
|
123
|
+
#
|
124
|
+
def uri=(uri)
|
125
|
+
@uri = normalize_uri(uri)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Alias for uri (backward compatibility)
|
129
|
+
def url
|
130
|
+
uri
|
131
|
+
end
|
132
|
+
|
133
|
+
# Alias for uri= (backward compatibility)
|
134
|
+
def url=(uri)
|
135
|
+
self.uri = uri
|
136
|
+
end
|
137
|
+
|
138
|
+
# Executes a Redis transaction (MULTI/EXEC) using this object's connection context
|
139
|
+
#
|
140
|
+
# Provides atomic execution of multiple Redis commands with automatic connection
|
141
|
+
# management and operation mode enforcement. Uses the object's database and
|
142
|
+
# connection settings. Returns a MultiResult object for consistency.
|
143
|
+
#
|
144
|
+
# @yield [Redis] conn The Redis connection configured for transaction mode
|
145
|
+
# @return [MultiResult] Result object with success status and command results
|
146
|
+
#
|
147
|
+
# @raise [Familia::OperationModeError] When called with incompatible connection handlers
|
148
|
+
#
|
149
|
+
# @example Basic transaction
|
150
|
+
# obj.transaction do |conn|
|
151
|
+
# conn.set('key1', 'value1')
|
152
|
+
# conn.set('key2', 'value2')
|
153
|
+
# conn.get('key1')
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# @example Reentrant behavior
|
157
|
+
# obj.transaction do |conn|
|
158
|
+
# conn.set('outer', 'value')
|
159
|
+
#
|
160
|
+
# # Nested transaction reuses same connection
|
161
|
+
# obj.transaction do |inner_conn|
|
162
|
+
# inner_conn.set('inner', 'value')
|
163
|
+
# end
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# @note Connection Inheritance:
|
167
|
+
# - Uses object's logical_database setting if configured
|
168
|
+
# - Inherits class-level database settings
|
169
|
+
# - Falls back to instance-level dbclient if set
|
170
|
+
# - Uses global connection chain as final fallback
|
171
|
+
#
|
172
|
+
# @note Transaction Context:
|
173
|
+
# - When called outside global transaction: Creates local MultiResult
|
174
|
+
# - When called inside global transaction: Yields to existing transaction
|
175
|
+
# - Maintains proper Fiber-local state for nested calls
|
176
|
+
#
|
177
|
+
# @see Familia.transaction For global transaction method
|
178
|
+
# @see MultiResult For details on the return value structure
|
179
|
+
#
|
180
|
+
def transaction(&)
|
181
|
+
ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
|
182
|
+
Familia::Connection::TransactionCore.execute_transaction(-> { dbclient }, &)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Alias for transaction (alternate naming)
|
186
|
+
def multi(&)
|
187
|
+
transaction(&)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Executes Redis commands in a pipeline using this object's connection context
|
191
|
+
#
|
192
|
+
# Batches multiple Redis commands together and sends them in a single network
|
193
|
+
# round-trip for improved performance. Uses the object's database and connection
|
194
|
+
# settings. Returns a MultiResult object for consistency.
|
195
|
+
#
|
196
|
+
# @yield [Redis] conn The Redis connection configured for pipelined mode
|
197
|
+
# @return [MultiResult] Result object with success status and command results
|
198
|
+
#
|
199
|
+
# @raise [Familia::OperationModeError] When called with incompatible connection handlers
|
200
|
+
#
|
201
|
+
# @example Basic pipeline
|
202
|
+
# obj.pipelined do |conn|
|
203
|
+
# conn.set('key1', 'value1')
|
204
|
+
# conn.incr('counter')
|
205
|
+
# conn.get('key1')
|
206
|
+
# end
|
207
|
+
#
|
208
|
+
# @example Performance optimization
|
209
|
+
# # Instead of multiple round-trips:
|
210
|
+
# obj.save # Round-trip 1
|
211
|
+
# obj.increment_count # Round-trip 2
|
212
|
+
# obj.update_timestamp # Round-trip 3
|
213
|
+
#
|
214
|
+
# # Use pipeline for single round-trip:
|
215
|
+
# obj.pipelined do |conn|
|
216
|
+
# conn.hmset(obj.dbkey, obj.to_h)
|
217
|
+
# conn.hincrby(obj.dbkey, 'count', 1)
|
218
|
+
# conn.hset(obj.dbkey, 'updated_at', Time.now.to_i)
|
219
|
+
# end
|
220
|
+
#
|
221
|
+
# @note Connection Inheritance:
|
222
|
+
# - Uses object's logical_database setting if configured
|
223
|
+
# - Inherits class-level database settings
|
224
|
+
# - Falls back to instance-level dbclient if set
|
225
|
+
# - Uses global connection chain as final fallback
|
226
|
+
#
|
227
|
+
# @note Pipeline Context:
|
228
|
+
# - When called outside global pipeline: Creates local MultiResult
|
229
|
+
# - When called inside global pipeline: Yields to existing pipeline
|
230
|
+
# - Maintains proper Fiber-local state for nested calls
|
231
|
+
#
|
232
|
+
# @note Performance Considerations:
|
233
|
+
# - Best for multiple independent operations
|
234
|
+
# - Reduces network latency by batching commands
|
235
|
+
# - Commands execute independently (some may succeed, others fail)
|
236
|
+
#
|
237
|
+
# @see Familia.pipelined For global pipeline method
|
238
|
+
# @see MultiResult For details on the return value structure
|
239
|
+
# @see #transaction For atomic command execution
|
240
|
+
#
|
241
|
+
def pipelined(&block)
|
242
|
+
ensure_relatives_initialized! if respond_to?(:ensure_relatives_initialized!, true)
|
243
|
+
Familia::Connection::PipelineCore.execute_pipeline(-> { dbclient }, &block)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Alias for pipelined (alternate naming)
|
247
|
+
def pipeline(&block)
|
248
|
+
pipelined(&block)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -219,5 +219,100 @@ module Familia
|
|
219
219
|
dbclient
|
220
220
|
end
|
221
221
|
end
|
222
|
+
|
223
|
+
# Handler for delegating connection resolution to parent object
|
224
|
+
#
|
225
|
+
# Used by DataType objects that are attached to a parent (Horreum instance or class).
|
226
|
+
# Delegates the connection resolution to the parent's dbclient method, which allows
|
227
|
+
# DataType objects to inherit connection settings, logical_database, and transaction
|
228
|
+
# context from their parent.
|
229
|
+
#
|
230
|
+
# This preserves the existing architectural pattern where DataType objects owned by
|
231
|
+
# Horreum models use the parent's connection chain. This is the primary behavior
|
232
|
+
# for DataType objects in typical usage.
|
233
|
+
#
|
234
|
+
# @example Instance-level DataType with parent
|
235
|
+
# user = User.new(userid: 'user_123')
|
236
|
+
# user.tags # DataType that delegates to user.dbclient
|
237
|
+
#
|
238
|
+
# @example Class-level DataType with parent
|
239
|
+
# User.global_users # DataType that delegates to User.dbclient
|
240
|
+
#
|
241
|
+
class ParentDelegationHandler < BaseConnectionHandler
|
242
|
+
@allows_transaction = true
|
243
|
+
@allows_pipelined = true
|
244
|
+
|
245
|
+
def initialize(data_type)
|
246
|
+
@data_type = data_type
|
247
|
+
end
|
248
|
+
|
249
|
+
def handle(uri)
|
250
|
+
return nil unless @data_type.parent
|
251
|
+
|
252
|
+
# Delegate to parent's connection chain
|
253
|
+
# Parent can be either a Horreum class or instance
|
254
|
+
parent_connection = @data_type.parent.dbclient(uri)
|
255
|
+
|
256
|
+
if parent_connection
|
257
|
+
Familia.trace :DBCLIENT_PARENT_DELEGATION, @data_type.dbkey,
|
258
|
+
"Using parent connection from #{@data_type.parent.class}"
|
259
|
+
end
|
260
|
+
|
261
|
+
parent_connection
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Handler for standalone DataType objects without a parent
|
266
|
+
#
|
267
|
+
# Provides connection resolution for DataType objects that are created independently
|
268
|
+
# rather than being attached to a Horreum model. Checks for instance-level @dbclient
|
269
|
+
# first, then falls back to creating a connection based on logical_database option
|
270
|
+
# or global Familia connection.
|
271
|
+
#
|
272
|
+
# This enables standalone DataType usage patterns like Rack::Session implementations
|
273
|
+
# where DataType objects need independent connection management and transaction support.
|
274
|
+
#
|
275
|
+
# @example Standalone DataType with custom connection
|
276
|
+
# leaderboard = Familia::SortedSet.new('game:leaderboard')
|
277
|
+
# leaderboard.dbclient = ConnectionPool.new { Redis.new }
|
278
|
+
#
|
279
|
+
# @example Standalone DataType with logical_database option
|
280
|
+
# cache = Familia::HashKey.new('app:cache', logical_database: 2)
|
281
|
+
#
|
282
|
+
class StandaloneConnectionHandler < BaseConnectionHandler
|
283
|
+
@allows_transaction = true
|
284
|
+
@allows_pipelined = true
|
285
|
+
|
286
|
+
def initialize(data_type)
|
287
|
+
@data_type = data_type
|
288
|
+
end
|
289
|
+
|
290
|
+
def handle(uri)
|
291
|
+
# If a specific URI is provided, always use it to get a connection.
|
292
|
+
if uri
|
293
|
+
connection = Familia.dbclient(uri)
|
294
|
+
Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
|
295
|
+
"Created standalone connection for specific URI: #{uri}"
|
296
|
+
return connection
|
297
|
+
end
|
298
|
+
|
299
|
+
# Use instance @dbclient if explicitly set and no URI was passed
|
300
|
+
instance_dbclient = @data_type.instance_variable_get(:@dbclient)
|
301
|
+
if instance_dbclient
|
302
|
+
Familia.trace :DBCLIENT_DATATYPE_INSTANCE, @data_type.dbkey,
|
303
|
+
'Using DataType instance @dbclient'
|
304
|
+
return instance_dbclient
|
305
|
+
end
|
306
|
+
|
307
|
+
# Fall back to creating connection based on opts or global
|
308
|
+
target_uri = @data_type.opts[:logical_database]
|
309
|
+
connection = Familia.dbclient(target_uri)
|
310
|
+
|
311
|
+
Familia.trace :DBCLIENT_STANDALONE_DATATYPE, @data_type.dbkey,
|
312
|
+
"Created standalone connection for #{target_uri || 'default'}"
|
313
|
+
|
314
|
+
connection
|
315
|
+
end
|
316
|
+
end
|
222
317
|
end
|
223
318
|
end
|
@@ -16,7 +16,7 @@ module Familia
|
|
16
16
|
#
|
17
17
|
# Handles pipeline execution based on connection handler capabilities.
|
18
18
|
# When handler doesn't support pipelines, fallback behavior is controlled
|
19
|
-
# by Familia.
|
19
|
+
# by Familia.pipelined_mode setting.
|
20
20
|
#
|
21
21
|
# @param dbclient_proc [Proc] Lambda that returns the Redis connection
|
22
22
|
# @param block [Proc] Block containing Redis commands to execute
|
@@ -32,7 +32,7 @@ module Familia
|
|
32
32
|
# result.results # => ["OK", 1]
|
33
33
|
#
|
34
34
|
# @example With fallback modes
|
35
|
-
# Familia.configure { |c| c.
|
35
|
+
# Familia.configure { |c| c.pipelined_mode = :permissive }
|
36
36
|
# result = PipelineCore.execute_pipeline(-> { cached_conn }) do |conn|
|
37
37
|
# conn.set('key', 'value') # Executes individually, no error
|
38
38
|
# end
|
@@ -34,27 +34,25 @@ module Familia
|
|
34
34
|
# result.successful? # => true/false
|
35
35
|
# result.results # => ["OK", 1]
|
36
36
|
#
|
37
|
-
def self.execute_transaction(dbclient_proc, &
|
37
|
+
def self.execute_transaction(dbclient_proc, &)
|
38
38
|
# First, get the connection to populate the handler class
|
39
|
-
|
39
|
+
dbclient_proc.call
|
40
40
|
handler_class = Fiber[:familia_connection_handler_class]
|
41
41
|
|
42
42
|
# Check transaction capability
|
43
43
|
transaction_capability = handler_class&.allows_transaction
|
44
44
|
|
45
45
|
if transaction_capability == false
|
46
|
-
handle_transaction_fallback(dbclient_proc, handler_class, &
|
46
|
+
handle_transaction_fallback(dbclient_proc, handler_class, &)
|
47
47
|
elsif transaction_capability == :reentrant
|
48
48
|
# Already in transaction, just yield the connection
|
49
49
|
yield(Fiber[:familia_transaction])
|
50
50
|
else
|
51
51
|
# Normal transaction flow (includes nil, true, and other values)
|
52
|
-
execute_normal_transaction(dbclient_proc, &
|
52
|
+
execute_normal_transaction(dbclient_proc, &)
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
-
private
|
57
|
-
|
58
56
|
# Handles transaction fallback based on configured transaction mode
|
59
57
|
#
|
60
58
|
# Delegates to OperationCore.handle_fallback for consistent behavior
|
@@ -65,8 +63,8 @@ module Familia
|
|
65
63
|
# @param block [Proc] Block containing Redis commands to execute
|
66
64
|
# @return [MultiResult] Result from individual command execution or raises error
|
67
65
|
#
|
68
|
-
def self.handle_transaction_fallback(dbclient_proc, handler_class, &
|
69
|
-
OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &
|
66
|
+
def self.handle_transaction_fallback(dbclient_proc, handler_class, &)
|
67
|
+
OperationCore.handle_fallback(:transaction, dbclient_proc, handler_class, &)
|
70
68
|
end
|
71
69
|
|
72
70
|
# Executes a normal Redis transaction using MULTI/EXEC
|
@@ -78,7 +76,7 @@ module Familia
|
|
78
76
|
# @param block [Proc] Block containing Redis commands to execute
|
79
77
|
# @return [MultiResult] Result object with transaction command results
|
80
78
|
#
|
81
|
-
def self.execute_normal_transaction(dbclient_proc
|
79
|
+
def self.execute_normal_transaction(dbclient_proc)
|
82
80
|
# Check for existing transaction context
|
83
81
|
return yield(Fiber[:familia_transaction]) if Fiber[:familia_transaction]
|
84
82
|
|
data/lib/familia/connection.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# lib/familia/connection.rb
|
2
2
|
|
3
|
+
require_relative 'connection/behavior'
|
3
4
|
require_relative 'connection/handlers'
|
4
5
|
require_relative 'connection/middleware'
|
5
6
|
require_relative 'connection/operations'
|
6
7
|
require_relative 'connection/individual_command_proxy'
|
7
8
|
require_relative 'connection/operation_core'
|
8
9
|
require_relative 'connection/transaction_core'
|
9
|
-
require_relative 'connection/
|
10
|
+
require_relative 'connection/pipelined_core'
|
10
11
|
|
11
12
|
# Familia
|
12
13
|
#
|
@@ -42,7 +43,7 @@ module Familia
|
|
42
43
|
@connection_chain = nil # Force rebuild of chain
|
43
44
|
end
|
44
45
|
|
45
|
-
# Sets the default URI for Database connections.
|
46
|
+
# Sets the default URI for Database connections.
|
46
47
|
#
|
47
48
|
# NOTE: uri is not a property of the Settings module b/c it's not
|
48
49
|
# configured in class defintions like default_expiration or logical DB index.
|
@@ -5,21 +5,104 @@ module Familia
|
|
5
5
|
# Connection - Instance-level connection and key generation methods
|
6
6
|
#
|
7
7
|
# This module provides instance methods for database connection resolution
|
8
|
-
# and Redis key generation for DataType objects.
|
8
|
+
# and Redis key generation for DataType objects. It includes shared connection
|
9
|
+
# behavior from Familia::Connection::Behavior, enabling transaction and pipeline
|
10
|
+
# support for both parent-owned and standalone DataType objects.
|
9
11
|
#
|
10
12
|
# Key features:
|
11
13
|
# * Database connection resolution with Chain of Responsibility pattern
|
12
14
|
# * Redis key generation based on parent context
|
13
15
|
# * Direct database access for advanced operations
|
16
|
+
# * Transaction support (MULTI/EXEC) for atomic operations
|
17
|
+
# * Pipeline support for batched command execution
|
18
|
+
# * Parent delegation for owned DataType objects
|
19
|
+
# * Standalone connection management for independent DataType objects
|
20
|
+
#
|
21
|
+
# Connection Chain Priority:
|
22
|
+
# 1. FiberTransactionHandler - Active transaction context
|
23
|
+
# 2. FiberConnectionHandler - Fiber-local connections
|
24
|
+
# 3. ProviderConnectionHandler - User-defined connection provider
|
25
|
+
# 4. ParentDelegationHandler - Delegate to parent object (primary for owned DataTypes)
|
26
|
+
# 5. StandaloneConnectionHandler - Independent DataType connection
|
27
|
+
#
|
28
|
+
# @example Parent-owned DataType (automatic delegation)
|
29
|
+
# class User < Familia::Horreum
|
30
|
+
# logical_database 2
|
31
|
+
# zset :scores
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# user = User.new(userid: 'user_123')
|
35
|
+
# user.scores.transaction do |conn|
|
36
|
+
# conn.zadd(user.scores.dbkey, 100, 'level1')
|
37
|
+
# conn.zadd(user.scores.dbkey, 200, 'level2')
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example Standalone DataType with transaction
|
41
|
+
# leaderboard = Familia::SortedSet.new('game:leaderboard')
|
42
|
+
# leaderboard.transaction do |conn|
|
43
|
+
# conn.zadd(leaderboard.dbkey, 500, 'player1')
|
44
|
+
# conn.zadd(leaderboard.dbkey, 600, 'player2')
|
45
|
+
# end
|
14
46
|
#
|
15
47
|
module Connection
|
16
|
-
|
17
|
-
|
48
|
+
include Familia::Connection::Behavior
|
49
|
+
|
50
|
+
# Returns the effective URI this DataType will use for connections
|
51
|
+
#
|
52
|
+
# For parent-owned DataTypes, delegates to parent's URI.
|
53
|
+
# For standalone DataTypes with logical_database option, constructs URI with that database.
|
54
|
+
# For standalone DataTypes without options, returns global Familia.uri.
|
55
|
+
# Explicit @uri assignment (via uri=) takes precedence.
|
56
|
+
#
|
57
|
+
# @return [URI, nil] The URI for database connections
|
58
|
+
#
|
59
|
+
def uri
|
60
|
+
return @uri if defined?(@uri) && @uri
|
61
|
+
return parent.uri if parent && parent.respond_to?(:uri)
|
62
|
+
|
63
|
+
# Check opts[:logical_database] first, then parent's logical_database
|
64
|
+
db_num = opts[:logical_database]
|
65
|
+
db_num ||= parent.logical_database if parent && parent.respond_to?(:logical_database)
|
66
|
+
|
67
|
+
if db_num
|
68
|
+
# Create a new URI with the database number but without custom port
|
69
|
+
# This ensures consistent URI representation (e.g., redis://host/db not redis://host:port/db)
|
70
|
+
base_uri = Familia.uri
|
71
|
+
URI.parse("redis://#{base_uri.host}/#{db_num}")
|
72
|
+
else
|
73
|
+
Familia.uri
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Retrieves a Database connection using the Chain of Responsibility pattern
|
78
|
+
#
|
79
|
+
# Implements connection resolution optimized for DataType usage patterns:
|
80
|
+
# - Fast path check for active transaction context
|
81
|
+
# - Full connection chain for comprehensive resolution
|
82
|
+
# - Parent delegation as primary behavior for owned DataTypes
|
83
|
+
# - Standalone connection handling for independent DataTypes
|
84
|
+
#
|
85
|
+
# Note: We don't cache the connection chain in an instance variable because
|
86
|
+
# DataType objects are frozen for thread safety. Building the chain is cheap
|
87
|
+
# (just creating handler objects), and the actual connection resolution work
|
88
|
+
# is done by the handlers themselves.
|
89
|
+
#
|
90
|
+
# @param uri [String, URI, Integer, nil] Optional URI for database selection
|
91
|
+
# @return [Redis] The Database client for the specified URI
|
92
|
+
#
|
93
|
+
# @example Getting connection from parent-owned DataType
|
94
|
+
# user.tags.dbclient # Delegates to user.dbclient
|
95
|
+
#
|
96
|
+
# @example Getting connection from standalone DataType
|
97
|
+
# cache = Familia::HashKey.new('app:cache', logical_database: 2)
|
98
|
+
# cache.dbclient # Uses standalone handler with db 2
|
99
|
+
#
|
100
|
+
def dbclient(uri = nil)
|
101
|
+
# Fast path for transaction context (highest priority)
|
18
102
|
return Fiber[:familia_transaction] if Fiber[:familia_transaction]
|
19
|
-
return @dbclient if @dbclient
|
20
103
|
|
21
|
-
#
|
22
|
-
|
104
|
+
# Build connection chain (not cached due to frozen objects)
|
105
|
+
build_connection_chain.handle(uri)
|
23
106
|
end
|
24
107
|
|
25
108
|
# Produces the full dbkey for this object.
|
@@ -75,8 +158,69 @@ module Familia
|
|
75
158
|
# Provides a structured way to "gear down" to run db commands that are
|
76
159
|
# not implemented in our DataType classes since we intentionally don't
|
77
160
|
# have a method_missing method.
|
161
|
+
#
|
162
|
+
# Enhanced to work seamlessly with transactions and pipelines. When called
|
163
|
+
# within a transaction or pipeline context, uses that connection automatically.
|
164
|
+
#
|
165
|
+
# @yield [Redis, String] Yields the connection and dbkey to the block
|
166
|
+
# @return The return value of the block
|
167
|
+
#
|
168
|
+
# @example Basic usage
|
169
|
+
# datatype.direct_access do |conn, key|
|
170
|
+
# conn.zadd(key, 100, 'member')
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# @example Within transaction (automatic context detection)
|
174
|
+
# datatype.transaction do |trans_conn|
|
175
|
+
# datatype.direct_access do |conn, key|
|
176
|
+
# # conn is the same as trans_conn
|
177
|
+
# conn.zadd(key, 200, 'member')
|
178
|
+
# end
|
179
|
+
# end
|
180
|
+
#
|
78
181
|
def direct_access
|
79
|
-
|
182
|
+
if Fiber[:familia_transaction]
|
183
|
+
# Already in transaction, use that connection
|
184
|
+
yield(Fiber[:familia_transaction], dbkey)
|
185
|
+
elsif Fiber[:familia_pipeline]
|
186
|
+
# Already in pipeline, use that connection
|
187
|
+
yield(Fiber[:familia_pipeline], dbkey)
|
188
|
+
else
|
189
|
+
yield(dbclient, dbkey)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
# Builds the connection chain with handlers in priority order
|
196
|
+
#
|
197
|
+
# Creates the Chain of Responsibility for connection resolution with
|
198
|
+
# DataType-specific handlers. Handlers are checked in order:
|
199
|
+
#
|
200
|
+
# 1. FiberTransactionHandler - Return active transaction connection
|
201
|
+
# 2. FiberConnectionHandler - Use fiber-local connection
|
202
|
+
# 3. ProviderConnectionHandler - Delegate to connection provider
|
203
|
+
# 4. ParentDelegationHandler - Delegate to parent's connection (primary for owned DataTypes)
|
204
|
+
# 5. StandaloneConnectionHandler - Handle standalone DataTypes
|
205
|
+
#
|
206
|
+
# @return [ResponsibilityChain] Configured connection chain
|
207
|
+
#
|
208
|
+
def build_connection_chain
|
209
|
+
# Create fresh handler instances each time since DataType objects are frozen
|
210
|
+
# The chain itself is cached in @connection_chain, so this only runs once
|
211
|
+
fiber_connection_handler = Familia::Connection::FiberConnectionHandler.new
|
212
|
+
provider_connection_handler = Familia::Connection::ProviderConnectionHandler.new
|
213
|
+
|
214
|
+
# DataType-specific handlers for parent delegation and standalone usage
|
215
|
+
parent_delegation_handler = Familia::Connection::ParentDelegationHandler.new(self)
|
216
|
+
standalone_connection_handler = Familia::Connection::StandaloneConnectionHandler.new(self)
|
217
|
+
|
218
|
+
Familia::Connection::ResponsibilityChain.new
|
219
|
+
.add_handler(Familia::Connection::FiberTransactionHandler.instance)
|
220
|
+
.add_handler(fiber_connection_handler)
|
221
|
+
.add_handler(provider_connection_handler)
|
222
|
+
.add_handler(parent_delegation_handler)
|
223
|
+
.add_handler(standalone_connection_handler)
|
80
224
|
end
|
81
225
|
end
|
82
226
|
end
|
@@ -22,11 +22,14 @@ module Familia
|
|
22
22
|
end
|
23
23
|
|
24
24
|
# Deletes the entire dbkey
|
25
|
-
#
|
25
|
+
#
|
26
|
+
# We return the dbclient.del command's return value instead of a friendly
|
27
|
+
# boolean b/c that logic doesn't work inside of a transaction. The return
|
28
|
+
# value in that case is a Redis::Future which based on the name indicates
|
29
|
+
# that the commend hasn't even run yet.
|
26
30
|
def delete!
|
27
|
-
Familia.trace :DELETE!, nil, uri if Familia.debug?
|
28
|
-
|
29
|
-
ret.positive?
|
31
|
+
Familia.trace :DELETE!, nil, self.class.uri if Familia.debug?
|
32
|
+
dbclient.del dbkey
|
30
33
|
end
|
31
34
|
alias clear delete!
|
32
35
|
|
@@ -117,7 +117,11 @@ module Familia
|
|
117
117
|
# for serialization since everything becomes a string in Valkey.
|
118
118
|
#
|
119
119
|
def deserialize_value(val)
|
120
|
+
# Handle Redis::Future objects during transactions first
|
121
|
+
return val if val.is_a?(Redis::Future)
|
122
|
+
|
120
123
|
return @opts[:default] if val.nil?
|
124
|
+
|
121
125
|
return val unless @opts[:class]
|
122
126
|
|
123
127
|
ret = deserialize_values val
|
@@ -152,7 +152,7 @@ module Familia
|
|
152
152
|
# puts "Oops! Our hash seems to have vanished into the Database void!"
|
153
153
|
# end
|
154
154
|
def refresh!
|
155
|
-
Familia.trace :REFRESH, nil, uri if Familia.debug?
|
155
|
+
Familia.trace :REFRESH, nil, self.class.uri if Familia.debug?
|
156
156
|
raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
|
157
157
|
|
158
158
|
fields = hgetall
|