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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +68 -0
  3. data/.github/workflows/docs.yml +64 -0
  4. data/.gitignore +4 -0
  5. data/.pre-commit-config.yaml +3 -1
  6. data/.rubocop.yml +16 -9
  7. data/.rubocop_todo.yml +177 -31
  8. data/.yardopts +9 -0
  9. data/CLAUDE.md +141 -0
  10. data/Gemfile +15 -2
  11. data/Gemfile.lock +76 -34
  12. data/README.md +39 -23
  13. data/bin/irb +3 -0
  14. data/docs/connection_pooling.md +317 -0
  15. data/familia.gemspec +9 -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 -110
  33. data/lib/familia/horreum/commands.rb +46 -51
  34. data/lib/familia/horreum/connection.rb +82 -0
  35. data/lib/familia/horreum/{relations_management.rb → related_fields_management.rb} +37 -35
  36. data/lib/familia/horreum/serialization.rb +61 -198
  37. data/lib/familia/horreum/settings.rb +6 -17
  38. data/lib/familia/horreum/utils.rb +11 -10
  39. data/lib/familia/horreum.rb +69 -60
  40. data/lib/familia/logging.rb +12 -12
  41. data/lib/familia/multi_result.rb +72 -0
  42. data/lib/familia/refinements.rb +7 -44
  43. data/lib/familia/settings.rb +11 -11
  44. data/lib/familia/utils.rb +123 -90
  45. data/lib/familia/version.rb +4 -21
  46. data/lib/familia.rb +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 -7
  68. data/try/edge_cases/json_serialization_try.rb +85 -0
  69. data/try/edge_cases/race_conditions_try.rb +60 -0
  70. data/try/edge_cases/reserved_keywords_try.rb +59 -0
  71. data/try/{93_string_coercion_try.rb → edge_cases/string_coercion_try.rb} +60 -59
  72. data/try/edge_cases/ttl_side_effects_try.rb +51 -0
  73. data/try/features/expiration_try.rb +86 -0
  74. data/try/features/quantization_try.rb +90 -0
  75. data/try/{35_feature_safedump_try.rb → features/safe_dump_advanced_try.rb} +7 -6
  76. data/try/features/safe_dump_try.rb +137 -0
  77. data/try/{test_helpers.rb → helpers/test_helpers.rb} +25 -60
  78. data/try/{27_redis_horreum_try.rb → horreum/base_try.rb} +39 -14
  79. data/try/horreum/class_methods_try.rb +41 -0
  80. data/try/horreum/commands_try.rb +49 -0
  81. data/try/{29_redis_horreum_initialization_try.rb → horreum/initialization_try.rb} +9 -7
  82. data/try/horreum/relations_try.rb +146 -0
  83. data/try/{28_redis_horreum_serialization_try.rb → horreum/serialization_try.rb} +13 -11
  84. data/try/horreum/settings_try.rb +43 -0
  85. data/try/integration/cross_component_try.rb +46 -0
  86. data/try/{41_customer_safedump_try.rb → models/customer_safe_dump_try.rb} +9 -7
  87. data/try/{40_customer_try.rb → models/customer_try.rb} +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 +140 -43
  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,434 @@
