SefazNfe.CircuitBreaker (sefaz_nfe v0.1.0)

Copy Markdown View Source

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

key()

@type key() :: String.t()

Functions

check(uf)

@spec check(key()) :: :ok | {:error, {:circuit_open, key()}}

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.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

record_failure(uf)

@spec record_failure(key()) :: :ok

Records a transport failure, opening the breaker at the threshold.

record_success(uf)

@spec record_success(key()) :: :ok

Records a call that reached SEFAZ, closing the breaker.

reset()

@spec reset() :: :ok

Forgets every breaker. For tests and for an operator forcing a retry.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()