README.md

## Ghoul

An undead cleanup crew for your processes.

[![Build Status](https://travis-ci.org/meyercm/ghoul.svg?branch=master)](https://travis-ci.org/meyercm/ghoul)
[![Hex.pm](https://img.shields.io/hexpm/v/ghoul.svg)](https://hex.pm/packages/ghoul)
[![Build Docs](https://img.shields.io/badge/documentation-v0.1.0-blue.svg)](https://hexdocs.pm/ghoul)

`{: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
GenServer:

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

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


## Installation

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