Class: Findbug::Alerts::Throttler

Inherits:
Object
  • Object
show all
Defined in:
lib/findbug/alerts/throttler.rb

Overview

Throttler prevents alert spam by limiting how often we alert for the same error.

THE PROBLEM

Without throttling:

  • 1000 users hit the same bug

  • 1000 Slack messages

  • Your team mutes the channel

  • You miss the NEXT important error

With throttling:

  • First occurrence: Alert sent

  • Next 999 in 5 minutes: Throttled

  • 5 minutes later, if still happening: Another alert

IMPLEMENTATION

We use Redis to store “last alerted at” timestamps:

Key: findbug:alert:throttle:{fingerprint}
Value: ISO8601 timestamp
TTL: throttle_period

If the key exists and isn’t expired, we’re throttled. Simple and fast.

Constant Summary collapse

THROTTLE_KEY_PREFIX =
"findbug:alert:throttle:"

Class Method Summary collapse

Class Method Details

.clear(fingerprint) ⇒ Object

Clear throttle for a specific error (e.g., when error is resolved)

Parameters:

  • fingerprint (String)

    error fingerprint



72
73
74
75
76
77
78
79
80
# File 'lib/findbug/alerts/throttler.rb', line 72

def clear(fingerprint)
  key = throttle_key(fingerprint)

  Storage::ConnectionPool.with do |redis|
    redis.del(key)
  end
rescue StandardError
  # Ignore errors during cleanup
end

.record(fingerprint) ⇒ Object

Record that we sent an alert (starts throttle period)

Parameters:

  • fingerprint (String)

    error fingerprint



57
58
59
60
61
62
63
64
65
66
# File 'lib/findbug/alerts/throttler.rb', line 57

def record(fingerprint)
  key = throttle_key(fingerprint)
  ttl = throttle_period

  Storage::ConnectionPool.with do |redis|
    redis.setex(key, ttl, Time.now.utc.iso8601)
  end
rescue StandardError => e
  Findbug.logger.debug("[Findbug] Throttle record failed: #{e.message}")
end

.remaining_seconds(fingerprint) ⇒ Integer?

Get remaining throttle time

Parameters:

  • fingerprint (String)

    error fingerprint

Returns:

  • (Integer, nil)

    seconds remaining, or nil if not throttled



87
88
89
90
91
92
93
94
95
96
# File 'lib/findbug/alerts/throttler.rb', line 87

def remaining_seconds(fingerprint)
  key = throttle_key(fingerprint)

  Storage::ConnectionPool.with do |redis|
    ttl = redis.ttl(key)
    ttl.positive? ? ttl : nil
  end
rescue StandardError
  nil
end

.throttled?(fingerprint) ⇒ Boolean

Check if an alert is currently throttled

Parameters:

  • fingerprint (String)

    error fingerprint

Returns:

  • (Boolean)

    true if throttled



42
43
44
45
46
47
48
49
50
51
# File 'lib/findbug/alerts/throttler.rb', line 42

def throttled?(fingerprint)
  key = throttle_key(fingerprint)

  Storage::ConnectionPool.with do |redis|
    redis.exists?(key)
  end
rescue StandardError => e
  Findbug.logger.debug("[Findbug] Throttle check failed: #{e.message}")
  false # If we can't check, allow the alert
end