Get Started
v0.1.14
Mishka Installer is an Elixir library that does two jobs. It lets your application expose named events that plugins attach to while the application runs, and it loads pre-built Elixir libraries into a running node. This tutorial builds the first half: an event with two plugins, from the dependency to a result you can read in iex.
What you will build
You add an after_register event to a small UserManagement module. Two plugins attach to it, and the one with the lower priority number runs first. You call the event, read the result, switch one plugin off, and switch it back on.
Before you begin
- Elixir 1.17 or later. The package requires
~> 1.17and sets no minimum for Erlang/OTP. Its tests run on Elixir 1.17.1 with Erlang/OTP 27.0, and on Elixir 1.20.1 with Erlang/OTP 29.0.2. - A Mix project with an application module that starts a supervisor. The steps name the project
UserManagement. - For the install half of the library, later: build each package with the same Erlang/OTP and Elixir as the node that loads it.
The library keeps its plugin registry in Mnesia, which ships with Erlang/OTP. In development it writes to .mnesia/dev under the directory where you start the application, so add .mnesia to your .gitignore.
Step 1: add the dependency
Add the package to deps in mix.exs, then run mix deps.get.
def deps do
[
{:mishka_installer, "~> 0.1.14"}
]
endOutside the :test environment, the library starts its own processes when your application starts: the Mnesia store, the event handler and the install handler. Events need no configuration.
Step 2: call an event from your code
An event is a string. You mark the place where plugins may act by calling Hook.call/3 with the event name and the data the plugins receive.
defmodule UserManagement do
alias MishkaInstaller.Event.Hook
def register(params) do
Hook.call("after_register", params)
end
endWith no plugin attached, Hook.call/3 returns the data it was given, so UserManagement.register(%{email: "[email protected]"}) returns %{email: "[email protected]"}.
Step 3: write two plugins
A plugin is a module that calls use MishkaInstaller.Event.Hook with an event name and defines call/1. Each plugin here adds its name to a steps list, so the result shows the order.
defmodule UserManagement.Plugins.SendOtp do
use MishkaInstaller.Event.Hook,
event: "after_register",
initial: %{priority: 20}
@impl true
def call(entries) do
{:reply, Map.update(entries, :steps, ["otp"], &(&1 ++ ["otp"]))}
end
end
defmodule UserManagement.Plugins.SendWelcomeEmail do
use MishkaInstaller.Event.Hook, event: "after_register"
@impl true
def call(entries) do
{:reply, Map.update(entries, :steps, ["welcome"], &(&1 ++ ["welcome"]))}
end
endThe project compiles. A plugin that names no event, or defines neither call/1 nor call/2, stops compilation with an error. {:reply, state} hands state to the next plugin. A plugin without a priority gets 100, so SendOtp runs first.
Step 4: start the plugins
Each plugin is a GenServer that registers itself when it starts. List both in the supervision tree of your application.
defmodule UserManagement.Application do
use Application
@impl true
def start(_type, _args) do
children = [
UserManagement.Plugins.SendOtp,
UserManagement.Plugins.SendWelcomeEmail
]
Supervisor.start_link(children, strategy: :one_for_one, name: UserManagement.Supervisor)
end
endRun iex -S mix. The log shows [mishka_installer.event] plugins synchronized. Then read a plugin's record from the registry:
iex> alias MishkaInstaller.Event.Event
iex> Event.get(:name, UserManagement.Plugins.SendOtp) |> Map.take([:event, :priority, :status])
%{event: "after_register", priority: 20, status: :started}Step 5: run the event
iex> UserManagement.register(%{email: "[email protected]"})
%{email: "[email protected]", steps: ["otp", "welcome"]}Both plugins ran, lowest priority number first. The plugins register a moment after the application starts. A call made before that returns the params unchanged.
Step 6: switch a plugin off and on
stop/0 removes a plugin from its event and keeps it off across restarts. enable/0 is the only call that brings it back.
iex> UserManagement.Plugins.SendOtp.stop()
{:ok, %MishkaInstaller.Event.Event{status: :stopped}}
iex> UserManagement.register(%{email: "[email protected]"})
%{email: "[email protected]", steps: ["welcome"]}
iex> UserManagement.Plugins.SendOtp.enable()
{:ok, %MishkaInstaller.Event.Event{status: :restarted}}
iex> UserManagement.register(%{email: "[email protected]"})
%{email: "[email protected]", steps: ["otp", "welcome"]}What you learned
- An event is a name you call with
Hook.call/3, and a plugin is a module that attaches to that name. - Plugins run in priority order, and each one passes its result to the next.
- A plugin registers itself when its process starts, so it must be in a supervision tree.
- An operator's
stop/0survives restarts, and onlyenable/0reverses it.
Next steps
- Write a plugin for an event: options,
call/2and halting the chain. - Call an event and use its result:
private,meta,returnandcannot_run. - Manage plugins while the app runs: stop, enable, restart, unregister and health checks.
- Override what a plugin does on start, restart and stop: add your own work to a plugin's start, stop and removal.
- Test plugins and events: start the library in ExUnit and wait until it is ready.
- Install a library at runtime: the second half of the library.
- Ship and remove plugins with a runtime-installed library: what an upgrade or an uninstall leaves behind.
- How events and plugins work: what happens behind
Hook.call/3. - Changes and upgrade notes: every release from 0.1.0 to 0.1.14 and what to do when you upgrade.
- Source on GitHub and the package on HexDocs.
- Hook reference, Event reference, Installer reference, Configuration reference, Telemetry and log reference, PubSub messages and readiness and Helper modules and your own tables.