View Source Existence (Existence v0.2.0)

Health-checks start and state access module.

Module provides functions for accessing an overall health-check state and individual dependencies checks results. Module is also used to start an Existence process as a part of an application supervision tree.

Existence works by asynchronously spawning user defined dependencies checks functions. Individual dependencies checks functions results are evaluated to establish an overall health-check state. Overall health-check state is healthy only when all user defined dependencies checks are healthy. It is assumed that healthy state is represented by an :ok atom for both dependencies checks and for the overall health-check. Any other result in dependencies checks is associated with an unhealthy dependency check state. Overall health-check unhealthy state is represented by an :error atom.

User defined dependencies checks functions are spawned as monitored isolated processes. If user dependency check function raises, throws an error, timeouts or fails in any way it doesn't have a negative impact on other processes, including user application.

Current dependencies checks functions results and current overall health-check state are stored in an ETS table. Whenever user executes any of available state getters, request is made against ETS table which has :read_concurrency set to true. In practice it means that library can handle huge numbers of requests per second without blocking any other processes.

Module provides few functions to access checks states:

Functions with bangs are negligibly cheaper computationally because they don't check if ETS table storing Existence state exists and they will raise if such table doesn't exist.

usage

Usage

After defining dependencies checks parameters, Existence can be started using your application supervisor:

#lib/my_app/application.ex
def start(_type, _args) do
  health_checks = [
    # minimal dependency check configuration:
    check_1: %{
      mfa: {MyApp.Checks, :check_1, []}
    },
    # complete dependency check configuration:
    check_2: %{
      mfa: {MyApp.Checks, :check_2, []},
      initial_delay: 1_000,
      interval: 30_000,
      state: :ok,
      timeout: 1_000
    }
  ]

  children = [
    {Existence, checks: health_checks, state: :ok}
  ]

  opts = [strategy: :one_for_one, name: MyApp.Supervisor]
  Supervisor.start_link(children, opts)
end

When Existence is started it has assigned an initial overall health-check state, which by default is equal to an :error atom, meaning an unhealthy state. Initial overall health-check state can be changed with a :state key. In a code example above initial overall health-check state is set to a healthy state with: state: :ok.

Existence supports starting multiple instances by using common Elixir child identifiers: :id and :name, for example:

children = [
  {Existence, checks: readiness_checks,  id: ExistenceReadiness, name: ReadinessCheck},
  {Existence, checks: liveness_checks, name: {:local, LivenessCheck}}
]

configuration

Configuration

Existence startup options:

  • :id - any term used to identify the child specification internally. Please refer to the Supervisor "Child specification" documentation section for details on child :id key. Default: Existence.
  • :name - name used to start Existence :gen_statem process locally. If defined as an atom() :gen_statem.start_link/3 is used to start Existence process without registration. If defined as a {:local, atom()} tuple, :gen_statem.start_link/4 is invoked and process is registered locally with a given name. Key value is used to select Existence instance when running any of the state getters, for example: get_state(CustomName). Default: Existence.
  • :checks - keyword list with user defined dependencies checks parameters, see description below for details. Default: [].
  • :state - initial overall Existence instance health-check state. Default: :error.

Dependencies checks are defined using a keyword list with configuration parameters defined as a maps.

Dependencies checks configuration options:

  • :mfa - {module, function, arguments} tuple specifying user defined function to spawn when executing given dependency check. Please refer to Kernel.apply/3 documentation for the MFA pattern explanation. Required.
  • :initial_delay - amount of time in milliseconds to wait before spawning a dependency check for the first time. Can be used to wait for a dependency process to properly initialize before executing dependency check function first time when application is started. Default: 100.
  • :interval - time interval in milliseconds specifying how frequently given check should be executed. Default: 30_000.
  • :state - initial dependency check state when starting Existence. Default: :error.
  • :timeout - after spawning dependency check function library will wait :timeout amount of milliseconds for the dependency check function to complete. If dependency check function will do not complete within a given timeout, dependency check function process will be killed, and dependency check state will assume a :killed atom value. Default: 5_000.

dependencies-checks

Dependencies checks

User defined dependencies checks functions must return an :ok atom for the healthy state. Any other values returned by dependencies checks functions are considered as an unhealthy state.

Example checks for two popular dependencies, PostgreSQL and Redis:

#lib/checks.ex
def check_postgres() do
  "SELECT 1;"
  |> MyApp.Repo.query()
  |> case do
    {:ok, %Postgrex.Result{num_rows: 1, rows: [[1]]}} -> :ok
    _ -> :error
  end
end

def check_redis() do
  case MyApp.Redix.command(["PING"]) do
    {:ok, "PONG"} -> :ok
    _ -> :error
  end
end

Please notice that dependencies checks functions in the code example above are not wrapped in a try/1 blocks. Dependencies checks functions are spawned as monitored processes. Whenever check function will raise, parent health-check process will be notified with an :info :DOWN message and dependency check status will be assigned a tuple containing an exception and a stack trace, for example:

# def check_1(), do: raise("CustomError")
iex> Existence.get_checks()
[
  check_1: {%RuntimeError{message: "CustomError"}, [ # ... stack trace ]}
]
iex> Existence.get_state()
:error

Link to this section Summary

Functions

Same as get_checks/1 but raises on error.

Get dependencies checks states.

Same as get_state/1 but raises on error.

Get an overall health-check state.

Link to this section Functions

Link to this function

get_checks!(name \\ __MODULE__)

View Source
@spec get_checks!(name :: atom()) :: [] | [{:key, :ok | any()}]

Same as get_checks/1 but raises on error.

Function will raise with an ArgumentError exception if Existence instance name doesn't exist.

Example:
iex> Existence.get_checks!()
[check_1: :ok, check_2: :ok]
iex> Existence.get_checks!(NotExisting)
** (ArgumentError) errors were found at the given arguments:
Link to this function

get_checks(name \\ __MODULE__)

View Source
@spec get_checks(name :: atom()) :: [] | [{:key, :ok | any()}] | :undefined

Get dependencies checks states.

Function gets current dependencies checks states for an Existence instance started with a given name. If name is not provided, checks states for instance with default :name (Existence) are returned.

Dependencies checks functions results are returned as a keyword list. If no checks were defined function will return an empty list.

Dependency check function result equal to an :ok atom means healthy state, any other term is associated with an unhealthy state.

Function returns :undefined if name instance doesn't exist.

Example:
iex> Existence.get_checks()
[check_1: :ok, check_2: :ok]
iex> Existence.get_checks(NotExisting)
:undefined
Link to this function

get_state!(name \\ __MODULE__)

View Source
@spec get_state!(name :: atom()) :: :ok | :error

Same as get_state/1 but raises on error.

Function will raise with an ArgumentError exception if Existence instance name doesn't exist.

Example:
iex> Existence.get_state!()
:ok
iex> Existence.get_state!(NotExisting)
** (ArgumentError) errors were found at the given arguments:
Link to this function

get_state(name \\ __MODULE__)

View Source
@spec get_state(name :: atom()) :: :ok | :error | :undefined

Get an overall health-check state.

Function gets current overall health-check state for an Existence instance started with a given name. If name is not provided, overall health-check state for an instance with default :name (Existence) is returned.

Function returns an :ok atom when overall health-check state is healthy and an :error atom when state is unhealthy. Overall health-check state is healthy only when all dependencies health checks are healthy.

Function returns :undefined if name instance doesn't exist.

Example:
iex> Existence.get_state()
:ok
iex> Existence.get_state(NotExisting)
:undefined