View Source Hooks
Hooks are used to trigger events on specific actions. Hooks can be registered by passing the
handlers to the :hooks option of the ProcessHub.t/0 configuration struct or by inserting them
dynamically using the ProcessHub.Service.HookManager module.
ProcessHub heavily uses hooks internally in the integration tests and strategy implementations.
Hooks have to be in the format of an mfa tuple. Basically, they are functions that will be called
when the hook is triggered.
It is possible to register a hook handler with a wildcard argument :_, which will be replaced with the hook data when the hook is dispatched.
Hook registration examples
Static hook registration
Static hook registration occurs when all hook handlers are known at the time of hub initialization
using the ProcessHub.t/0 configuration struct.
Example:
# Register a hook handler for the `:pre_cluster_join_hook` event with a wildcard argument.
defmodule MyApp.Application do
alias ProcessHub.Service.HookManager
alias alias ProcessHub.Constant.Hook
use Application
def start(_type, _args) do
children = [
ProcessHub.child_spec(%ProcessHub{
hub_id: :my_hub,
hooks: %{
Hook.pre_cluster_join() => [
%HookManager{
id: :hook_id_1,
m: MyModule,
f: :my_function,
a: [:some_data, :_]
},
%HookManager{
id: :hook_id_2,
m: MyModule2,
f: :my_function2,
a: [:some_data]
}
],
Hook.post_cluster_join() => [
%HookManager{
id: :hook_id_3,
m: MyModule,
f: :my_function,
a: [:some_data, :_]
}
]
}
})
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endDynamic hook registration
Dynamic hook registration is useful when modifying hook handlers at runtime. It allows us to add new handlers or remove existing ones.
Example:
alias ProcessHub.Service.HookManager
alias ProcessHub.Constant.Hook
# Register a hook handler for the `:cluster_join` event with a wildcard argument.
ProcessHub.register_hook_handlers(
:my_hub,
Hook.post_cluster_join(),
%HookManager{
id: :hook_id_30,
m: MyModule,
f: :my_function,
a: [:some_data, :_]
}
)Hook handler examples
# The hook handler should be in the following format:
defmodule MyModule do
def my_function(:some_data, dynamic_hook_data) do
# Do something with the data.
end
endUsing hooks to react and handle asynchronous events
Sometimes when starting a child process, it is necessary to know when the child process has been
started and is ready to receive messages. We could use the :awaitable option with the ProcessHub.Future.await/1 but that would
block the process that is starting the child process. Instead, we can use hooks to react to the
:pre_children_start event asynchronously.
Example:
# Register hook handler
alias ProcessHub.Service.HookManager
alias ProcessHub.Constant.Hook
...
hub_id = :my_hub
hook_handler1 = %HookManager{
id: :handler_id_1,
m: MyModule,
f: :my_first_func,
a: [:_]
}
hook_handler2 = %HookManager{
id: :handler_id_2,
m: MyModule,
f: :my_second_func,
a: [:hub_id, :_]
}
# We're triggering the hook when child process has been registered under the process registry.
ProcessHub.register_hook_handlers(:my_hub, Hook.registry_pid_inserted(), [hook_handler1, hook_handler2])
...
# The hook handlers
defmodule MyModule do
def my_first_func(hub_id, {child_id, node_pids}) do
# Send message to all started child processes.
Enum.each(node_pids, fn {_node, pid} ->
Process.send(pid, :hello_world, [])
end)
end
def my_second_func(hub_id, hook_data) do
...
end
endInternally registered hook handlers
ProcessHub is using hooks internally to depending on the active configuration. These hook handlers should never be removed or modified. The list of internally registered hooks ids are:
:ch_join:ch_leave:ch_shutdown:mhs_shutdown:mhs_process_startups:rr_post_start:rr_post_stop:rr_post_update:dg_pre_start_handler
Hooks in custom strategy implementations
Hooks can be used in custom strategy implementations to trigger events on specific actions and extend the functionality of the strategy.
Here's an example of ProcessHub using hooks internally in the
ProcessHub.Strategy.Distribution.ConsistentHashing strategy implementation:
alias ProcessHub.Constant.StorageKey
alias ProcessHub.Service.Storage
alias ProcessHub.Service.HookManager
# Register hook handlers to update the hash ring when node joins or leaves the cluster.
def init(_strategy, hub) do
hub_nodes = Storage.get(hub.storage.misc, :hub_nodes)
Storage.insert(hub.storage.misc, StorageKey.hr, Ring.create_ring(hub_nodes))
join_handler = %HookManager{
id: :ch_join,
m: ProcessHub.Strategy.Distribution.ConsistentHashing,
f: :handle_node_join,
a: [hub, :_]
}
leave_handler = %HookManager{
id: :ch_join,
m: ProcessHub.Strategy.Distribution.ConsistentHashing,
f: :handle_node_leave,
a: [hub, :_]
}
shutdown_handler = %HookManager{
id: :ch_shutdown,
m: ProcessHub.Strategy.Distribution.ConsistentHashing,
f: :handle_shutdown,
a: [hub],
p: 100
}
HookManager.register_handler(hub.storage.hook, Hook.pre_cluster_join(), join_handler)
HookManager.register_handler(hub.storage.hook, Hook.pre_cluster_leave(), leave_handler)
HookManager.register_handler(hub.storage.hook, Hook.coordinator_shutdown(), shutdown_handler)
end
# Handler functions that will be called when the hooks are triggered.
def handle_node_join(hub, node) do
hash_ring = Ring.get_ring(hub.storage.misc) |> Ring.add_node(node)
Storage.insert(hub.storage.misc, StorageKey.hr, hash_ring)
end
def handle_node_leave(hub, node) do
hash_ring = Ring.get_ring(hub.storage.misc) |> Ring.remove_node(node)
Storage.insert(hub.storage.misc, StorageKey.hr, hash_ring)
endHook handler priority
ProcessHub by default does not guarantee the order of hook handler execution unless
the hook handler specifies a priority level. The priority level is an integer value that
determines the order of execution of the hook handlers. The higher the priority level, the
earlier the hook handler will be executed. By default all hooks have a priority level of 0.
We define the priority level of a hook handler by setting the :p field of the ProcessHub.Service.HookManager.t/0
Here an example of how to set the priority level of a hook handler to 15:
alias ProcessHub.Service.HookManager
alias ProcessHub.Constant.Hook
...
hook_handler = %HookManager{
id: :handler_id_1,
m: MyModule,
f: :handle_child_start,
a: [],
p: 15 # here we set the priority level to 15.
}
ProcessHub.register_hook_handlers(:my_hub, Hook.pre_cluster_leave(), hook_handler)Hook handler arguments
The hook handler expects a arguments which is a list of values.
These values will then be passed to the hook handler function in the same order.
Some hooks dispatchers will pass additional data to the hook handler function which
then can be accessed using the wildcard argument in the handler function.
These are called wildcard arguments and are represented by the :_ symbol.
Example:
alias ProcessHub.Service.HookManager
alias ProcessHub.Constant.Hook
...
hook_handler = %HookManager{
id: :handler_id_1,
m: MyModule,
f: :handle_child_start,
a: [:_, :some, :something_else] # here the first argument will be the wildcard argument.
}
ProcessHub.register_hook_handlers(:my_hub, Hook.pre_cluster_join(), hook_handler)
# Here the handler function receives the wildcard argument as the first argument.
defmodule MyModule do
def handle_child_start(wild_card_value, :some, :something_else) do
# Because we used the `:pre_cluster_join_hook` hook event the wild_card_value will be a `node()`
end
endYou can familiarize yourself with the available hooks and their data in the last section of this guide.
Listing hook handlers
You can list all registered hook handlers for a specific hook event by calling
the registered_handlers/2 function from the ProcessHub.Service.HookManager module.
Example:
iex> alias ProcessHub.Service.HookManager
iex> alias ProcessHub.Constant.Hook
iex> HookManager.registered_handlers :my_hub, Hook.registry_pid_inserted()
[
%ProcessHub.Service.HookManager{
id: :handler_id_2,
m: MyModule,
f: :my_second_func,
a: [:hub_id, :_],
p: 0
}
]Removing hook handlers
You can remove a specific hook handler by calling the cancel_handler/3 function.
Example:
iex> alias ProcessHub.Service.HookManager
iex> alias ProcessHub.Constant.Hook
iex> HookManager.cancel_handler(:my_hub, Hook.registry_pid_inserted(), :handler_id_1)
:okIt's also possible to dispatch hooks manually by calling the dispatch_hook/3 or
dispatch_hooks/3 functions from the ProcessHub.Service.HookManager module.
Although, it's not recommended to dispatch hooks manually as it can lead to unexpected behavior.
Available hooks
| Event Key | Trigger | Data |
|---|---|---|
:pre_cluster_join_hook | Node joins hub cluster, not handled | node() |
:post_cluster_join_hook | Node joins hub cluster, handled | node() |
:pre_cluster_leave_hook | Node leaves hub cluster, not handled | node() |
:post_cluster_leave_hook | Node leaves hub cluster, handled | node() |
:registry_pid_insert_hook | Process registered | {child_id(), [{node(), pid()}]} |
:registry_pid_remove_hook | Process unregistered | child_id() |
:children_migrated_hook | Processes migrated | {node(), [child_spec()], map()} |
:priority_state_updated_hook | Priority state updated | {priority_level(), list()} |
:pre_nodes_redistribution_hook | Nodes redistribution start | {:nodeup or :nodedown, node()} |
:post_nodes_redistribution_hook | Nodes redistribution end | {:nodeup or :nodedown, node()} |
:forwarded_migration_hook | Migration was forwarded and handled | [{node(), [children_start_data()]}] |
:pre_children_start_hook | Before child process is started | ProcessHub.Handler.ChildrenAdd.StartHandle.t() |
:post_children_start_hook | After child process is started | [{child_id(), result(), pid(), [node()]}] |
:pre_children_redistribution_hook | Before redistribution of children is called | [{child_id(), node(), {:pid, pid()}}] |
:coordinator_shutdown_hook | Coordinator shutdown | any() |
:process_startups_hook | Processes have been started | [%{cid: child_id, pid: pid()}] |
:child_process_pid_update_hook | Child process restarted with new pid | {node(), child_id()} |
Alter hooks
Alter hooks differ from the other hooks in that they are used to modify the hook data before it is passed to the function that is using that data. This allows us to alter the data instead of just reacting to it and cause side effects as in the regular hooks.
Registering an alter hook is exactly the same as registering a normal hook.
Alter hooks must return the modified data in the same format as the original data.
⚠️ Warning Alter hooks should be used with caution as they can lead to unexpected behavior if not used properly. For example, if you modify the hook data in an alter hook, it will be passed to the next hook handler and can cause unexpected behavior if the next hook handler or the code is not expecting the modified data. Never change the underlying data structure of the hook data such as from map to list etc.
List of alter hooks
| Event Key | Trigger | Data |
|---|---|---|
:child_data_alter_hook | Before supervisor starts the child process | map() including child spec, metadata and nodes |
See ProcessHub.Constant.Hook module for more information on available hooks.