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.
- 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 +15 -2
- data/Gemfile.lock +76 -34
- data/README.md +39 -23
- data/bin/irb +3 -0
- data/docs/connection_pooling.md +317 -0
- data/familia.gemspec +9 -5
- 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 +17 -12
- 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} +5 -3
- 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} +20 -17
- data/try/models/datatype_base_try.rb +101 -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 +140 -43
- 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,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
|