Hook reference
v0.1.14
Reference for MishkaInstaller.Event.Hook, the module that turns a module into a plugin and that calls events. It covers the options of use, the callbacks a plugin can define, the functions the macro adds, the functions of the module, the arguments of Hook.call/3, and the shapes of results and reports. For the same material as a task, see Write a plugin for an event, Call an event and use its result and Override what a plugin does on start, restart and stop.
Options of use
use MishkaInstaller.Event.Hook, opts takes a keyword list.
| Option | Default | Meaning |
|---|---|---|
event | none | The event name, a string. Compilation fails with should be dedicated to an event. if it is missing. |
initial | %{} | A map of registration fields: priority (integer, 0 to 100, default 100), depends (list of plugin modules, default []) and extra (list of maps, default []). Applied again on every boot. |
queue | true | With true, the lifecycle functions queue the rebuild of the event and return before it finishes. With false, they return after it. |
checking | 1000 | Milliseconds between the plugin process's reads of its stored status, which get/0 reports. |
Callbacks
| Callback | Default | Meaning |
|---|---|---|
call(state) | none | Runs the plugin. Return {:reply, state} to continue or {:reply, :halt, state} to end the chain. |
call(state, meta) | none | The same, with the read-only meta of the call, or nil. When a plugin defines both, call/2 is used. A plugin must define call/1 or call/2, or compilation fails with should define a call/1 or call/2 function. |
health_check() | :ok | Returns :ok, {:degraded, reason} or {:error, reason}. |
on_dependency_error(error) | returns error | Runs when the plugin's registration at boot returns an error, such as a dependency cycle or a priority out of range. |
register(), start(), restart(), stop(), enable(), unregister(), get() | see below | Defined by the macro, and can be overridden. See Overriding the generated functions. |
Functions added to a plugin
| Function | Returns | Meaning |
|---|---|---|
config/0 | keyword list | The plugin's options: :__plugin__, :__event__, :__initial__, :__queue__ and :__checking__, merged with the values of :app, :version, :elixir, :deps, :name, :source_url, :homepage_url and :description from the Mix project at compile time that can be stored as plain data. |
config/1 | value | One key of config/0. |
register/0 | {:ok, record} or an error | Stores the plugin. See Event.register/3. |
start/0 | {:ok, record} or an error | Writes :started. |
restart/0 | {:ok, record} or an error | Writes :restarted. Refuses a stopped plugin. |
stop/0 | {:ok, record} or an error | Writes :stopped. |
enable/0 | {:ok, record} or an error | Switches a stopped plugin back on. |
unregister/0 | {:ok, record} or an error | Deletes the record and stops the process. |
get/0 | keyword list | The process's state: :name, :event and :status, and :depends once the plugin has registered. Right after the process starts it is [name: module, event: event, status: :starting] with no :depends. Calls the process, so it fails when the process is not running. |
health_check/0, on_dependency_error/1 | see callbacks | Defaults, overridable. |
start_link/1 | GenServer.on_start() | Starts the process under the module's name. The argument defaults to []. |
child_spec/1 | child spec | From use GenServer with restart: :transient. |
The macro also defines init/1, handle_continue/2, handle_call/3 and handle_info/2. Errors have the shape {:error, [%{action: atom(), field: atom(), message: String.t()}]}.
Overriding the generated functions
A plugin module can define any of these nine functions again, and its version can call the default with super. on_dependency_error/1 takes an argument, so its override calls super(error). The override documentation has a sample for each. All nine can be overridden in 0.1.13 and 0.1.14.
| Function | Default | Who calls it | An override returns | Overridable since |
|---|---|---|---|---|
register/0 | Event.register/3 with the plugin's event and initial | The plugin's process, when it starts. | {:ok, record} or an error. Any other answer counts as a failed registration. | 0.1.0 |
start/0 | Event.start/3 with the plugin's queue option | The plugin's process, when register/0 found a :registered plugin, and on its retry. | {:ok, record} or an error. The process reads status and depends from the record. | 0.1.0 |
restart/0 | Event.restart/3 | Your code only. | {:ok, record} or an error. | 0.1.0 |
stop/0 | Event.stop/3 | Your code only. | {:ok, record} or an error. | 0.1.0 |
enable/0 | Event.enable/2 | Your code only. | {:ok, record} or an error. | 0.1.11 |
unregister/0 | Event.unregister/3 | Your code only. | {:ok, record} or an error. | 0.1.0 |
get/0 | A call to the plugin's process | Your code only. | A keyword list. | 0.1.0 |
health_check/0 | Returns :ok | Hook.run_health/2, which plugin_health/2, event_health/2 and health/1 use. | :ok, {:degraded, reason} or {:error, reason}. Any other value is reported as {:error, {:bad_return, value}}. | 0.1.6 |
on_dependency_error/1 | Returns its argument | The plugin's process, when register/0 at start returns anything but {:ok, record}. | Any value. The library ignores it. | 0.1.6 |
The functions of Event do not call these. A call such as Event.stop(:name, plugin, queue), the start of every event on boot and the hold of a plugin whose dependency stopped change the record without running an override.
A plugin's process calls register/0 and start/0 when it starts. If the library has not finished its boot by then, the process tries once more, about one second later, without checking readiness again, and nothing schedules another try. The retry calls register/0 for a plugin with no record, start/0 for a record that the compiled event does not hold, and neither otherwise.
Functions of the Hook module
| Function | Returns | Meaning |
|---|---|---|
call(event, data, args \\ []) | the chain's result, or data | Calls the event. Returns data when the event has no compiled module. |
call!(event, data, args \\ []) | the chain's result | Skips the check for a compiled module. Raises UndefinedFunctionError for an event never compiled. |
profile(event, state) | {:ok, [%{plugin: module, microseconds: n}]} or {:error, :not_compiled} | Runs each plugin of the chain once and times it. A plugin that raises reports 0. Ignores :halt. |
plugin_health(plugin, timeout \\ 1000) | report map | Health of one plugin. |
event_health(event, timeout \\ 1000) | report map | Health of one event. The plugin probes run concurrently. |
health(timeout \\ 1000) | list of event reports | Health of every event in the registry. |
run_health(plugin, timeout \\ 1000) | probe result | Runs health_check/0 in a monitored process, bounded by timeout. |
Arguments of call
| Key | Meaning |
|---|---|
private | Merged into a map or keyword list result after the chain. Plugins never see it. |
return | Any value other than nil makes the call return data as given. The plugins still run. |
meta | Handed to the plugins that define call/2. Never merged into the result. nil when absent. |
cannot_run | The answer when the event cannot run, meaning a plugin of it is started but not loaded. Without it, the answer is data. An event whose plugins all run ignores it. Before 0.1.13 the answer was an error tuple, and the changes documentation shows how to update code that matched it. |
What a plugin may return
| Return value | Effect |
|---|---|
{:reply, state} | The next plugin receives state. After the last plugin, state is the chain's result. |
{:reply, :halt, state} | The chain ends and state is its result. |
| Anything else, or an exception | The library logs plugin pipeline raised in event and the call returns data, as it was given. |
How the final state is shaped
These rules apply to the final state s when the call has no return.
| Final state | With private | Without |
|---|---|---|
{:ok, keyword_list} | {:ok, Keyword.merge(list, private)} | unchanged |
{:ok, list}, not a keyword list | unchanged | unchanged |
{:ok, map} | {:ok, Map.merge(map, private)} | unchanged |
{:ok, other} | unchanged | unchanged |
{:error, _} | unchanged | unchanged |
| a keyword list | Keyword.merge(s, private) | unchanged |
| a list that is not a keyword list | unchanged | unchanged |
| a map | Map.merge(s, private) | unchanged |
anything else, such as {:noreply, socket} | unchanged | unchanged |
Reports
A plugin report from plugin_health/2 has these keys:
| Key | Meaning |
|---|---|
plugin | The plugin module. |
status | The stored status, or nil without a record. |
alive? | Whether the plugin's process is registered under its module name. |
callable? | Whether the module is loaded and exports call/1 or call/2. |
probe | The result of health_check/0. |
healthy? | True when the process is alive, the module is callable, the probe is :ok and the status is :started or :restarted. |
An event report from event_health/2 has event, compiled?, mode (:ok, :error or :unknown), plugin_count, plugins (a list of plugin reports) and healthy?. An event is healthy when it is compiled, its mode is :ok and every plugin is healthy.
A probe result is one of:
| Probe | Cause |
|---|---|
:ok | The plugin reported itself healthy. |
{:degraded, reason} | The plugin returned it. |
{:error, reason} | The plugin returned it. |
{:error, :timeout} | The probe ran longer than the timeout and was killed. |
{:error, :no_health_check} | The module defines no health_check/0. |
{:error, {:raised, exception}} | The probe raised. |
{:error, {:exit, reason}} | The probe process exited. |
{:error, {:bad_return, value}} | The probe returned another value. |
Event names
The compiled module is MishkaInstaller.Event.ModuleStateCompiler.State.<Name>. The name is the event with surrounding whitespace removed, spaces replaced by underscores, leading digits removed, and the result camelized with Macro.camelize/1.