fast_exists 1.0.0

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +242 -0
  4. data/benchmarks/benchmark_suite.rb +58 -0
  5. data/bin/fast_exists +5 -0
  6. data/docs/ADR/0001_probabilistic_data_structures.md +12 -0
  7. data/docs/ADR/0002_backend_abstraction.md +11 -0
  8. data/docs/ADR/0003_active_record_hooks.md +11 -0
  9. data/docs/ARCHITECTURE.md +36 -0
  10. data/docs/BENCHMARKS.md +9 -0
  11. data/docs/DEPLOYMENT.md +11 -0
  12. data/docs/FAQ.md +10 -0
  13. data/docs/MEMORY_SIZING.md +22 -0
  14. data/docs/PERFORMANCE.md +9 -0
  15. data/docs/PERFORMANCE_INTELLIGENCE.md +114 -0
  16. data/docs/REDIS_GUIDE.md +15 -0
  17. data/docs/SCALING.md +74 -0
  18. data/docs/TROUBLESHOOTING.md +9 -0
  19. data/lib/fast_exists/active_record/extension.rb +36 -0
  20. data/lib/fast_exists/active_record/hooks.rb +25 -0
  21. data/lib/fast_exists/active_record/model_methods.rb +109 -0
  22. data/lib/fast_exists/backends/base.rb +60 -0
  23. data/lib/fast_exists/backends/file.rb +70 -0
  24. data/lib/fast_exists/backends/memory.rb +41 -0
  25. data/lib/fast_exists/backends/null.rb +31 -0
  26. data/lib/fast_exists/backends/redis.rb +98 -0
  27. data/lib/fast_exists/backends/redis_bloom.rb +83 -0
  28. data/lib/fast_exists/backends/registry.rb +38 -0
  29. data/lib/fast_exists/bit_array.rb +90 -0
  30. data/lib/fast_exists/bloom/counting.rb +85 -0
  31. data/lib/fast_exists/bloom/filter.rb +133 -0
  32. data/lib/fast_exists/bloom/scalable.rb +79 -0
  33. data/lib/fast_exists/cli.rb +117 -0
  34. data/lib/fast_exists/configuration.rb +45 -0
  35. data/lib/fast_exists/engine.rb +66 -0
  36. data/lib/fast_exists/errors.rb +10 -0
  37. data/lib/fast_exists/generators/install_generator.rb +15 -0
  38. data/lib/fast_exists/generators/templates/fast_exists_initializer.rb +24 -0
  39. data/lib/fast_exists/instrumentation/event_subscriber.rb +35 -0
  40. data/lib/fast_exists/instrumentation/open_telemetry.rb +18 -0
  41. data/lib/fast_exists/instrumentation/prometheus.rb +32 -0
  42. data/lib/fast_exists/intelligence/analyzer.rb +135 -0
  43. data/lib/fast_exists/intelligence/auditor.rb +116 -0
  44. data/lib/fast_exists/intelligence/data_model.rb +57 -0
  45. data/lib/fast_exists/intelligence/doctor.rb +190 -0
  46. data/lib/fast_exists/intelligence/health.rb +109 -0
  47. data/lib/fast_exists/intelligence/report/builder.rb +53 -0
  48. data/lib/fast_exists/intelligence/report/console.rb +40 -0
  49. data/lib/fast_exists/intelligence/report/csv.rb +37 -0
  50. data/lib/fast_exists/intelligence/report/html.rb +118 -0
  51. data/lib/fast_exists/intelligence/report/json.rb +17 -0
  52. data/lib/fast_exists/intelligence/report/markdown.rb +41 -0
  53. data/lib/fast_exists/intelligence/report/pdf.rb +43 -0
  54. data/lib/fast_exists/intelligence/report/yaml.rb +17 -0
  55. data/lib/fast_exists/intelligence/stats.rb +78 -0
  56. data/lib/fast_exists/multi_tenancy/resolver.rb +24 -0
  57. data/lib/fast_exists/multi_tenant/allocator.rb +43 -0
  58. data/lib/fast_exists/multi_tenant/base.rb +25 -0
  59. data/lib/fast_exists/multi_tenant/key_layout.rb +19 -0
  60. data/lib/fast_exists/multi_tenant/pool.rb +42 -0
  61. data/lib/fast_exists/multi_tenant/recommendation_engine.rb +76 -0
  62. data/lib/fast_exists/multi_tenant/strategies/adaptive_strategy.rb +81 -0
  63. data/lib/fast_exists/multi_tenant/strategies/base_strategy.rb +31 -0
  64. data/lib/fast_exists/multi_tenant/strategies/global_strategy.rb +37 -0
  65. data/lib/fast_exists/multi_tenant/strategies/per_tenant_strategy.rb +54 -0
  66. data/lib/fast_exists/multi_tenant/strategies/shared_strategy.rb +52 -0
  67. data/lib/fast_exists/optimizer/ai_advisor.rb +52 -0
  68. data/lib/fast_exists/probabilistic/count_min_sketch.rb +67 -0
  69. data/lib/fast_exists/probabilistic/cuckoo.rb +128 -0
  70. data/lib/fast_exists/probabilistic/hyper_log_log.rb +84 -0
  71. data/lib/fast_exists/railtie.rb +17 -0
  72. data/lib/fast_exists/statistics/tracker.rb +83 -0
  73. data/lib/fast_exists/tasks/fast_exists.rake +117 -0
  74. data/lib/fast_exists/version.rb +5 -0
  75. data/lib/fast_exists.rb +131 -0
  76. metadata +218 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module FastExists
