GenServer that owns the :break_glass_rate_limit ETS table and serialises
all write operations to prevent TOCTOU races.
Tracks failed authentication attempts per IP address. When the attempt count
reaches :max_attempts, the IP is locked out for :lockout_seconds.
Configuration
:max_attempts— number of failures before lockout (default:5):lockout_seconds— lockout window duration in seconds (default:900)
ETS Table
Table name: :break_glass_rate_limit
Record format: {ip :: String.t(), attempt_count :: non_neg_integer(), locked_at :: integer() | nil}
locked_atisniluntil the attempt count reaches:max_attempts.- Once set,
locked_atis never reset by subsequent failures — the lockout clock does not slide. reset_attempts/1deletes the row entirely, removing both the count and lockout.
Hot-path reads (locked_out?/1, remaining_attempts/1) query ETS directly
without a GenServer round-trip.
Summary
Functions
Returns a specification to start this module under a supervisor.
Returns true if ip is currently locked out (lockout window still active),
false otherwise.
Records a failed authentication attempt for ip.
Returns the number of remaining authentication attempts for ip.
Resets the failed attempt counter and any lockout for ip by deleting the
ETS row entirely.
Starts the RateLimiter GenServer and registers it under Elixir.BreakGlass.RateLimiter.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
Returns true if ip is currently locked out (lockout window still active),
false otherwise.
Reads ETS directly — no GenServer round-trip.
@spec record_failed_attempt(ip :: String.t()) :: :ok
Records a failed authentication attempt for ip.
If the resulting attempt count reaches :max_attempts, a lockout timestamp
is stored and a Logger.warning is emitted with the IP and count.
Returns :ok.
@spec remaining_attempts(ip :: String.t()) :: non_neg_integer()
Returns the number of remaining authentication attempts for ip.
Reads ETS directly — no GenServer round-trip.
Returns the configured :max_attempts (default 5) when no
failures have been recorded for ip.
@spec reset_attempts(ip :: String.t()) :: :ok
Resets the failed attempt counter and any lockout for ip by deleting the
ETS row entirely.
Returns :ok.
@spec start_link(keyword()) :: GenServer.on_start()
Starts the RateLimiter GenServer and registers it under Elixir.BreakGlass.RateLimiter.