Current section
Files
Jump to
Current section
Files
lib/command/circuit_breaker.ex
defmodule Argos.Command.CircuitBreaker do
@moduledoc """
Circuit Breaker para comandos del sistema.
Implementa el patrón Circuit Breaker para prevenir la ejecución de comandos
cuando hay demasiados fallos consecutivos. Esto mejora la resiliencia del sistema.
## Estados
- `:closed` - Circuito cerrado, comandos se ejecutan normalmente
- `:open` - Circuito abierto, comandos se rechazan inmediatamente
- `:half_open` - Circuito semi-abierto, permite un intento de prueba
## Configuración
config :argos, :circuit_breaker,
threshold: 5,
timeout: 60_000,
enabled: true
## Uso
# Ejecutar comando con circuit breaker
Argos.Command.exec("risky_command", circuit_breaker: true)
"""
use GenServer
require Logger
@default_threshold 5
@default_timeout 60_000
@default_enabled true
defstruct [
:state,
:failure_count,
:last_failure_time,
:threshold,
:timeout,
:enabled
]
@type state :: :closed | :open | :half_open
@type t :: %__MODULE__{
state: state(),
failure_count: non_neg_integer(),
last_failure_time: integer() | nil,
threshold: non_neg_integer(),
timeout: non_neg_integer(),
enabled: boolean()
}
@doc """
Inicia el Circuit Breaker.
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Verifica si el circuito permite ejecutar un comando.
"""
@spec call(atom(), (-> result)) :: {:ok, result} | {:error, :circuit_open} | {:error, term()}
when result: term()
def call(key, fun) when is_function(fun, 0) do
GenServer.call(__MODULE__, {:call, key, fun}, :infinity)
end
@doc """
Registra un éxito en el circuito.
"""
@spec success(atom()) :: :ok
def success(key) do
GenServer.cast(__MODULE__, {:success, key})
end
@doc """
Registra un fallo en el circuito.
"""
@spec failure(atom()) :: :ok
def failure(key) do
GenServer.cast(__MODULE__, {:failure, key})
end
@doc """
Obtiene el estado actual del circuito.
"""
@spec get_state(atom()) :: state()
def get_state(key) do
GenServer.call(__MODULE__, {:get_state, key}, :infinity)
end
@doc """
Resetea el circuito a estado cerrado.
"""
@spec reset(atom()) :: :ok
def reset(key) do
GenServer.cast(__MODULE__, {:reset, key})
end
@impl true
def init(opts) do
threshold = Keyword.get(opts, :threshold, @default_threshold)
timeout = Keyword.get(opts, :timeout, @default_timeout)
enabled = Keyword.get(opts, :enabled, @default_enabled)
state = %__MODULE__{
state: :closed,
failure_count: 0,
last_failure_time: nil,
threshold: threshold,
timeout: timeout,
enabled: enabled
}
{:ok, %{default: state}}
end
@impl true
def handle_call({:call, key, fun}, _from, state) do
circuit = Map.get(state, key, state.default)
if circuit.enabled do
handle_enabled_circuit(circuit, key, fun, state)
else
execute_without_circuit_breaker(fun, state)
end
end
def handle_call({:get_state, key}, _from, state) do
circuit = Map.get(state, key, state.default)
{:reply, circuit.state, state}
end
defp execute_without_circuit_breaker(fun, state) do
result = fun.()
{:reply, {:ok, result}, state}
rescue
e -> {:reply, {:error, e}, state}
catch
:exit, reason -> {:reply, {:error, {:exit, reason}}, state}
:throw, reason -> {:reply, {:error, {:throw, reason}}, state}
end
defp handle_enabled_circuit(circuit, key, fun, state) do
case circuit.state do
:closed -> handle_closed_circuit(circuit, key, fun, state)
:open -> handle_open_circuit(circuit, key, fun, state)
:half_open -> handle_half_open_circuit(circuit, key, fun, state)
end
end
defp handle_closed_circuit(circuit, key, fun, state) do
result = fun.()
new_circuit = %{circuit | failure_count: 0}
new_state = Map.put(state, key, new_circuit)
{:reply, {:ok, result}, new_state}
rescue
e ->
new_circuit = record_failure(circuit)
new_state = Map.put(state, key, new_circuit)
{:reply, {:error, e}, new_state}
catch
:exit, reason ->
new_circuit = record_failure(circuit)
new_state = Map.put(state, key, new_circuit)
{:reply, {:error, {:exit, reason}}, new_state}
:throw, reason ->
new_circuit = record_failure(circuit)
new_state = Map.put(state, key, new_circuit)
{:reply, {:error, {:throw, reason}}, new_state}
end
defp handle_open_circuit(circuit, key, fun, state) do
if should_transition_to_half_open?(circuit) do
new_circuit = %{circuit | state: :half_open, failure_count: 0}
new_state = Map.put(state, key, new_circuit)
try do
result = fun.()
new_circuit = %{new_circuit | state: :closed, failure_count: 0}
new_state = Map.put(new_state, key, new_circuit)
{:reply, {:ok, result}, new_state}
rescue
e ->
new_circuit = %{new_circuit | state: :open, last_failure_time: System.system_time(:millisecond)}
new_state = Map.put(new_state, key, new_circuit)
{:reply, {:error, e}, new_state}
catch
:exit, reason ->
new_circuit = %{new_circuit | state: :open, last_failure_time: System.system_time(:millisecond)}
new_state = Map.put(new_state, key, new_circuit)
{:reply, {:error, {:exit, reason}}, new_state}
:throw, reason ->
new_circuit = %{new_circuit | state: :open, last_failure_time: System.system_time(:millisecond)}
new_state = Map.put(new_state, key, new_circuit)
{:reply, {:error, {:throw, reason}}, new_state}
end
else
{:reply, {:error, :circuit_open}, state}
end
end
defp handle_half_open_circuit(circuit, key, fun, state) do
result = fun.()
new_circuit = %{circuit | state: :closed, failure_count: 0}
new_state = Map.put(state, key, new_circuit)
{:reply, {:ok, result}, new_state}
rescue
e ->
new_circuit = %{circuit | state: :open, last_failure_time: System.system_time(:millisecond)}
new_state = Map.put(state, key, new_circuit)
{:reply, {:error, e}, new_state}
catch
:exit, reason ->
new_circuit = %{circuit | state: :open, last_failure_time: System.system_time(:millisecond)}
new_state = Map.put(state, key, new_circuit)
{:reply, {:error, {:exit, reason}}, new_state}
:throw, reason ->
new_circuit = %{circuit | state: :open, last_failure_time: System.system_time(:millisecond)}
new_state = Map.put(state, key, new_circuit)
{:reply, {:error, {:throw, reason}}, new_state}
end
@impl true
def handle_cast({:success, key}, state) do
circuit = Map.get(state, key, state.default)
new_circuit = %{circuit | failure_count: 0, state: :closed}
new_state = Map.put(state, key, new_circuit)
{:noreply, new_state}
end
def handle_cast({:failure, key}, state) do
circuit = Map.get(state, key, state.default)
new_circuit = record_failure(circuit)
new_state = Map.put(state, key, new_circuit)
{:noreply, new_state}
end
def handle_cast({:reset, key}, state) do
circuit = Map.get(state, key, state.default)
new_circuit = %{circuit | state: :closed, failure_count: 0, last_failure_time: nil}
new_state = Map.put(state, key, new_circuit)
{:noreply, new_state}
end
defp record_failure(circuit) do
new_count = circuit.failure_count + 1
new_state = if new_count >= circuit.threshold, do: :open, else: circuit.state
%{
circuit
| failure_count: new_count,
last_failure_time: System.system_time(:millisecond),
state: new_state
}
end
defp should_transition_to_half_open?(circuit) do
case circuit.last_failure_time do
nil -> true
last_time -> System.system_time(:millisecond) - last_time >= circuit.timeout
end
end
end