familia 1.2.0 → 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,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
|