Class: RailsErrorDashboard::Services::StormProtection::Gate
- Inherits:
-
Object
- Object
- RailsErrorDashboard::Services::StormProtection::Gate
- Defined in:
- lib/rails_error_dashboard/services/storm_protection/gate.rb
Overview
Facade for the storm-protection hot path. One call per capture attempt:
Gate.admit!(exception, context) # => :full | :lite | :count_only
:full — capture everything (the normal path)
:lite — capture the error + occurrence row, shed context
(breadcrumbs / system health / locals / ivars)
:count_only — nothing stored now; counted in memory, reconciled
onto ErrorLog.occurrence_count by the flush job
Safety contract (mirrors SwallowedExceptionTracker):
-
FAILS OPEN: any internal error → :full. Protection must never be the thing that loses an error.
-
Zero I/O on the hot path. The only DB-adjacent work is enqueueing the flush job at most once per flush interval.
-
Budget: digest + atomic increment + comparisons (~µs). Benchmarked.
-
Per-process state; Puma workers each run their own breaker. No thread-locals — shared atomics, so no Thread.current cleanup needed.
IMPORTANT ordering: callers must run ExceptionFilter (ignore list + static sampling) BEFORE this gate — ignored exceptions must never count toward storm state or be reconciled into ErrorLogs.
Class Method Summary collapse
- .admit!(exception, context = {}) ⇒ Object
-
.breaker ⇒ Object
Exposed for the flush job (episode metadata for storm_events).
- .count_buffer ⇒ Object
- .fingerprint_buckets ⇒ Object
-
.issue_creation_allowed? ⇒ Boolean
Always-on cap for auto-created issues (a storm of NEW critical fingerprints must not open 500 GitHub/Linear issues).
-
.notifications_suppressed? ⇒ Boolean
While the breaker is not closed, per-error notifications are suppressed (a single storm notification replaces them).
-
.reset! ⇒ Object
Test hook + fork hygiene: fresh state, no leftover episodes.
- .state ⇒ Object
Class Method Details
.admit!(exception, context = {}) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/rails_error_dashboard/services/storm_protection/gate.rb', line 30 def admit!(exception, context = {}) return :full unless enabled? state = breaker.record! maybe_storm_notification(state) decision = decide(state, exception, context) maybe_flush! decision rescue => e RailsErrorDashboard::Logger.error( "[RailsErrorDashboard] StormProtection failed open: #{e.class} - #{e.}" ) :full end |
.breaker ⇒ Object
Exposed for the flush job (episode metadata for storm_events).
85 86 87 |
# File 'lib/rails_error_dashboard/services/storm_protection/gate.rb', line 85 def breaker @breaker ||= CircuitBreaker.new end |
.count_buffer ⇒ Object
89 90 91 |
# File 'lib/rails_error_dashboard/services/storm_protection/gate.rb', line 89 def count_buffer @count_buffer ||= CountBuffer.new end |
.fingerprint_buckets ⇒ Object
93 94 95 |
# File 'lib/rails_error_dashboard/services/storm_protection/gate.rb', line 93 def fingerprint_buckets @fingerprint_buckets ||= FingerprintBuckets.new end |
.issue_creation_allowed? ⇒ Boolean
Always-on cap for auto-created issues (a storm of NEW critical fingerprints must not open 500 GitHub/Linear issues). Token bucket: N per rolling window, per process. Each call consumes a token — call only when actually about to create an issue.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/rails_error_dashboard/services/storm_protection/gate.rb', line 58 def issue_creation_allowed? return true unless enabled? now = monotonic_now window = RailsErrorDashboard.configuration.auto_issue_rate_limit_window_minutes.to_i * 60 limit = RailsErrorDashboard.configuration.auto_issue_rate_limit_count.to_i @issue_window_start ||= now @issue_window_count ||= Concurrent::AtomicFixnum.new(0) if now - @issue_window_start >= window @issue_window_start = now @issue_window_count = Concurrent::AtomicFixnum.new(0) end @issue_window_count.increment <= limit rescue true end |
.notifications_suppressed? ⇒ Boolean
While the breaker is not closed, per-error notifications are suppressed (a single storm notification replaces them).
48 49 50 51 52 |
# File 'lib/rails_error_dashboard/services/storm_protection/gate.rb', line 48 def notifications_suppressed? enabled? && breaker.state != :closed rescue false end |
.reset! ⇒ Object
Test hook + fork hygiene: fresh state, no leftover episodes.
98 99 100 101 102 103 104 105 106 107 |
# File 'lib/rails_error_dashboard/services/storm_protection/gate.rb', line 98 def reset! @breaker = nil @count_buffer = nil @fingerprint_buckets = nil @probe_counter = nil @issue_window_start = nil @issue_window_count = nil @last_flush = nil @storm_notified_episode = nil end |
.state ⇒ Object
78 79 80 81 82 |
# File 'lib/rails_error_dashboard/services/storm_protection/gate.rb', line 78 def state enabled? ? breaker.state : :closed rescue :closed end |