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.17 and 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.

elixir
def deps do
  [
    {:mishka_installer, "~> 0.1.14"}
  ]
end

Outside 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.

elixir
defmodule UserManagement do
  alias MishkaInstaller.Event.Hook

  def register(params) do
    Hook.call("after_register", params)
  end
end

With 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.

elixir
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
end

The 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.

elixir
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
end

Run iex -S mix. The log shows [mishka_installer.event] plugins synchronized. Then read a plugin's record from the registry:

elixir
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

elixir
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.

elixir
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/0 survives restarts, and only enable/0 reverses it.

Next steps