Defines the core behavior for a Malla plugins.
By use Malla.Plugin, a module is transformed into a plugin that can be
inserted into a Malla.Service. Malla.Plugin enables the creation of reusable,
composable modules that provide or modify service behavior through compile-time callback chaining.
This module also provides a set of callbacks that will have a default implementation if not overriden in your plugin module.
For comprehensive documentation, please see the guides:
Summary
Types
Options for configuring a service when using Malla.Plugin.
Callbacks
Optional callback called before service's start. It allows the plugin to check and, if needed, modify the config.
Optional callback called when merging configuration updates.
Optional compile-time callback that injects code into the service module.
Optional callback called during service's start.
Optional callback called if service needs to 'stop' this plugin.
Optional callback called after service's reconfiguration.
(See Malla.Service.reconfigure/2).
You are presented with old and new config, and you have the chance
to restart the plugin.
Types
@type child_spec() :: Supervisor.child_spec() | {module(), term()} | module() | (old_erlang_child_spec :: :supervisor.child_spec())
@type id() :: Malla.id()
@type start_opt() :: {:children, child_spec()} | Supervisor.option() | Supervisor.init_option()
@type updated_opts() :: {:restart, boolean()}
@type use_opt() :: {:plugin_deps, [module() | {module(), [{:optional, boolean()}]}]} | {:group, atom()}
Options for configuring a service when using Malla.Plugin.
Options include:
:plugin_deps- Declares this plugin depends on these other plugins. Optional plugins are included only if they can be found in the source code.For example:
use Malla.Plugin, plugin_deps: [Plugin1, {Plugin2, optional: true}]This plugin will be marked as dependant on
Plugin1andPlugin2, meaning that they will be inserted first in the plugin chain list, so:- they will be started first, before this plugin.
- callbacks implemented by all will reach first our plugin, then Plugin1 and Plugin2.
- since Plugin2 is marked as optional, if it is not found, it is not included in the list.
:group- Declares plugin group. All plugins belonging to the same 'group' are added a dependency on the previous plugin in the same groupFor example: , so for example, if we define in our service
use MallaService, plugins: [PluginA, PluginB, PluginC]If they all declare the same group, PluginB will depend on Plugin A and PluginC will depend on PluginB, so they will be started in the exact order PluginA -> PluginB -> PluginC. Callbacks will call first PluginC, then B and A.
Callbacks
@callback plugin_config(Malla.id(), config :: keyword()) :: :ok | {:ok, config :: keyword()} | {:error, term()}
Optional callback called before service's start. It allows the plugin to check and, if needed, modify the config.
See Configuration.
Top level plugins are called first, so they could update the config for other plugins they declared as dependants.
@callback plugin_config_merge(Malla.id(), old_config :: keyword(), update :: keyword()) :: :ok | {:ok, merged_config :: keyword()} | {:error, term()}
Optional callback called when merging configuration updates.
This callback is invoked during service initialization (when runtime config
is merged with static config) and when Malla.Service.reconfigure/2 is used.
By default, if a plugin doesn't implement this callback, configurations are deep-merged automatically. Implement this callback when you need custom merge logic for your plugin's configuration keys.
Top-level plugins (service itself, then declared plugins) are called first, allowing higher-level plugins to process configuration before lower-level ones.
@callback plugin_module(service :: Malla.Service.t()) :: Macro.t()
Optional compile-time callback that injects code into the service module.
Unlike the other plugin callbacks, plugin_module/1 is invoked while the
service module is being compiled (via a @before_compile hook), not at
runtime. For each plugin in the service's Malla.Service.t/0 plugin chain,
if plugin_module/1 is exported, its return value (a quoted expression) is
spliced into the body of the service module.
This allows a plugin to generate functions, module attributes, or even
nested submodules that become part of the service module itself,
parameterized by the service's compile-time data: id, plugin_chain,
static config, the resolved callbacks map, etc.
The callback is invoked once per service compilation, in plugin chain order
(top-level plugins first, Malla.Plugins.Base last). It runs after the
callback dispatch logic has been generated, so service.callbacks is fully
populated.
Because the injected code lives on the service module, it is visible across the cluster through the same virtual-module mechanism used for remote calls.
Use sparingly
Code injection makes the service module's surface area depend on which
plugins were compiled into it, which can be surprising to readers and
harder to trace than ordinary defcb callbacks or plugin functions.
Reach for plugin_module/1 only when the desired behavior cannot be
expressed as a normal callback or helper — typical cases are generating
a function whose body must close over compile-time data (the service
id, the resolved callbacks map, etc.) or building a nested submodule
under the service.
Plugins that use plugin_module/1 must document, in their own
@moduledoc, exactly what is injected into the service module: the
names and arities of any generated functions, any module attributes,
and any nested submodules. Without this, users of the plugin have no
way to know which symbols on their service module came from where.
Example
defmodule MyApp.MetricsPlugin do
use Malla.Plugin
def plugin_module(service) do
prefix = "malla." <> Atom.to_string(service.id)
quote do
def metric_prefix, do: unquote(prefix)
end
end
endAfter the service compiles, MyService.metric_prefix/0 is a regular
function on the service module.
@callback plugin_start(Malla.id(), config :: keyword()) :: :ok | {:ok, [start_opt()]} | {:error, term()}
Optional callback called during service's start.
Plugins will be started on service init, starting with lower-level plugins up to the service itself (that is also a Plugin). See Lifecycle for details.
Plugin can return a child specification, in this case a Supervisor will be
started with the specified children.
The service will monitor this supervisor, and, if it fails, the whole service will be marked as 'failed' and we will retry to start it, calling this function again.
Optional callback called if service needs to 'stop' this plugin.
This can happen if we mark the service as inactive. Top-level plugins will be stopped first starting with the service itself (that is also a Plugin), up to lower level plugins in order.
It can also happen if this plugin is removed from the service.
After calling this function, service will stop the started children supervisor, if it was defined and it is already running.
@callback plugin_updated( Malla.id(), old_config :: keyword(), new_config :: keyword() ) :: :ok | {:ok, [updated_opts()]} | {:error, term()}
Optional callback called after service's reconfiguration.
(See Malla.Service.reconfigure/2).
You are presented with old and new config, and you have the chance
to restart the plugin.
Lower level plugins are called first.
Functions
Macro that transforms a module into a Malla plugin.
This macro inserts required functions, implements plugin callbacks and registers Malla callbacks
See use_opt/0 for configuration options.
Macro for defining plugin callbacks.
Callbacks will appear at service's module and will participate in the callback chain.
A Malla callback can return any of the following:
:cont: continues the call to the next function in the call chain{:cont, [:a, b:]}: continues the call, but changing the parameters used for the next call in chain. The list of the array must fit the number of arguments.{:cont, :a, :b}: equivalent to {:cont, [:a, :b]}- any: any other response stops the call chain and returns this value to the caller