familia 1.2.3 → 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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +68 -0
  3. data/.github/workflows/docs.yml +64 -0
  4. data/.gitignore +3 -0
  5. data/.pre-commit-config.yaml +3 -1
  6. data/.rubocop.yml +16 -9
  7. data/.rubocop_todo.yml +177 -31
  8. data/.yardopts +9 -0
  9. data/CLAUDE.md +141 -0
  10. data/Gemfile +15 -2
  11. data/Gemfile.lock +61 -61
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +317 -0
  15. data/familia.gemspec +8 -5
  16. data/lib/familia/base.rb +19 -9
  17. data/lib/familia/connection.rb +232 -65
  18. data/lib/familia/core_ext.rb +1 -1
  19. data/lib/familia/datatype/commands.rb +59 -0
  20. data/lib/familia/{redistype → datatype}/serialization.rb +9 -13
  21. data/lib/familia/{redistype → datatype}/types/hashkey.rb +25 -25
  22. data/lib/familia/{redistype → datatype}/types/list.rb +13 -13
  23. data/lib/familia/{redistype → datatype}/types/sorted_set.rb +20 -20
  24. data/lib/familia/{redistype → datatype}/types/string.rb +22 -21
  25. data/lib/familia/{redistype → datatype}/types/unsorted_set.rb +11 -11
  26. data/lib/familia/datatype.rb +243 -0
  27. data/lib/familia/errors.rb +5 -2
  28. data/lib/familia/features/expiration.rb +33 -34
  29. data/lib/familia/features/quantization.rb +9 -3
  30. data/lib/familia/features/safe_dump.rb +2 -3
  31. data/lib/familia/features.rb +2 -2
  32. data/lib/familia/horreum/class_methods.rb +97 -130
  33. data/lib/familia/horreum/commands.rb +46 -51
  34. data/lib/familia/horreum/connection.rb +82 -0
  35. data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
  36. data/lib/familia/horreum/serialization.rb +61 -198
  37. data/lib/familia/horreum/settings.rb +6 -17
  38. data/lib/familia/horreum/utils.rb +11 -10
  39. data/lib/familia/horreum.rb +69 -60
  40. data/lib/familia/logging.rb +12 -12
  41. data/lib/familia/multi_result.rb +72 -0
  42. data/lib/familia/refinements.rb +7 -44
  43. data/lib/familia/settings.rb +11 -11
  44. data/lib/familia/utils.rb +123 -90
  45. data/lib/familia/version.rb +4 -21
  46. data/lib/familia.rb +17 -12
  47. data/lib/middleware/database_middleware.rb +150 -0
  48. data/try/configuration/scenarios_try.rb +65 -0
  49. data/try/core/connection_try.rb +58 -0
  50. data/try/core/errors_try.rb +93 -0
  51. data/try/core/extensions_try.rb +26 -0
  52. data/try/{10_familia_try.rb → core/familia_extended_try.rb} +11 -10
  53. data/try/{00_familia_try.rb → core/familia_try.rb} +5 -3
  54. data/try/core/middleware_try.rb +68 -0
  55. data/try/core/refinements_try.rb +39 -0
  56. data/try/core/settings_try.rb +76 -0
  57. data/try/core/tools_try.rb +54 -0
  58. data/try/core/utils_try.rb +189 -0
  59. data/try/{26_redis_bool_try.rb → datatypes/boolean_try.rb} +4 -2
  60. data/try/datatypes/datatype_base_try.rb +69 -0
  61. data/try/{25_redis_type_hash_try.rb → datatypes/hash_try.rb} +5 -3
  62. data/try/{23_redis_type_list_try.rb → datatypes/list_try.rb} +5 -3
  63. data/try/{22_redis_type_set_try.rb → datatypes/set_try.rb} +5 -3
  64. data/try/{21_redis_type_zset_try.rb → datatypes/sorted_set_try.rb} +6 -4
  65. data/try/{24_redis_type_string_try.rb → datatypes/string_try.rb} +8 -8
  66. data/try/edge_cases/empty_identifiers_try.rb +48 -0
  67. data/try/{92_symbolize_try.rb → edge_cases/hash_symbolization_try.rb} +12 -8
  68. data/try/edge_cases/json_serialization_try.rb +85 -0
  69. data/try/edge_cases/race_conditions_try.rb +60 -0
  70. data/try/edge_cases/reserved_keywords_try.rb +59 -0
  71. data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +63 -60
  72. data/try/edge_cases/ttl_side_effects_try.rb +51 -0
  73. data/try/features/expiration_try.rb +86 -0
  74. data/try/features/quantization_try.rb +90 -0
  75. data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
  76. data/try/features/safe_dump_try.rb +137 -0
  77. data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
  78. data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
  79. data/try/horreum/class_methods_try.rb +41 -0
  80. data/try/horreum/commands_try.rb +49 -0
  81. data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
  82. data/try/horreum/relations_try.rb +146 -0
  83. data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
  84. data/try/horreum/settings_try.rb +43 -0
  85. data/try/integration/cross_component_try.rb +46 -0
  86. data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
  87. data/try/{40_customer_try.rb → models/customer_try.rb} +20 -17
  88. data/try/models/datatype_base_try.rb +101 -0
  89. data/try/{30_familia_object_try.rb → models/familia_object_try.rb} +18 -16
  90. data/try/performance/benchmarks_try.rb +55 -0
  91. data/try/pooling/README.md +20 -0
  92. data/try/pooling/configurable_stress_test_try.rb +435 -0
  93. data/try/pooling/connection_pool_test_try.rb +273 -0
  94. data/try/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  95. data/try/pooling/lib/connection_pool_metrics.rb +372 -0
  96. data/try/pooling/lib/connection_pool_stress_test.rb +959 -0
  97. data/try/pooling/lib/connection_pool_threading_models.rb +421 -0
  98. data/try/pooling/lib/visualize_stress_results.rb +434 -0
  99. data/try/pooling/pool_siege_try.rb +509 -0
  100. data/try/pooling/run_stress_tests_try.rb +482 -0
  101. data/try/prototypes/atomic_saves_v1_context_proxy.rb +121 -0
  102. data/try/prototypes/atomic_saves_v2_connection_switching.rb +161 -0
  103. data/try/prototypes/atomic_saves_v3_connection_pool.rb +189 -0
  104. data/try/prototypes/atomic_saves_v4.rb +105 -0
  105. data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +124 -0
  106. data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +192 -0
  107. metadata +124 -38
  108. data/.github/workflows/ruby.yml +0 -71
  109. data/VERSION.yml +0 -4
  110. data/lib/familia/redistype/commands.rb +0 -59
  111. data/lib/familia/redistype.rb +0 -228
  112. data/lib/familia/tools.rb +0 -68
  113. data/lib/redis_middleware.rb +0 -109
  114. data/try/20_redis_type_try.rb +0 -70
  115. data/try/91_json_bug_try.rb +0 -86