1
+ #!/usr/bin/env ruby
2
+ # try/prototypes/visualize_stress_results.rb
3
+ #
4
+ # Simple Visualization Tool for Connection Pool Stress Test Results
5
+ #
6
+ # This script reads CSV output from stress tests and generates:
7
+ # - ASCII charts for terminal display
8
+ # - Markdown-formatted reports
9
+ # - Comparison tables
10
+ # - Performance trend analysis
11
+
12
+ require 'csv'
13
+ require 'optparse'
14
+
15
+ class StressTestVisualizer
16
+ def initialize(csv_files = [])
17
+ @csv_files = csv_files
18
+ @data = {}
19
+ load_data
20
+ end
21
+
22
+ def load_data
23
+ @csv_files.each do |file|
24
+ next unless File.exist?(file)
25
+
26
+ type = detect_csv_type(file)
27
+ @data[type] ||= []
28
+
29
+ CSV.foreach(file, headers: true) do |row|
30
+ @data[type] << row.to_h
31
+ end
32
+ end
33
+ end
34
+
35
+ def detect_csv_type(filename)
36
+ case filename
37
+ when /operations/ then :operations
38
+ when /errors/ then :errors
39
+ when /pool_stats/ then :pool_stats
40
+ when /summary/ then :summary
41
+ when /comparison/ then :comparison
42
+ else :unknown
43
+ end
44
+ end
45
+
46
+ def generate_report
47
+ report = []
48
+ report << "# Connection Pool Stress Test Results"
49
+ report << "\nGenerated: #{Time.now}"
50
+ report << "\n"
51
+
52
+ # Summary section
53
+ if @data[:summary]
54
+ report << "## Summary"
55
+ report << generate_summary_table
56
+ report << "\n"
57
+ end
58
+
59
+ # Performance charts
60
+ if @data[:operations]
61
+ report << "## Performance Analysis"
62
+ report << generate_performance_analysis
63
+ report << "\n"
64
+ end
65
+
66
+ # Pool utilization
67
+ if @data[:pool_stats]
68
+ report << "## Pool Utilization"
69
+ report << generate_pool_utilization_chart
70
+ report << "\n"
71
+ end
72
+
73
+ # Error analysis
74
+ if @data[:errors] && @data[:errors].any?
75
+ report << "## Error Analysis"
76
+ report << generate_error_analysis
77
+ report << "\n"
78
+ end
79
+
80
+ # Comparison
81
+ if @data[:comparison]
82
+ report << "## Configuration Comparison"
83
+ report << generate_comparison_table
84
+ report << "\n"
85
+ end
86
+
87
+ report.join("\n")
88
+ end
89
+
90
+ def generate_summary_table
91
+ return "No summary data available" unless @data[:summary]
92
+
93
+ table = []
94
+ table << "| Metric | Value |"
95
+ table << "|--------|-------|"
96
+
97
+ @data[:summary].each do |row|
98
+ metric = row['metric'] || row[:metric]
99
+ value = row['value'] || row[:value]
100
+
101
+ # Format numeric values
102
+ if value.to_s =~ /^\d+\.?\d*$/
103
+ value = format_number(value.to_f)
104
+ end
105
+
106
+ table << "| #{metric} | #{value} |"
107
+ end
108
+
109
+ table.join("\n")
110
+ end
111
+
112
+ def generate_performance_analysis
113
+ return "No operations data available" unless @data[:operations]
114
+
115
+ analysis = []
116
+
117
+ # Calculate percentiles
118
+ durations = @data[:operations].map { |op| op['duration'].to_f }.sort
119
+
120
+ analysis << "### Response Time Distribution"
121
+ analysis << "```"
122
+ analysis << generate_histogram(durations, 20)
123
+ analysis << "```"
124
+
125
+ # Operations over time
126
+ analysis << "\n### Operations Timeline"
127
+ analysis << "```"
128
+ analysis << generate_timeline_chart(@data[:operations])
129
+ analysis << "```"
130
+
131
+ # Success rate by operation type
132
+ by_type = @data[:operations].group_by { |op| op['type'] }
133
+
134
+ analysis << "\n### Success Rate by Operation Type"
135
+ analysis << "| Type | Total | Success | Rate |"
136
+ analysis << "|------|-------|---------|------|"
137
+
138
+ by_type.each do |type, ops|
139
+ total = ops.size
140
+ success = ops.count { |op| op['success'] == 'true' }
141
+ rate = (success.to_f / total * 100).round(2)
142
+
143
+ analysis << "| #{type} | #{total} | #{success} | #{rate}% |"
144
+ end
145
+
146
+ analysis.join("\n")
147
+ end
148
+
149
+ def generate_pool_utilization_chart
150
+ return "No pool stats available" unless @data[:pool_stats]
151
+
152
+ chart = []
153
+ chart << "```"
154
+
155
+ # Get utilization values
156
+ utils = @data[:pool_stats].map { |stat| stat['utilization'].to_f }
157
+
158
+ # Create time-series chart
159
+ chart_height = 15
160
+ chart_width = [utils.size, 80].min
161
+
162
+ # Sample if too many data points
163
+ if utils.size > chart_width
164
+ sample_rate = utils.size / chart_width
165
+ sampled_utils = []
166
+ (0...chart_width).each do |i|
167
+ sampled_utils << utils[i * sample_rate]
168
+ end
169
+ utils = sampled_utils
170
+ end
171
+
172
+ # Build chart
173
+ (0..10).each do |i|
174
+ level = 100 - (i * 10)
175
+ line = sprintf("%3d%% |", level)
176
+
177
+ utils.each do |util|
178
+ if util >= level - 5 && util < level + 5
179
+ line += "●"
180
+ elsif util >= level
181
+ line += "│"
182
+ else
183
+ line += " "
184
+ end
185
+ end
186
+
187
+ chart << line
188
+ end
189
+
190
+ chart << " 0% |" + "─" * utils.size
191
+ chart << " " + " " * (utils.size / 2 - 2) + "Time"
192
+ chart << "```"
193
+
194
+ # Add statistics
195
+ chart << "\n**Pool Utilization Statistics:**"
196
+ chart << "- Average: #{(utils.sum / utils.size).round(2)}%"
197
+ chart << "- Maximum: #{utils.max.round(2)}%"
198
+ chart << "- Minimum: #{utils.min.round(2)}%"
199
+
200
+ chart.join("\n")
201
+ end
202
+
203
+ def generate_error_analysis
204
+ return "No errors recorded" unless @data[:errors] && @data[:errors].any?
205
+
206
+ analysis = []
207
+
208
+ # Group by error type
209
+ by_type = @data[:errors].group_by { |err| err['error_type'] }
210
+
211
+ analysis << "### Error Distribution"
212
+ analysis << "| Error Type | Count | Percentage |"
213
+ analysis << "|------------|-------|------------|"
214
+
215
+ total_errors = @data[:errors].size
216
+
217
+ by_type.each do |type, errors|
218
+ count = errors.size
219
+ percentage = (count.to_f / total_errors * 100).round(2)
220
+ analysis << "| #{type} | #{count} | #{percentage}% |"
221
+ end
222
+
223
+ # Error timeline
224
+ analysis << "\n### Error Timeline"
225
+ analysis << "```"
226
+ analysis << generate_error_timeline(@data[:errors])
227
+ analysis << "```"
228
+
229
+ analysis.join("\n")
230
+ end
231
+
232
+ def generate_comparison_table
233
+ return "No comparison data available" unless @data[:comparison]
234
+
235
+ table = []
236
+ table << "### Test Configuration Comparison"
237
+ table << ""
238
+
239
+ # Create comparison table
240
+ headers = @data[:comparison].first.keys
241
+ table << "| " + headers.join(" | ") + " |"
242
+ table << "|" + headers.map { "-" * 10 }.join("|") + "|"
243
+
244
+ @data[:comparison].each do |row|
245
+ values = headers.map do |header|
246
+ value = row[header]
247
+ if value.to_s =~ /^\d+\.?\d*$/
248
+ format_number(value.to_f)
249
+ else
250
+ value
251
+ end
252
+ end
253
+ table << "| " + values.join(" | ") + " |"
254
+ end
255
+
256
+ table.join("\n")
257
+ end
258
+
259
+ private
260
+
261
+ def generate_histogram(values, bins = 20)
262
+ return "No data" if values.empty?
263
+
264
+ min_val = values.min
265
+ max_val = values.max
266
+ range = max_val - min_val
267
+ bin_width = range / bins.to_f
268
+
269
+ # Count values in each bin
270
+ histogram = Array.new(bins, 0)
271
+ values.each do |val|
272
+ bin = ((val - min_val) / bin_width).floor
273
+ bin = bins - 1 if bin >= bins
274
+ histogram[bin] += 1
275
+ end
276
+
277
+ # Find max count for scaling
278
+ max_count = histogram.max
279
+ chart_width = 50
280
+
281
+ # Generate chart
282
+ chart = []
283
+ histogram.each_with_index do |count, i|
284
+ bar_length = (count.to_f / max_count * chart_width).round
285
+ label = sprintf("%.3f-%.3f", min_val + i * bin_width, min_val + (i + 1) * bin_width)
286
+ bar = "█" * bar_length
287
+ chart << sprintf("%-15s |%-#{chart_width}s| %d", label, bar, count)
288
+ end
289
+
290
+ chart.join("\n")
291
+ end
292
+
293
+ def generate_timeline_chart(operations)
294
+ return "No data" if operations.empty?
295
+
296
+ # Group by time buckets
297
+ start_time = operations.map { |op| op['timestamp'].to_f }.min
298
+ end_time = operations.map { |op| op['timestamp'].to_f }.max
299
+ duration = end_time - start_time
300
+
301
+ buckets = 40
302
+ bucket_width = duration / buckets
303
+ timeline = Array.new(buckets) { { success: 0, failure: 0 } }
304
+
305
+ operations.each do |op|
306
+ bucket = ((op['timestamp'].to_f - start_time) / bucket_width).floor
307
+ bucket = buckets - 1 if bucket >= buckets
308
+
309
+ if op['success'] == 'true'
310
+ timeline[bucket][:success] += 1
311
+ else
312
+ timeline[bucket][:failure] += 1
313
+ end
314
+ end
315
+
316
+ # Generate chart
317
+ max_ops = timeline.map { |b| b[:success] + b[:failure] }.max
318
+ chart_height = 10
319
+
320
+ chart = []
321
+ chart_height.downto(0) do |level|
322
+ line = ""
323
+ timeline.each do |bucket|
324
+ total = bucket[:success] + bucket[:failure]
325
+ if total >= (level.to_f / chart_height * max_ops)
326
+ if bucket[:failure] > 0
327
+ line += "✗"
328
+ else
329
+ line += "●"
330
+ end
331
+ else
332
+ line += " "
333
+ end
334
+ end
335
+ chart << line
336
+ end
337
+
338
+ chart << "─" * buckets
339
+ chart << "0" + " " * (buckets / 2 - 3) + "Time (s)" + " " * (buckets / 2 - 5) + sprintf("%.1f", duration)
340
+
341
+ chart.join("\n")
342
+ end
343
+
344
+ def generate_error_timeline(errors)
345
+ return "No errors" if errors.empty?
346
+
347
+ # Group errors by time
348
+ start_time = errors.map { |e| e['timestamp'].to_f }.min
349
+ end_time = errors.map { |e| e['timestamp'].to_f }.max
350
+ duration = end_time - start_time
351
+
352
+ buckets = 60
353
+ bucket_width = duration / buckets
354
+ timeline = Array.new(buckets, 0)
355
+
356
+ errors.each do |error|
357
+ bucket = ((error['timestamp'].to_f - start_time) / bucket_width).floor
358
+ bucket = buckets - 1 if bucket >= buckets
359
+ timeline[bucket] += 1
360
+ end
361
+
362
+ # Generate sparkline
363
+ max_errors = timeline.max
364
+ sparkline = timeline.map do |count|
365
+ if count == 0
366
+ "▁"
367
+ elsif count <= max_errors * 0.25
368
+ "▂"
369
+ elsif count <= max_errors * 0.5
370
+ "▄"
371
+ elsif count <= max_errors * 0.75
372
+ "▆"
373
+ else
374
+ "█"
375
+ end
376
+ end.join
377
+
378
+ "Error frequency: #{sparkline}"
379
+ end
380
+
381
+ def format_number(num)
382
+ if num < 0.001
383
+ sprintf("%.6f", num)
384
+ elsif num < 1
385
+ sprintf("%.4f", num)
386
+ elsif num < 100
387
+ sprintf("%.2f", num)
388
+ else
389
+ num.round.to_s
390
+ end
391
+ end
392
+ end
393
+
394
+ # Command-line interface
395
+ if __FILE__ == $0
396
+ options = {}
397
+
398
+ OptionParser.new do |opts|
399
+ opts.banner = "Usage: visualize_stress_results.rb [options] file1.csv file2.csv ..."
400
+
401
+ opts.on("-o", "--output FILE", "Output file (default: stdout)") do |file|
402
+ options[:output] = file
403
+ end
404
+
405
+ opts.on("-f", "--format FORMAT", "Output format: markdown, text (default: markdown)") do |format|
406
+ options[:format] = format
407
+ end
408
+
409
+ opts.on("-h", "--help", "Show this help") do
410
+ puts opts
411
+ exit
412
+ end
413
+ end.parse!
414
+
415
+ # Get CSV files
416
+ csv_files = ARGV.empty? ? Dir.glob("*.csv") : ARGV
417
+
418
+ if csv_files.empty?
419
+ puts "No CSV files found. Please specify files or run in a directory with CSV files."
420
+ exit 1
421
+ end
422
+
423
+ # Generate visualization
424
+ visualizer = StressTestVisualizer.new(csv_files)
425
+ report = visualizer.generate_report
426
+
427
+ # Output
428
+ if options[:output]
429
+ File.write(options[:output], report)
430
+ puts "Report written to: #{options[:output]}"
431
+ else
432
+ puts report
433
+ end
434
+ end