## Ghoul

An undead cleanup crew for your processes.

[![Build Status](](
[![Build Docs](](

`{:ghoul, "~> 0.1"},`

### Motivation

Ghoul solves two problems for the OTP developer:

1) Robust execution of cleanup code after a process exits
2) 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

defmodule Led.Worker do
  use GenServer

  # ...snip...

  def init([]) do
    Ghoul.summon(LedExample, on_death: &cleanup/3)
    {:ok, %State{}}

  # This method will be invoked by a separate process after the GenServer dies.
  def cleanup(LedExample, _reason, _ghoul_state) do

  # ...snip...

#### Important

`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.

See the [sequence diagram][led_sequence] for this example for a detailed flow
and race condition analysis of the above example.

A useful side effect of this property is being able to rate-limit how quickly a
`GenServer` can be restarted.  Simply add `Process.sleep(time_ms)` as the last
line of the `on_death/3` callback, and restarts of the process will be spaced
out by `time_ms`.

### Timeout Example

In this 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.

Internally, our GenServer will transition from `:booting` -> `:initing` ->
`:testing` -> `:ready`

Details of the `~M` sigil can be found at the [ShorterMaps][shorter_maps] repo.

defmodule FsmExample do
  use GenServer
  import ShorterMaps

  defmodule State do
    defstruct [port: nil, fsm: :not_init]

  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}}

  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}}
  def handle_info({port, "INIT COMPLETE"}, ~M{port, fsm: :initing}) do
    Ghoul.reap_in(FsmExample, :example_timeout, 20)
    {:noreply, %{state|fsm: :testing}}
  def handle_info({port, "TEST COMPLETE"}, ~M{port, fsm: :testing}) do
    # prevent killing this process
    {:noreply, %{state|fsm: :ready}}

  def cleanup(FsmExample, :boot_timeout, port) do
    # server didn't boot, just close the port:
  def cleanup(FsmExample, _reason, port) do
  # ...snip...

## API

#### `summon/2`

Summon a Ghoul to watch a process.  When the pid terminates, the Ghoul will
execute the function in the `on_death` option, passing in the `process_key`,
the `reason` the pid exited, and the current `ghoul_state`.


* `process_key` - How this pid should be known to Ghoul. Will be passed to the
  `on_death` function as the first parameter.
* `opts` - a keyword list or map with the following options:
  - `:pid` - which pid to have the Ghoul stalk.  Defaults to the calling pid.
  - `:on_death` - a function to be executed after the process dies. Defaults to
    `nil`, and nothing will be executed. Expects 3-arity function, to be
    called as `fun.(process_key, exit_reason, ghoul_state)` by the Ghoul.
  - `:initial_state` - the initial `ghoul_state` for this worker. Defaults to
    `nil`. The `ghoul_state` will be passed to the `on_death` function as the
    third parameter, and can be queried using `Ghoul.get_state/1` and changed
    using `Ghoul.set_state/2`

Return value:
`:ok | {:error, reason}`

#### `banish/1`

Stop the Ghoul for a process, preventing the `on_death/3` callback from
executing and preventing any upcoming reaping.


* `process_key` - the `process_key` of the Ghoul

Return value:
`:ok | {:error, reason}`

#### `get_state/1`

Gets the current state of a Ghoul worker, i.e. the 2nd argument for the
`on_death/3` callback.


* `process_key` - the `process_key` of the Ghoul

Return value:

{:ok, state}|{:error, reason}

#### `set_state/2`

Sets the current state of a Ghoul worker, to be passed as the second argument
to the `on_death/3` callback.


* `process_key` - the `process_key` of the Ghoul

Return value:

{:ok, state}|{:error, reason}

#### `reap_in/3`

Instruct the ghoul to kill the process after a delay. Each time this method is
called for a process, previous `reap_in` directives are canceled. This lets
the Ghoul act as a deadman switch for a process, killing it should it fail to
progress in an expected manner.


* `process_key` - the `process_key` of the Ghoul
* `reason` - the reason to pass to `Process.exit/2`
* `delay_ms` - how long to wait until reaping the process.

Return value:

`:ok | {:error, reason}`

#### `cancel_reap/1`

Cancel a pending reap.


* `process_key` - the `process_key` of the Ghoul

Return value:
`:ok | {:error, reason}`

#### `ttl/1`

Query a Ghoul to see how much time remains unil a reaping.  Result is in
milliseconds, or `false` if the process has already reaped.


* `process_key` - the `process_key` of the Ghoul

Return value:

`integer|false | {:error, reason}`

## Installation

Add `{:ghoul, "~> 0.1"},` to your mix deps.