@@ -0,0 +1,20 @@
1
+ # Connection Pool Stress Testing
2
+
3
+ ## Quick Start
4
+ ```bash
5
+ # Basic stress test
6
+ ruby run_stress_tests.rb --config light
7
+
8
+ # Full comparison
9
+ ruby configurable_stress_test.rb --ci
10
+ ```
11
+
12
+ ## Documentation
13
+ - [Basic Usage](docs/README_stress_testing.md)
14
+ - [Advanced Configuration](docs/README_advanced_usage.md)
15
+
16
+ ## Structure
17
+ - `run_stress_tests.rb` - Main test orchestrator
18
+ - `configurable_stress_test.rb` - Configurable test runner
19
+ - `lib/` - Core testing libraries
20
+ - `docs/` - Documentation
@@ -0,0 +1,435 @@
1
+ # try/pooling/configurable_stress_test.rb
2
+ #
3
+ # Configurable Stress Test - Systematic testing using StressTestConfig
4
+ #
5
+ # This class provides methodical approaches to testing specific aspects
6
+ # of the connection pool by using the centralized StressTestConfig.
7
+
8
+ require_relative 'lib/connection_pool_stress_test'
9
+ require_relative 'lib/connection_pool_threading_models'
10
+ require_relative 'lib/connection_pool_metrics'
11
+
12
+ class ConfigurableStressTest
13
+ attr_reader :results_aggregator
14
+
15
+ def initialize
16
+ @results_aggregator = ConnectionPoolMetrics::ResultAggregator.new
17
+ end
18
+
19
+ class << self
20
+ # Generate test matrices for specific testing goals
21
+ def generate_test_matrix(scope = :all)
22
+ case scope
23
+ when :thread_scaling
24
+ # Test how performance scales with thread count
25
+ StressTestConfig::THREAD_COUNTS.map do |thread_count|
26
+ StressTestConfig.merge_and_validate(
27
+ StressTestConfig.default,
28
+ {
29
+ thread_count: thread_count,
30
+ pool_size: [thread_count / 2, 5].max, # Keep pool smaller than threads
31
+ operations_per_thread: 100,
32
+ scenario: :pool_starvation
33
+ }
34
+ )
35
+ end
36
+
37
+ when :pool_sizing
38
+ # Test optimal pool sizes for different workloads
39
+ StressTestConfig::POOL_SIZES.map do |pool_size|
40
+ StressTestConfig.merge_and_validate(
41
+ StressTestConfig.default,
42
+ {
43
+ thread_count: pool_size * 2, # Create pressure
44
+ pool_size: pool_size,
45
+ operations_per_thread: 50,
46
+ scenario: :mixed_workload
47
+ }
48
+ )
49
+ end
50
+
51
+ when :operation_mixes
52
+ # Test all operation patterns
53
+ StressTestConfig::OPERATION_MIXES.keys.map do |mix|
54
+ StressTestConfig.merge_and_validate(
55
+ StressTestConfig.default,
56
+ {
57
+ thread_count: 20,
58
+ pool_size: 10,
59
+ operations_per_thread: 100,
60
+ operation_mix: mix,
61
+ scenario: :mixed_workload
62
+ }
63
+ )
64
+ end
65
+
66
+ when :timeout_behavior
67
+ # Test different timeout scenarios
68
+ StressTestConfig::POOL_TIMEOUTS.map do |timeout|
69
+ StressTestConfig.merge_and_validate(
70
+ StressTestConfig.default,
71
+ {
72
+ thread_count: 50, # High contention
73
+ pool_size: 5, # Small pool
74
+ pool_timeout: timeout,
75
+ operations_per_thread: 20, # Quick to see timeout effects
76
+ scenario: :pool_starvation
77
+ }
78
+ )
79
+ end
80
+
81
+ when :scenario_comparison
82
+ # Test all scenarios with consistent parameters
83
+ StressTestConfig::SCENARIOS.map do |scenario|
84
+ StressTestConfig.merge_and_validate(
85
+ StressTestConfig.default,
86
+ {
87
+ scenario: scenario,
88
+ thread_count: 20,
89
+ pool_size: 10,
90
+ operations_per_thread: 50
91
+ }
92
+ )
93
+ end
94
+
95
+ when :threading_models
96
+ # Test different threading models
97
+ [:traditional, :thread_pool, :fiber, :hybrid].map do |model|
98
+ StressTestConfig.merge_and_validate(
99
+ StressTestConfig.default,
100
+ {
101
+ threading_model: model,
102
+ thread_count: 20,
103
+ pool_size: 10,
104
+ operations_per_thread: 100,
105
+ scenario: :mixed_workload
106
+ }
107
+ )
108
+ end
109
+
110
+ when :high_contention
111
+ # Test extreme contention scenarios
112
+ [
113
+ { thread_count: 100, pool_size: 5, scenario: :pool_starvation },
114
+ { thread_count: 200, pool_size: 10, scenario: :rapid_fire },
115
+ { thread_count: 50, pool_size: 5, scenario: :long_transactions }
116
+ ].map do |config|
117
+ StressTestConfig.merge_and_validate(
118
+ StressTestConfig.default,
119
+ config.merge(operations_per_thread: 50)
120
+ )
121
+ end
122
+
123
+ when :comprehensive
124
+ # Comprehensive test combining multiple dimensions
125
+ configs = []
126
+ StressTestConfig::SCENARIOS.each do |scenario|
127
+ [10, 50].each do |threads|
128
+ [5, 20].each do |pool_size|
129
+ [:balanced, :transaction_heavy].each do |mix|
130
+ configs << StressTestConfig.merge_and_validate(
131
+ StressTestConfig.default,
132
+ {
133
+ scenario: scenario,
134
+ thread_count: threads,
135
+ pool_size: pool_size,
136
+ operation_mix: mix,
137
+ operations_per_thread: 50
138
+ }
139
+ )
140
+ end
141
+ end
142
+ end
143
+ end
144
+ configs
145
+
146
+ else
147
+ raise ArgumentError, "Unknown test matrix scope: #{scope}. Valid: #{valid_scopes.join(', ')}"
148
+ end
149
+ end
150
+
151
+ def valid_scopes
152
+ [:thread_scaling, :pool_sizing, :operation_mixes, :timeout_behavior,
153
+ :scenario_comparison, :threading_models, :high_contention, :comprehensive]
154
+ end
155
+
156
+ # Run targeted tests for a specific scope
157
+ def run_targeted_tests(scope, options = {})
158
+ puts "\n" + "=" * 80
159
+ puts "RUNNING TARGETED TESTS: #{scope.to_s.upcase}"
160
+ puts "=" * 80
161
+
162
+ test_runner = new
163
+ configs = generate_test_matrix(scope)
164
+
165
+ puts "Generated #{configs.size} test configurations"
166
+
167
+ results = test_runner.run_test_matrix(configs, options)
168
+
169
+ puts "\n" + "=" * 80
170
+ puts "TARGETED TEST RESULTS SUMMARY"
171
+ puts "=" * 80
172
+ test_runner.display_matrix_summary(results, scope)
173
+
174
+ results
175
+ end
176
+
177
+ # Quick development tests using StressTestConfig
178
+ def run_development_tests
179
+ config_set = StressTestConfig.for_development
180
+ run_config_set(config_set, "DEVELOPMENT")
181
+ end
182
+
183
+ # CI-appropriate tests
184
+ def run_ci_tests
185
+ config_set = StressTestConfig.for_ci
186
+ run_config_set(config_set, "CI/CD")
187
+ end
188
+
189
+ # Production validation tests
190
+ def run_production_validation_tests
191
+ config_set = StressTestConfig.for_production_validation
192
+ run_config_set(config_set, "PRODUCTION VALIDATION")
193
+ end
194
+
195
+ def run_config_set(config_set, name)
196
+ puts "\n" + "=" * 80
197
+ puts "#{name} TEST SUITE"
198
+ puts "=" * 80
199
+
200
+ test_runner = new
201
+ total_configs = config_set.values.map(&:size).reduce(:*)
202
+ puts "Total test configurations: #{total_configs}"
203
+
204
+ # Generate all combinations
205
+ configs = []
206
+ config_set[:scenarios].each do |scenario|
207
+ config_set[:thread_counts].each do |threads|
208
+ config_set[:pool_sizes].each do |pool_size|
209
+ config_set[:pool_timeouts].each do |timeout|
210
+ config_set[:operation_mixes].each do |mix|
211
+ configs << StressTestConfig.merge_and_validate(
212
+ StressTestConfig.default,
213
+ {
214
+ scenario: scenario,
215
+ thread_count: threads,
216
+ pool_size: pool_size,
217
+ pool_timeout: timeout,
218
+ operation_mix: mix,
219
+ operations_per_thread: config_set[:operations_per_thread].first
220
+ }
221
+ )
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ results = test_runner.run_test_matrix(configs, { verbose: false })
229
+ test_runner.display_matrix_summary(results, name.downcase.to_sym)
230
+
231
+ results
232
+ end
233
+ end
234
+
235
+ # Instance methods for running test matrices
236
+ def run_test_matrix(configs, options = {})
237
+ results = []
238
+ verbose = options.fetch(:verbose, true)
239
+
240
+ configs.each_with_index do |config, index|
241
+ puts "\n[#{index + 1}/#{configs.size}] #{format_config_summary(config)}" if verbose
242
+
243
+ begin
244
+ # Clean database before each test
245
+ BankAccount.dbclient.flushdb
246
+
247
+ # Run the test
248
+ result = run_single_config(config)
249
+ results << { config: config, result: result, success: true }
250
+
251
+ # Add to aggregator
252
+ model_info = { name: config[:threading_model] || 'traditional' }
253
+ @results_aggregator.add_result(config, result[:summary], model_info)
254
+
255
+ # Quick result summary
256
+ if verbose
257
+ puts " ✅ Success: #{result[:summary][:success_rate]}%, " \
258
+ "Duration: #{(result[:summary][:avg_duration] * 1000).round(2)}ms"
259
+ else
260
+ print "."
261
+ end
262
+
263
+ rescue => e
264
+ puts " ❌ Error: #{e.message}" if verbose
265
+ results << { config: config, error: e, success: false }
266
+
267
+ # Record failed test
268
+ error_summary = { success_rate: 0, failed_operations: 1, total_operations: 0 }
269
+ @results_aggregator.add_result(config, error_summary, { error: e.message })
270
+ end
271
+ end
272
+
273
+ puts "" unless verbose # newline after dots
274
+ results
275
+ end
276
+
277
+ def run_single_config(config)
278
+ if config[:threading_model] && config[:threading_model] != :traditional
279
+ # Use enhanced test for non-traditional threading
280
+ test = EnhancedConnectionPoolStressTest.new(config)
281
+ model_info = test.run_with_model(config[:threading_model])
282
+ { summary: test.metrics.summary, model_info: model_info }
283
+ else
284
+ # Use standard test
285
+ test = ConnectionPoolStressTest.new(config)
286
+ test.run
287
+ { summary: test.metrics.summary, model_info: { name: 'traditional' } }
288
+ end
289
+ end
290
+
291
+ def display_matrix_summary(results, scope)
292
+ successful = results.count { |r| r[:success] }
293
+ failed = results.count { |r| !r[:success] }
294
+
295
+ puts "Results: #{successful}/#{results.size} tests passed (#{failed} failed)"
296
+
297
+ if successful > 0
298
+ # Find best and worst performers
299
+ successful_results = results.select { |r| r[:success] }
300
+
301
+ best = successful_results.max_by { |r| r[:result][:summary][:success_rate] }
302
+ worst = successful_results.min_by { |r| r[:result][:summary][:success_rate] }
303
+
304
+ puts "\nBest performer:"
305
+ puts " Config: #{format_config_summary(best[:config])}"
306
+ puts " Results: #{best[:result][:summary][:success_rate]}% success, " \
307
+ "#{(best[:result][:summary][:avg_duration] * 1000).round(2)}ms avg"
308
+
309
+ if best != worst
310
+ puts "\nWorst performer:"
311
+ puts " Config: #{format_config_summary(worst[:config])}"
312
+ puts " Results: #{worst[:result][:summary][:success_rate]}% success, " \
313
+ "#{(worst[:result][:summary][:avg_duration] * 1000).round(2)}ms avg"
314
+ end
315
+
316
+ # Specific insights based on scope
317
+ display_scope_specific_insights(successful_results, scope)
318
+ end
319
+
320
+ if failed > 0
321
+ puts "\nFailed configurations:"
322
+ results.select { |r| !r[:success] }.each do |failure|
323
+ puts " #{format_config_summary(failure[:config])} - #{failure[:error].message}"
324
+ end
325
+ end
326
+ end
327
+
328
+ private
329
+
330
+ def format_config_summary(config)
331
+ parts = []
332
+ parts << "#{config[:scenario]}" if config[:scenario]
333
+ parts << "T#{config[:thread_count]}" if config[:thread_count]
334
+ parts << "P#{config[:pool_size]}" if config[:pool_size]
335
+ parts << "#{config[:operation_mix]}" if config[:operation_mix]
336
+ parts << "#{config[:threading_model]}" if config[:threading_model] && config[:threading_model] != :traditional
337
+ parts.join("/")
338
+ end
339
+
340
+ def display_scope_specific_insights(results, scope)
341
+ case scope
342
+ when :thread_scaling
343
+ puts "\nThread Scaling Insights:"
344
+ thread_performance = results.group_by { |r| r[:config][:thread_count] }
345
+ thread_performance.each do |threads, group|
346
+ avg_success = group.map { |r| r[:result][:summary][:success_rate] }.sum / group.size
347
+ puts " #{threads} threads: #{avg_success.round(1)}% avg success rate"
348
+ end
349
+
350
+ when :pool_sizing
351
+ puts "\nPool Sizing Insights:"
352
+ pool_performance = results.group_by { |r| r[:config][:pool_size] }
353
+ pool_performance.each do |pool_size, group|
354
+ avg_duration = group.map { |r| r[:result][:summary][:avg_duration] }.sum / group.size
355
+ puts " Pool size #{pool_size}: #{(avg_duration * 1000).round(2)}ms avg duration"
356
+ end
357
+
358
+ when :operation_mixes
359
+ puts "\nOperation Mix Insights:"
360
+ mix_performance = results.group_by { |r| r[:config][:operation_mix] }
361
+ mix_performance.each do |mix, group|
362
+ avg_success = group.map { |r| r[:result][:summary][:success_rate] }.sum / group.size
363
+ puts " #{mix}: #{avg_success.round(1)}% avg success rate"
364
+ end
365
+ end
366
+ end
367
+ end
368
+
369
+ # Example usage and CLI runner
370
+ if __FILE__ == $0
371
+ require 'optparse'
372
+
373
+ options = { scope: :thread_scaling, verbose: true }
374
+
375
+ OptionParser.new do |opts|
376
+ opts.banner = "Usage: configurable_stress_test.rb [options]"
377
+
378
+ opts.on("-s", "--scope SCOPE", "Test scope: #{ConfigurableStressTest.valid_scopes.join(', ')}") do |scope|
379
+ options[:scope] = scope.to_sym
380
+ end
381
+
382
+ opts.on("-q", "--quiet", "Quiet output") do
383
+ options[:verbose] = false
384
+ end
385
+
386
+ opts.on("--development", "Run development test suite") do
387
+ options[:preset] = :development
388
+ end
389
+
390
+ opts.on("--ci", "Run CI test suite") do
391
+ options[:preset] = :ci
392
+ end
393
+
394
+ opts.on("--production", "Run production validation suite") do
395
+ options[:preset] = :production_validation
396
+ end
397
+
398
+ opts.on("--list-scopes", "List available test scopes") do
399
+ puts "Available test scopes:"
400
+ ConfigurableStressTest.valid_scopes.each do |scope|
401
+ puts " #{scope}"
402
+ end
403
+ exit
404
+ end
405
+
406
+ opts.on("-h", "--help", "Show this help") do
407
+ puts opts
408
+ exit
409
+ end
410
+ end.parse!
411
+
412
+ # Initialize Familia
413
+ require_relative '../helpers/test_helpers'
414
+ Familia.debug = false
415
+
416
+ if options[:preset]
417
+ case options[:preset]
418
+ when :development
419
+ ConfigurableStressTest.run_development_tests
420
+ when :ci
421
+ ConfigurableStressTest.run_ci_tests
422
+ when :production_validation
423
+ ConfigurableStressTest.run_production_validation_tests
424
+ end
425
+ else
426
+ # Run targeted tests
427
+ unless ConfigurableStressTest.valid_scopes.include?(options[:scope])
428
+ puts "Error: Unknown scope '#{options[:scope]}'"
429
+ puts "Valid scopes: #{ConfigurableStressTest.valid_scopes.join(', ')}"
430
+ exit 1
431
+ end
432
+
433
+ ConfigurableStressTest.run_targeted_tests(options[:scope], options)
434
+ end
435
+ end