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
@@ -0,0 +1,421 @@
|
|
1
|
+
# try/pooling/lib/connection_pool_threading_models.rb
|
2
|
+
#
|
3
|
+
# Different Threading Models for Connection Pool Stress Testing
|
4
|
+
#
|
5
|
+
# This module provides various concurrency models to test the connection pool:
|
6
|
+
# 1. Traditional Threads - Ruby's Thread class
|
7
|
+
# 2. Fiber-based concurrency - Using Fibers (with or without scheduler)
|
8
|
+
# 3. Thread Pool Pattern - Fixed pool of worker threads
|
9
|
+
# 4. Actor Model - Message-passing concurrency (simplified)
|
10
|
+
|
11
|
+
require 'fiber'
|
12
|
+
require 'thread'
|
13
|
+
require_relative 'connection_pool_stress_test'
|
14
|
+
|
15
|
+
module ThreadingModels
|
16
|
+
# Base class for all threading models
|
17
|
+
class BaseModel
|
18
|
+
attr_reader :metrics, :config
|
19
|
+
|
20
|
+
def initialize(config, metrics)
|
21
|
+
@config = config
|
22
|
+
@metrics = metrics
|
23
|
+
@operations_queue = Queue.new if respond_to?(:uses_queue?) && uses_queue?
|
24
|
+
end
|
25
|
+
|
26
|
+
def run(&block)
|
27
|
+
raise NotImplementedError, "Subclasses must implement #run"
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def create_test_account(identifier)
|
33
|
+
account = StressTestAccount.new
|
34
|
+
account.balance = 1000
|
35
|
+
account.holder_name = "#{self.class.name.split('::').last}_#{identifier}"
|
36
|
+
account
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Traditional Ruby Threads
|
41
|
+
class TraditionalThreads < BaseModel
|
42
|
+
def run(&block)
|
43
|
+
threads = []
|
44
|
+
|
45
|
+
@config[:thread_count].times do |i|
|
46
|
+
threads << Thread.new(i) do |thread_id|
|
47
|
+
account = create_test_account(thread_id)
|
48
|
+
account.save
|
49
|
+
|
50
|
+
@config[:operations_per_thread].times do |op_num|
|
51
|
+
yield(account, thread_id, op_num)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
threads.each(&:join)
|
57
|
+
|
58
|
+
{
|
59
|
+
model: 'TraditionalThreads',
|
60
|
+
thread_count: threads.size,
|
61
|
+
all_completed: threads.all? { |t| !t.alive? }
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Fiber-based concurrency (cooperative)
|
67
|
+
class FiberBased < BaseModel
|
68
|
+
def run(&block)
|
69
|
+
fibers = []
|
70
|
+
completed = 0
|
71
|
+
|
72
|
+
# Create fibers
|
73
|
+
@config[:thread_count].times do |i|
|
74
|
+
fibers << Fiber.new do
|
75
|
+
account = create_test_account(i)
|
76
|
+
account.save
|
77
|
+
|
78
|
+
@config[:operations_per_thread].times do |op_num|
|
79
|
+
yield(account, i, op_num)
|
80
|
+
Fiber.yield # Cooperative yield
|
81
|
+
end
|
82
|
+
|
83
|
+
completed += 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Run fibers in round-robin fashion
|
88
|
+
while fibers.any? { |f| f.alive? }
|
89
|
+
fibers.each do |fiber|
|
90
|
+
fiber.resume if fiber.alive?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
{
|
95
|
+
model: 'FiberBased',
|
96
|
+
fiber_count: fibers.size,
|
97
|
+
completed: completed
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Thread Pool Pattern
|
103
|
+
class ThreadPool < BaseModel
|
104
|
+
def uses_queue?
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
def run(&block)
|
109
|
+
pool_size = @config[:worker_pool_size] || 10
|
110
|
+
workers = []
|
111
|
+
work_items = Queue.new
|
112
|
+
completed = Concurrent::AtomicFixnum.new(0) rescue completed = 0
|
113
|
+
|
114
|
+
# Populate work queue
|
115
|
+
@config[:thread_count].times do |i|
|
116
|
+
@config[:operations_per_thread].times do |op_num|
|
117
|
+
work_items << [i, op_num]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Create worker threads
|
122
|
+
pool_size.times do |worker_id|
|
123
|
+
workers << Thread.new do
|
124
|
+
loop do
|
125
|
+
begin
|
126
|
+
work = work_items.pop(true) # non-blocking pop
|
127
|
+
account_id, op_num = work
|
128
|
+
|
129
|
+
account = create_test_account("#{worker_id}_#{account_id}")
|
130
|
+
yield(account, account_id, op_num)
|
131
|
+
|
132
|
+
if defined?(Concurrent) && completed.respond_to?(:increment)
|
133
|
+
completed.increment
|
134
|
+
elsif completed.is_a?(Numeric)
|
135
|
+
completed += 1
|
136
|
+
end
|
137
|
+
rescue ThreadError
|
138
|
+
# Queue is empty
|
139
|
+
break
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
workers.each(&:join)
|
146
|
+
|
147
|
+
{
|
148
|
+
model: 'ThreadPool',
|
149
|
+
pool_size: pool_size,
|
150
|
+
total_operations: @config[:thread_count] * @config[:operations_per_thread],
|
151
|
+
completed: defined?(Concurrent) ? completed.value : 'N/A'
|
152
|
+
}
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Hybrid: Threads with Fiber-based operations
|
157
|
+
class HybridThreadFiber < BaseModel
|
158
|
+
def run(&block)
|
159
|
+
threads = []
|
160
|
+
|
161
|
+
@config[:thread_count].times do |thread_id|
|
162
|
+
threads << Thread.new do
|
163
|
+
# Each thread runs multiple fibers
|
164
|
+
fibers = []
|
165
|
+
fibers_per_thread = [@config[:operations_per_thread] / 10, 1].max
|
166
|
+
ops_per_fiber = @config[:operations_per_thread] / fibers_per_thread
|
167
|
+
|
168
|
+
fibers_per_thread.times do |fiber_id|
|
169
|
+
fibers << Fiber.new do
|
170
|
+
account = create_test_account("#{thread_id}_#{fiber_id}")
|
171
|
+
account.save
|
172
|
+
|
173
|
+
ops_per_fiber.times do |op_num|
|
174
|
+
yield(account, thread_id, op_num)
|
175
|
+
Fiber.yield
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Run fibers within thread
|
181
|
+
while fibers.any? { |f| f.alive? }
|
182
|
+
fibers.each { |f| f.resume if f.alive? }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
threads.each(&:join)
|
188
|
+
|
189
|
+
{
|
190
|
+
model: 'HybridThreadFiber',
|
191
|
+
thread_count: threads.size,
|
192
|
+
fibers_per_thread: @config[:operations_per_thread] / 10
|
193
|
+
}
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Actor Model (simplified)
|
198
|
+
class ActorModel < BaseModel
|
199
|
+
class Actor
|
200
|
+
def initialize(id, metrics)
|
201
|
+
@id = id
|
202
|
+
@metrics = metrics
|
203
|
+
@mailbox = Queue.new
|
204
|
+
@thread = Thread.new { process_messages }
|
205
|
+
end
|
206
|
+
|
207
|
+
def send_message(message)
|
208
|
+
@mailbox << message
|
209
|
+
end
|
210
|
+
|
211
|
+
def stop
|
212
|
+
@mailbox << :stop
|
213
|
+
@thread.join
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def process_messages
|
219
|
+
loop do
|
220
|
+
message = @mailbox.pop
|
221
|
+
break if message == :stop
|
222
|
+
|
223
|
+
operation, account, callback = message
|
224
|
+
result = perform_operation(operation, account)
|
225
|
+
callback.call(result) if callback
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def perform_operation(operation, account)
|
230
|
+
start = Time.now
|
231
|
+
|
232
|
+
case operation[:type]
|
233
|
+
when :read
|
234
|
+
account.refresh!
|
235
|
+
{ success: true, duration: Time.now - start }
|
236
|
+
when :write
|
237
|
+
account.balance += operation[:amount] || 0
|
238
|
+
account.save
|
239
|
+
{ success: true, duration: Time.now - start }
|
240
|
+
when :transaction
|
241
|
+
Familia.atomic do
|
242
|
+
account.complex_operation
|
243
|
+
end
|
244
|
+
{ success: true, duration: Time.now - start }
|
245
|
+
end
|
246
|
+
rescue => e
|
247
|
+
{ success: false, error: e, duration: Time.now - start }
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def run(&block)
|
252
|
+
actors = []
|
253
|
+
results = Queue.new
|
254
|
+
|
255
|
+
# Create actors
|
256
|
+
actor_count = [@config[:thread_count] / 2, 1].max
|
257
|
+
actor_count.times do |i|
|
258
|
+
actors << Actor.new(i, @metrics)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Distribute work among actors
|
262
|
+
@config[:thread_count].times do |i|
|
263
|
+
account = create_test_account(i)
|
264
|
+
account.save
|
265
|
+
|
266
|
+
@config[:operations_per_thread].times do |op_num|
|
267
|
+
actor = actors[i % actors.size]
|
268
|
+
|
269
|
+
actor.send_message([
|
270
|
+
{ type: [:read, :write, :transaction].sample },
|
271
|
+
account,
|
272
|
+
->(result) { results << result }
|
273
|
+
])
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Wait for all operations to complete
|
278
|
+
expected_ops = @config[:thread_count] * @config[:operations_per_thread]
|
279
|
+
received = 0
|
280
|
+
|
281
|
+
while received < expected_ops
|
282
|
+
result = results.pop
|
283
|
+
received += 1
|
284
|
+
|
285
|
+
@metrics.record_operation(
|
286
|
+
result[:type] || :unknown,
|
287
|
+
result[:duration],
|
288
|
+
result[:success]
|
289
|
+
)
|
290
|
+
end
|
291
|
+
|
292
|
+
# Stop actors
|
293
|
+
actors.each(&:stop)
|
294
|
+
|
295
|
+
{
|
296
|
+
model: 'ActorModel',
|
297
|
+
actor_count: actors.size,
|
298
|
+
operations_completed: received
|
299
|
+
}
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Factory method to create threading model
|
304
|
+
def self.create(model_name, config, metrics)
|
305
|
+
case model_name
|
306
|
+
when :traditional
|
307
|
+
TraditionalThreads.new(config, metrics)
|
308
|
+
when :fiber
|
309
|
+
FiberBased.new(config, metrics)
|
310
|
+
when :thread_pool
|
311
|
+
ThreadPool.new(config, metrics)
|
312
|
+
when :hybrid
|
313
|
+
HybridThreadFiber.new(config, metrics)
|
314
|
+
when :actor
|
315
|
+
ActorModel.new(config, metrics)
|
316
|
+
else
|
317
|
+
raise ArgumentError, "Unknown threading model: #{model_name}"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Enhanced stress test with threading models
|
323
|
+
class EnhancedConnectionPoolStressTest < ConnectionPoolStressTest
|
324
|
+
THREADING_MODELS = [:traditional, :fiber, :thread_pool, :hybrid, :actor]
|
325
|
+
|
326
|
+
def initialize(config = {})
|
327
|
+
super
|
328
|
+
@threading_model = config[:threading_model] || :traditional
|
329
|
+
end
|
330
|
+
|
331
|
+
def run_with_model(model_name = nil)
|
332
|
+
model_name ||= @threading_model
|
333
|
+
model = ThreadingModels.create(model_name, @config, @metrics)
|
334
|
+
|
335
|
+
puts "\n=== Running with #{model_name} model ==="
|
336
|
+
|
337
|
+
start_time = Time.now
|
338
|
+
|
339
|
+
result = model.run do |account, thread_id, op_num|
|
340
|
+
operation = select_operation_from_mix(
|
341
|
+
StressTestConfig::OPERATION_MIXES[@config[:operation_mix]]
|
342
|
+
)
|
343
|
+
execute_operation(account, operation)
|
344
|
+
end
|
345
|
+
|
346
|
+
duration = Time.now - start_time
|
347
|
+
|
348
|
+
result.merge(
|
349
|
+
total_duration: duration,
|
350
|
+
operations_per_second: (@config[:thread_count] * @config[:operations_per_thread]) / duration
|
351
|
+
)
|
352
|
+
end
|
353
|
+
|
354
|
+
def compare_all_models
|
355
|
+
results = {}
|
356
|
+
|
357
|
+
THREADING_MODELS.each do |model|
|
358
|
+
@metrics = MetricsCollector.new # Fresh metrics for each model
|
359
|
+
results[model] = run_with_model(model)
|
360
|
+
results[model][:summary] = @metrics.summary
|
361
|
+
end
|
362
|
+
|
363
|
+
display_comparison(results)
|
364
|
+
results
|
365
|
+
end
|
366
|
+
|
367
|
+
private
|
368
|
+
|
369
|
+
def display_comparison(results)
|
370
|
+
puts "\n=== Threading Model Comparison ==="
|
371
|
+
puts sprintf("%-15s %-10s %-10s %-10s %-10s %-10s",
|
372
|
+
"Model", "Duration", "Ops/Sec", "Success%", "Errors", "Max Pool%")
|
373
|
+
puts "-" * 75
|
374
|
+
|
375
|
+
results.each do |model, data|
|
376
|
+
summary = data[:summary]
|
377
|
+
puts sprintf("%-15s %-10.2f %-10.2f %-10.2f %-10d %-10.2f",
|
378
|
+
model,
|
379
|
+
data[:total_duration],
|
380
|
+
data[:operations_per_second],
|
381
|
+
summary[:success_rate],
|
382
|
+
summary[:failed_operations],
|
383
|
+
summary[:max_pool_utilization])
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
# Run comparison if executed directly
|
389
|
+
if __FILE__ == $0
|
390
|
+
Familia.debug = false
|
391
|
+
BankAccount.dbclient.flushdb
|
392
|
+
|
393
|
+
test = EnhancedConnectionPoolStressTest.new(
|
394
|
+
thread_count: 20,
|
395
|
+
operations_per_thread: 50,
|
396
|
+
pool_size: 10,
|
397
|
+
pool_timeout: 5,
|
398
|
+
operation_mix: :balanced,
|
399
|
+
scenario: :mixed_workload,
|
400
|
+
worker_pool_size: 8
|
401
|
+
)
|
402
|
+
|
403
|
+
results = test.compare_all_models
|
404
|
+
|
405
|
+
# Output results as CSV
|
406
|
+
puts "\n=== CSV Output for Import ==="
|
407
|
+
CSV do |csv|
|
408
|
+
csv << ['model', 'duration', 'ops_per_sec', 'success_rate', 'failed_ops', 'max_pool_util']
|
409
|
+
results.each do |model, data|
|
410
|
+
summary = data[:summary]
|
411
|
+
csv << [
|
412
|
+
model,
|
413
|
+
data[:total_duration],
|
414
|
+
data[:operations_per_second],
|
415
|
+
summary[:success_rate],
|
416
|
+
summary[:failed_operations],
|
417
|
+
summary[:max_pool_utilization]
|
418
|
+
]
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|