Ghoul
An undead cleanup crew for your processes.
{:ghoul, "~> 0.1"},
Motivation
Ghoul solves two problems for the OTP developer:
- Robust execution of cleanup code after a process exits
- Robust termination of a process that has exceeded timing expectations
Both of these problems can be handled in one-off manners, and the :timeout set
of responses for GenServer provides a builtin solution for simple use cases.
Ghoul steps in once the builtin functionality is no longer sufficient.
Cleanup Example
Hardware interaction is a common motivation for wanting cleanup code. This is a simple, notional example of tying an LED to the lifecycle of a particular GenServer:
defmodule LedExample do
use GenServer
# ...snip...
def init([]) do
Ghoul.summon(LedExample, on_death: &cleanup/3)
turn_on_led()
{:ok, %State{}}
end
def cleanup(LedExample, _reason, _ghoul_state) do
turn_off_led()
end
# ...snip...
end
It is important to note that Ghoul.summon/2 will block during subsequent calls
for a given process_key (in this example, LedServer) until the cleanup code
has completed. Thus, the call to Ghoul.summon/2 should happen before any
side-effect code (e.g. turn_on_led/0), and any side-effect code in the cleanup
method should be synchronous to avoid race-conditions when, e.g., a Supervisor
restarts the GenServer in question.
Timeout Example
In this highly notional example, a GenServer managing an external server transitions between multiple states with varying timeout rules and cleanup logic.
The server should boot within 100ms, initialize within 50ms, and then respond to a test request within 20ms
defmodule FsmExample do
use GenServer
import ShorterMaps
defmodule State do
defstruct [port: nil, fsm: :not_init]
end
def init([]) do
Ghoul.summon(FsmExample, on_death: &cleanup/3)
# start the external server
{:ok, port} = start_external_server()
# provide the port to Ghoul for use during cleanup:
Ghoul.set_state(FsmExample, port)
# schedule this process for destruction if the external server fails to boot
# within the specified timeout of 100ms.
Ghoul.reap_in(FsmExample, :boot_timeout, 100)
{:ok, ~M{%State port, fsm: :booting}}
end
def handle_info({port, "BOOTED"}, ~M{port, fsm: :booting}) do
:ok = initialize_server(port)
# this cancels the boot reaping, and replaces it with an init reaping:
Ghoul.reap_in(FsmExample, :init_timeout, 50)
{:noreply, %{state|fsm: :initing}}
end
def handle_info({port, "INIT COMPLETE"}, ~M{port, fsm: :initing}) do
send_test_query(port)
Ghoul.reap_in(FsmExample, :example_timeout, 20)
{:noreply, %{state|fsm: :testing}}
end
def handle_info({port, "TEST COMPLETE"}, ~M{port, fsm: :testing}) do
# prevent killing this process
Ghoul.cancel_reap(FsmExample)
{:noreply, %{state|fsm: :ready}}
end
def cleanup(FsmExample, :boot_timeout, port) do
# server didn't boot, just close the port:
close_server_port(port)
end
def cleanup(FsmExample, _reason, port) do
disconnect_server(port)
close_server_port(port)
end
# ...snip...
endInstallation
Add {:ghoul, "~> 0.1"}, to your mix deps