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.

OptionDefaultMeaning
eventnoneThe 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.
queuetrueWith true, the lifecycle functions queue the rebuild of the event and return before it finishes. With false, they return after it.
checking1000Milliseconds between the plugin process's reads of its stored status, which get/0 reports.

Callbacks

CallbackDefaultMeaning
call(state)noneRuns the plugin. Return {:reply, state} to continue or {:reply, :halt, state} to end the chain.
call(state, meta)noneThe 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():okReturns :ok, {:degraded, reason} or {:error, reason}.
on_dependency_error(error)returns errorRuns 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 belowDefined by the macro, and can be overridden. See Overriding the generated functions.

Functions added to a plugin

FunctionReturnsMeaning
config/0keyword listThe 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/1valueOne key of config/0.
register/0{:ok, record} or an errorStores the plugin. See Event.register/3.
start/0{:ok, record} or an errorWrites :started.
restart/0{:ok, record} or an errorWrites :restarted. Refuses a stopped plugin.
stop/0{:ok, record} or an errorWrites :stopped.
enable/0{:ok, record} or an errorSwitches a stopped plugin back on.
unregister/0{:ok, record} or an errorDeletes the record and stops the process.
get/0keyword listThe 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/1see callbacksDefaults, overridable.
start_link/1GenServer.on_start()Starts the process under the module's name. The argument defaults to [].
child_spec/1child specFrom 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.

FunctionDefaultWho calls itAn override returnsOverridable since
register/0Event.register/3 with the plugin's event and initialThe plugin's process, when it starts.{:ok, record} or an error. Any other answer counts as a failed registration.0.1.0
start/0Event.start/3 with the plugin's queue optionThe 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/0Event.restart/3Your code only.{:ok, record} or an error.0.1.0
stop/0Event.stop/3Your code only.{:ok, record} or an error.0.1.0
enable/0Event.enable/2Your code only.{:ok, record} or an error.0.1.11
unregister/0Event.unregister/3Your code only.{:ok, record} or an error.0.1.0
get/0A call to the plugin's processYour code only.A keyword list.0.1.0
health_check/0Returns :okHook.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/1Returns its argumentThe 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

FunctionReturnsMeaning
call(event, data, args \\ [])the chain's result, or dataCalls the event. Returns data when the event has no compiled module.
call!(event, data, args \\ [])the chain's resultSkips 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 mapHealth of one plugin.
event_health(event, timeout \\ 1000)report mapHealth of one event. The plugin probes run concurrently.
health(timeout \\ 1000)list of event reportsHealth of every event in the registry.
run_health(plugin, timeout \\ 1000)probe resultRuns health_check/0 in a monitored process, bounded by timeout.

Arguments of call

KeyMeaning
privateMerged into a map or keyword list result after the chain. Plugins never see it.
returnAny value other than nil makes the call return data as given. The plugins still run.
metaHanded to the plugins that define call/2. Never merged into the result. nil when absent.
cannot_runThe 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 valueEffect
{: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 exceptionThe 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 stateWith privateWithout
{:ok, keyword_list}{:ok, Keyword.merge(list, private)}unchanged
{:ok, list}, not a keyword listunchangedunchanged
{:ok, map}{:ok, Map.merge(map, private)}unchanged
{:ok, other}unchangedunchanged
{:error, _}unchangedunchanged
a keyword listKeyword.merge(s, private)unchanged
a list that is not a keyword listunchangedunchanged
a mapMap.merge(s, private)unchanged
anything else, such as {:noreply, socket}unchangedunchanged

Reports

A plugin report from plugin_health/2 has these keys:

KeyMeaning
pluginThe plugin module.
statusThe 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.
probeThe 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:

ProbeCause
:okThe 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.