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.
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 +16 -2
  11. data/Gemfile.lock +97 -36
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +192 -0
  15. data/familia.gemspec +10 -6
  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 +18 -13
  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} +7 -5
  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} +21 -18
  88. data/try/models/datatype_base_try.rb +100 -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 +143 -46
  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,959 @@
1
+ # try/pooling/lib/connection_pool_stress_test.rb
2
+ #
3
+ # Connection Pool Stress Test Framework
4
+ #
5
+ # This stress test validates the connection pool implementation under various
6
+ # threading models and load conditions. It aims to identify failure modes,
7
+ # bottlenecks, and ensure predictable behavior under pressure.
8
+ #
9
+ # Key Testing Dimensions:
10
+ # - Threading models (Threads, Fibers, Thread Pools)
11
+ # - Load patterns (read-heavy, write-heavy, transaction-heavy)
12
+ # - Pool configurations (size, timeout)
13
+ # - Failure scenarios (starvation, timeouts, errors)
14
+
15
+ require 'bundler/setup'
16
+ require 'securerandom'
17
+ require 'thread'
18
+ require 'fiber'
19
+
20
+ begin
21
+ require 'csv'
22
+ rescue LoadError
23
+ puts "CSV gem not available, using basic output"
24
+ end
25
+
26
+ begin
27
+ require 'concurrent-ruby'
28
+ rescue LoadError
29
+ puts "concurrent-ruby gem not available, falling back to mutex-based metrics"
30
+ end
31
+
32
+ require_relative '../../helpers/test_helpers'
33
+ require_relative 'atomic_saves_v3_connection_pool_helpers'
34
+
35
+ # Stress Test Configuration - Central hub for all testing parameters
36
+ module StressTestConfig
37
+ # Threading Configuration
38
+ THREAD_COUNTS = [5, 10, 50, 100]
39
+ FIBER_COUNTS = [10, 50, 100, 500]
40
+ WORKER_POOL_SIZES = [5, 10, 20]
41
+
42
+ # Operations Configuration
43
+ OPERATIONS_PER_THREAD = [10, 100, 500]
44
+
45
+ # Connection Pool Configuration
46
+ POOL_SIZES = [5, 10, 20, 50]
47
+ POOL_TIMEOUTS = [1, 5, 10] # seconds
48
+
49
+ # Operation Types
50
+ OPERATION_MIXES = {
51
+ read_heavy: { read: 80, write: 15, transaction: 5 },
52
+ write_heavy: { read: 20, write: 70, transaction: 10 },
53
+ transaction_heavy: { read: 10, write: 20, transaction: 70 },
54
+ balanced: { read: 33, write: 33, transaction: 34 }
55
+ }
56
+
57
+ # Test Scenarios
58
+ SCENARIOS = [
59
+ :pool_starvation, # More threads than connections
60
+ :rapid_fire, # Minimal work per connection
61
+ :long_transactions, # Hold connections longer
62
+ :nested_transactions, # Test transaction isolation
63
+ :error_injection, # Inject failures
64
+ :mixed_workload # Combine different operations
65
+ ]
66
+
67
+ class << self
68
+ # Intelligent configuration selection based on testing goals
69
+ def for_development
70
+ {
71
+ thread_counts: THREAD_COUNTS.first(2),
72
+ operations_per_thread: OPERATIONS_PER_THREAD.first(2),
73
+ pool_sizes: POOL_SIZES.first(2),
74
+ pool_timeouts: [POOL_TIMEOUTS.first],
75
+ scenarios: [:mixed_workload, :rapid_fire],
76
+ operation_mixes: [:balanced]
77
+ }
78
+ end
79
+
80
+ def for_ci
81
+ {
82
+ thread_counts: THREAD_COUNTS.first(3),
83
+ operations_per_thread: OPERATIONS_PER_THREAD.first(2),
84
+ pool_sizes: POOL_SIZES.first(3),
85
+ pool_timeouts: POOL_TIMEOUTS.first(2),
86
+ scenarios: [:pool_starvation, :rapid_fire, :mixed_workload],
87
+ operation_mixes: [:balanced, :read_heavy]
88
+ }
89
+ end
90
+
91
+ def for_production_validation
92
+ {
93
+ thread_counts: THREAD_COUNTS,
94
+ operations_per_thread: OPERATIONS_PER_THREAD,
95
+ pool_sizes: POOL_SIZES,
96
+ pool_timeouts: POOL_TIMEOUTS,
97
+ scenarios: SCENARIOS,
98
+ operation_mixes: OPERATION_MIXES.keys
99
+ }
100
+ end
101
+
102
+ def for_bottleneck_analysis
103
+ {
104
+ thread_counts: THREAD_COUNTS.select { |t| t >= 50 },
105
+ operations_per_thread: [1000, 5000],
106
+ pool_sizes: POOL_SIZES.select { |p| p <= 20 },
107
+ pool_timeouts: POOL_TIMEOUTS.select { |t| t >= 5 },
108
+ scenarios: [:pool_starvation, :long_transactions, :error_injection],
109
+ operation_mixes: [:transaction_heavy, :balanced]
110
+ }
111
+ end
112
+
113
+ def for_performance_tuning
114
+ {
115
+ thread_counts: [20, 50, 100],
116
+ operations_per_thread: [500, 1000],
117
+ pool_sizes: [10, 20, 50],
118
+ pool_timeouts: [5, 10, 30],
119
+ scenarios: [:rapid_fire, :mixed_workload],
120
+ operation_mixes: OPERATION_MIXES.keys
121
+ }
122
+ end
123
+
124
+ # Runtime configuration from environment variables
125
+ def runtime_config
126
+ {
127
+ thread_counts: parse_env_array('STRESS_THREADS', THREAD_COUNTS),
128
+ operations_per_thread: parse_env_array('STRESS_OPS', OPERATIONS_PER_THREAD),
129
+ pool_sizes: parse_env_array('STRESS_POOLS', POOL_SIZES),
130
+ pool_timeouts: parse_env_array('STRESS_TIMEOUTS', POOL_TIMEOUTS),
131
+ scenarios: parse_env_symbols('STRESS_SCENARIOS', SCENARIOS),
132
+ operation_mixes: parse_env_symbols('STRESS_MIXES', OPERATION_MIXES.keys)
133
+ }
134
+ end
135
+
136
+ # Configuration validation
137
+ def validate_config(config)
138
+ errors = []
139
+ warnings = []
140
+
141
+ # Thread count vs pool size validation
142
+ if config[:thread_count] && config[:pool_size]
143
+ if config[:thread_count] <= config[:pool_size]
144
+ warnings << "Thread count (#{config[:thread_count]}) <= pool size (#{config[:pool_size]}) - may not create enough pressure"
145
+ end
146
+
147
+ if config[:thread_count] > config[:pool_size] * 10
148
+ warnings << "Thread count (#{config[:thread_count]}) >> pool size (#{config[:pool_size]}) - may cause excessive queueing"
149
+ end
150
+ end
151
+
152
+ # Operations per thread validation
153
+ if config[:operations_per_thread]
154
+ if config[:operations_per_thread] > 1000
155
+ warnings << "High operation count (#{config[:operations_per_thread]}) may cause long test duration"
156
+ end
157
+
158
+ if config[:operations_per_thread] < 10
159
+ warnings << "Low operation count (#{config[:operations_per_thread]}) may not provide reliable metrics"
160
+ end
161
+ end
162
+
163
+ # Pool timeout validation
164
+ if config[:pool_timeout]
165
+ if config[:pool_timeout] < 1
166
+ errors << "Pool timeout (#{config[:pool_timeout]}s) too low - may cause premature failures"
167
+ end
168
+
169
+ if config[:pool_timeout] > 60
170
+ warnings << "Pool timeout (#{config[:pool_timeout]}s) very high - failures may take long to detect"
171
+ end
172
+ end
173
+
174
+ # Scenario validation
175
+ if config[:scenario] && !SCENARIOS.include?(config[:scenario])
176
+ errors << "Unknown scenario: #{config[:scenario]}. Valid: #{SCENARIOS.join(', ')}"
177
+ end
178
+
179
+ # Operation mix validation
180
+ if config[:operation_mix] && !OPERATION_MIXES.key?(config[:operation_mix])
181
+ errors << "Unknown operation mix: #{config[:operation_mix]}. Valid: #{OPERATION_MIXES.keys.join(', ')}"
182
+ end
183
+
184
+ { errors: errors, warnings: warnings }
185
+ end
186
+
187
+ # Default configuration for general testing
188
+ def default
189
+ {
190
+ thread_count: 20,
191
+ operations_per_thread: 100,
192
+ pool_size: 10,
193
+ pool_timeout: 5,
194
+ operation_mix: :balanced,
195
+ scenario: :mixed_workload
196
+ }
197
+ end
198
+
199
+ # Merge configurations with validation
200
+ def merge_and_validate(*configs)
201
+ merged = configs.reduce({}) { |acc, config| acc.merge(config) }
202
+ validation = validate_config(merged)
203
+
204
+ if validation[:errors].any?
205
+ raise ArgumentError, "Configuration errors: #{validation[:errors].join('; ')}"
206
+ end
207
+
208
+ if validation[:warnings].any?
209
+ puts "Configuration warnings: #{validation[:warnings].join('; ')}"
210
+ end
211
+
212
+ merged
213
+ end
214
+
215
+ private
216
+
217
+ def parse_env_array(env_var, default)
218
+ env_value = ENV[env_var]
219
+ return default unless env_value
220
+
221
+ env_value.split(',').map(&:strip).map(&:to_i)
222
+ end
223
+
224
+ def parse_env_symbols(env_var, default)
225
+ env_value = ENV[env_var]
226
+ return default unless env_value
227
+
228
+ env_value.split(',').map(&:strip).map(&:to_sym)
229
+ end
230
+ end
231
+ end
232
+
233
+ # Metrics Collection
234
+ class MetricsCollector
235
+ attr_reader :operations, :errors, :pool_stats
236
+
237
+ def initialize
238
+ if defined?(Concurrent)
239
+ # Use lock-free concurrent collections
240
+ @operations = Concurrent::Array.new
241
+ @errors = Concurrent::Array.new
242
+ @pool_stats = Concurrent::Array.new
243
+ @use_concurrent = true
244
+ else
245
+ # Fallback to mutex-based approach
246
+ @metrics = {
247
+ operations: [],
248
+ errors: [],
249
+ wait_times: [],
250
+ pool_stats: []
251
+ }
252
+ @mutex = Mutex.new
253
+ @use_concurrent = false
254
+ end
255
+ end
256
+
257
+ def record_operation(type, duration, success, wait_time = nil)
258
+ operation_data = {
259
+ type: type,
260
+ duration: duration,
261
+ success: success,
262
+ wait_time: wait_time,
263
+ timestamp: Time.now.to_f
264
+ }
265
+
266
+ if @use_concurrent
267
+ @operations << operation_data
268
+ else
269
+ @mutex.synchronize do
270
+ @metrics[:operations] << operation_data
271
+ end
272
+ end
273
+ end
274
+
275
+ def record_error(error, context = {})
276
+ error_data = {
277
+ error: error.class.name,
278
+ message: error.message,
279
+ context: context,
280
+ timestamp: Time.now.to_f
281
+ }
282
+
283
+ if @use_concurrent
284
+ @errors << error_data
285
+ else
286
+ @mutex.synchronize do
287
+ @metrics[:errors] << error_data
288
+ end
289
+ end
290
+ end
291
+
292
+ def record_pool_stats(available, size)
293
+ # Calculate utilization: if all connections are available, utilization is 0%
294
+ # If no connections are available, utilization is 100%
295
+ in_use = [size - available, 0].max # Ensure non-negative
296
+ utilization_alt = size > 0 ? (in_use.to_f / size * 100).round(2) : 0.0
297
+ utilization = ((size - available).to_f / size * 100).round(2)
298
+ stats_data = {
299
+ available: available,
300
+ size: size,
301
+ utilization: utilization,
302
+ utilization_alt: utilization_alt,
303
+ in_use: in_use,
304
+ timestamp: Time.now.to_f
305
+ }
306
+
307
+ if @use_concurrent
308
+ @pool_stats << stats_data
309
+ else
310
+ @mutex.synchronize do
311
+ @metrics[:pool_stats] << stats_data
312
+ end
313
+ end
314
+ end
315
+
316
+ def summary
317
+ if @use_concurrent
318
+ operations = @operations.to_a
319
+ errors = @errors.to_a
320
+ pool_stats = @pool_stats.to_a
321
+ else
322
+ operations = @metrics[:operations]
323
+ errors = @metrics[:errors]
324
+ pool_stats = @metrics[:pool_stats]
325
+ end
326
+
327
+ successful = operations.select { |op| op[:success] }
328
+ failed = operations.reject { |op| op[:success] }
329
+
330
+ {
331
+ total_operations: operations.size,
332
+ successful_operations: successful.size,
333
+ failed_operations: failed.size,
334
+ success_rate: operations.size > 0 ? (successful.size.to_f / operations.size * 100).round(2) : 0.0,
335
+ avg_duration: operations.size > 0 ? operations.map { |op| op[:duration] }.sum.to_f / operations.size : 0.0,
336
+ avg_wait_time: operations.size > 0 ? operations.compact.map { |op| op[:wait_time] || 0 }.sum.to_f / operations.size : 0.0,
337
+ errors_by_type: errors.group_by { |e| e[:error] }.transform_values(&:size),
338
+ max_pool_utilization: pool_stats.map { |s| s[:utilization] }.max || 0
339
+ }
340
+ end
341
+
342
+ def to_csv
343
+ if defined?(CSV)
344
+ CSV.generate do |csv|
345
+ csv << ['metric', 'value']
346
+ summary.each do |key, value|
347
+ csv << [key, value]
348
+ end
349
+ end
350
+ else
351
+ # Fallback to simple CSV format
352
+ lines = ['metric,value']
353
+ summary.each do |key, value|
354
+ lines << "#{key},#{value}"
355
+ end
356
+ lines.join("\n")
357
+ end
358
+ end
359
+
360
+ # Expose the legacy metrics hash interface for compatibility
361
+ def metrics
362
+ if @use_concurrent
363
+ {
364
+ operations: @operations.to_a,
365
+ errors: @errors.to_a,
366
+ pool_stats: @pool_stats.to_a
367
+ }
368
+ else
369
+ @metrics
370
+ end
371
+ end
372
+ end
373
+
374
+ # Extended test models for stress testing - for now, just use BankAccount directly
375
+ # to avoid the subclass identifier issue
376
+ StressTestAccount = BankAccount
377
+
378
+ # Define helper methods on BankAccount for testing
379
+ class BankAccount
380
+ def complex_operation
381
+ # Simulate complex read-modify-write pattern
382
+ refresh!
383
+ current = balance || 0
384
+ # sleep(0.001) # Simulate processing time
385
+ self.balance = current + rand(-10..10)
386
+ save
387
+ end
388
+
389
+ def batch_update(updates)
390
+ updates.each do |field, value|
391
+ send("#{field}=", value)
392
+ end
393
+ save
394
+ end
395
+
396
+ # Generate metadata of specified size
397
+ def generate_metadata(size_class = :small)
398
+ metadata_sizes = {
399
+ tiny: 1, # 1 key-value pair
400
+ small: 10, # 10 key-value pairs
401
+ medium: 100, # 100 key-value pairs
402
+ large: 1000, # 1000 key-value pairs
403
+ huge: 10000 # 10000 key-value pairs
404
+ }
405
+
406
+ num_keys = metadata_sizes[size_class] || 10
407
+
408
+ # Generate hash with specified number of keys
409
+ metadata = {}
410
+ num_keys.times do |i|
411
+ key = "field_#{i}"
412
+ # Create varied value types for more realistic JSON
413
+ value = case i % 5
414
+ when 0 then SecureRandom.hex(8) # String
415
+ when 1 then rand(1..1000) # Integer
416
+ when 2 then rand * 1000 # Float
417
+ when 3 then [rand(1..10), rand(1..10), rand(1..10)] # Array
418
+ when 4 then { nested: SecureRandom.hex(4), value: rand(1..100) } # Hash
419
+ end
420
+ metadata[key] = value
421
+ end
422
+
423
+ self.metadata = metadata
424
+ end
425
+ end
426
+
427
+ # Stress Test Runner
428
+ class ConnectionPoolStressTest
429
+ attr_reader :config, :metrics
430
+
431
+ def initialize(config = {})
432
+ @config = {
433
+ thread_count: 10,
434
+ operations_per_thread: 100,
435
+ pool_size: 10,
436
+ pool_timeout: 5,
437
+ operation_mix: :balanced,
438
+ scenario: :mixed_workload,
439
+ shared_accounts: nil, # nil means one account per thread (original behavior)
440
+ fresh_records: false, # false means reuse accounts, true means create new each operation
441
+ duration: nil, # nil means operations-based, otherwise time-based in seconds
442
+ workload_size: :small, # Size of metadata: tiny, small, medium, large, huge
443
+ profile_id_generation: false, # if true, pre-generate account IDs
444
+ }.merge(config)
445
+
446
+ @metrics = MetricsCollector.new
447
+ @shared_accounts = [] # Will hold shared account instances
448
+ @pre_generated_ids = [] # For ID generation profiling
449
+ setup_connection_pool
450
+ setup_shared_accounts if @config[:shared_accounts]
451
+ setup_pre_generated_ids if @config[:profile_id_generation]
452
+ end
453
+
454
+ def setup_connection_pool
455
+ # Reconfigure connection pool with test parameters
456
+ pool_size = @config[:pool_size]
457
+ pool_timeout = @config[:pool_timeout]
458
+
459
+ # Use class_variable_set to properly set the class variable
460
+ Familia.class_variable_set(:@@connection_pool, ConnectionPool.new(
461
+ size: pool_size,
462
+ timeout: pool_timeout
463
+ ) do
464
+ Redis.new(url: Familia.uri.to_s)
465
+ end)
466
+
467
+ # Verify the pool was configured correctly
468
+ actual_size = Familia.connection_pool.size
469
+ if actual_size != pool_size
470
+ puts "WARNING: Pool size mismatch! Configured: #{pool_size}, Actual: #{actual_size}"
471
+ end
472
+ end
473
+
474
+ def setup_shared_accounts
475
+ # Create a limited set of accounts that all threads will contend over
476
+ @config[:shared_accounts].times do |i|
477
+ account = StressTestAccount.new
478
+ account.balance = 1000
479
+ account.holder_name = "SharedAccount#{i}"
480
+ account.generate_metadata(@config[:workload_size])
481
+ account.save
482
+ @shared_accounts << account
483
+ end
484
+ puts "Created #{@shared_accounts.size} shared accounts for high-contention testing"
485
+ end
486
+
487
+ def setup_pre_generated_ids
488
+ # Pre-generate account IDs to test if SecureRandom is a bottleneck
489
+ total_needed = if @config[:duration]
490
+ # Estimate based on typical throughput
491
+ @config[:thread_count] * 100 # Assume ~100 ops/thread/sec as starting point
492
+ else
493
+ @config[:thread_count] * @config[:operations_per_thread]
494
+ end
495
+
496
+ puts "Pre-generating #{total_needed} account IDs for profiling..."
497
+ total_needed.times do
498
+ @pre_generated_ids << SecureRandom.hex(8)
499
+ end
500
+ @id_index = 0
501
+ @id_mutex = Mutex.new
502
+ puts "ID pre-generation complete"
503
+ end
504
+
505
+ def get_next_pre_generated_id
506
+ @id_mutex.synchronize do
507
+ id = @pre_generated_ids[@id_index % @pre_generated_ids.length]
508
+ @id_index += 1
509
+ id
510
+ end
511
+ end
512
+
513
+ def get_account_for_thread(thread_index)
514
+ if @config[:shared_accounts]
515
+ # Return one of the shared accounts (round-robin distribution)
516
+ @shared_accounts[thread_index % @shared_accounts.size]
517
+ else
518
+ # Original behavior: create unique account per thread
519
+ account = StressTestAccount.new
520
+ account.balance = 1000
521
+ account.holder_name = "Thread#{thread_index}"
522
+ account.generate_metadata(@config[:workload_size])
523
+ account.save
524
+ account
525
+ end
526
+ end
527
+
528
+ def get_account_for_operation(thread_index, operation_index)
529
+ if @config[:fresh_records]
530
+ # Create a new account for every operation
531
+ account = StressTestAccount.new
532
+ if @config[:profile_id_generation]
533
+ # Use pre-generated ID
534
+ account.instance_variable_set(:@account_number, get_next_pre_generated_id)
535
+ end
536
+ account.balance = 1000
537
+ account.holder_name = "T#{thread_index}_Op#{operation_index}"
538
+ account.generate_metadata(@config[:workload_size])
539
+ account.save
540
+ account
541
+ else
542
+ # Use the existing account for this thread
543
+ get_account_for_thread(thread_index)
544
+ end
545
+ end
546
+
547
+ def run
548
+ puts "\n=== Starting Stress Test ==="
549
+ puts "Configuration: #{@config.inspect}"
550
+
551
+ case @config[:scenario]
552
+ when :pool_starvation
553
+ run_pool_starvation_test
554
+ when :rapid_fire
555
+ run_rapid_fire_test
556
+ when :long_transactions
557
+ run_long_transactions_test
558
+ when :nested_transactions
559
+ run_nested_transactions_test
560
+ when :error_injection
561
+ run_error_injection_test
562
+ else
563
+ run_mixed_workload_test
564
+ end
565
+
566
+ puts "\n=== Test Complete ==="
567
+ display_summary
568
+ end
569
+
570
+ private
571
+
572
+ def run_pool_starvation_test
573
+ # Create more threads than pool connections
574
+ thread_count = @config[:pool_size] * 2
575
+ threads = []
576
+
577
+ puts "Running pool starvation test with #{thread_count} threads and pool size #{@config[:pool_size]}"
578
+
579
+ thread_count.times do |i|
580
+ threads << Thread.new do
581
+ begin
582
+ @config[:operations_per_thread].times do |op_index|
583
+ account = get_account_for_operation(i, op_index)
584
+ begin
585
+ start = Time.now
586
+ wait_start = Time.now
587
+
588
+ Familia.atomic do
589
+ wait_time = Time.now - wait_start
590
+ account.complex_operation
591
+ @metrics.record_operation(:transaction, Time.now - start, true, wait_time)
592
+ end
593
+ rescue => e
594
+ @metrics.record_error(e, { thread: i })
595
+ @metrics.record_operation(:transaction, Time.now - start, false)
596
+ end
597
+ end
598
+ rescue => e
599
+ puts "Thread #{i} setup error: #{e.message} (#{e.class})" if ENV['FAMILIA_DEBUG']
600
+ @metrics.record_error(e, { thread: i, phase: :setup })
601
+ end
602
+ end
603
+ end
604
+
605
+ # Monitor pool utilization
606
+ monitor_thread = Thread.new do
607
+ begin
608
+ while threads.any?(&:alive?)
609
+ if Familia.connection_pool.respond_to?(:available)
610
+ @metrics.record_pool_stats(
611
+ Familia.connection_pool.available,
612
+ Familia.connection_pool.size
613
+ )
614
+ end
615
+ sleep 0.01 # Poll every 10ms for finer granularity
616
+ end
617
+ rescue => e
618
+ puts "Monitor thread error: #{e.message}" if ENV['FAMILIA_DEBUG']
619
+ end
620
+ end
621
+
622
+ threads.each(&:join)
623
+ monitor_thread.kill if monitor_thread.alive?
624
+ end
625
+
626
+ def run_rapid_fire_test
627
+ threads = []
628
+
629
+ puts "Running rapid fire test with #{@config[:thread_count]} threads"
630
+
631
+ if @config[:duration]
632
+ end_time = Time.now + @config[:duration]
633
+
634
+ @config[:thread_count].times do |i|
635
+ threads << Thread.new do
636
+ op_index = 0
637
+ while Time.now < end_time
638
+ account = get_account_for_operation(i, op_index)
639
+ operation = select_operation
640
+ execute_operation(account, operation)
641
+ op_index += 1
642
+ end
643
+ end
644
+ end
645
+ else
646
+ @config[:thread_count].times do |i|
647
+ threads << Thread.new do
648
+ @config[:operations_per_thread].times do |op_index|
649
+ account = get_account_for_operation(i, op_index)
650
+ operation = select_operation
651
+ execute_operation(account, operation)
652
+ end
653
+ end
654
+ end
655
+ end
656
+
657
+ # Monitor pool utilization
658
+ monitor_thread = Thread.new do
659
+ begin
660
+ while threads.any?(&:alive?)
661
+ if Familia.connection_pool.respond_to?(:available)
662
+ @metrics.record_pool_stats(
663
+ Familia.connection_pool.available,
664
+ Familia.connection_pool.size
665
+ )
666
+ end
667
+ sleep 0.01 # Poll every 10ms for finer granularity
668
+ end
669
+ rescue => e
670
+ puts "Monitor thread error: #{e.message}" if ENV['FAMILIA_DEBUG']
671
+ end
672
+ end
673
+
674
+ threads.each(&:join)
675
+ monitor_thread.kill if monitor_thread.alive?
676
+ end
677
+
678
+ def run_long_transactions_test
679
+ threads = []
680
+
681
+ puts "Running long transactions test"
682
+
683
+ @config[:thread_count].times do |i|
684
+ threads << Thread.new do
685
+ account1 = StressTestAccount.new
686
+ account1.balance = 1000
687
+ account1.holder_name = "Long1_#{i}"
688
+ account2 = StressTestAccount.new
689
+ account2.balance = 1000
690
+ account2.holder_name = "Long2_#{i}"
691
+
692
+ @config[:operations_per_thread].times do
693
+ begin
694
+ start = Time.now
695
+
696
+ Familia.atomic do
697
+ # Simulate long-running transaction
698
+ account1.refresh!
699
+ account2.refresh!
700
+ # sleep(0.1) # Hold connection longer
701
+
702
+ account1.withdraw(100)
703
+ account2.deposit(100)
704
+
705
+ account1.save
706
+ account2.save
707
+ end
708
+
709
+ @metrics.record_operation(:long_transaction, Time.now - start, true)
710
+ rescue => e
711
+ @metrics.record_error(e, { thread: i })
712
+ @metrics.record_operation(:long_transaction, Time.now - start, false)
713
+ end
714
+ end
715
+ end
716
+ end
717
+
718
+ threads.each(&:join)
719
+ end
720
+
721
+ def run_nested_transactions_test
722
+ threads = []
723
+
724
+ puts "Running nested transactions test"
725
+
726
+ @config[:thread_count].times do |i|
727
+ threads << Thread.new do
728
+ account = StressTestAccount.new
729
+ account.balance = 1000
730
+ account.holder_name = "Nested#{i}"
731
+
732
+ @config[:operations_per_thread].times do
733
+ begin
734
+ start = Time.now
735
+
736
+ Familia.atomic do
737
+ account.deposit(50)
738
+ account.save
739
+
740
+ # Nested transaction (should be separate)
741
+ Familia.atomic do
742
+ account.deposit(25)
743
+ account.save
744
+ end
745
+
746
+ account.withdraw(10)
747
+ account.save
748
+ end
749
+
750
+ @metrics.record_operation(:nested_transaction, Time.now - start, true)
751
+ rescue => e
752
+ @metrics.record_error(e, { thread: i })
753
+ @metrics.record_operation(:nested_transaction, Time.now - start, false)
754
+ end
755
+ end
756
+ end
757
+ end
758
+
759
+ threads.each(&:join)
760
+ end
761
+
762
+ def run_error_injection_test
763
+ threads = []
764
+ error_rate = 0.1 # 10% error rate
765
+
766
+ puts "Running error injection test with #{(error_rate * 100).to_i}% error rate"
767
+
768
+ @config[:thread_count].times do |i|
769
+ threads << Thread.new do
770
+ account = StressTestAccount.new
771
+ account.balance = 1000
772
+ account.holder_name = "Error#{i}"
773
+
774
+ @config[:operations_per_thread].times do |op_num|
775
+ begin
776
+ start = Time.now
777
+
778
+ if rand < error_rate
779
+ # Inject an error
780
+ raise "Simulated error in thread #{i}, operation #{op_num}"
781
+ end
782
+
783
+ Familia.atomic do
784
+ account.complex_operation
785
+ end
786
+
787
+ @metrics.record_operation(:with_errors, Time.now - start, true)
788
+ rescue => e
789
+ @metrics.record_error(e, { thread: i, operation: op_num })
790
+ @metrics.record_operation(:with_errors, Time.now - start, false)
791
+ end
792
+ end
793
+ end
794
+ end
795
+
796
+ threads.each(&:join)
797
+ end
798
+
799
+ def run_mixed_workload_test
800
+ threads = []
801
+ mix = StressTestConfig::OPERATION_MIXES[@config[:operation_mix]]
802
+
803
+ puts "Running mixed workload test with mix: #{mix.inspect}"
804
+
805
+ if @config[:duration]
806
+ run_duration_based_test(mix)
807
+ else
808
+ run_operations_based_test(mix)
809
+ end
810
+ end
811
+
812
+ def run_duration_based_test(mix)
813
+ threads = []
814
+ end_time = Time.now + @config[:duration]
815
+
816
+ @config[:thread_count].times do |i|
817
+ threads << Thread.new do
818
+ op_index = 0
819
+ while Time.now < end_time
820
+ account = get_account_for_operation(i, op_index)
821
+ operation = select_operation_from_mix(mix)
822
+ execute_operation(account, operation)
823
+ op_index += 1
824
+ end
825
+ end
826
+ end
827
+
828
+ # Monitor pool utilization
829
+ monitor_thread = Thread.new do
830
+ begin
831
+ while threads.any?(&:alive?)
832
+ if Familia.connection_pool.respond_to?(:available)
833
+ @metrics.record_pool_stats(
834
+ Familia.connection_pool.available,
835
+ Familia.connection_pool.size
836
+ )
837
+ end
838
+ sleep 0.001 # Poll every 1ms for better granularity with fast operations
839
+ end
840
+ rescue => e
841
+ puts "Monitor thread error: #{e.message}" if ENV['FAMILIA_DEBUG']
842
+ end
843
+ end
844
+
845
+ threads.each(&:join)
846
+ monitor_thread.kill if monitor_thread.alive?
847
+ end
848
+
849
+ def run_operations_based_test(mix)
850
+ threads = []
851
+
852
+ @config[:thread_count].times do |i|
853
+ threads << Thread.new do
854
+ @config[:operations_per_thread].times do |op_index|
855
+ account = get_account_for_operation(i, op_index)
856
+ operation = select_operation_from_mix(mix)
857
+ execute_operation(account, operation)
858
+ end
859
+ end
860
+ end
861
+
862
+ # Monitor pool utilization
863
+ monitor_thread = Thread.new do
864
+ begin
865
+ while threads.any?(&:alive?)
866
+ if Familia.connection_pool.respond_to?(:available)
867
+ @metrics.record_pool_stats(
868
+ Familia.connection_pool.available,
869
+ Familia.connection_pool.size
870
+ )
871
+ end
872
+ sleep 0.001 # Poll every 1ms for better granularity with fast operations
873
+ end
874
+ rescue => e
875
+ puts "Monitor thread error: #{e.message}" if ENV['FAMILIA_DEBUG']
876
+ end
877
+ end
878
+
879
+ threads.each(&:join)
880
+ monitor_thread.kill if monitor_thread.alive?
881
+ end
882
+
883
+ def select_operation
884
+ [:read, :write, :transaction].sample
885
+ end
886
+
887
+ def select_operation_from_mix(mix)
888
+ rand_num = rand(100)
889
+ cumulative = 0
890
+
891
+ mix.each do |op, percentage|
892
+ cumulative += percentage
893
+ return op if rand_num < cumulative
894
+ end
895
+
896
+ :read # fallback
897
+ end
898
+
899
+ def execute_operation(account, operation)
900
+ begin
901
+ start = Time.now
902
+
903
+ case operation
904
+ when :read
905
+ account.refresh!
906
+ _ = account.balance
907
+ @metrics.record_operation(:read, Time.now - start, true)
908
+ when :write
909
+ current = account.balance || 0
910
+ account.balance = current + rand(-10..10)
911
+ account.save
912
+ @metrics.record_operation(:write, Time.now - start, true)
913
+ when :transaction
914
+ Familia.atomic do
915
+ account.refresh!
916
+ current = account.balance || 0
917
+ account.balance = current + rand(-10..10)
918
+ account.save
919
+ end
920
+ @metrics.record_operation(:transaction, Time.now - start, true)
921
+ end
922
+ rescue => e
923
+ puts "Operation error: #{e.message} (#{e.class})" if ENV['FAMILIA_DEBUG']
924
+ @metrics.record_error(e, { operation: operation })
925
+ @metrics.record_operation(operation, Time.now - start, false)
926
+ end
927
+ end
928
+
929
+ def display_summary
930
+ summary = @metrics.summary
931
+
932
+ puts "\n=== Summary ==="
933
+ summary.each do |key, value|
934
+ puts "#{key}: #{value}"
935
+ end
936
+ end
937
+ end
938
+
939
+ # Run basic test if executed directly
940
+ if __FILE__ == $0
941
+ Familia.debug = false
942
+ BankAccount.dbclient.flushdb
943
+
944
+ # Run a simple test
945
+ test = ConnectionPoolStressTest.new(
946
+ thread_count: 20,
947
+ operations_per_thread: 50,
948
+ pool_size: 10,
949
+ pool_timeout: 5,
950
+ operation_mix: :balanced,
951
+ scenario: :pool_starvation
952
+ )
953
+
954
+ test.run
955
+
956
+ # Output CSV
957
+ puts "\n=== CSV Output ==="
958
+ puts test.metrics.to_csv
959
+ end