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,509 @@
1
+ #!/usr/bin/env ruby
2
+ # pool_siege.rb
3
+ #
4
+ # Simple Connection Pool Load Tester - Like siege, but for Database connection pools
5
+ #
6
+ # Usage:
7
+ # ruby pool_siege.rb -t 20 -p 5 -o 100 # 20 threads, 5 pool size, 100 ops each
8
+ # ruby pool_siege.rb --stress # Find breaking point
9
+ # ruby pool_siege.rb --light # Quick validation
10
+
11
+ require 'optparse'
12
+ require 'io/console'
13
+ require_relative '../helpers/test_helpers'
14
+ require_relative 'lib/connection_pool_stress_test'
15
+
16
+ class PoolSiege
17
+ def initialize(args)
18
+ @options = parse_options(args)
19
+ validate_options
20
+ end
21
+
22
+ def run
23
+ setup_connection_pool
24
+ print_test_description
25
+
26
+ if @options[:profile]
27
+ run_with_profiling
28
+ else
29
+ start_time = Time.now
30
+
31
+ if @options[:quiet]
32
+ run_silent_test
33
+ else
34
+ run_with_progress
35
+ end
36
+
37
+ end_time = Time.now
38
+ print_final_results(end_time - start_time)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def percentile(values, percentile)
45
+ return 0 if values.empty?
46
+ sorted = values.sort
47
+ index = (percentile * (sorted.size - 1)).round
48
+ sorted[index]
49
+ end
50
+
51
+ def parse_options(args)
52
+ options = {}
53
+
54
+ OptionParser.new do |opts|
55
+ opts.banner = "Usage: pool_siege.rb [options]"
56
+
57
+ opts.on("-t", "--threads N", Integer, "Number of concurrent threads (default: 10)") do |n|
58
+ options[:threads] = n
59
+ end
60
+
61
+ opts.on("-p", "--pool N", Integer, "Connection pool size (default: 5)") do |n|
62
+ options[:pool_size] = n
63
+ end
64
+
65
+ opts.on("-o", "--ops N", Integer, "Operations per thread (default: 100)") do |n|
66
+ options[:operations] = n
67
+ end
68
+
69
+ opts.on("-d", "--duration N", Integer, "Run for N seconds instead of fixed ops") do |n|
70
+ options[:duration] = n
71
+ end
72
+
73
+ opts.on("-s", "--scenario NAME", String, "Test scenario (starvation, rapid_fire, long_transactions, mixed)") do |s|
74
+ options[:scenario] = s.to_sym
75
+ end
76
+
77
+ opts.on("-a", "--accounts N", Integer, "Number of shared accounts for all threads (default: one per thread)") do |n|
78
+ options[:shared_accounts] = n
79
+ end
80
+
81
+ opts.on("--fresh-records", "Create a new account for every operation (tests record creation load)") do
82
+ options[:fresh_records] = true
83
+ end
84
+
85
+ opts.on("-m", "--threading-model MODEL", "Threading model: traditional, fiber, thread_pool, hybrid, actor") do |model|
86
+ valid_models = %w[traditional fiber thread_pool hybrid actor]
87
+ unless valid_models.include?(model)
88
+ puts "Invalid threading model: #{model}. Valid options: #{valid_models.join(', ')}"
89
+ exit 1
90
+ end
91
+ options[:threading_model] = model.to_sym
92
+ end
93
+
94
+ opts.on("-w", "--workload SIZE", "Workload size: tiny (1 field), small (10), medium (100), large (1000), huge (10000)") do |size|
95
+ valid_sizes = %w[tiny small medium large huge]
96
+ unless valid_sizes.include?(size)
97
+ puts "Invalid workload size: #{size}. Valid options: #{valid_sizes.join(', ')}"
98
+ exit 1
99
+ end
100
+ options[:workload_size] = size.to_sym
101
+ end
102
+
103
+ opts.on("--light", "Light load validation (5 threads, 5 pool, 50 ops)") do
104
+ options.merge!(threads: 5, pool_size: 5, operations: 50, scenario: :mixed_workload)
105
+ end
106
+
107
+ opts.on("--quick", "Quick validation (2 threads, 2 pool, 10 ops)") do
108
+ options.merge!(threads: 2, pool_size: 2, operations: 10, scenario: :mixed_workload)
109
+ end
110
+
111
+ opts.on("--stress", "Find breaking point (50 threads, 5 pool, 100 ops)") do
112
+ options.merge!(threads: 50, pool_size: 5, operations: 100, scenario: :pool_starvation)
113
+ end
114
+
115
+ opts.on("--production", "Realistic load (20 threads, 10 pool, 200 ops)") do
116
+ options.merge!(threads: 20, pool_size: 10, duration: 5, scenario: :mixed_workload)
117
+ end
118
+
119
+ opts.on("-q", "--quiet", "Suppress progress, show only final results") do
120
+ options[:quiet] = true
121
+ end
122
+
123
+ opts.on("--profile", "Enable profiling for performance analysis") do
124
+ options[:profile] = true
125
+ end
126
+
127
+ opts.on("--profile-id-generation", "Profile account ID generation (pre-generate vs dynamic)") do
128
+ options[:profile_id_generation] = true
129
+ end
130
+
131
+ opts.on("-h", "--help", "Show this help") do
132
+ puts opts
133
+ puts ""
134
+ puts "Examples:"
135
+ puts " pool_siege.rb --light # Quick validation"
136
+ puts " pool_siege.rb --stress # Find breaking point"
137
+ puts " pool_siege.rb -t 20 -p 5 -o 100 # Custom: 20 threads, 5 pool, 100 ops"
138
+ puts " pool_siege.rb -t 10 -d 30 # Run 10 threads for 30 seconds"
139
+ puts " pool_siege.rb -t 20 -a 3 # 20 threads contending over 3 shared accounts"
140
+ puts " pool_siege.rb --light --fresh-records # Create new account for every operation"
141
+ puts " pool_siege.rb -t 10 -p 5 -w large # Use large metadata payloads (1000 fields)"
142
+ puts " pool_siege.rb -m thread_pool -w huge # Thread pool model with huge payloads"
143
+ puts ""
144
+ puts "Scenarios:"
145
+ puts " starvation - More threads than connections (tests queueing)"
146
+ puts " rapid_fire - Minimal work per connection (tests throughput)"
147
+ puts " long_transactions - Hold connections longer (tests timeout behavior)"
148
+ puts " mixed - Balanced workload (default)"
149
+ puts ""
150
+ puts "Threading Models:"
151
+ puts " traditional - Standard Ruby threads (default)"
152
+ puts " fiber - Cooperative fiber-based concurrency"
153
+ puts " thread_pool - Fixed pool of worker threads"
154
+ puts " hybrid - Threads with fiber-based operations"
155
+ puts " actor - Actor model with message passing"
156
+ exit
157
+ end
158
+ end.parse!(args)
159
+
160
+ # Set defaults
161
+ options[:threads] ||= 10
162
+ options[:pool_size] ||= 5
163
+ options[:operations] ||= 100 unless options[:duration]
164
+ options[:scenario] ||= :mixed_workload
165
+
166
+ options
167
+ end
168
+
169
+ def validate_options
170
+ if @options[:duration] && @options[:operations]
171
+ puts "Error: Cannot specify both --ops and --duration"
172
+ exit 1
173
+ end
174
+
175
+ if @options[:threads] < 1 || @options[:pool_size] < 1
176
+ puts "Error: Threads and pool size must be positive integers"
177
+ exit 1
178
+ end
179
+
180
+ valid_scenarios = [:pool_starvation, :rapid_fire, :long_transactions, :mixed_workload]
181
+ unless valid_scenarios.include?(@options[:scenario])
182
+ puts "Error: Invalid scenario. Valid options: #{valid_scenarios.join(', ')}"
183
+ exit 1
184
+ end
185
+ end
186
+
187
+ def setup_connection_pool
188
+ pool_size = @options[:pool_size]
189
+
190
+ Familia.class_eval do
191
+ @@connection_pool = ConnectionPool.new(
192
+ size: pool_size,
193
+ timeout: 10
194
+ ) do
195
+ Redis.new(url: Familia.uri.to_s)
196
+ end
197
+ end
198
+ end
199
+
200
+ def print_test_description
201
+ scenario_desc = case @options[:scenario]
202
+ when :pool_starvation
203
+ "Connection starvation test"
204
+ when :rapid_fire
205
+ "Rapid-fire operations test"
206
+ when :long_transactions
207
+ "Long-running transactions test"
208
+ when :mixed_workload
209
+ "Mixed workload test"
210
+ end
211
+
212
+ if @options[:duration]
213
+ puts "#{scenario_desc}: #{@options[:threads]} threads using #{@options[:pool_size]} connections for #{@options[:duration]}s"
214
+ else
215
+ total_ops = @options[:threads] * @options[:operations]
216
+ puts "#{scenario_desc}: #{@options[:threads]} threads using #{@options[:pool_size]} connections, #{total_ops} total operations"
217
+ end
218
+
219
+ if @options[:shared_accounts]
220
+ puts "High-contention mode: #{@options[:threads]} threads contending over #{@options[:shared_accounts]} shared accounts"
221
+ elsif @options[:fresh_records]
222
+ puts "Fresh records mode: Creating new account for every operation"
223
+ end
224
+
225
+ if @options[:threading_model] && @options[:threading_model] != :traditional
226
+ puts "Threading model: #{@options[:threading_model]}"
227
+ end
228
+
229
+ if @options[:profile_id_generation]
230
+ puts "ID generation profiling: Using pre-generated account IDs"
231
+ end
232
+
233
+ puts ""
234
+ end
235
+
236
+ def run_silent_test
237
+ test = create_stress_test
238
+ test.run
239
+ @results = test.metrics.summary
240
+ end
241
+
242
+ def run_with_progress
243
+ test = create_stress_test
244
+
245
+ # Hook into the metrics collector to track progress
246
+ total_ops = @options[:duration] ? nil : (@options[:threads] * @options[:operations])
247
+ progress_tracker = ProgressTracker.new(total_ops)
248
+
249
+ # Replace the metrics collector with our tracking version
250
+ original_record = test.metrics.method(:record_operation)
251
+ test.metrics.define_singleton_method(:record_operation) do |type, duration, success, wait_time = nil|
252
+ original_record.call(type, duration, success, wait_time)
253
+ progress_tracker.update(success)
254
+ end
255
+
256
+ # Start the test in a background thread
257
+ test_thread = Thread.new { test.run }
258
+
259
+ # Show progress while test runs
260
+ # progress_tracker.show_progress while test_thread.alive?
261
+ while test_thread.alive?
262
+ progress_tracker.show_progress
263
+ sleep(0.5) # Sleep 100ms between progress updates
264
+ end
265
+
266
+ test_thread.join
267
+
268
+ progress_tracker.finish
269
+ @results = test.metrics.summary
270
+ end
271
+
272
+ def run_with_profiling
273
+ puts "🔍 Running with lightweight performance profiling..."
274
+
275
+ # Simple profiling data collection
276
+ profile_data = {
277
+ operation_times: [],
278
+ connection_wait_times: [],
279
+ database_operation_times: [],
280
+ method_counts: Hash.new(0)
281
+ }
282
+
283
+ # Monkey patch to collect timing data
284
+ original_atomic = Familia.method(:atomic)
285
+ Familia.define_singleton_method(:atomic) do |&block|
286
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
287
+ wait_start = start_time
288
+
289
+ result = nil
290
+ connection_pool.with do |conn|
291
+ wait_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
292
+ profile_data[:connection_wait_times] << (wait_end - wait_start) * 1000
293
+
294
+ op_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
295
+ begin
296
+ self.current_transaction = conn
297
+ result = yield
298
+ ensure
299
+ self.current_transaction = nil
300
+ end
301
+ op_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
302
+ profile_data[:database_operation_times] << (op_end - op_start) * 1000
303
+ end
304
+
305
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
306
+ profile_data[:operation_times] << (end_time - start_time) * 1000
307
+ profile_data[:method_counts][:atomic] += 1
308
+
309
+ result
310
+ end
311
+
312
+ # Run the test
313
+ start_time = Time.now
314
+ run_silent_test
315
+ end_time = Time.now
316
+ total_time = end_time - start_time
317
+
318
+ # Restore original method
319
+ Familia.define_singleton_method(:atomic, original_atomic)
320
+
321
+ # Generate report
322
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
323
+ scenario_name = @options[:scenario].to_s
324
+ report_file = "pool_siege_#{scenario_name}_#{timestamp}_profile.txt"
325
+
326
+ File.open(report_file, 'w') do |f|
327
+ f.puts "Pool Siege Lightweight Profiling Report"
328
+ f.puts "=" * 50
329
+ f.puts "Scenario: #{scenario_name}"
330
+ f.puts "Total Time: #{total_time.round(3)}s"
331
+ f.puts "Total Operations: #{profile_data[:method_counts][:atomic]}"
332
+ f.puts ""
333
+
334
+ if profile_data[:operation_times].any?
335
+ f.puts "Operation Timing (ms):"
336
+ f.puts " Average: #{(profile_data[:operation_times].sum / profile_data[:operation_times].size).round(3)}"
337
+ f.puts " Min: #{profile_data[:operation_times].min.round(3)}"
338
+ f.puts " Max: #{profile_data[:operation_times].max.round(3)}"
339
+ f.puts " P95: #{percentile(profile_data[:operation_times], 0.95).round(3)}"
340
+ f.puts ""
341
+ end
342
+
343
+ if profile_data[:connection_wait_times].any?
344
+ f.puts "Connection Pool Wait Time (ms):"
345
+ f.puts " Average: #{(profile_data[:connection_wait_times].sum / profile_data[:connection_wait_times].size).round(3)}"
346
+ f.puts " Min: #{profile_data[:connection_wait_times].min.round(3)}"
347
+ f.puts " Max: #{profile_data[:connection_wait_times].max.round(3)}"
348
+ f.puts " P95: #{percentile(profile_data[:connection_wait_times], 0.95).round(3)}"
349
+ f.puts ""
350
+ end
351
+
352
+ if profile_data[:database_operation_times].any?
353
+ f.puts "Database Operation Time (ms):"
354
+ f.puts " Average: #{(profile_data[:database_operation_times].sum / profile_data[:database_operation_times].size).round(3)}"
355
+ f.puts " Min: #{profile_data[:database_operation_times].min.round(3)}"
356
+ f.puts " Max: #{profile_data[:database_operation_times].max.round(3)}"
357
+ f.puts " P95: #{percentile(profile_data[:database_operation_times], 0.95).round(3)}"
358
+ end
359
+ end
360
+
361
+ # Show results
362
+ print_final_results(total_time)
363
+
364
+ puts ""
365
+ puts "📊 Lightweight Profiling Complete:"
366
+ puts " Report: #{report_file}"
367
+ puts ""
368
+ puts "💡 View the report with: cat #{report_file}"
369
+ end
370
+
371
+ def create_stress_test
372
+ config = {
373
+ thread_count: @options[:threads],
374
+ operations_per_thread: @options[:operations],
375
+ pool_size: @options[:pool_size],
376
+ pool_timeout: 10,
377
+ duration: @options[:duration],
378
+ operation_mix: :balanced,
379
+ scenario: @options[:scenario],
380
+ shared_accounts: @options[:shared_accounts], # Pass through the shared accounts option
381
+ fresh_records: @options[:fresh_records], # Pass through the fresh records option
382
+ workload_size: @options[:workload_size] || :small, # Default to small workload
383
+ threading_model: @options[:threading_model] || :traditional,
384
+ profile_id_generation: @options[:profile_id_generation],
385
+ worker_pool_size: @options[:threads] / 2, # For thread pool model
386
+ }
387
+
388
+ if @options[:scenario] == :pool_starvation
389
+ # For starvation test, use 2x threads as specified (like the original implementation)
390
+ config[:thread_count] = @options[:pool_size] * 2 if @options[:scenario] == :pool_starvation
391
+ end
392
+
393
+ # Use enhanced version if threading model specified
394
+ if @options[:threading_model] && @options[:threading_model] != :traditional
395
+ require_relative 'lib/connection_pool_threading_models'
396
+ config[:threading_model] = @options[:threading_model]
397
+ EnhancedConnectionPoolStressTest.new(config)
398
+ else
399
+ ConnectionPoolStressTest.new(config)
400
+ end
401
+ end
402
+
403
+ def print_final_results(elapsed_time)
404
+ puts ""
405
+ puts "Connection Pool Load Test Results:"
406
+ puts "Transactions: #{@results[:total_operations]} hits"
407
+ puts "Availability: #{@results[:success_rate]}%"
408
+ puts "Elapsed time: #{'%.2f' % elapsed_time} secs"
409
+ puts "Response time: #{'%.4f' % @results[:avg_duration]} secs"
410
+ puts "Transaction rate: #{'%.2f' % (@results[:total_operations] / elapsed_time)} trans/sec"
411
+ puts "Avg connection wait: #{'%.4f' % @results[:avg_wait_time]} secs"
412
+ puts "Pool utilization: #{'%.1f' % @results[:max_pool_utilization]}%"
413
+ puts "Successful transactions: #{@results[:successful_operations]}"
414
+ puts "Failed transactions: #{@results[:failed_operations]}"
415
+
416
+ if @results[:failed_operations] > 0
417
+ puts ""
418
+ puts "Error breakdown:"
419
+ @results[:errors_by_type].each do |error_type, count|
420
+ puts " #{error_type}: #{count}"
421
+ end
422
+ end
423
+
424
+ # Simple health assessment
425
+ puts ""
426
+ if @results[:success_rate] >= 99.0
427
+ puts "🟢 HEALTHY - Connection pool performing well"
428
+ elsif @results[:success_rate] >= 95.0
429
+ puts "🟡 DEGRADED - Some connection issues detected"
430
+ else
431
+ puts "🔴 CRITICAL - Significant connection pool problems"
432
+ end
433
+ end
434
+ end
435
+
436
+ # Simple progress tracker without external dependencies
437
+ class ProgressTracker
438
+ def initialize(total_ops)
439
+ @total_ops = total_ops
440
+ @completed = 0
441
+ @successful = 0
442
+ @start_time = Time.now
443
+ @last_update = Time.now
444
+ end
445
+
446
+ def update(success)
447
+ @completed += 1
448
+ @successful += 1 if success
449
+ end
450
+
451
+ def show_progress
452
+ return unless should_update?
453
+
454
+ if @total_ops
455
+ show_ops_progress
456
+ else
457
+ show_time_progress
458
+ end
459
+ end
460
+
461
+ def finish
462
+ print "\r" + " " * 80 + "\r" # Clear progress line
463
+ end
464
+
465
+ private
466
+
467
+ def should_update?
468
+ now = Time.now
469
+ return false if (now - @last_update) < 0.5 # Update at most every 500ms
470
+ @last_update = now
471
+ true
472
+ end
473
+
474
+ def show_ops_progress
475
+ percent = (@completed.to_f / @total_ops * 100).round(1)
476
+ success_rate = (@successful.to_f / @completed * 100).round(1) if @completed > 0
477
+ elapsed = Time.now - @start_time
478
+ rate = (@completed / elapsed).round(1) if elapsed > 0
479
+
480
+ bar_width = 20
481
+ filled = [(percent / 100.0 * bar_width).round, bar_width - 1].min
482
+ spaces = [bar_width - filled - 1, 0].max
483
+ bar = "=" * filled + ">" + " " * spaces
484
+
485
+ progress_line = sprintf("\rProgress: [%s] %5.1f%% (%d/%d ops) | Success: %5.1f%% | Rate: %5.1f ops/sec",
486
+ bar, percent, @completed, @total_ops, success_rate || 0.0, rate || 0.0)
487
+
488
+ print progress_line
489
+ end
490
+
491
+ def show_time_progress
492
+ elapsed = Time.now - @start_time
493
+ success_rate = (@successful.to_f / @completed * 100).round(1) if @completed > 0
494
+ rate = (@completed / elapsed).round(1) if elapsed > 0
495
+
496
+ progress_line = sprintf("\rRunning: %5.1fs | Operations: %d | Success: %5.1f%% | Rate: %5.1f ops/sec",
497
+ elapsed, @completed, success_rate || 0.0, rate || 0.0)
498
+
499
+ print progress_line
500
+ end
501
+ end
502
+
503
+ # Initialize Familia and run
504
+ if __FILE__ == $0
505
+ Familia.debug = false
506
+ BankAccount.dbclient.flushdb
507
+
508
+ PoolSiege.new(ARGV).run
509
+ end