towerops/lib/towerops/workers/sync_errors.ex
Graham McIntire 6b82205a58 fix(sync): classify unexpected 5xx statuses as transient so vendor syncs retry
SyncErrors.transient?/1 only had explicit clauses for the {:http_error, ...}
shape, but vendor API clients return {:unexpected_status, status, body}. A 502
was retried only by accident via the catch-all, and a 4xx in that shape would
have been retried 20 times. Add explicit {:unexpected_status, ...} clauses: 5xx
is transient (Oban backs off and retries), 4xx is permanent.
2026-05-14 09:27:17 -05:00

32 lines
1.5 KiB
Elixir

defmodule Towerops.Workers.SyncErrors do
@moduledoc """
Classifier for vendor-sync error reasons.
Sync workers (UISP, Preseem, cnMaestro, etc.) call third-party APIs that
can fail for permanent reasons (bad credentials, missing resources) or
transient ones (network blips, rate limits, 5xx). Permanent failures
should not be retried — they will keep failing until config is fixed.
Transient failures should retry so data doesn't go stale waiting on the
next cron cycle.
"""
@doc """
Returns `true` if the error reason is likely transient and worth retrying.
Unknown error shapes default to transient: stale monitoring data is the
bigger risk than an extra retry on a permanent failure.
"""
@spec transient?(term()) :: boolean()
def transient?(:unauthorized), do: false
def transient?(:forbidden), do: false
def transient?(:not_found), do: false
def transient?(:invalid_credentials), do: false
def transient?(:bad_request), do: false
def transient?({:http_error, status, _}) when status in 400..499, do: false
def transient?({:http_error, status}) when status in 400..499, do: false
def transient?({:unexpected_status, status, _}) when status in 400..499, do: false
def transient?({:unexpected_status, status}) when status in 400..499, do: false
def transient?({:unexpected_status, status, _}) when status in 500..599, do: true
def transient?({:unexpected_status, status}) when status in 500..599, do: true
def transient?(_), do: true
end