Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d1bb503
refactor: use structured errors, add error codes
v0idpwn Jan 14, 2026
e080f30
refactor: improve DbHandler.checkout errors
v0idpwn Jan 27, 2026
0721926
refactor: improvements and fixes across different contexts
v0idpwn Jan 27, 2026
5bcfea4
fix: improve circuit breaker open errors
v0idpwn Jan 27, 2026
826ec51
wip
v0idpwn Jan 27, 2026
4ae0913
bugfix
v0idpwn Jan 27, 2026
40a7235
refactor: use exceptions in more places where its appropriate
v0idpwn Jan 27, 2026
29ad311
wip: test that the errors are loggable, etc
v0idpwn Jan 27, 2026
cc52c04
fix: bad error
v0idpwn Jan 27, 2026
6dc3469
fix: typespec
v0idpwn Jan 27, 2026
ec7da59
minor improvements
v0idpwn Jan 27, 2026
bec742a
Merge remote-tracking branch 'origin/main' into refactor/rework-error…
v0idpwn Feb 13, 2026
b868609
After merge test fixes
v0idpwn Feb 13, 2026
13dd9f5
Merge branch 'main' into refactor/rework-errors-add-codes
v0idpwn Feb 13, 2026
7b1feb0
chore: credo
v0idpwn Feb 13, 2026
4cd0a8c
wip
v0idpwn Feb 13, 2026
ec04e48
fix: dialyzer
v0idpwn Feb 13, 2026
cc95aa3
fix: remove unnecessary link
v0idpwn Feb 13, 2026
5d9c2b9
chore: add missing impl
v0idpwn Feb 13, 2026
e980022
fix: correct invalid reraise calls in assert_valid_error macro
v0idpwn Feb 13, 2026
641825a
fix typo
v0idpwn Feb 13, 2026
df34e57
bugfix
v0idpwn Feb 13, 2026
d6420ab
chore: move shared constant to a public function
v0idpwn Feb 13, 2026
5fd3e64
fix: error path coverage and fixes
v0idpwn Feb 13, 2026
fa3e09c
Merge branch 'main' into refactor/rework-errors-add-codes
v0idpwn Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions lib/supavisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,36 +58,38 @@ defmodule Supavisor do
end
end

@spec stop(id) :: :ok | {:error, :tenant_not_found}
@spec stop(id) :: :ok | {:error, Supavisor.Errors.WorkerNotFoundError.t()}
def stop(id) do
case get_global_sup(id) do
nil ->
{:error, :tenant_not_found}
{:error, %Supavisor.Errors.WorkerNotFoundError{id: id}}

pid ->
Supervisor.stop(pid)
end
end

@spec get_local_workers(id) :: {:ok, workers} | {:error, :worker_not_found}
@spec get_local_workers(id) ::
{:ok, workers} | {:error, Supavisor.Errors.WorkerNotFoundError.t()}
def get_local_workers(id) do
workers = %{
manager: get_local_manager(id),
pool: get_local_pool(id)
}

if nil in Map.values(workers) do
Logger.error("Could not get workers for tenant #{inspect(id)}")
{:error, :worker_not_found}
{:error, %Supavisor.Errors.WorkerNotFoundError{id: id}}
else
{:ok, workers}
end
end

@spec subscribe_local(pid, id) ::
{:ok, subscribe_opts}
| {:error, :max_clients_reached}
| {:error, :terminating, term()}
| {:error, Supavisor.Errors.SessionMaxClientsError.t()}
| {:error, Supavisor.Errors.MaxClientConnectionsError.t()}
| {:error, Supavisor.Errors.PoolTerminatingError.t()}
| {:error, Supavisor.Errors.WorkerNotFoundError.t()}
def subscribe_local(pid, id) do
with {:ok, workers} <- get_local_workers(id),
{:ok, ps, idle_timeout} <- Manager.subscribe(workers.manager, pid) do
Expand All @@ -97,8 +99,10 @@ defmodule Supavisor do

