One breaker per UF, so a SEFAZ that is down cannot drag the others with it.
A UF whose authorizer is unreachable would otherwise cost every caller a full
timeout, and enough of those in flight exhausts the pool that other UFs also
need. After :threshold consecutive transport failures the breaker opens and
calls to that UF fail immediately with {:error, {:circuit_open, uf}}; after
:cooldown one call is let through, and it either closes the breaker or
re-opens it.
What counts as a failure
Transport only — timeouts, TLS alerts, unreachable hosts, HTTP 5xx. A SEFAZ
rejection is a business answer that arrived successfully, so cStat 204 or
656 leaves the breaker closed. Tripping on rejections would take a UF offline
for a caller sending bad documents.
Why ETS rather than a process
The check runs on every request. A GenServer per UF would serialise exactly the traffic this is meant to protect; the table is read directly and updated with atomic counters, so a healthy UF pays one lookup.
Summary
Functions
Whether a call to uf may proceed.
Returns a specification to start this module under a supervisor.
Records a transport failure, opening the breaker at the threshold.
Records a call that reached SEFAZ, closing the breaker.
Forgets every breaker. For tests and for an operator forcing a retry.
Types
@type key() :: String.t()
Functions
Whether a call to uf may proceed.
A breaker past its cooldown is let through as a trial: the next
record_success/1 closes it, and the next record_failure/1 re-opens it for
another cooldown.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec record_failure(key()) :: :ok
Records a transport failure, opening the breaker at the threshold.
@spec record_success(key()) :: :ok
Records a call that reached SEFAZ, closing the breaker.
@spec reset() :: :ok
Forgets every breaker. For tests and for an operator forcing a retry.
@spec start_link(keyword()) :: GenServer.on_start()