Current section

Files

Jump to
exile lib exile watcher.ex
Raw

lib/exile/watcher.ex

defmodule Exile.Watcher do
@moduledoc false
use GenServer, restart: :temporary
require Logger
alias Exile.Process.Nif, as: Nif
def start_link(args) do
{:ok, _pid} = GenServer.start_link(__MODULE__, args)
end
def watch(pid, os_pid, socket_path) do
spec = {Exile.Watcher, %{pid: pid, os_pid: os_pid, socket_path: socket_path}}
DynamicSupervisor.start_child(Exile.WatcherSupervisor, spec)
end
@impl true
def init(args) do
%{pid: pid, os_pid: os_pid, socket_path: socket_path} = args
Process.flag(:trap_exit, true)
ref = Elixir.Process.monitor(pid)
{:ok, %{pid: pid, os_pid: os_pid, socket_path: socket_path, ref: ref}}
end
@impl true
def handle_info({:DOWN, ref, :process, pid, _reason}, %{pid: pid, ref: ref} = state) do
%{socket_path: socket_path, os_pid: os_pid} = state
_ = File.rm(socket_path)
# Ideally we should skip checking if program is alive if the Exile
# process is terminated with reason `:normal`. But at the moment
# when owner process terminates we unconditionally exit
# potentially with the reason `:normal`, so we can not rely on
# `:normal` reason
if !process_dead?(os_pid, 50) do
attempt_graceful_exit(os_pid)
end
{:stop, :normal, nil}
end
def handle_info({:EXIT, _, reason}, nil), do: {:stop, reason, nil}
# when watcher is attempted to be killed, we forcefully kill external os process.
# This can happen when beam receive SIGTERM
def handle_info({:EXIT, _, reason}, %{pid: pid, socket_path: socket_path, os_pid: os_pid}) do
Logger.debug("Watcher exiting. reason: #{inspect(reason)}")
File.rm!(socket_path)
Elixir.Process.exit(pid, :watcher_exit)
attempt_graceful_exit(os_pid)
{:stop, reason, nil}
end
# certain programs such as older version of `parallel` expects
# multiple `SIGTERM` signals
@term_seq [sigterm: 150, sigterm: 100, sigkill: 100]
@spec attempt_graceful_exit(Exile.Process.os_pid()) :: :ok | no_return
defp attempt_graceful_exit(os_pid) do
Logger.debug("Starting graceful termination sequence: #{inspect(@term_seq)}")
dead? =
Enum.reduce_while(@term_seq, false, fn {signal, wait_time}, _dead? ->
Logger.debug("Sending signal: #{signal} with wait_time: #{wait_time}")
Nif.nif_kill(os_pid, signal)
# check process_status first before going to sleep
if process_dead?(os_pid, wait_time) do
Logger.debug("External program terminated successfully with signal: #{signal}")
{:halt, true}
else
Logger.debug("Failed to stop with signal: #{signal}")
{:cont, false}
end
end)
if dead? do
:ok
else
Logger.error("Failed to kill external process, os_pid #{os_pid}")
raise "Failed to kill external process"
end
end
@spec process_dead?(Exile.Process.os_pid(), pos_integer) :: boolean
defp process_dead?(os_pid, wait_time) do
# check process_status first before going to sleep
if process_status(os_pid) == :dead do
true
else
Elixir.Process.sleep(wait_time)
process_status(os_pid) == :dead
end
end
@spec process_status(Exile.Process.os_pid()) :: :alive | :dead
defp process_status(os_pid) do
if Nif.nif_is_os_pid_alive(os_pid) do
:alive
else
:dead
end
end
end