Skip to content

Commit

Permalink
Merge pull request #4 from Recruitee/improve_documentation
Browse files Browse the repository at this point in the history
Improve documentation
  • Loading branch information
andrzej-mag authored Jan 5, 2023
2 parents f2221f7 + 738abeb commit 681bcca
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 101 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## v0.3.1 [2023-01-05]

* Improved documentation.
* Minor code cleanup.
* Added CI workflow.

## v0.3.0 [2022-06-27]

### Enhancements
Expand Down
75 changes: 44 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,37 @@
Asynchronous dependency health checks library.

## Features
* User defined dependencies checks with flexible settings.
* Dependencies checks functions are executed asynchronously.
* Built-in `Plug` module providing customizable response for a http health-check endpoint.
* Support for multiple independent `Existence` instances and associated health-checks
endpoints (example use case: separate Kubernetes readiness and liveness http probes).
* Checks states are stored and accessed using a dedicated ETS tables per `Existence` instance,
which means practically unlimited requests per second processing capacity.
* User defined dependencies health-checks with flexible settings, including configurable
health-check callback function timeout, startup delay and initial check state.
* `Existence.Plug` module providing customizable response for a http health-check endpoint.
* Support for multiple `Existence` instances and associated health-checks endpoints
(example use case: separate Kubernetes readiness and liveness http probes).
* Dependencies health-checks states are cached and accessed using a dedicated ETS table per
`Existence` instance, which means practically unlimited requests per second processing capacity
without putting unnecessary load on a health-checked dependency.

## Installation
Add `Existence` library to your application dependencies:
Add `Existence` library to your application `mix.exs` file:
```elixir
# mix.exs
def deps do
[
{:existence, "~> 0.3.0"}
{:existence, "~> 0.3.1"}
]
end
```

## Usage
Define dependencies checks functions MFA's and start `Existence` child with your application
supervisor
Define dependencies checks callback functions MFA's and start `Existence` child with your application
supervisor:
```elixir
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application

def start(_type, _args) do
health_checks = [
check_1: %{mfa: {MyApp.Checks, :check_1, []}},
check_2: %{mfa: {MyApp.Checks, :check_2, []}}
check_postgres: %{mfa: {MyApp.Checks, :check_postgres, []}}
]

children = [{Existence, checks: health_checks}]
Expand All @@ -46,35 +48,46 @@ defmodule MyApp.Application do
end
```

Declare your dependencies checks functions:
Declare your dependencies checks callback functions, in our example case PostgreSQL health-check:
```elixir
# lib/my_app/checks.ex
defmodule MyApp.Checks do
def check_1(), do: :ok
def check_2(), do: :ok
def check_postgres() do
"SELECT 1;"
|> MyApp.Repo.query()
|> case do
{:ok, %Postgrex.Result{num_rows: 1, rows: [[1]]}} -> :ok
_ -> :error
end
end
end
```

Dependencies checks functions above are for illustrative purposes only, please refer to the
`Existence` module documentation for more realistic dependencies checks examples.

Configure your Phoenix router to respond to the `/healthcheck` endpoint requests using for example
`Plug.Router.forward/2`:
```elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router

forward("/healthcheck", Existence.Plug)
end
```
Dependency health-check function `check_postgres/0` will be spawned asynchronously by default every
30 seconds, checking if PostgreSQL instance associated with `MyApp.Repo` is healthy.
`check_postgres/0` results will be cached in the ETS table used further to provide responses to user
requests. If `/healthcheck` endpoint requests are issued thousands of times per second, they do not
hit and overload PostgreSQL, instead cached results from ETS are returned.

Get current overall health-check state:
Current overall health-check state can be examined with `get_state/1`:
```elixir
iex> Existence.get_state()
:ok
```

List individual dependencies checks current states:
Dependencies health-checks states can be examined with `get_checks/1`:
```elixir
iex> Existence.get_checks()
[check_1: :ok, check_2: :ok]
[check_postgres: :ok]
```

Library provides Plug module generating responses to the `/healthcheck` endpoint requests.
Example `Existence.Plug` usage with `Plug.Router.forward/2`:
```elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router

forward("/healthcheck", Existence.Plug)
end
```