@spec subscribe(pid, id, pid) ::
{:ok, subscribe_opts}
| {:error, :max_clients_reached}
| {:error, :terminating, term()}
| {:error, Supavisor.Errors.SessionMaxClientsError.t()}
| {:error, Supavisor.Errors.MaxClientConnectionsError.t()}
| {:error, Supavisor.Errors.PoolTerminatingError.t()}
| {:error, Supavisor.Errors.WorkerNotFoundError.t()}
def subscribe(sup, id, pid \\ self()) do
dest_node = node(sup)

Expand Down Expand Up @@ -319,7 +323,7 @@ defmodule Supavisor do
def try_start_local_pool(id, secrets, log_level) do
if count_pools(tenant(id)) < @max_pools,
do: start_local_pool(id, secrets, log_level),
else: {:error, :max_pools_reached}
else: {:error, %Supavisor.Errors.MaxPoolsReachedError{}}
end

@spec start_local_pool(id, secrets, atom()) :: {:ok, pid} | {:error, any}
Expand Down Expand Up @@ -374,9 +378,8 @@ defmodule Supavisor do
end

error ->
Logger.error("Can't find tenant with external_id #{inspect(id)} #{inspect(error)}")

{:error, :tenant_not_found}
Logger.error("Can't find pool config for #{inspect(id)} #{inspect(error)}")
{:error, %Supavisor.Errors.PoolConfigNotFoundError{id: id}}
end
end

Expand All @@ -391,11 +394,11 @@ defmodule Supavisor do
end
end

@spec get_pool_ranch(id) :: {:ok, map()} | {:error, :not_found}
@spec get_pool_ranch(id) :: {:ok, map()} | {:error, Supavisor.Errors.PoolRanchNotFoundError.t()}
def get_pool_ranch(id) do
case :syn.lookup(:tenants, id) do
{_sup_pid, %{port: _port, host: _host} = meta} -> {:ok, meta}
_ -> {:error, :not_found}
_ -> {:error, %Supavisor.Errors.PoolRanchNotFoundError{id: id}}
end
end

Expand Down
22 changes: 13 additions & 9 deletions lib/supavisor/circuit_breaker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,33 @@ defmodule Supavisor.CircuitBreaker do

require Logger

alias Supavisor.Errors.CircuitBreakerError

@table __MODULE__

@config %{
get_secrets: %{
max_failures: 5,
window_seconds: 600,
block_seconds: 600,
propagate?: false,
explanation: "Failed to retrieve database credentials"
explanation:
"failed to retrieve database credentials after multiple attempts, new connections are temporarily blocked",
propagate?: false
},
db_connection: %{
max_failures: 100,
window_seconds: 300,
block_seconds: 600,
propagate?: false,
explanation: "Unable to establish connection to upstream database"
explanation:
"too many failed attempts to connect to the database, new connections are temporarily blocked",
propagate?: false
},
auth_error: %{
max_failures: 10,
window_seconds: 300,
block_seconds: 600,
propagate?: true,
explanation: "Too many authentication errors"
explanation: "too many authentication failures, new connections are temporarily blocked",
propagate?: true
}
}

Expand Down Expand Up @@ -73,9 +77,9 @@ defmodule Supavisor.CircuitBreaker do

@doc """
Checks if a circuit breaker is open for a given key and operation.
Returns :ok if operation is allowed, {:error, :circuit_open, blocked_until} otherwise.
Returns :ok if operation is allowed, {:error, CircuitBreakerError.t()} otherwise.
"""
@spec check(term(), atom()) :: :ok | {:error, :circuit_open, integer()}
@spec check(term(), atom()) :: :ok | {:error, CircuitBreakerError.t()}
def check(key, operation) when is_atom(operation) do
now = System.system_time(:second)
ets_key = {key, operation}
Expand All @@ -88,7 +92,7 @@ defmodule Supavisor.CircuitBreaker do
:ok

[{^ets_key, %{blocked_until: blocked_until}}] when blocked_until > now ->
{:error, :circuit_open, blocked_until}
{:error, %CircuitBreakerError{operation: operation, blocked_until: blocked_until}}

[{^ets_key, state}] ->
:ets.insert(@table, {ets_key, %{state | blocked_until: nil}})
Expand Down
Loading
Loading