Packages
snakepit
0.1.1
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.1
0.9.0
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
High-performance pooler and session manager for external language integrations. Supports Python, Node.js, Ruby, and more with gRPC streaming, session management, and production-ready process cleanup.
Current section
Files
Jump to
Current section
Files
lib/snakepit/pool/worker_supervisor.ex
defmodule Snakepit.Pool.WorkerSupervisor do
@moduledoc """
DynamicSupervisor for pool worker processes.
This supervisor manages the lifecycle of workers:
- Starts workers on demand
- Handles crashes with automatic restarts
- Provides clean shutdown of workers
"""
use DynamicSupervisor
require Logger
@doc """
Starts the worker supervisor.
"""
def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
extra_arguments: []
)
end
@doc """
Starts a new pool worker with the given ID.
## Examples
iex> Snakepit.Pool.WorkerSupervisor.start_worker("worker_123")
{:ok, #PID<0.123.0>}
"""
def start_worker(worker_id) when is_binary(worker_id) do
# Start the permanent starter supervisor, not the transient worker directly
# This gives us automatic worker restarts without Pool intervention
child_spec = {Snakepit.Pool.Worker.Starter, worker_id}
case DynamicSupervisor.start_child(__MODULE__, child_spec) do
{:ok, starter_pid} ->
Logger.info("Started worker starter for #{worker_id} with PID #{inspect(starter_pid)}")
{:ok, starter_pid}
{:error, {:already_started, starter_pid}} ->
Logger.debug(
"Worker starter for #{worker_id} already running with PID #{inspect(starter_pid)}"
)
{:ok, starter_pid}
{:error, reason} = error ->
Logger.error("Failed to start worker starter for #{worker_id}: #{inspect(reason)}")
error
end
end
@doc """
Stops a worker gracefully.
"""
def stop_worker(worker_id) do
case Snakepit.Pool.Registry.get_worker_pid(worker_id) do
{:ok, pid} ->
DynamicSupervisor.terminate_child(__MODULE__, pid)
{:error, :not_found} ->
{:error, :worker_not_found}
end
end
@doc """
Lists all supervised workers.
"""
def list_workers do
DynamicSupervisor.which_children(__MODULE__)
|> Enum.map(fn {_, pid, _, _} -> pid end)
|> Enum.filter(&Process.alive?/1)
end
@doc """
Returns the count of active workers.
"""
def worker_count do
DynamicSupervisor.count_children(__MODULE__).active
end
@doc """
Restarts a worker by ID.
"""
def restart_worker(worker_id) do
case Snakepit.Pool.Registry.get_worker_pid(worker_id) do
{:ok, old_pid} ->
# Worker exists, terminate it and wait for cleanup before starting new one
with :ok <- DynamicSupervisor.terminate_child(__MODULE__, old_pid),
:ok <- wait_for_worker_cleanup(old_pid) do
start_worker(worker_id)
else
# Propagate termination/cleanup errors
error -> error
end
{:error, :not_found} ->
# Worker doesn't exist, so we just need to start it
start_worker(worker_id)
end
end
@cleanup_retry_interval Application.compile_env(:snakepit, :cleanup_retry_interval, 100)
@cleanup_max_retries Application.compile_env(:snakepit, :cleanup_max_retries, 10)
# Wait for a specific PID to terminate to avoid race conditions
defp wait_for_worker_cleanup(pid, retries \\ @cleanup_max_retries) do
if retries > 0 and Process.alive?(pid) do
# Monitor the specific PID we want to wait for
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, _reason} ->
:ok
after
@cleanup_retry_interval ->
Process.demonitor(ref, [:flush])
wait_for_worker_cleanup(pid, retries - 1)
end
else
if Process.alive?(pid) do
{:error, :cleanup_timeout}
else
:ok
end
end
end
end