Class: RailsErrorDashboard::Services::StormProtection::CountBuffer

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_error_dashboard/services/storm_protection/count_buffer.rb

Overview

In-memory accumulator for events that are counted but not stored per-event (Layer 1 overflow and the breaker’s count-only mode).

Stores exact counts plus just enough identity to reconcile onto the right ErrorLog at flush time: the flush command recomputes the canonical error_hash from these parts (with application resolved in the background job, where DB access is allowed) and issues a single ‘occurrence_count = occurrence_count + N` UPDATE per fingerprint. Fingerprints first seen during count-only mode get a minimal ErrorLog created from the stored exemplar. Counting is exact — no extrapolation.

Memory: bounded map; beyond the cap events land in a single overflow counter (still exact in total, anonymous in identity).

Concurrency: snapshot! atomically swaps the whole map out via AtomicReference, so flushing never races with recording.

Defined Under Namespace

Classes: Entry

Instance Method Summary collapse

Constructor Details

#initializeCountBuffer

Returns a new instance of CountBuffer.



29
30
31
# File 'lib/rails_error_dashboard/services/storm_protection/count_buffer.rb', line 29

def initialize
  reset!
end

Instance Method Details

#any?Boolean

Returns:

  • (Boolean)


63
64
65
# File 'lib/rails_error_dashboard/services/storm_protection/count_buffer.rb', line 63

def any?
  @overflow.value.positive? || !@map_ref.get.empty?
end

#record(gate_key, parts) ⇒ Object

Record one counted-not-stored event.

Parameters:

  • gate_key (String)

    cheap in-process bucketing key

  • parts (Hash)

    identity parts captured at the gate



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/rails_error_dashboard/services/storm_protection/count_buffer.rb', line 41

def record(gate_key, parts)
  map = @map_ref.get
  entry = map[gate_key]

  unless entry
    if map.size >= max_tracked
      @overflow.increment
      return
    end
    entry = map.compute_if_absent(gate_key) do
      Entry.new(
        parts[:error_class], parts[:message], parts[:first_app_frame],
        parts[:controller_name], parts[:action_name], parts[:custom_hash],
        Concurrent::AtomicFixnum.new(0), Time.current, Time.current
      )
    end
  end

  entry.count.increment
  entry.last_seen_at = Time.current
end

#reset!Object



33
34
35
36
# File 'lib/rails_error_dashboard/services/storm_protection/count_buffer.rb', line 33

def reset!
  @map_ref = Concurrent::AtomicReference.new(Concurrent::Map.new)
  @overflow = Concurrent::AtomicFixnum.new(0)
end

#snapshot!Hash

Atomically swap the buffer out and return serializable entry hashes.

Returns:

  • (Hash)

    { entries: Array<Hash>, overflow: Integer }



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/rails_error_dashboard/services/storm_protection/count_buffer.rb', line 69

def snapshot!
  old_map = @map_ref.get_and_set(Concurrent::Map.new)
  overflow = @overflow.value
  @overflow.update { |v| v - overflow }

  entries = []
  old_map.each_pair do |_key, entry|
    entries << {
      "error_class" => entry.error_class,
      "message" => entry.message,
      "first_app_frame" => entry.first_app_frame,
      "controller_name" => entry.controller_name,
      "action_name" => entry.action_name,
      "custom_hash" => entry.custom_hash,
      "count" => entry.count.value,
      "first_seen_at" => entry.first_seen_at.iso8601,
      "last_seen_at" => entry.last_seen_at.iso8601
    }
  end

  { entries: entries, overflow: overflow }
end