115 changes: 55 additions & 60 deletions lib/existence.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Existence do
@moduledoc """
Health-checks start and state access module.
Health-checks management and access module.
Module provides functions for accessing an overall health-check state and individual dependencies
checks results.
Expand All @@ -16,28 +16,29 @@ defmodule Existence do
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.
If user dependency check function raises, throws an error, timeouts or fails in any other way it
doesn't have a negative impact on other processes and it is gracefully handled by the library.
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.
In practice it means that library can handle unlimited numbers of requests per second
without blocking any other processes, adding latency to the response or overloading dependency
with synchronous requests.
Module provides few functions to access checks states:
Module provides following functions to access checks states:
* `get_state/1` and `get_state!/1` to get overall health-check state,
* `get_checks/1` and `get_checks!/1` to get dependencies 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.
storing given Existence instance state exists and they will raise if such table doesn't exist.
## Usage
After defining dependencies checks parameters, `Existence` can be started using
After defining dependencies checks options, `Existence` can be started using
your application supervisor:
```elixir
#lib/my_app/application.ex
# lib/my_app/application.ex
def start(_type, _args) do
health_checks = [
# minimal dependency check configuration:
Expand Down Expand Up @@ -67,7 +68,7 @@ defmodule Existence do
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:
Multiple `Existence` instances can be started by using common Elixir child identifiers
`:id` and `:name`, for example:
```elixir
children = [
Expand All @@ -77,19 +78,18 @@ defmodule Existence do
```
## 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`.
`Existence` options:
* `:id` - 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` 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: `[]`.
below for details. Default: `[]`.
* `:state` - initial overall `Existence` instance health-check state. Default: `:error`.
* `:on_state_change` - MFA tuple pointing at user function which will be synchronously applied
on the overall health-check state change.
Expand All @@ -98,29 +98,31 @@ defmodule Existence do
Default: `nil`.
Dependencies checks are defined using a keyword list with configuration parameters defined
as a maps.
as a maps, see code example above.
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.
executing given dependency check. Please refer to `Kernel.spawn_monitor/3` documentation for
the MFA pattern explanation. Function will be spawned with arguments given in the `:mfa` key.
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`.
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`.
executed and dependency checked. 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`.
* `:timeout` - after spawning dependency check function we 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` value.
Default: `5_000`.
## Dependencies checks
User defined dependencies checks functions must return an `:ok` atom for the healthy state.
User defined dependencies checks functions must return an `:ok` atom within given `:timeout`
interval to acquire a 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:
Example health-checks callback functions for two popular dependencies, PostgreSQL and Redis:
```elixir
#lib/checks.ex
def check_postgres() do
Expand Down Expand Up @@ -169,21 +171,18 @@ defmodule Existence do
]

@doc """
Get dependencies checks states.
Get dependencies checks states for `name` instance.
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.
Function gets current dependencies checks states for an instance started with a given `name`,
by default `Existence`.
Dependencies checks functions results are returned as a keyword list.
If no checks were defined function will return an empty list.
Function returns `:undefined` if `name` instance doesn't exist.
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:
```elixir
iex> Existence.get_checks()
Expand All @@ -207,10 +206,9 @@ defmodule Existence do
end

@doc """
Same as `get_checks/1` but raises on error.
Same as `get_checks/1` but raises if `name` instance doesn't exist.
Function will raise with an `ArgumentError` exception if `Existence` instance `name`
doesn't exist.
Function will raise with an `ArgumentError` exception if instance `name` doesn't exist.
##### Example:
```elixir
Expand All @@ -231,19 +229,16 @@ defmodule Existence do
end

@doc """
Get an overall health-check state.
Get an overall health-check state for `name` instance.
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 gets current overall health-check state for an instance started with a given `name`,
by default `Existence`.
Function returns an `:ok` when overall health-check state is healthy and an `:error` when state
is unhealthy.
Function returns `:undefined` if `name` instance doesn't exist.
Overall health-check state is healthy only when all dependencies health checks are healthy.
##### Example:
```elixir
Expand All @@ -268,10 +263,9 @@ defmodule Existence do
end

@doc """
Same as `get_state/1` but raises on error.
Same as `get_state/1` but raises if `name` instance doesn't exist.
Function will raise with an `ArgumentError` exception if `Existence` instance `name`
doesn't exist.
Function will raise with an `ArgumentError` exception if instance `name` doesn't exist.
##### Example:
```elixir
Expand Down Expand Up @@ -315,7 +309,6 @@ defmodule Existence do

@impl true
def init(args) do
Process.flag(:trap_exit, true)
ets_tab = Keyword.fetch!(args, :ets_name)

:ets.new(ets_tab, [
Expand Down Expand Up @@ -370,7 +363,8 @@ defmodule Existence do
end

def unhealthy(:info, {:DOWN, ref, :process, pid, :normal}, data) do
find_check(pid, ref, data)
pid
|> find_check(ref, data)
|> maybe_respawn_check()

:keep_state_and_data
Expand Down Expand Up @@ -404,7 +398,8 @@ defmodule Existence do
end

def healthy(:info, {:DOWN, ref, :process, pid, :normal}, data) do
find_check(pid, ref, data)
pid
|> find_check(ref, data)
|> maybe_respawn_check()

:keep_state_and_data
Expand Down
4 changes: 2 additions & 2 deletions lib/plug.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Existence.Plug do
Plug responding with a health-check state.
Plug sends a `text/plain` response depending on the `Existence` overall health-check state
returned by an `Existence.get_state/1` function.
returned by an `Existence.get_state/1` or `Existence.get_state!/1` function.
## Configuration
Plug is configured with a keyword list.
Expand All @@ -19,7 +19,7 @@ defmodule Existence.Plug do
get an overall health-check state.
If set to false, not raising `Existence.get_state/1` function will be used.
Default: `false`.
* `:name` - `Existence` name same as a name used when starting instance with supervision tree.
* `:name` - instance name defined when starting `Existence` child with supervision tree.
Default: `Existence`.
## Usage
Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Existence.MixProject do
use Mix.Project

@source_url "https://github.com/Recruitee/existence"
@version "0.3.0"
@version "0.3.1"

def project do
[
Expand All @@ -25,7 +25,7 @@ defmodule Existence.MixProject do
defp deps do
[
{:plug, "~> 1.10"},
{:ex_doc, "~> 0.28.0", only: :dev, runtime: false}
{:ex_doc, "~> 0.29.0", only: :dev, runtime: false}
]
end

Expand Down
Loading

0 comments on commit 681bcca

Please sign in to comment.