familia 1.2.1 → 2.0.0.pre2
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 +16 -2
- data/Gemfile.lock +97 -36
- data/README.md +39 -23
- data/bin/irb +3 -0
- data/docs/connection_pooling.md +192 -0
- data/familia.gemspec +10 -6
- 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 +18 -13
- 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} +7 -5
- 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} +21 -18
- data/try/models/datatype_base_try.rb +100 -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 +143 -46
- 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/connection.rb
CHANGED
@@ -1,103 +1,270 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/connection.rb
|
2
2
|
|
3
|
-
require_relative '../../lib/
|
3
|
+
require_relative '../../lib/middleware/database_middleware'
|
4
|
+
require_relative 'multi_result'
|
4
5
|
|
6
|
+
# Familia
|
7
|
+
#
|
8
|
+
# A family warehouse for your keystore data.
|
5
9
|
#
|
6
10
|
module Familia
|
7
|
-
@uri = URI.parse 'redis://127.0.0.1'
|
8
|
-
@
|
9
|
-
@redis_uri_by_class = {}
|
11
|
+
@uri = URI.parse 'redis://127.0.0.1:6379'
|
12
|
+
@database_clients = {}
|
10
13
|
|
11
|
-
# The Connection module provides
|
12
|
-
# It allows easy setup and access to
|
14
|
+
# The Connection module provides Database connection management for Familia.
|
15
|
+
# It allows easy setup and access to Database clients across different URIs
|
16
|
+
# with robust connection pooling for thread safety.
|
13
17
|
module Connection
|
14
|
-
# @return [
|
15
|
-
attr_reader :redis_clients
|
16
|
-
|
17
|
-
# @return [URI] The default URI for Redis connections
|
18
|
+
# @return [URI] The default URI for Database connections
|
18
19
|
attr_reader :uri
|
19
20
|
|
20
|
-
# @return [
|
21
|
-
|
21
|
+
# @return [Hash] A hash of Database clients, keyed by server ID
|
22
|
+
attr_reader :database_clients
|
23
|
+
|
24
|
+
# @return [Boolean] Whether Database command logging is enabled
|
25
|
+
attr_accessor :enable_database_logging
|
26
|
+
|
27
|
+
# @return [Boolean] Whether Database command counter is enabled
|
28
|
+
attr_accessor :enable_database_counter
|
22
29
|
|
23
|
-
# @return [
|
24
|
-
attr_accessor :
|
30
|
+
# @return [Proc] A callable that provides Database connections
|
31
|
+
attr_accessor :connection_provider
|
32
|
+
|
33
|
+
# @return [Boolean] Whether to require external connections (no fallback)
|
34
|
+
attr_accessor :connection_required
|
35
|
+
|
36
|
+
# Sets the default URI for Database connections.
|
37
|
+
#
|
38
|
+
# NOTE: uri is not a property of the Settings module b/c it's not
|
39
|
+
# configured in class defintions like default_expiration or logical DB index.
|
40
|
+
#
|
41
|
+
# @param v [String, URI] The new default URI
|
42
|
+
# @example
|
43
|
+
# Familia.uri = 'redis://localhost:6379'
|
44
|
+
def uri=(uri)
|
45
|
+
@uri = normalize_uri(uri)
|
46
|
+
end
|
47
|
+
alias url uri
|
48
|
+
alias url= uri=
|
25
49
|
|
26
|
-
# Establishes a connection to a
|
50
|
+
# Establishes a connection to a Database server.
|
27
51
|
#
|
28
|
-
# @param uri [String, URI, nil] The URI of the
|
29
|
-
# If nil, uses the default URI from `@
|
30
|
-
# @return [Redis] The connected
|
52
|
+
# @param uri [String, URI, nil] The URI of the Database server to connect to.
|
53
|
+
# If nil, uses the default URI from `@database_clients` or `Familia.uri`.
|
54
|
+
# @return [Redis] The connected Database client.
|
31
55
|
# @raise [ArgumentError] If no URI is specified.
|
32
56
|
# @example
|
33
57
|
# Familia.connect('redis://localhost:6379')
|
34
58
|
def connect(uri = nil)
|
35
|
-
|
36
|
-
|
37
|
-
uri ||= Familia.uri
|
59
|
+
parsed_uri = normalize_uri(uri)
|
60
|
+
serverid = parsed_uri.serverid
|
38
61
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
62
|
+
if Familia.enable_database_logging
|
63
|
+
DatabaseLogger.logger = Familia.logger
|
64
|
+
RedisClient.register(DatabaseLogger)
|
65
|
+
end
|
43
66
|
|
44
|
-
if Familia.
|
45
|
-
|
46
|
-
|
67
|
+
if Familia.enable_database_counter
|
68
|
+
# NOTE: This middleware uses AtommicFixnum from concurrent-ruby which is
|
69
|
+
# less contentious than Mutex-based counters. Safe for
|
70
|
+
RedisClient.register(DatabaseCommandCounter)
|
47
71
|
end
|
48
72
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
73
|
+
dbclient = Redis.new(parsed_uri.conf)
|
74
|
+
|
75
|
+
if @database_clients.key?(serverid)
|
76
|
+
msg = "Overriding existing connection for #{serverid}"
|
77
|
+
Familia.warn(msg)
|
53
78
|
end
|
54
79
|
|
55
|
-
|
80
|
+
@database_clients[serverid] = dbclient
|
81
|
+
end
|
82
|
+
|
83
|
+
def reconnect(uri = nil)
|
84
|
+
parsed_uri = normalize_uri(uri)
|
85
|
+
serverid = parsed_uri.serverid
|
56
86
|
|
57
87
|
# Close the existing connection if it exists
|
58
|
-
@
|
59
|
-
|
88
|
+
@database_clients[serverid].close if @database_clients.key?(serverid)
|
89
|
+
|
90
|
+
connect(parsed_uri)
|
60
91
|
end
|
61
92
|
|
62
|
-
# Retrieves
|
93
|
+
# Retrieves a Database connection from the appropriate pool.
|
94
|
+
# Handles DB selection automatically based on the URI.
|
63
95
|
#
|
64
|
-
# @
|
65
|
-
# If nil, uses the default URI.
|
66
|
-
# @return [Redis] The Redis client for the specified URI
|
96
|
+
# @return [Redis] The Database client for the specified URI
|
67
97
|
# @example
|
68
|
-
# Familia.
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
98
|
+
# Familia.dbclient('redis://localhost:6379/1')
|
99
|
+
# Familia.dbclient(2) # Use DB 2 with default server
|
100
|
+
def dbclient(uri = nil)
|
101
|
+
# First priority: Thread-local connection (middleware pattern)
|
102
|
+
return Thread.current[:familia_connection] if Thread.current.key?(:familia_connection)
|
103
|
+
|
104
|
+
# Second priority: Connection provider
|
105
|
+
if connection_provider
|
106
|
+
# Always pass normalized URI with database to provider
|
107
|
+
# Provider MUST return connection already on the correct database
|
108
|
+
parsed_uri = normalize_uri(uri)
|
109
|
+
connection = connection_provider.call(parsed_uri.to_s)
|
110
|
+
|
111
|
+
# In debug mode, verify the provider honored the contract
|
112
|
+
if Familia.debug? && connection.respond_to?(:client)
|
113
|
+
current_db = connection.client.db
|
114
|
+
expected_db = parsed_uri.db || 0
|
115
|
+
if current_db != expected_db
|
116
|
+
Familia.warn "Connection provider returned connection on DB #{current_db}, expected #{expected_db}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
return connection
|
121
|
+
end
|
122
|
+
|
123
|
+
# Third priority: Fallback behavior or error
|
124
|
+
raise Familia::NoConnectionAvailable, 'No connection available.' if connection_required
|
125
|
+
|
126
|
+
# Legacy behavior: create connection
|
127
|
+
parsed_uri = normalize_uri(uri)
|
128
|
+
|
129
|
+
# Only cache when no specific URI/DB is requested to avoid DB conflicts
|
130
|
+
if uri.nil?
|
131
|
+
@dbclient ||= connect(parsed_uri)
|
132
|
+
@dbclient.select(parsed_uri.db) if parsed_uri.db
|
133
|
+
@dbclient
|
134
|
+
else
|
135
|
+
# When a specific DB is requested, create a new connection
|
136
|
+
# to avoid conflicts with cached connections
|
137
|
+
connection = connect(parsed_uri)
|
138
|
+
connection.select(parsed_uri.db) if parsed_uri.db
|
139
|
+
connection
|
76
140
|
end
|
77
|
-
uri ||= Familia.uri
|
78
|
-
connect(uri) unless @redis_clients[uri.serverid]
|
79
|
-
@redis_clients[uri.serverid]
|
80
141
|
end
|
81
142
|
|
82
|
-
#
|
143
|
+
# Executes Database commands atomically within a transaction (MULTI/EXEC).
|
83
144
|
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
145
|
+
# Database transactions queue commands and execute them atomically as a single unit.
|
146
|
+
# All commands succeed together or all fail together, ensuring data consistency.
|
147
|
+
#
|
148
|
+
# @yield [Redis] The Database transaction connection
|
149
|
+
# @return [Array] Results of all commands executed in the transaction
|
150
|
+
#
|
151
|
+
# @example Basic transaction usage
|
152
|
+
# Familia.transaction do |trans|
|
153
|
+
# trans.set("key1", "value1")
|
154
|
+
# trans.incr("counter")
|
155
|
+
# trans.lpush("list", "item")
|
156
|
+
# end
|
157
|
+
# # Returns: ["OK", 2, 1] - results of all commands
|
158
|
+
#
|
159
|
+
# @note **Comparison of Database batch operations:**
|
160
|
+
#
|
161
|
+
# | Feature | Multi/Exec | Pipeline |
|
162
|
+
# |-----------------|-----------------|-----------------|
|
163
|
+
# | Atomicity | Yes | No |
|
164
|
+
# | Performance | Good | Better |
|
165
|
+
# | Error handling | All-or-nothing | Per-command |
|
166
|
+
# | Use case | Data consistency| Bulk operations |
|
167
|
+
#
|
168
|
+
def transaction(&)
|
169
|
+
block_result = nil
|
170
|
+
result = dbclient.multi do |conn|
|
171
|
+
Fiber[:familia_transaction] = conn
|
172
|
+
begin
|
173
|
+
block_result = yield(conn) # rubocop:disable Lint/UselessAssignment
|
174
|
+
ensure
|
175
|
+
Fiber[:familia_transaction] = nil # cleanup reference
|
176
|
+
end
|
177
|
+
end
|
178
|
+
# Return the multi result which contains the transaction results
|
179
|
+
result
|
89
180
|
end
|
181
|
+
alias multi transaction
|
90
182
|
|
91
|
-
#
|
183
|
+
# Executes Database commands in a pipeline for improved performance.
|
92
184
|
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
|
97
|
-
|
185
|
+
# Pipelines send multiple commands without waiting for individual responses,
|
186
|
+
# reducing network round-trips. Commands execute independently and can
|
187
|
+
# succeed or fail without affecting other commands in the pipeline.
|
188
|
+
#
|
189
|
+
# @yield [Redis] The Database pipeline connection
|
190
|
+
# @return [Array] Results of all commands executed in the pipeline
|
191
|
+
#
|
192
|
+
# @example Basic pipeline usage
|
193
|
+
# Familia.pipeline do |pipe|
|
194
|
+
# pipe.set("key1", "value1")
|
195
|
+
# pipe.incr("counter")
|
196
|
+
# pipe.lpush("list", "item")
|
197
|
+
# end
|
198
|
+
# # Returns: ["OK", 2, 1] - results of all commands
|
199
|
+
#
|
200
|
+
# @example Error handling - commands succeed/fail independently
|
201
|
+
# results = Familia.pipeline do |conn|
|
202
|
+
# conn.set("valid_key", "value") # This will succeed
|
203
|
+
# conn.incr("string_key") # This will fail (wrong type)
|
204
|
+
# conn.set("another_key", "value2") # This will still succeed
|
205
|
+
# end
|
206
|
+
# # Returns: ["OK", Redis::CommandError, "OK"]
|
207
|
+
# # Notice how the error doesn't prevent other commands from executing
|
208
|
+
#
|
209
|
+
# @example Contrast with transaction behavior
|
210
|
+
# results = Familia.transaction do |conn|
|
211
|
+
# conn.set("inventory:item1", 100)
|
212
|
+
# conn.incr("invalid_key") # Fails, rolls back everything
|
213
|
+
# conn.set("inventory:item2", 200) # Won't be applied
|
214
|
+
# end
|
215
|
+
# # Result: neither item1 nor item2 are set due to the error
|
216
|
+
#
|
217
|
+
def pipeline(&)
|
218
|
+
block_result = nil
|
219
|
+
result = dbclient.pipelined do |conn|
|
220
|
+
Fiber[:familia_pipeline] = conn
|
221
|
+
begin
|
222
|
+
block_result = yield(conn) # rubocop:disable Lint/UselessAssignment
|
223
|
+
ensure
|
224
|
+
Fiber[:familia_pipeline] = nil # cleanup reference
|
225
|
+
end
|
226
|
+
end
|
227
|
+
# Return the pipeline result which contains the command results
|
228
|
+
result
|
98
229
|
end
|
99
230
|
|
100
|
-
|
101
|
-
|
231
|
+
# Provides explicit access to a Database connection.
|
232
|
+
#
|
233
|
+
# This method is useful when you need direct access to a connection
|
234
|
+
# for operations not covered by other methods. The connection is
|
235
|
+
# properly managed and returned to the pool (if using connection_provider).
|
236
|
+
#
|
237
|
+
# @yield [Redis] A Database connection
|
238
|
+
# @return The result of the block
|
239
|
+
#
|
240
|
+
# @example Using with_connection for custom operations
|
241
|
+
# Familia.with_connection do |conn|
|
242
|
+
# conn.set("custom_key", "value")
|
243
|
+
# conn.expire("custom_key", 3600)
|
244
|
+
# end
|
245
|
+
#
|
246
|
+
def with_connection(&block)
|
247
|
+
yield dbclient
|
248
|
+
end
|
249
|
+
|
250
|
+
private
|
251
|
+
|
252
|
+
# Normalizes various URI formats to a consistent URI object
|
253
|
+
def normalize_uri(uri)
|
254
|
+
case uri
|
255
|
+
when Integer
|
256
|
+
new_uri = Familia.uri.dup
|
257
|
+
new_uri.db = uri
|
258
|
+
new_uri
|
259
|
+
when ->(obj) { obj.is_a?(String) || obj.instance_of?(::String) }
|
260
|
+
URI.parse(uri)
|
261
|
+
when URI
|
262
|
+
uri
|
263
|
+
when nil
|
264
|
+
Familia.uri
|
265
|
+
else
|
266
|
+
raise ArgumentError, "Invalid URI type: #{uri.class.name}"
|
267
|
+
end
|
268
|
+
end
|
102
269
|
end
|
103
270
|
end
|
data/lib/familia/core_ext.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
# lib/familia/datatype/commands.rb
|
2
|
+
|
3
|
+
class Familia::DataType
|
4
|
+
|
5
|
+
# Must be included in all DataType classes to provide Redis
|
6
|
+
# commands. The class must have a dbkey method.
|
7
|
+
module Commands
|
8
|
+
|
9
|
+
def move(logical_database)
|
10
|
+
dbclient.move dbkey, logical_database
|
11
|
+
end
|
12
|
+
|
13
|
+
def rename(newkey)
|
14
|
+
dbclient.rename dbkey, newkey
|
15
|
+
end
|
16
|
+
|
17
|
+
def renamenx(newkey)
|
18
|
+
dbclient.renamenx dbkey, newkey
|
19
|
+
end
|
20
|
+
|
21
|
+
def type
|
22
|
+
dbclient.type dbkey
|
23
|
+
end
|
24
|
+
|
25
|
+
# Deletes the entire dbkey
|
26
|
+
# @return [Boolean] true if the key was deleted, false otherwise
|
27
|
+
def delete!
|
28
|
+
Familia.trace :DELETE!, dbclient, uri, caller(1..1) if Familia.debug?
|
29
|
+
ret = dbclient.del dbkey
|
30
|
+
ret.positive?
|
31
|
+
end
|
32
|
+
alias clear delete!
|
33
|
+
|
34
|
+
def exists?
|
35
|
+
dbclient.exists(dbkey) && !size.zero?
|
36
|
+
end
|
37
|
+
|
38
|
+
def current_expiration
|
39
|
+
dbclient.ttl dbkey
|
40
|
+
end
|
41
|
+
|
42
|
+
def expire(sec)
|
43
|
+
dbclient.expire dbkey, sec.to_i
|
44
|
+
end
|
45
|
+
|
46
|
+
def expireat(unixtime)
|
47
|
+
dbclient.expireat dbkey, unixtime
|
48
|
+
end
|
49
|
+
|
50
|
+
def persist
|
51
|
+
dbclient.persist dbkey
|
52
|
+
end
|
53
|
+
|
54
|
+
def echo(meth, trace)
|
55
|
+
dbclient.echo "[#{self.class}\##{meth}] #{trace} (#{@opts[:class]}\#)"
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/datatype/serialization.rb
|
2
2
|
|
3
|
-
class Familia::
|
3
|
+
class Familia::DataType
|
4
4
|
|
5
5
|
module Serialization
|
6
6
|
|
@@ -29,7 +29,7 @@ class Familia::RedisType
|
|
29
29
|
def serialize_value(val, strict_values: true)
|
30
30
|
prepared = nil
|
31
31
|
|
32
|
-
Familia.trace :TOREDIS,
|
32
|
+
Familia.trace :TOREDIS, dbclient, "#{val}<#{val.class}|#{opts[:class]}>", caller(1..1) if Familia.debug?
|
33
33
|
|
34
34
|
if opts[:class]
|
35
35
|
prepared = Familia.distinguisher(opts[:class], strict_values: strict_values)
|
@@ -42,12 +42,11 @@ class Familia::RedisType
|
|
42
42
|
Familia.ld " from <#{val.class}> => <#{prepared.class}>"
|
43
43
|
end
|
44
44
|
|
45
|
-
Familia.trace :TOREDIS,
|
45
|
+
Familia.trace :TOREDIS, dbclient, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>", caller(1..1) if Familia.debug?
|
46
46
|
|
47
47
|
Familia.warn "[#{self.class}\#serialize_value] nil returned for #{opts[:class]}\##{name}" if prepared.nil?
|
48
48
|
prepared
|
49
49
|
end
|
50
|
-
alias to_redis serialize_value
|
51
50
|
|
52
51
|
# Deserializes multiple values from Redis, removing nil values.
|
53
52
|
#
|
@@ -63,7 +62,6 @@ class Familia::RedisType
|
|
63
62
|
# expected value.
|
64
63
|
deserialize_values_with_nil(*values).compact
|
65
64
|
end
|
66
|
-
alias from_redis deserialize_values
|
67
65
|
|
68
66
|
# Deserializes multiple values from Redis, preserving nil values.
|
69
67
|
#
|
@@ -97,16 +95,15 @@ class Familia::RedisType
|
|
97
95
|
val
|
98
96
|
rescue StandardError => e
|
99
97
|
Familia.info val
|
100
|
-
Familia.info "Parse error for #{
|
98
|
+
Familia.info "Parse error for #{dbkey} (#{load_method}): #{e.message}"
|
101
99
|
Familia.info e.backtrace
|
102
100
|
nil
|
103
101
|
end
|
104
102
|
|
105
103
|
values
|
106
104
|
end
|
107
|
-
alias from_redis_with_nil deserialize_values_with_nil
|
108
105
|
|
109
|
-
# Deserializes a single value from
|
106
|
+
# Deserializes a single value from the database.
|
110
107
|
#
|
111
108
|
# @param val [String, nil] The value to deserialize.
|
112
109
|
# @return [Object, nil] The deserialized object, the default value if
|
@@ -115,10 +112,10 @@ class Familia::RedisType
|
|
115
112
|
# @note If no class option is specified, the original value is
|
116
113
|
# returned unchanged.
|
117
114
|
#
|
118
|
-
# NOTE: Currently only the
|
115
|
+
# NOTE: Currently only the DataType class uses this method. Horreum
|
119
116
|
# fields are a newer addition and don't support the full range of
|
120
|
-
# deserialization options that
|
121
|
-
# for serialization since everything becomes a string in
|
117
|
+
# deserialization options that DataType supports. It uses serialize_value
|
118
|
+
# for serialization since everything becomes a string in Valkey.
|
122
119
|
#
|
123
120
|
def deserialize_value(val)
|
124
121
|
return @opts[:default] if val.nil?
|
@@ -127,7 +124,6 @@ class Familia::RedisType
|
|
127
124
|
ret = deserialize_values val
|
128
125
|
ret&.first # return the object or nil
|
129
126
|
end
|
130
|
-
alias from_redis deserialize_value
|
131
127
|
end
|
132
128
|
|
133
129
|
end
|
@@ -1,11 +1,11 @@
|
|
1
|
-
#
|
1
|
+
# lib/familia/datatype/types/hashkey.rb
|
2
2
|
|
3
3
|
module Familia
|
4
|
-
class HashKey <
|
4
|
+
class HashKey < DataType
|
5
5
|
# Returns the number of fields in the hash
|
6
6
|
# @return [Integer] number of fields
|
7
7
|
def field_count
|
8
|
-
|
8
|
+
dbclient.hlen dbkey
|
9
9
|
end
|
10
10
|
alias size field_count
|
11
11
|
|
@@ -16,22 +16,22 @@ module Familia
|
|
16
16
|
# +return+ [Integer] Returns 1 if the field is new and added, 0 if the
|
17
17
|
# field already existed and the value was updated.
|
18
18
|
def []=(field, val)
|
19
|
-
ret =
|
19
|
+
ret = dbclient.hset dbkey, field.to_s, serialize_value(val)
|
20
20
|
update_expiration
|
21
21
|
ret
|
22
22
|
rescue TypeError => e
|
23
23
|
Familia.le "[hset]= #{e.message}"
|
24
|
-
Familia.ld "[hset]= #{
|
25
|
-
echo :hset, caller(1..1).first if Familia.debug # logs via echo to
|
24
|
+
Familia.ld "[hset]= #{dbkey} #{field}=#{val}" if Familia.debug
|
25
|
+
echo :hset, caller(1..1).first if Familia.debug # logs via echo to the db and back
|
26
26
|
klass = val.class
|
27
|
-
msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{
|
27
|
+
msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{dbkey}"
|
28
28
|
raise e.class, msg
|
29
29
|
end
|
30
30
|
alias put []=
|
31
31
|
alias store []=
|
32
32
|
|
33
33
|
def [](field)
|
34
|
-
deserialize_value
|
34
|
+
deserialize_value dbclient.hget(dbkey, field.to_s)
|
35
35
|
end
|
36
36
|
alias get []
|
37
37
|
|
@@ -47,22 +47,22 @@ module Familia
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def keys
|
50
|
-
|
50
|
+
dbclient.hkeys dbkey
|
51
51
|
end
|
52
52
|
|
53
53
|
def values
|
54
|
-
|
54
|
+
dbclient.hvals(dbkey).map { |v| deserialize_value v }
|
55
55
|
end
|
56
56
|
|
57
57
|
def hgetall
|
58
|
-
|
58
|
+
dbclient.hgetall(dbkey).each_with_object({}) do |(k,v), ret|
|
59
59
|
ret[k] = deserialize_value v
|
60
60
|
end
|
61
61
|
end
|
62
62
|
alias all hgetall
|
63
63
|
|
64
64
|
def key?(field)
|
65
|
-
|
65
|
+
dbclient.hexists dbkey, field.to_s
|
66
66
|
end
|
67
67
|
alias has_key? key?
|
68
68
|
alias include? key?
|
@@ -72,12 +72,12 @@ module Familia
|
|
72
72
|
# @param field [String] The field to remove
|
73
73
|
# @return [Integer] The number of fields that were removed (0 or 1)
|
74
74
|
def remove_field(field)
|
75
|
-
|
75
|
+
dbclient.hdel dbkey, field.to_s
|
76
76
|
end
|
77
77
|
alias remove remove_field # deprecated
|
78
78
|
|
79
79
|
def increment(field, by = 1)
|
80
|
-
|
80
|
+
dbclient.hincrby(dbkey, field.to_s, by).to_i
|
81
81
|
end
|
82
82
|
alias incr increment
|
83
83
|
alias incrby increment
|
@@ -93,7 +93,7 @@ module Familia
|
|
93
93
|
|
94
94
|
data = hsh.inject([]) { |ret, pair| ret << [pair[0], serialize_value(pair[1])] }.flatten
|
95
95
|
|
96
|
-
ret =
|
96
|
+
ret = dbclient.hmset(dbkey, *data)
|
97
97
|
update_expiration
|
98
98
|
ret
|
99
99
|
end
|
@@ -101,11 +101,11 @@ module Familia
|
|
101
101
|
|
102
102
|
def values_at *fields
|
103
103
|
string_fields = fields.flatten.compact.map(&:to_s)
|
104
|
-
elements =
|
104
|
+
elements = dbclient.hmget(dbkey, *string_fields)
|
105
105
|
deserialize_values(*elements)
|
106
106
|
end
|
107
107
|
|
108
|
-
# The Great
|
108
|
+
# The Great Database Refresh-o-matic 3000 for HashKey!
|
109
109
|
#
|
110
110
|
# This method performs a complete refresh of the hash's state from Redis.
|
111
111
|
# It's like giving your hash a memory transfusion - out with the old state,
|
@@ -117,7 +117,7 @@ module Familia
|
|
117
117
|
# @return [void] Returns nothing, but your hash will be sparkling clean
|
118
118
|
# with all its fields synchronized with Redis.
|
119
119
|
#
|
120
|
-
# @raise [Familia::KeyNotFoundError] If the
|
120
|
+
# @raise [Familia::KeyNotFoundError] If the dbkey for this hash no
|
121
121
|
# longer exists. Time travelers beware!
|
122
122
|
#
|
123
123
|
# @example Basic usage
|
@@ -127,14 +127,14 @@ module Familia
|
|
127
127
|
# begin
|
128
128
|
# my_hash.refresh!
|
129
129
|
# rescue Familia::KeyNotFoundError
|
130
|
-
# puts "Oops! Our hash seems to have vanished into the
|
130
|
+
# puts "Oops! Our hash seems to have vanished into the Database void!"
|
131
131
|
# end
|
132
132
|
def refresh!
|
133
|
-
Familia.trace :REFRESH,
|
134
|
-
raise Familia::KeyNotFoundError,
|
133
|
+
Familia.trace :REFRESH, dbclient, uri, caller(1..1) if Familia.debug?
|
134
|
+
raise Familia::KeyNotFoundError, dbkey unless dbclient.exists(dbkey)
|
135
135
|
|
136
136
|
fields = hgetall
|
137
|
-
Familia.ld "[refresh!] #{self.class} #{
|
137
|
+
Familia.ld "[refresh!] #{self.class} #{dbkey} #{fields.keys}"
|
138
138
|
|
139
139
|
# For HashKey, we update by merging the fresh data
|
140
140
|
update(fields)
|
@@ -148,7 +148,7 @@ module Familia
|
|
148
148
|
#
|
149
149
|
# @return [self] Returns the refreshed hash, ready for more adventures!
|
150
150
|
#
|
151
|
-
# @raise [Familia::KeyNotFoundError] If the
|
151
|
+
# @raise [Familia::KeyNotFoundError] If the dbkey does not exist.
|
152
152
|
# The hash must exist in Redis-land for this to work!
|
153
153
|
#
|
154
154
|
# @example Refresh and chain
|
@@ -161,7 +161,7 @@ module Familia
|
|
161
161
|
self
|
162
162
|
end
|
163
163
|
|
164
|
-
Familia::
|
165
|
-
Familia::
|
164
|
+
Familia::DataType.register self, :hash # legacy, deprecated
|
165
|
+
Familia::DataType.register self, :hashkey
|
166
166
|
end
|
167
167
|
end
|