familia 1.2.1 → 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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +68 -0
  3. data/.github/workflows/docs.yml +64 -0
  4. data/.gitignore +4 -0
  5. data/.pre-commit-config.yaml +3 -1
  6. data/.rubocop.yml +16 -9
  7. data/.rubocop_todo.yml +177 -31
  8. data/.yardopts +9 -0
  9. data/CLAUDE.md +141 -0
  10. data/Gemfile +15 -2
  11. data/Gemfile.lock +76 -34
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +317 -0
  15. data/familia.gemspec +9 -5
  16. data/lib/familia/base.rb +19 -9
  17. data/lib/familia/connection.rb +232 -65
  18. data/lib/familia/core_ext.rb +1 -1
  19. data/lib/familia/datatype/commands.rb +59 -0
  20. data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
  21. data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
  22. data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
  23. data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
  24. data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
  25. data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
  26. data/lib/familia/datatype.rb +243 -0
  27. data/lib/familia/errors.rb +5 -2
  28. data/lib/familia/features/expiration.rb +33 -34
  29. data/lib/familia/features/quantization.rb +9 -3
  30. data/lib/familia/features/safe_dump.rb +2 -3
  31. data/lib/familia/features.rb +2 -2
  32. data/lib/familia/horreum/class_methods.rb +97 -110
  33. data/lib/familia/horreum/commands.rb +46 -51
  34. data/lib/familia/horreum/connection.rb +82 -0
  35. data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
  36. data/lib/familia/horreum/serialization.rb +61 -198
  37. data/lib/familia/horreum/settings.rb +6 -17
  38. data/lib/familia/horreum/utils.rb +11 -10
  39. data/lib/familia/horreum.rb +69 -60
  40. data/lib/familia/logging.rb +12 -12
  41. data/lib/familia/multi_result.rb +72 -0
  42. data/lib/familia/refinements.rb +7 -44
  43. data/lib/familia/settings.rb +11 -11
  44. data/lib/familia/utils.rb +123 -90
  45. data/lib/familia/version.rb +4 -21
  46. data/lib/familia.rb +17 -12
  47. data/lib/middleware/database_middleware.rb +150 -0
  48. data/try/configuration/scenarios_try.rb +65 -0
  49. data/try/core/connection_try.rb +58 -0
  50. data/try/core/errors_try.rb +93 -0
  51. data/try/core/extensions_try.rb +26 -0
  52. data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
  53. data/try/{00_familia_try.rb → core/familia_try.rb} +5 -3
  54. data/try/core/middleware_try.rb +68 -0
  55. data/try/core/refinements_try.rb +39 -0
  56. data/try/core/settings_try.rb +76 -0
  57. data/try/core/tools_try.rb +54 -0
  58. data/try/core/utils_try.rb +189 -0
  59. data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
  60. data/try/datatypes/datatype_base_try.rb +69 -0
  61. data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
  62. data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
  63. data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
  64. data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
  65. data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
  66. data/try/edge_cases/empty_identifiers_try.rb +48 -0
  67. data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -7
  68. data/try/edge_cases/json_serialization_try.rb +85 -0
  69. data/try/edge_cases/race_conditions_try.rb +60 -0
  70. data/try/edge_cases/reserved_keywords_try.rb +59 -0
  71. data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
  72. data/try/edge_cases/ttl_side_effects_try.rb +51 -0
  73. data/try/features/expiration_try.rb +86 -0
  74. data/try/features/quantization_try.rb +90 -0
  75. data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
  76. data/try/features/safe_dump_try.rb +137 -0
  77. data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
  78. data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
  79. data/try/horreum/class_methods_try.rb +41 -0
  80. data/try/horreum/commands_try.rb +49 -0
  81. data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
  82. data/try/horreum/relations_try.rb +146 -0
  83. data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
  84. data/try/horreum/settings_try.rb +43 -0
  85. data/try/integration/cross_component_try.rb +46 -0
  86. data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
  87. data/try/{40_customer_try.rb → models/customer_try.rb} +20 -17
  88. data/try/models/datatype_base_try.rb +101 -0
  89. data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
  90. data/try/performance/benchmarks_try.rb +55 -0
  91. data/try/pooling/README.md +20 -0
  92. data/try/pooling/configurable_stress_test_try.rb +435 -0
  93. data/try/pooling/connection_pool_test_try.rb +273 -0
  94. data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  95. data/try/pooling/lib/connection_pool_metrics.rb +372 -0
  96. data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
  97. data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
  98. data/try/pooling/lib/visualize_stress_results.rb +434 -0
  99. data/try/pooling/pool_siege_try.rb +509 -0
  100. data/try/pooling/run_stress_tests_try.rb +482 -0
  101. data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
  102. data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
  103. data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
  104. data/try/prototypes/atomic_saves_v4.rb +105 -0
  105. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
  106. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  107. metadata +140 -43
  108. data/.github/workflows/ruby.yml +0 -71
  109. data/VERSION.yml +0 -4
  110. data/lib/familia/redistype/commands.rb +0 -59
  111. data/lib/familia/redistype.rb +0 -228
  112. data/lib/familia/tools.rb +0 -68
  113. data/lib/redis_middleware.rb +0 -109
  114. data/try/20_redis_type_try.rb +0 -70
  115. 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