6
+ module Intelligence
7
+ module Report
8
+ class Builder
9
+ def self.build(format: :console, output: nil, include: nil, compare: nil)
10
+ data = FastExists::Intelligence::DataModel.new
11
+ data.health = FastExists::Intelligence::Health.check
12
+ data.analysis = FastExists::Intelligence::Analyzer.analyze
13
+ data.audit = FastExists::Intelligence::Auditor.audit
14
+ data.doctor = FastExists::Intelligence::Doctor.diagnose(format: :json)
15
+
16
+ comparison_data = load_comparison(compare) if compare
17
+
18
+ rendered_content = case format.to_sym
19
+ when :json
20
+ FastExists::Intelligence::Report::Json.render(data, comparison: comparison_data)
21
+ when :yaml
22
+ FastExists::Intelligence::Report::Yaml.render(data, comparison: comparison_data)
23
+ when :markdown
24
+ FastExists::Intelligence::Report::Markdown.render(data, comparison: comparison_data)
25
+ when :html
26
+ FastExists::Intelligence::Report::Html.render(data, comparison: comparison_data)
27
+ when :csv
28
+ FastExists::Intelligence::Report::Csv.render(data, comparison: comparison_data)
29
+ when :pdf
30
+ FastExists::Intelligence::Report::Pdf.render(data, comparison: comparison_data)
31
+ else
32
+ FastExists::Intelligence::Report::Console.render(data, comparison: comparison_data)
33
+ end
34
+
35
+ if output
36
+ FileUtils.mkdir_p(File.dirname(output))
37
+ File.write(output, rendered_content)
38
+ puts "Report generated successfully at #{output}"
39
+ end
40
+
41
+ rendered_content
42
+ end
43
+
44
+ private
45
+
46
+ def self.load_comparison(file_path)
47
+ return nil unless file_path && File.exist?(file_path)
48
+ JSON.parse(File.read(file_path), symbolize_names: true) rescue nil
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastExists
4
+ module Intelligence
5
+ module Report
6
+ class Console
7
+ def self.render(data, comparison: nil)
8
+ lines = []
9
+ lines << "=========================================================="
10
+ lines << "⚡ FAST_EXISTS PERFORMANCE & ARCHITECTURE REPORT"
11
+ lines << "=========================================================="
12
+ lines << "Timestamp: #{data.timestamp}"
13
+ lines << "Environment: Ruby #{data.environment[:ruby_version]} | Rails #{data.environment[:rails_version]} | Adapter #{data.environment[:database_adapter]}"
14
+ lines << "Active Backend: #{data.environment[:backend]}"
15
+ lines << ""
16
+ lines << "1. OPERATIONAL HEALTH: #{data.health[:overall_status].to_s.upcase}"
17
+ data.health[:checks].each do |check|
18
+ mark = check[:status] == :pass ? "✓" : "⚠"
19
+ lines << " #{mark} #{check[:name]}: #{check[:message]}"
20
+ end
21
+ lines << ""
22
+ lines << "2. ARCHITECTURAL AUDIT & GRADE"
23
+ lines << " Audit Score: #{data.audit[:audit_score]}/100 (Grade: #{data.audit[:grade]})"
24
+ data.audit[:findings].each do |f|
25
+ lines << " - [#{f[:severity].to_s.upcase}] #{f[:location]}: #{f[:problem]}"
26
+ end
27
+ lines << ""
28
+ lines << "3. RUNTIME STATISTICS"
29
+ lines << " Queries Avoided: #{data.stats[:queries_avoided]}"
30
+ lines << " Database Lookups: #{data.stats[:database_lookups]}"
31
+ lines << " Bloom Hits: #{data.stats[:bloom_hits]}"
32
+ lines << " Bloom Misses: #{data.stats[:bloom_misses]}"
33
+ lines << " Hit Ratio: #{(data.stats[:hit_ratio] * 100).round(1)}%"
34
+ lines << "=========================================================="
35
+ lines.join("\n")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module FastExists
6
+ module Intelligence
7
+ module Report
8
+ class Csv
9
+ def self.render(data, comparison: nil)
10
+ CSV.generate(headers: true) do |csv|
11
+ csv << ["Category", "Metric / Location", "Value / Message", "Status / Severity"]
12
+
13
+ csv << ["Environment", "Ruby Version", data.environment[:ruby_version], "info"]
14
+ csv << ["Environment", "Rails Version", data.environment[:rails_version], "info"]
15
+ csv << ["Environment", "Backend", data.environment[:backend], "info"]
16
+
17
+ csv << ["Health", "Overall Status", data.health[:overall_status], data.health[:overall_status]]
18
+ data.health[:checks].each do |c|
19
+ csv << ["Health Check", c[:name], c[:message], c[:status]]
20
+ end
21
+
22
+ csv << ["Stats", "Queries Avoided", data.stats[:queries_avoided], "info"]
23
+ csv << ["Stats", "Database Lookups", data.stats[:database_lookups], "info"]
24
+ csv << ["Stats", "Bloom Hits", data.stats[:bloom_hits], "info"]
25
+ csv << ["Stats", "False Positives", data.stats[:false_positives], "info"]
26
+ csv << ["Stats", "Hit Ratio", "#{(data.stats[:hit_ratio] * 100).round(1)}%", "info"]
27
+
28
+ csv << ["Audit", "Score", data.audit[:audit_score], data.audit[:grade]]
29
+ data.audit[:findings].each do |f|
30
+ csv << ["Audit Finding", f[:location], f[:problem], f[:severity]]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastExists
4
+ module Intelligence
5
+ module Report
6
+ class Html
7
+ def self.render(data, comparison: nil)
8
+ hit_pct = (data.stats[:hit_ratio] * 100).round(1)
9
+ miss_pct = (data.stats[:miss_ratio] * 100).round(1)
10
+
11
+ <<-HTML
12
+ <!DOCTYPE html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="UTF-8">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
17
+ <title>FastExists Performance & Architecture Report</title>
18
+ <style>
19
+ :root {
20
+ --bg: #0f172a;
21
+ --card-bg: #1e293b;
22
+ --text: #f8fafc;
23
+ --text-muted: #94a3b8;
24
+ --accent: #38bdf8;
25
+ --border: #334155;
26
+ --pass: #22c55e;
27
+ --warn: #eab308;
28
+ --fail: #ef4444;
29
+ }
30
+ @media print {
31
+ body { background: #fff !important; color: #000 !important; }
32
+ .card { border: 1px solid #ccc !important; background: #fff !important; color: #000 !important; }
33
+ }
34
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2.5rem; line-height: 1.5; }
35
+ .header { border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; margin-bottom: 2rem; }
36
+ .title { font-size: 2.25rem; font-weight: 800; color: var(--accent); margin: 0 0 0.5rem 0; display: flex; align-items: center; gap: 0.5rem; }
37
+ .subtitle { color: var(--text-muted); font-size: 1rem; margin: 0; }
38
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.5rem; margin-bottom: 2.5rem; }
39
+ .card { background: var(--card-bg); border-radius: 0.75rem; padding: 1.5rem; border: 1px solid var(--border); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); }
40
+ .card-title { color: var(--text-muted); font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
41
+ .card-val { font-size: 2.25rem; font-weight: 800; color: var(--accent); }
42
+ .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; font-weight: 700; font-size: 0.875rem; text-transform: uppercase; }
43
+ .badge-pass { background: rgba(34, 197, 94, 0.2); color: var(--pass); border: 1px solid var(--pass); }
44
+ .badge-warn { background: rgba(234, 179, 8, 0.2); color: var(--warn); border: 1px solid var(--warn); }
45
+ .badge-fail { background: rgba(239, 68, 68, 0.2); color: var(--fail); border: 1px solid var(--fail); }
46
+ section { margin-bottom: 3rem; }
47
+ h2 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 1.5rem; color: var(--text); }
48
+ table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
49
+ th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
50
+ th { background: rgba(255,255,255,0.03); color: var(--text-muted); font-weight: 600; }
51
+ pre { background: #090d16; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; color: #a5f3fc; border: 1px solid var(--border); font-family: monospace; }
52
+ .chart-bar { background: var(--border); height: 1.25rem; border-radius: 0.25rem; overflow: hidden; display: flex; margin-top: 0.5rem; }
53
+ .chart-fill-hit { background: var(--accent); height: 100%; width: #{hit_pct}%; }
54
+ .chart-fill-miss { background: #64748b; height: 100%; width: #{miss_pct}%; }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <div class="header">
59
+ <h1 class="title">⚡ FastExists Performance Intelligence Report</h1>
60
+ <p class="subtitle">Generated on #{data.timestamp} • Ruby #{data.environment[:ruby_version]} • Rails #{data.environment[:rails_version]} • Backend: <strong>#{data.environment[:backend]}</strong></p>
61
+ </div>
62
+
63
+ <div class="grid">
64
+ <div class="card">
65
+ <div class="card-title">Overall Health</div>
66
+ <div class="card-val"><span class="badge badge-#{data.health[:overall_status] == :healthy ? 'pass' : 'warn'}">#{data.health[:overall_status]}</span></div>
67
+ </div>
68
+ <div class="card">
69
+ <div class="card-title">Architecture Grade</div>
70
+ <div class="card-val">#{data.audit[:grade]} <span style="font-size:1.1rem; color:var(--text-muted)">(&nbsp;#{data.audit[:audit_score]}/100&nbsp;)</span></div>
71
+ </div>
72
+ <div class="card">
73
+ <div class="card-title">Queries Avoided</div>
74
+ <div class="card-val">#{data.stats[:queries_avoided]}</div>
75
+ </div>
76
+ <div class="card">
77
+ <div class="card-title">Database Lookups</div>
78
+ <div class="card-val">#{data.stats[:database_lookups]}</div>
79
+ </div>
80
+ </div>
81
+
82
+ <section>
83
+ <h2>📊 Query Distribution & Bloom Hit Ratio</h2>
84
+ <p>Hit Ratio: <strong>#{hit_pct}%</strong> | Miss Ratio: <strong>#{miss_pct}%</strong></p>
85
+ <div class="chart-bar">
86
+ <div class="chart-fill-hit" title="Hits: #{hit_pct}%"></div>
87
+ <div class="chart-fill-miss" title="Avoided: #{miss_pct}%"></div>
88
+ </div>
89
+ </section>
90
+
91
+ <section>
92
+ <h2>🩺 Operational Health Checks</h2>
93
+ <table>
94
+ <thead>
95
+ <tr><th>Check Name</th><th>Status</th><th>Message</th></tr>
96
+ </thead>
97
+ <tbody>
98
+ #{data.health[:checks].map { |c| "<tr><td><strong>#{c[:name]}</strong></td><td><span class=\"badge badge-#{c[:status] == :pass ? 'pass' : 'warn'}\">#{c[:status]}</span></td><td>#{c[:message]}</td></tr>" }.join("\n")}
99
+ </tbody>
100
+ </table>
101
+ </section>
102
+
103
+ <section>
104
+ <h2>🔍 Architectural Audit Findings</h2>
105
+ #{data.audit[:findings].map { |f| "<div class=\"card\" style=\"margin-bottom:1rem;\"><div style=\"display:flex; justify-between; align-items:center;\"><strong>[#{f[:severity].to_s.upcase}] #{f[:location]}</strong></div><p style=\"color:var(--text-muted)\">#{f[:problem]}</p><p><strong>Recommendation:</strong> #{f[:recommendation]}</p></div>" }.join("\n")}
106
+ </section>
107
+ </body>
108
+ </html>
109
+ HTML
110
+ end
111
+
112
+ def self.render_doctor(recs)
113
+ render(FastExists::Intelligence::DataModel.new)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module FastExists
6
+ module Intelligence
7
+ module Report
8
+ class Json
9
+ def self.render(data, comparison: nil)
10
+ payload = data.to_h
11
+ payload[:comparison] = comparison if comparison
12
+ JSON.pretty_generate(payload)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastExists
4
+ module Intelligence
5
+ module Report
6
+ class Markdown
7
+ def self.render(data, comparison: nil)
8
+ lines = []
9
+ lines << "# ⚡ FastExists Performance & Architecture Report"
10
+ lines << ""
11
+ lines << "> **Generated**: `#{data.timestamp}` "
12
+ lines << "> **Environment**: Ruby #{data.environment[:ruby_version]} | Rails #{data.environment[:rails_version]} | DB: `#{data.environment[:database_adapter]}` "
13
+ lines << "> **Active Backend**: `#{data.environment[:backend]}`"
14
+ lines << ""
15
+ lines << "## Executive Summary"
16
+ lines << "- **Health Status**: `#{data.health[:overall_status].to_s.upcase}`"
17
+ lines << "- **Architecture Grade**: **#{data.audit[:grade]}** (#{data.audit[:audit_score]}/100)"
18
+ lines << "- **Queries Avoided**: **#{data.stats[:queries_avoided]}**"
19
+ lines << "- **Database Lookups**: #{data.stats[:database_lookups]}"
20
+ lines << "- **Hit Ratio**: #{(data.stats[:hit_ratio] * 100).round(1)}%"
21
+ lines << ""
22
+ lines << "## Health Check Findings"
23
+ data.health[:checks].each do |check|
24
+ badge = check[:status] == :pass ? "✅" : "⚠️"
25
+ lines << "- #{badge} **#{check[:name]}**: #{check[:message]}"
26
+ end
27
+ lines << ""
28
+ lines << "## Audit & Optimizations"
29
+ data.audit[:findings].each do |f|
30
+ lines << "### [#{f[:severity].to_s.upcase}] #{f[:location]}"
31
+ lines << "- **Problem**: #{f[:problem]}"
32
+ lines << "- **Recommendation**: #{f[:recommendation]}"
33
+ lines << "- **Impact**: #{f[:estimated_impact]}"
34
+ lines << ""
35
+ end
36
+ lines.join("\n")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastExists
4
+ module Intelligence
5
+ module Report
6
+ class Pdf
7
+ def self.render(data, comparison: nil)
8
+ # Formats executive PDF-compatible markdown/text report
9
+ lines = []
10
+ lines << "% FAST_EXISTS PERFORMANCE & ARCHITECTURE REPORT"
11
+ lines << "% Executive Summary"
12
+ lines << "% Date: #{data.timestamp}"
13
+ lines << ""
14
+ lines << "================================================================================"
15
+ lines << "OVERALL OPERATIONAL HEALTH: #{data.health[:overall_status].to_s.upcase}"
16
+ lines << "ARCHITECTURAL GRADE: #{data.audit[:grade]} (Score: #{data.audit[:audit_score]}/100)"
17
+ lines << "QUERIES AVOIDED BY BLOOM FILTERS: #{data.stats[:queries_avoided]}"
18
+ lines << "================================================================================"
19
+ lines << ""
20
+ lines << "1. APPLICATION & ENVIRONMENT"
21
+ lines << " - Ruby Version: #{data.environment[:ruby_version]}"
22
+ lines << " - Rails Version: #{data.environment[:rails_version]}"
23
+ lines << " - DB Adapter: #{data.environment[:database_adapter]}"
24
+ lines << " - Backend Storage: #{data.environment[:backend]}"
25
+ lines << ""
26
+ lines << "2. HEALTH DIAGNOSTICS"
27
+ data.health[:checks].each do |c|
28
+ lines << " - [#{c[:status].to_s.upcase}] #{c[:name]}: #{c[:message]}"
29
+ end
30
+ lines << ""
31
+ lines << "3. AUDIT & DOCTOR RECOMMENDATIONS"
32
+ data.audit[:findings].each do |f|
33
+ lines << " * Location: #{f[:location]}"
34
+ lines << " Severity: #{f[:severity].to_s.upcase}"
35
+ lines << " Problem: #{f[:problem]}"
36
+ lines << " Recommendation: #{f[:recommendation]}"
37
+ end
38
+ lines.join("\n")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module FastExists
6
+ module Intelligence
7
+ module Report
8
+ class Yaml
9
+ def self.render(data, comparison: nil)
10
+ payload = data.to_h
11
+ payload[:comparison] = comparison if comparison
12
+ payload.transform_keys(&:to_s).to_yaml
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+
6
+ module FastExists
7
+ module Intelligence
8
+ class Stats
9
+ def self.render(format: :console)
10
+ raw_stats = FastExists.stats.merge(
11
+ active_backend: FastExists.configuration.backend,
12
+ uptime_seconds: (FastExists.uptime rescue 0),
13
+ sync_status: FastExists.configuration.auto_sync ? :active : :disabled
14
+ )
15
+
16
+ case format.to_sym
17
+ when :json
18
+ JSON.pretty_generate(raw_stats)
19
+ when :yaml
20
+ raw_stats.transform_keys(&:to_s).to_yaml
21
+ when :markdown
22
+ to_markdown(raw_stats)
23
+ else
24
+ to_console(raw_stats)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def self.to_console(s)
31
+ <<~CONSOLE
32
+ ==================================================
33
+ ⚡ FastExists Runtime Statistics
34
+ ==================================================
35
+ Active Backend: #{s[:active_backend]}
36
+ Memory Usage: #{s[:memory_usage] || 'N/A'}
37
+ Capacity: #{s[:capacity] || 'N/A'}
38
+ Occupancy: #{s[:occupancy] || 'N/A'}
39
+ Bit Array Size: #{s[:bit_size] || 'N/A'}
40
+ Hash Count: #{s[:hash_count] || 'N/A'}
41
+ Inserted Elements: #{s[:inserted_items] || 0}
42
+ Expected Elements: #{s[:expected_elements] || 0}
43
+ Est. False Positive Rate: #{s[:estimated_false_positive_rate] || 0.0}%
44
+ Actual False Positives: #{s[:false_positives]}
45
+ Bloom Hits: #{s[:bloom_hits]}
46
+ Bloom Misses: #{s[:bloom_misses]}
47
+ Queries Avoided: #{s[:queries_avoided]}
48
+ Database Lookups: #{s[:database_lookups]}
49
+ Hit Ratio: #{(s[:hit_ratio] * 100).round(2)}%
50
+ Miss Ratio: #{(s[:miss_ratio] * 100).round(2)}%
51
+ Synchronization Status: #{s[:sync_status]}
52
+ ==================================================
53
+ CONSOLE
54
+ end
55
+
56
+ def self.to_markdown(s)
57
+ <<~MARKDOWN
58
+ # ⚡ FastExists Runtime Statistics
59
+
60
+ | Metric | Value |
61
+ |:---|:---|
62
+ | **Active Backend** | `#{s[:active_backend]}` |
63
+ | **Capacity** | #{s[:capacity] || 'N/A'} |
64
+ | **Inserted Elements** | #{s[:inserted_items] || 0} |
65
+ | **Queries Avoided** | **#{s[:queries_avoided]}** |
66
+ | **Database Lookups** | #{s[:database_lookups]} |
67
+ | **Bloom Hits** | #{s[:bloom_hits]} |
68
+ | **Bloom Misses** | #{s[:bloom_misses]} |
69
+ | **False Positives** | #{s[:false_positives]} |
70
+ | **Hit Ratio** | #{(s[:hit_ratio] * 100).round(2)}% |
71
+ | **Miss Ratio** | #{(s[:miss_ratio] * 100).round(2)}% |
72
+ | **Est. FP Rate** | #{s[:estimated_false_positive_rate] || 0.0}% |
73
+ | **Sync Status** | `#{s[:sync_status]}` |
74
+ MARKDOWN
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastExists
4
+ module MultiTenancy
5
+ class Resolver
6
+ def self.resolve(namespace_spec, record_or_context = nil)
7
+ case namespace_spec
8
+ when Proc
9
+ namespace_spec.call(record_or_context).to_s
10
+ when Symbol
11
+ if record_or_context && record_or_context.respond_to?(namespace_spec)
12
+ record_or_context.send(namespace_spec).to_s
13
+ else
14
+ namespace_spec.to_s
15
+ end
16
+ when String
17
+ namespace_spec
18
+ else
19
+ "default"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastExists
4
+ module MultiTenant
5
+ class Allocator
6
+ attr_reader :strategy
7
+
8
+ def initialize(strategy_type = FastExists.configuration.tenant_strategy, options = {})
9
+ @strategy_type = strategy_type.to_sym
10
+ @strategy = build_strategy(@strategy_type, options)
11
+ end
12
+
13
+ def add(tenant_id, attribute, value)
14
+ @strategy.add(tenant_id, attribute, value)
15
+ end
16
+
17
+ def contains?(tenant_id, attribute, value)
18
+ @strategy.contains?(tenant_id, attribute, value)
19
+ end
20
+
21
+ def stats
22
+ @strategy.stats.merge(strategy_type: @strategy_type)
23
+ end
24
+
25
+ private
26
+
27
+ def build_strategy(type, options)
28
+ case type
29
+ when :global
30
+ FastExists::MultiTenant::Strategies::GlobalStrategy.new(options)
31
+ when :per_tenant
32
+ FastExists::MultiTenant::Strategies::PerTenantStrategy.new(options)
33
+ when :shared
34
+ FastExists::MultiTenant::Strategies::SharedStrategy.new(options)
35
+ when :adaptive
36
+ FastExists::MultiTenant::Strategies::AdaptiveStrategy.new(options)
37
+ else
38
+ raise UnsupportedBackendError, "Unknown tenant strategy: #{type}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "key_layout"
4
+ require_relative "pool"
5
+ require_relative "strategies/base_strategy"
6
+ require_relative "strategies/global_strategy"
7
+ require_relative "strategies/per_tenant_strategy"
8
+ require_relative "strategies/shared_strategy"
9
+ require_relative "strategies/adaptive_strategy"
10
+ require_relative "allocator"
11
+ require_relative "recommendation_engine"
12
+
13
+ module FastExists
14
+ module MultiTenant
15
+ class << self
16
+ def allocator
17
+ @allocator ||= FastExists::MultiTenant::Allocator.new
18
+ end
19
+
20
+ def reset!
21
+ @allocator = nil
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastExists
4
+ module MultiTenant
5
+ class KeyLayout
6
+ def self.global_key(prefix = FastExists.configuration.key_prefix)
7
+ "#{prefix}:global"
8
+ end
9
+
10
+ def self.pool_key(pool_name, prefix = FastExists.configuration.key_prefix)
11
+ "#{prefix}:pool:#{pool_name}"
12
+ end
13
+
14
+ def self.tenant_key(tenant_id, prefix = FastExists.configuration.key_prefix)
15
+ "#{prefix}:tenant:#{tenant_id}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FastExists
4
+ module MultiTenant
5
+ class Pool
6
+ attr_reader :name, :expected_elements, :false_positive_rate, :backend
7
+
8
+ def initialize(name:, expected_elements:, false_positive_rate: 0.001, backend_type: nil)
9
+ @name = name
10
+ @expected_elements = expected_elements
11
+ @false_positive_rate = false_positive_rate
12
+ @backend_type = backend_type || FastExists.configuration.backend
13
+ @key = FastExists::MultiTenant::KeyLayout.pool_key(name)
14
+
15
+ @backend = FastExists.backends.fetch(
16
+ @backend_type,
17
+ namespace: "pool:#{name}",
18
+ expected_elements: expected_elements,
19
+ false_positive_rate: false_positive_rate
20
+ )
21
+ end
22
+
23
+ def add(tenant_id, attribute, value)
24
+ composite_key = "#{tenant_id}:#{attribute}:#{value}"
25
+ @backend.add(composite_key)
26
+ end
27
+
28
+ def contains?(tenant_id, attribute, value)
29
+ composite_key = "#{tenant_id}:#{attribute}:#{value}"
30
+ @backend.contains?(composite_key)
31
+ end
32
+
33
+ def clear
34
+ @backend.clear
35
+ end
36
+
37
+ def stats
38
+ @backend.stats.merge(pool_name: @name, pool_key: @key)
39
+ end
40
+ end
41
+ end
42
+ end