Skip to content

meyercm/ghoul

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ghoul

An undead cleanup crew for your processes.

Build Status Hex.pm 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 GenServer:

defmodule Led.Worker do
  use GenServer

  # ...snip...

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

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

  # ...snip...
end

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

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

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.

Parameters:

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

Parameters:

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

Parameters:

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

Parameters:

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

Parameters:

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

Parameters:

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

Parameters:

  • process_key - the process_key of the Ghoul

Return value:

integer|false | {:error, reason}

Installation

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

About

An undead cleanup crew for your processes.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages