Class: RailsErrorDashboard::Services::StormProtection::FingerprintBuckets
- Inherits:
-
Object
- Object
- RailsErrorDashboard::Services::StormProtection::FingerprintBuckets
- Defined in:
- lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb
Overview
Layer 1: per-fingerprint rate limiting with graceful degradation.
Each fingerprint gets a 60-second window. Within the window:
- first N events → :full (everything captured)
- past N, every Mth event → :lite (row captured, context shed)
- everything else → :count_only (in-memory count, flushed later)
The first event of every window is ALWAYS at least :lite, so a melting-down fingerprint still has a fresh exemplar each minute —deterministic, unlike rand-based sampling.
Calm-mode adaptive context sampling rides the same entries: after K full-context captures per fingerprint per day, context is captured only every Mth time (an error firing 1000×/day doesn’t need 1000 breadcrumb trails). Occurrence rows are unaffected in calm mode.
Concurrency: entries are mutable structs in a Concurrent::Map with plain (unlocked) field increments — races can miscount by a handful of events, which is acceptable for rate limiting. No mutex anywhere.
Memory: the map is bounded. Once full, unseen fingerprints are NOT tracked and decide as :full — in calm weather that’s harmless; in a storm of unique fingerprints the global breaker (Layer 2) takes over.
Defined Under Namespace
Classes: Entry
Constant Summary collapse
- WINDOW_SECONDS =
60- DAY_SECONDS =
86_400
Instance Method Summary collapse
-
#decide(gate_key) ⇒ Symbol
Decide capture fidelity for one event of this fingerprint.
-
#initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }) ⇒ FingerprintBuckets
constructor
A new instance of FingerprintBuckets.
- #reset! ⇒ Object
Constructor Details
#initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }) ⇒ FingerprintBuckets
Returns a new instance of FingerprintBuckets.
35 36 37 38 |
# File 'lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb', line 35 def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }) @clock = clock reset! end |
Instance Method Details
#decide(gate_key) ⇒ Symbol
Decide capture fidelity for one event of this fingerprint.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb', line 46 def decide(gate_key) now = @clock.call entry = fetch_entry(gate_key, now) return :full unless entry # map full — Layer 2 owns the storm case roll_windows(entry, now) entry.window_count += 1 n = entry.window_count if n <= full_per_minute decide_calm_context(entry) elsif n == full_per_minute + 1 || ((n - full_per_minute) % keep_every).zero? :lite else :count_only end end |
#reset! ⇒ Object
40 41 42 |
# File 'lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb', line 40 def reset! @entries = Concurrent::Map.new end |