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,482 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# try/prototypes/run_stress_tests.rb
|
3
|
+
#
|
4
|
+
# Main Test Runner for Connection Pool Stress Tests
|
5
|
+
#
|
6
|
+
# This script orchestrates comprehensive stress testing of the connection pool
|
7
|
+
# implementation across different scenarios, threading models, and configurations.
|
8
|
+
# It generates detailed reports and comparisons to identify bottlenecks and
|
9
|
+
# failure modes.
|
10
|
+
|
11
|
+
require 'optparse'
|
12
|
+
require 'fileutils'
|
13
|
+
|
14
|
+
require_relative 'lib/connection_pool_stress_test'
|
15
|
+
require_relative 'lib/connection_pool_threading_models'
|
16
|
+
require_relative 'lib/connection_pool_metrics'
|
17
|
+
require_relative 'lib/visualize_stress_results'
|
18
|
+
|
19
|
+
class StressTestRunner
|
20
|
+
# Updated to use StressTestConfig systematically
|
21
|
+
PREDEFINED_CONFIGS = {
|
22
|
+
light: StressTestConfig.for_development,
|
23
|
+
moderate: StressTestConfig.for_ci,
|
24
|
+
heavy: StressTestConfig.for_production_validation,
|
25
|
+
extreme: StressTestConfig.for_bottleneck_analysis,
|
26
|
+
tuning: StressTestConfig.for_performance_tuning
|
27
|
+
}
|
28
|
+
|
29
|
+
def initialize(options = {})
|
30
|
+
@options = {
|
31
|
+
config_set: :moderate,
|
32
|
+
output_dir: "stress_test_results_#{Time.now.strftime('%Y%m%d_%H%M%S')}",
|
33
|
+
threading_models: [:traditional, :thread_pool, :fiber],
|
34
|
+
operation_mixes: [:balanced, :read_heavy, :write_heavy],
|
35
|
+
generate_visualizations: true,
|
36
|
+
verbose: false,
|
37
|
+
use_runtime_config: false
|
38
|
+
}.merge(options)
|
39
|
+
|
40
|
+
# Use runtime config from environment if requested
|
41
|
+
if @options[:use_runtime_config]
|
42
|
+
runtime_overrides = StressTestConfig.runtime_config
|
43
|
+
puts "Using runtime configuration overrides from environment" if @options[:verbose]
|
44
|
+
@config_set = runtime_overrides
|
45
|
+
else
|
46
|
+
@config_set = PREDEFINED_CONFIGS[@options[:config_set]]
|
47
|
+
raise ArgumentError, "Unknown config set: #{@options[:config_set]}" unless @config_set
|
48
|
+
end
|
49
|
+
|
50
|
+
@results_aggregator = ConnectionPoolMetrics::ResultAggregator.new
|
51
|
+
|
52
|
+
setup_output_directory
|
53
|
+
end
|
54
|
+
|
55
|
+
def run_all_tests
|
56
|
+
puts "=" * 80
|
57
|
+
puts "CONNECTION POOL STRESS TEST SUITE"
|
58
|
+
puts "=" * 80
|
59
|
+
puts "Configuration: #{@options[:config_set]}"
|
60
|
+
puts "Output directory: #{@options[:output_dir]}"
|
61
|
+
puts "Threading models: #{@options[:threading_models].join(', ')}"
|
62
|
+
puts "=" * 80
|
63
|
+
|
64
|
+
total_tests = calculate_total_tests(@config_set)
|
65
|
+
current_test = 0
|
66
|
+
|
67
|
+
puts "Total tests to run: #{total_tests}"
|
68
|
+
puts ""
|
69
|
+
|
70
|
+
start_time = Time.now
|
71
|
+
|
72
|
+
@config_set[:scenarios].each do |scenario|
|
73
|
+
puts "\n--- Testing Scenario: #{scenario} ---"
|
74
|
+
|
75
|
+
@config_set[:thread_counts].each do |thread_count|
|
76
|
+
@config_set[:operations_per_thread].each do |ops_per_thread|
|
77
|
+
@config_set[:pool_sizes].each do |pool_size|
|
78
|
+
@config_set[:pool_timeouts].each do |pool_timeout|
|
79
|
+
@options[:operation_mixes].each do |operation_mix|
|
80
|
+
@options[:threading_models].each do |threading_model|
|
81
|
+
current_test += 1
|
82
|
+
|
83
|
+
test_config = StressTestConfig.merge_and_validate(
|
84
|
+
StressTestConfig.default,
|
85
|
+
{
|
86
|
+
thread_count: thread_count,
|
87
|
+
operations_per_thread: ops_per_thread,
|
88
|
+
pool_size: pool_size,
|
89
|
+
pool_timeout: pool_timeout,
|
90
|
+
operation_mix: operation_mix,
|
91
|
+
scenario: scenario,
|
92
|
+
threading_model: threading_model
|
93
|
+
}
|
94
|
+
)
|
95
|
+
|
96
|
+
puts sprintf("[%d/%d] Running test: %s",
|
97
|
+
current_test, total_tests, format_test_config(test_config))
|
98
|
+
|
99
|
+
run_single_test(test_config)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
duration = Time.now - start_time
|
109
|
+
puts "\n" + "=" * 80
|
110
|
+
puts "ALL TESTS COMPLETED"
|
111
|
+
puts "Total duration: #{format_duration(duration)}"
|
112
|
+
puts "Results saved to: #{@options[:output_dir]}"
|
113
|
+
|
114
|
+
generate_final_reports
|
115
|
+
|
116
|
+
puts "=" * 80
|
117
|
+
end
|
118
|
+
|
119
|
+
def run_single_test(config)
|
120
|
+
# Clean database
|
121
|
+
BankAccount.dbclient.flushdb
|
122
|
+
|
123
|
+
begin
|
124
|
+
if config[:threading_model] == :traditional
|
125
|
+
# Use original stress test for traditional threading
|
126
|
+
test = ConnectionPoolStressTest.new(config)
|
127
|
+
test.run
|
128
|
+
metrics_summary = test.metrics.summary
|
129
|
+
model_info = { name: 'traditional', details: {} }
|
130
|
+
else
|
131
|
+
# Use enhanced test for other threading models
|
132
|
+
test = EnhancedConnectionPoolStressTest.new(config)
|
133
|
+
model_info = test.run_with_model(config[:threading_model])
|
134
|
+
metrics_summary = test.metrics.summary
|
135
|
+
end
|
136
|
+
|
137
|
+
# Save detailed results
|
138
|
+
save_test_results(config, test.metrics, model_info)
|
139
|
+
|
140
|
+
# Add to aggregator
|
141
|
+
@results_aggregator.add_result(config, metrics_summary, model_info)
|
142
|
+
|
143
|
+
# Print summary if verbose
|
144
|
+
if @options[:verbose]
|
145
|
+
puts " Success rate: #{metrics_summary[:success_rate]}%"
|
146
|
+
puts " Avg duration: #{(metrics_summary[:avg_duration] * 1000).round(2)}ms"
|
147
|
+
puts " Errors: #{metrics_summary[:failed_operations]}"
|
148
|
+
end
|
149
|
+
|
150
|
+
return true
|
151
|
+
|
152
|
+
rescue => e
|
153
|
+
puts " ERROR: #{e.message}"
|
154
|
+
puts " #{e.backtrace.first}" if @options[:verbose]
|
155
|
+
|
156
|
+
# Record failed test
|
157
|
+
error_info = {
|
158
|
+
error: e.class.name,
|
159
|
+
message: e.message,
|
160
|
+
backtrace: e.backtrace.first(5)
|
161
|
+
}
|
162
|
+
|
163
|
+
@results_aggregator.add_result(
|
164
|
+
config,
|
165
|
+
{ success_rate: 0, failed_operations: 1, total_operations: 0 },
|
166
|
+
{ name: config[:threading_model], error: error_info }
|
167
|
+
)
|
168
|
+
|
169
|
+
return false
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def setup_output_directory
|
176
|
+
FileUtils.mkdir_p(@options[:output_dir])
|
177
|
+
FileUtils.mkdir_p(File.join(@options[:output_dir], 'individual_tests'))
|
178
|
+
|
179
|
+
# Create README
|
180
|
+
readme_content = generate_readme
|
181
|
+
File.write(File.join(@options[:output_dir], 'README.md'), readme_content)
|
182
|
+
end
|
183
|
+
|
184
|
+
def calculate_total_tests(config_set)
|
185
|
+
config_set[:scenarios].size *
|
186
|
+
config_set[:thread_counts].size *
|
187
|
+
config_set[:operations_per_thread].size *
|
188
|
+
config_set[:pool_sizes].size *
|
189
|
+
config_set[:pool_timeouts].size *
|
190
|
+
@options[:operation_mixes].size *
|
191
|
+
@options[:threading_models].size
|
192
|
+
end
|
193
|
+
|
194
|
+
def format_test_config(config)
|
195
|
+
"#{config[:threading_model]}/#{config[:scenario]}/T#{config[:thread_count]}/O#{config[:operations_per_thread]}/P#{config[:pool_size]}/#{config[:operation_mix]}"
|
196
|
+
end
|
197
|
+
|
198
|
+
def format_duration(seconds)
|
199
|
+
if seconds < 60
|
200
|
+
"#{seconds.round(2)}s"
|
201
|
+
elsif seconds < 3600
|
202
|
+
"#{(seconds / 60).round(2)}m"
|
203
|
+
else
|
204
|
+
"#{(seconds / 3600).round(2)}h"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def save_test_results(config, metrics, model_info)
|
209
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%L')
|
210
|
+
test_id = "#{config[:threading_model]}_#{config[:scenario]}_#{timestamp}"
|
211
|
+
|
212
|
+
# Export detailed CSV files
|
213
|
+
if metrics.respond_to?(:export_detailed_csv)
|
214
|
+
csv_prefix = File.join(@options[:output_dir], 'individual_tests', test_id)
|
215
|
+
metrics.export_detailed_csv(csv_prefix)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Save test configuration and results
|
219
|
+
test_data = {
|
220
|
+
timestamp: Time.now,
|
221
|
+
config: config,
|
222
|
+
model_info: model_info,
|
223
|
+
summary: metrics.respond_to?(:detailed_summary) ? metrics.detailed_summary : metrics.summary
|
224
|
+
}
|
225
|
+
|
226
|
+
File.write(
|
227
|
+
File.join(@options[:output_dir], 'individual_tests', "#{test_id}_config.json"),
|
228
|
+
JSON.pretty_generate(test_data)
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
def generate_final_reports
|
233
|
+
puts "\nGenerating final reports..."
|
234
|
+
|
235
|
+
# Export aggregated comparison
|
236
|
+
comparison_file = File.join(@options[:output_dir], 'comparison_results.csv')
|
237
|
+
@results_aggregator.export_comparison_csv(comparison_file)
|
238
|
+
|
239
|
+
# Generate comparison report
|
240
|
+
comparison_report = @results_aggregator.generate_comparison_report
|
241
|
+
File.write(File.join(@options[:output_dir], 'comparison_report.md'), comparison_report)
|
242
|
+
|
243
|
+
# Generate visualizations if requested
|
244
|
+
if @options[:generate_visualizations]
|
245
|
+
generate_visualizations(comparison_file)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Create executive summary
|
249
|
+
executive_summary = generate_executive_summary
|
250
|
+
File.write(File.join(@options[:output_dir], 'executive_summary.md'), executive_summary)
|
251
|
+
|
252
|
+
puts "Reports generated:"
|
253
|
+
puts " - comparison_results.csv"
|
254
|
+
puts " - comparison_report.md"
|
255
|
+
puts " - executive_summary.md"
|
256
|
+
puts " - visualization_report.md" if @options[:generate_visualizations]
|
257
|
+
end
|
258
|
+
|
259
|
+
def generate_visualizations(comparison_file)
|
260
|
+
visualizer = StressTestVisualizer.new([comparison_file])
|
261
|
+
report = visualizer.generate_report
|
262
|
+
|
263
|
+
File.write(File.join(@options[:output_dir], 'visualization_report.md'), report)
|
264
|
+
end
|
265
|
+
|
266
|
+
def generate_readme
|
267
|
+
<<~README
|
268
|
+
# Connection Pool Stress Test Results
|
269
|
+
|
270
|
+
Generated: #{Time.now}
|
271
|
+
Configuration: #{@options[:config_set]}
|
272
|
+
|
273
|
+
## Directory Structure
|
274
|
+
|
275
|
+
- `comparison_results.csv` - Aggregated comparison data
|
276
|
+
- `comparison_report.md` - Analysis of all test configurations
|
277
|
+
- `executive_summary.md` - High-level summary and recommendations
|
278
|
+
- `visualization_report.md` - Charts and graphs (if generated)
|
279
|
+
- `individual_tests/` - Detailed results for each test run
|
280
|
+
|
281
|
+
## Test Configuration
|
282
|
+
|
283
|
+
- **Threading models tested**: #{@options[:threading_models].join(', ')}
|
284
|
+
- **Operation mixes tested**: #{@options[:operation_mixes].join(', ')}
|
285
|
+
- **Scenarios covered**: #{PREDEFINED_CONFIGS[@options[:config_set]][:scenarios].join(', ')}
|
286
|
+
|
287
|
+
## How to Analyze Results
|
288
|
+
|
289
|
+
1. Start with `executive_summary.md` for key findings
|
290
|
+
2. Review `comparison_report.md` for detailed analysis
|
291
|
+
3. Check `visualization_report.md` for charts
|
292
|
+
4. Examine individual test files in `individual_tests/` for deep dives
|
293
|
+
|
294
|
+
## Reproducing Tests
|
295
|
+
|
296
|
+
To reproduce these tests, run:
|
297
|
+
|
298
|
+
```bash
|
299
|
+
ruby run_stress_tests.rb --config #{@options[:config_set]} --output #{@options[:output_dir]}
|
300
|
+
```
|
301
|
+
README
|
302
|
+
end
|
303
|
+
|
304
|
+
def generate_executive_summary
|
305
|
+
summary = <<~SUMMARY
|
306
|
+
# Executive Summary - Connection Pool Stress Testing
|
307
|
+
|
308
|
+
**Generated**: #{Time.now}
|
309
|
+
**Test Configuration**: #{@options[:config_set]}
|
310
|
+
|
311
|
+
## Key Findings
|
312
|
+
|
313
|
+
*[This would be populated with actual analysis results in a real implementation]*
|
314
|
+
|
315
|
+
### Performance Highlights
|
316
|
+
|
317
|
+
- **Best performing threading model**: *TBD based on results*
|
318
|
+
- **Most reliable configuration**: *TBD based on results*
|
319
|
+
- **Recommended pool size**: *TBD based on results*
|
320
|
+
|
321
|
+
### Identified Issues
|
322
|
+
|
323
|
+
- **Connection starvation threshold**: *TBD*
|
324
|
+
- **Error patterns**: *TBD*
|
325
|
+
- **Performance bottlenecks**: *TBD*
|
326
|
+
|
327
|
+
## Recommendations
|
328
|
+
|
329
|
+
1. **Production Configuration**:
|
330
|
+
- Pool size: *TBD*
|
331
|
+
- Timeout: *TBD*
|
332
|
+
- Threading model: *TBD*
|
333
|
+
|
334
|
+
2. **Monitoring**:
|
335
|
+
- Watch for pool utilization > X%
|
336
|
+
- Alert on connection wait times > X seconds
|
337
|
+
- Monitor error rates by operation type
|
338
|
+
|
339
|
+
3. **Future Testing**:
|
340
|
+
- Test with production-like workloads
|
341
|
+
- Validate under network latency
|
342
|
+
- Test failover scenarios
|
343
|
+
|
344
|
+
## Files for Deep Dive
|
345
|
+
|
346
|
+
- `comparison_results.csv` - Raw performance data
|
347
|
+
- `visualization_report.md` - Performance charts
|
348
|
+
- `individual_tests/` - Detailed test results
|
349
|
+
SUMMARY
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# Command-line interface
|
354
|
+
if __FILE__ == $0
|
355
|
+
options = {}
|
356
|
+
|
357
|
+
OptionParser.new do |opts|
|
358
|
+
opts.banner = "Usage: run_stress_tests.rb [options]"
|
359
|
+
|
360
|
+
opts.on("-c", "--config CONFIG", "Test configuration: light, moderate, heavy, extreme, tuning") do |config|
|
361
|
+
options[:config_set] = config.to_sym
|
362
|
+
end
|
363
|
+
|
364
|
+
opts.on("-o", "--output DIR", "Output directory") do |dir|
|
365
|
+
options[:output_dir] = dir
|
366
|
+
end
|
367
|
+
|
368
|
+
opts.on("-m", "--models MODELS", "Threading models (comma-separated): traditional,fiber,thread_pool,hybrid,actor") do |models|
|
369
|
+
options[:threading_models] = models.split(',').map(&:strip).map(&:to_sym)
|
370
|
+
end
|
371
|
+
|
372
|
+
opts.on("-x", "--mixes MIXES", "Operation mixes (comma-separated): balanced,read_heavy,write_heavy,transaction_heavy") do |mixes|
|
373
|
+
options[:operation_mixes] = mixes.split(',').map(&:strip).map(&:to_sym)
|
374
|
+
end
|
375
|
+
|
376
|
+
opts.on("-v", "--verbose", "Verbose output") do
|
377
|
+
options[:verbose] = true
|
378
|
+
end
|
379
|
+
|
380
|
+
opts.on("--no-visualizations", "Skip visualization generation") do
|
381
|
+
options[:generate_visualizations] = false
|
382
|
+
end
|
383
|
+
|
384
|
+
opts.on("--runtime-config", "Use configuration from environment variables") do
|
385
|
+
options[:use_runtime_config] = true
|
386
|
+
end
|
387
|
+
|
388
|
+
opts.on("--validate-config", "Validate configuration and show warnings") do
|
389
|
+
options[:validate_only] = true
|
390
|
+
end
|
391
|
+
|
392
|
+
opts.on("--list-configs", "List available configurations") do
|
393
|
+
puts "Available configurations:"
|
394
|
+
StressTestRunner::PREDEFINED_CONFIGS.each do |name, config|
|
395
|
+
puts " #{name}: #{config[:scenarios].join(', ')}"
|
396
|
+
end
|
397
|
+
puts "\nEnvironment variables for runtime config:"
|
398
|
+
puts " STRESS_THREADS=5,10,20 - Thread counts to test"
|
399
|
+
puts " STRESS_OPS=50,100 - Operations per thread"
|
400
|
+
puts " STRESS_POOLS=5,10,20 - Pool sizes to test"
|
401
|
+
puts " STRESS_TIMEOUTS=5,10 - Pool timeouts (seconds)"
|
402
|
+
puts " STRESS_SCENARIOS=rapid_fire,mixed_workload - Scenarios to run"
|
403
|
+
puts " STRESS_MIXES=balanced,read_heavy - Operation mixes"
|
404
|
+
exit
|
405
|
+
end
|
406
|
+
|
407
|
+
opts.on("--list-scenarios", "List available test scenarios") do
|
408
|
+
puts "Available test scenarios:"
|
409
|
+
StressTestConfig::SCENARIOS.each do |scenario|
|
410
|
+
puts " #{scenario}"
|
411
|
+
end
|
412
|
+
exit
|
413
|
+
end
|
414
|
+
|
415
|
+
opts.on("-h", "--help", "Show this help") do
|
416
|
+
puts opts
|
417
|
+
puts "\nExamples:"
|
418
|
+
puts " # Quick development test"
|
419
|
+
puts " ruby run_stress_tests.rb --config light --verbose"
|
420
|
+
puts ""
|
421
|
+
puts " # Use environment configuration"
|
422
|
+
puts " STRESS_THREADS=10,50 STRESS_POOLS=5,10 ruby run_stress_tests.rb --runtime-config"
|
423
|
+
puts ""
|
424
|
+
puts " # Validate a configuration"
|
425
|
+
puts " ruby run_stress_tests.rb --config extreme --validate-config"
|
426
|
+
exit
|
427
|
+
end
|
428
|
+
end.parse!
|
429
|
+
|
430
|
+
# Validate configuration
|
431
|
+
if options[:config_set] && !StressTestRunner::PREDEFINED_CONFIGS.key?(options[:config_set])
|
432
|
+
puts "Error: Unknown configuration '#{options[:config_set]}'"
|
433
|
+
puts "Available: #{StressTestRunner::PREDEFINED_CONFIGS.keys.join(', ')}"
|
434
|
+
exit 1
|
435
|
+
end
|
436
|
+
|
437
|
+
# Initialize Familia
|
438
|
+
require_relative '../helpers/test_helpers'
|
439
|
+
Familia.debug = false
|
440
|
+
|
441
|
+
# Handle validation-only mode
|
442
|
+
if options[:validate_only]
|
443
|
+
puts "Validating configuration: #{options[:config_set] || 'runtime'}"
|
444
|
+
|
445
|
+
if options[:use_runtime_config]
|
446
|
+
config = StressTestConfig.runtime_config
|
447
|
+
puts "Runtime configuration from environment:"
|
448
|
+
config.each do |key, value|
|
449
|
+
puts " #{key}: #{value.join(', ')}"
|
450
|
+
end
|
451
|
+
else
|
452
|
+
config_set = options[:config_set] || :moderate
|
453
|
+
config = StressTestRunner::PREDEFINED_CONFIGS[config_set]
|
454
|
+
puts "Predefined configuration: #{config_set}"
|
455
|
+
config.each do |key, value|
|
456
|
+
puts " #{key}: #{value.join(', ')}"
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Sample validation
|
461
|
+
sample_config = StressTestConfig.merge_and_validate(
|
462
|
+
StressTestConfig.default,
|
463
|
+
{
|
464
|
+
thread_count: 20,
|
465
|
+
pool_size: 10,
|
466
|
+
operations_per_thread: 100,
|
467
|
+
scenario: :mixed_workload
|
468
|
+
}
|
469
|
+
)
|
470
|
+
|
471
|
+
puts "\nSample configuration validation passed ✅"
|
472
|
+
exit
|
473
|
+
end
|
474
|
+
|
475
|
+
puts "Initializing stress test runner..."
|
476
|
+
runner = StressTestRunner.new(options)
|
477
|
+
|
478
|
+
puts "Starting stress test suite..."
|
479
|
+
runner.run_all_tests
|
480
|
+
|
481
|
+
puts "\nStress test suite completed successfully!"
|
482
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# try/prototypes/atomic_saves_v1_context_proxy.rb
|
2
|
+
|
3
|
+
# try -vf try/prototypes/atomic_saves_v1_context_proxy.rb
|
4
|
+
|
5
|
+
# ⏺ 🎉 Perfect! All Tests Pass!
|
6
|
+
#
|
7
|
+
# ✅ Complete Neutralization Confirmed
|
8
|
+
#
|
9
|
+
# The intervention test successfully demonstrates that the Context-Aware Database Proxy directly neutralizes the tight coupling mechanism:
|
10
|
+
#
|
11
|
+
# 📈 Final Results: 5/5 Tests Passed
|
12
|
+
#
|
13
|
+
# 1. ✅ Baseline behavior shows immediate execution (coupled)
|
14
|
+
# - Database command count increases when no atomic context
|
15
|
+
# 2. ✅ Context-aware proxy queues commands instead of executing
|
16
|
+
# - Commands return :queued when Fiber[:atomic_context] is set
|
17
|
+
# - Database command count remains unchanged (neutralized!)
|
18
|
+
# 3. ✅ Queued commands can be executed later
|
19
|
+
# - Deferred execution works perfectly
|
20
|
+
# - Field exists in Database after execution
|
21
|
+
# 4. ✅ Proxy logs all method calls regardless of execution context
|
22
|
+
# - Call tracking works in both modes
|
23
|
+
# 5. ✅ Atomic context can be cleared
|
24
|
+
# - Fiber-local storage management works
|
25
|
+
#
|
26
|
+
# 🔓 Mechanism Successfully Unlocked
|
27
|
+
#
|
28
|
+
# The exact trigger point where dbclient.method_name() is called now responds to execution context:
|
29
|
+
#
|
30
|
+
# - Without context: Immediate execution (preserves existing behavior)
|
31
|
+
# - With atomic context: Command queuing (enables atomic operations)
|
32
|
+
#
|
33
|
+
# The tight coupling is broken. Context-aware atomic operations spanning multiple objects and keys are now achievable through this proxy
|
34
|
+
# pattern, proving the neutralization intervention works as designed.
|
35
|
+
#
|
36
|
+
# You were absolutely right about the @bone.delete! causing tryouts issues!
|
37
|
+
|
38
|
+
require_relative '../helpers/test_helpers'
|
39
|
+
|
40
|
+
|
41
|
+
# Minimal Context-Aware Database Proxy
|
42
|
+
# Tests whether the tight coupling between method invocation and Database execution
|
43
|
+
# can be neutralized through context-aware command dispatch
|
44
|
+
class ContextAwareRedisProxy
|
45
|
+
def initialize(database_connection)
|
46
|
+
@dbclient = database_connection
|
47
|
+
@call_log = []
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_reader :call_log
|
51
|
+
|
52
|
+
def method_missing(method, *args, **kwargs)
|
53
|
+
@call_log << "#{method}(#{args.join(', ')})"
|
54
|
+
|
55
|
+
if Fiber[:atomic_context]
|
56
|
+
# NEUTRALIZED: Queue instead of execute
|
57
|
+
Fiber[:atomic_context] << { method: method, args: args, kwargs: kwargs }
|
58
|
+
return :queued
|
59
|
+
else
|
60
|
+
# COUPLED: Execute immediately
|
61
|
+
@dbclient.send(method, *args, **kwargs)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def respond_to_missing?(method, include_private = false)
|
66
|
+
@dbclient.respond_to?(method, include_private) || super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Test class that uses the proxy
|
71
|
+
class ContextProxyBone < Bone
|
72
|
+
def dbclient
|
73
|
+
@proxy ||= ContextAwareRedisProxy.new(super)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
Familia.connect # Important, it registers DatabaseCommandCounter
|
79
|
+
|
80
|
+
@bone = Bone.new('test123', 'test')
|
81
|
+
@proxy = ContextAwareRedisProxy.new(@bone.dbclient)
|
82
|
+
@bone.delete! # Causes tryouts issues
|
83
|
+
|
84
|
+
## Baseline behavior shows immediate execution (coupled)
|
85
|
+
command_count_before = DatabaseCommandCounter.count
|
86
|
+
@proxy.hset(@bone.dbkey, 'test_field', 'test_value')
|
87
|
+
command_count_after = DatabaseCommandCounter.count
|
88
|
+
command_count_after > command_count_before
|
89
|
+
#=> true
|
90
|
+
|
91
|
+
## Context-aware proxy queues commands instead of executing
|
92
|
+
@proxy.call_log.clear
|
93
|
+
Fiber[:atomic_context] = []
|
94
|
+
command_count_before = DatabaseCommandCounter.count
|
95
|
+
result = @proxy.hset(@bone.dbkey, 'test_field2', 'test_value2')
|
96
|
+
command_count_after = DatabaseCommandCounter.count
|
97
|
+
[result, command_count_after == command_count_before, Fiber[:atomic_context].size > 0]
|
98
|
+
#=> [:queued, true, true]
|
99
|
+
|
100
|
+
## Queued commands can be executed later
|
101
|
+
command_count_before = DatabaseCommandCounter.count
|
102
|
+
Fiber[:atomic_context].each do |cmd|
|
103
|
+
@bone.dbclient.send(cmd[:method], *cmd[:args], **cmd[:kwargs])
|
104
|
+
end
|
105
|
+
command_count_after = DatabaseCommandCounter.count
|
106
|
+
executed = command_count_after > command_count_before
|
107
|
+
field_exists = @bone.dbclient.hexists(@bone.dbkey, 'test_field2')
|
108
|
+
[executed, field_exists]
|
109
|
+
#=> [true, true]
|
110
|
+
|
111
|
+
## Proxy logs all method calls regardless of execution context
|
112
|
+
@proxy.call_log.size >= 1
|
113
|
+
#=> true
|
114
|
+
|
115
|
+
## Atomic context can be cleared
|
116
|
+
Fiber[:atomic_context] = nil
|
117
|
+
Fiber[:atomic_context]
|
118
|
+
#=> nil
|
119
|
+
|
120
|
+
# Cleanup
|
121
|
+
@bone.clear
|