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/application_cleanup.ex
defmodule Snakepit.Pool.ApplicationCleanup do
@moduledoc """
Provides hard guarantees for worker process cleanup when the application exits.
This module ensures that NO worker processes survive application shutdown,
preventing orphaned processes while still allowing normal pool operations.
"""
use GenServer
require Logger
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
# Trap exits so we can cleanup before the VM dies
Process.flag(:trap_exit, true)
# Register for VM shutdown notifications
:erlang.process_flag(:priority, :high)
Logger.info("🛡️ Application cleanup handler started")
{:ok, %{}}
end
# Note: Worker process tracking is handled entirely by ProcessRegistry.
# ApplicationCleanup queries ProcessRegistry during shutdown for process cleanup.
@doc """
Force cleanup all tracked worker processes.
"""
def force_cleanup_all do
GenServer.call(__MODULE__, :force_cleanup_all)
end
def handle_call(:force_cleanup_all, _from, state) do
# Query ProcessRegistry for ALL registered PIDs (workers may already be terminated)
all_pids = Snakepit.Pool.ProcessRegistry.get_all_process_pids()
killed_count = force_kill_worker_processes(all_pids)
{:reply, killed_count, state}
end
# This is called when the VM is shutting down
def terminate(reason, _state) do
Logger.warning("🛑 Application cleanup final check initiated by shutdown: #{inspect(reason)}")
# The Pool and Workers have already attempted a graceful shutdown.
# Our job is to be the final, brutal guarantee.
all_pids = Snakepit.Pool.ProcessRegistry.get_all_process_pids()
if Enum.any?(all_pids) do
Logger.warning(
"🔥 ApplicationCleanup found #{length(all_pids)} surviving processes. Forcefully terminating with SIGKILL."
)
# No more grace. Just kill everything that's left.
sigkill_count = send_signal_to_processes(all_pids, "KILL")
Logger.warning("✅ SIGKILL sent to #{sigkill_count} surviving processes")
else
Logger.info(
"✅ ApplicationCleanup confirms all external processes were shut down correctly."
)
end
:ok
end
# Send a signal to multiple processes and count successes
defp send_signal_to_processes(pids, signal) do
Enum.reduce(pids, 0, fn pid, acc ->
case send_signal_to_process(pid, signal) do
:ok -> acc + 1
:error -> acc
end
end)
end
# Send a signal to a single process
defp send_signal_to_process(pid, signal) when is_integer(pid) do
try do
case System.cmd("kill", ["-#{signal}", "#{pid}"], stderr_to_stdout: true) do
{_output, 0} -> :ok
{_error, _} -> :error
end
rescue
_ -> :error
end
end
# Keep the old function for manual cleanup calls
defp force_kill_worker_processes(pids) do
Enum.reduce(pids, 0, fn pid, acc ->
try do
# Kill process group first (negative PID)
case System.cmd("kill", ["-KILL", "-#{pid}"], stderr_to_stdout: true) do
{_output, 0} ->
acc + 1
{_error, _} ->
# Fallback to single process kill
case System.cmd("kill", ["-KILL", "#{pid}"], stderr_to_stdout: true) do
{_output, 0} -> acc + 1
{_error, _} -> acc
end
end
rescue
# Only rescue specific, expected errors
e in [ArgumentError] ->
Logger.error("Failed to kill process with invalid PID #{inspect(pid)}: #{inspect(e)}")
# Continue, but log the problem
acc
# Log other unexpected errors explicitly
e ->
Logger.error(
"Unexpected exception during worker cleanup for PID #{inspect(pid)}: #{inspect(e)}"
)
# Continue cleanup for other processes
acc
end
end)
end
end