diff --git a/OPTIONS.md b/OPTIONS.md
index 8b612f99..6b530594 100644
--- a/OPTIONS.md
+++ b/OPTIONS.md
@@ -7,6 +7,9 @@ Please see [`syn_event_handler`](syn_event_handler.html) for information on call
The event handler module can be defined either by using the [`set_event_handler/1`](syn.html#set_event_handler/1) method, or
by setting the `event_handler` configuration variable.
+Syn does not call this module for a Scope that has its own handler, set with
+[`set_event_handler/2`](syn.html#set_event_handler/2) or with the [`scope_event_handlers`](#scope_event_handlers) key.
+
#### Elixir
```elixir
@@ -22,6 +25,39 @@ config :syn,
]}
```
+## scope_event_handlers
+Please see [`syn_event_handler`](syn_event_handler.html) for information on callbacks.
+
+The `scope_event_handlers` configuration variable sets the event handler modules of individual Scopes, as a list of
+`{Scope, Module}` tuples. It is meant for Scopes known before Syn starts, typically those listed under `scopes`.
+Syn starts those Scopes during its own startup, usually before your application code runs. It applies this list before
+it starts any Scope, so the handlers are in place for the first events of these Scopes. If an entry is malformed or
+names a module that cannot be loaded, Syn fails to start.
+
+Syn applies the list every time it starts, and an entry replaces a handler set earlier for that Scope with
+[`set_event_handler/2`](syn.html#set_event_handler/2). For a Scope added at runtime, call
+[`set_event_handler/2`](syn.html#set_event_handler/2) before [`add_node_to_scopes/1`](syn.html#add_node_to_scopes/1).
+
+Syn uses a Scope's own handler for all of the Scope's callbacks, otherwise the node-wide `event_handler`, and otherwise
+its defaults.
+
+#### Elixir
+
+```elixir
+config :syn,
+ scopes: [:devices, :users],
+ scope_event_handlers: [users: MyUsersEventHandler]
+```
+
+#### Erlang
+
+```erlang
+{syn, [
+ {scopes, [devices, users]},
+ {scope_event_handlers, [{users, my_users_event_handler}]}
+]}
+```
+
## scopes
Please see [`here`](syn.html) for information on Scopes.
diff --git a/src/syn.erl b/src/syn.erl
index 7a81cbc5..7e1990ce 100755
--- a/src/syn.erl
+++ b/src/syn.erl
@@ -57,7 +57,7 @@
%% scopes
-export([node_scopes/0, add_node_to_scopes/1]).
-export([subcluster_nodes/2]).
--export([set_event_handler/1]).
+-export([set_event_handler/1, set_event_handler/2]).
%% registry
-export([lookup/2]).
-export([register/3, register/4, update_registry/3]).
@@ -152,6 +152,9 @@ subcluster_nodes(pg, Scope) ->
%%
%% Please see {@link syn_event_handler} for information on callbacks.
%%
+%% Syn does not call this module for a Scope that has its own handler, set with {@link set_event_handler/2} or with
+%% the `scope_event_handlers' key.
+%%
%% There are 2 ways to set a handler module. One is by using this method, the other is to set the environment variable `syn'
%% with the key `event_handler'. In this latter case, you're probably best off using an application configuration file:
%%
@@ -184,6 +187,58 @@ set_event_handler(Module) ->
%% ensure event handler is loaded
syn_event_handler:ensure_event_handler_loaded().
+%% @doc Sets the handler module for the specified `Scope'.
+%%
+%% Please see {@link syn_event_handler} for information on callbacks.
+%%
+%% Syn selects the handler for a Scope as a whole, not per callback:
+%%
+%% - If the Scope has a handler set with this method, Syn uses only that module for all of the Scope's callbacks.
+%% If the module does not export a callback, Syn skips that callback for the Scope and does not fall back to the
+%% node-wide handler. Without `resolve_registry_conflict/4', Syn applies its default rule: it keeps the later
+%% registration and kills the other process.
+%% - Otherwise Syn uses the node-wide handler, set with {@link set_event_handler/1} or with the `event_handler' key
+%% of the `syn' application environment.
+%% - Without either, Syn uses its defaults.
+%%
+%%
+%% You can call this method before Syn is started and before the node is added to the Scope.
+%% The handler stays set when the `syn' application stops and starts again.
+%% Calling it again for the same Scope replaces the handler.
+%%
+%% There are 2 ways to set the handler module of a Scope. One is by using this method, the other is to set the environment
+%% variable `syn' with the key `scope_event_handlers', a list of `{Scope, Module}' tuples. Syn applies this list every time
+%% it starts, before it starts any Scope, so use it for the Scopes listed under the `scopes' key: Syn starts them during its
+%% own startup, which usually happens before your code runs. An entry in the list replaces a handler set earlier with this
+%% method for the same Scope. If an entry is malformed or names a module that cannot be loaded, Syn fails to start.
+%%
+%% Elixir
+%% ```
+%% config :syn,
+%% scope_event_handlers: [devices: MyDevicesEventHandler]
+%% '''
+%% Erlang
+%% ```
+%% {syn, [
+%% {scope_event_handlers, [{devices, my_devices_event_handler}]}
+%% ]}
+%% '''
+%%
+%% Examples
+%% Elixir
+%% ```
+%% iex> :syn.set_event_handler(:devices, MyDevicesEventHandler)
+%% :ok
+%% '''
+%% Erlang
+%% ```
+%% 1> syn:set_event_handler(devices, my_devices_event_handler).
+%% ok
+%% '''
+-spec set_event_handler(Scope :: atom(), Module :: module()) -> ok.
+set_event_handler(Scope, Module) when is_atom(Scope), is_atom(Module), Module =/= undefined ->
+ syn_event_handler:set_scope_event_handler(Scope, Module).
+
%% ----- \/ registry -------------------------------------------------
%% @doc Looks up a registry entry in the specified Scope.
%%
diff --git a/src/syn_app.erl b/src/syn_app.erl
index 3fb8f38f..8ed65bfa 100755
--- a/src/syn_app.erl
+++ b/src/syn_app.erl
@@ -40,6 +40,8 @@
start(_StartType, _StartArgs) ->
%% ensure event handler is loaded
syn_event_handler:ensure_event_handler_loaded(),
+ %% set scope event handlers from the configuration before any scope starts
+ syn_event_handler:set_configured_scope_event_handlers(),
%% start main sup
syn_sup:start_link().
diff --git a/src/syn_event_handler.erl b/src/syn_event_handler.erl
index 500ad51e..e036fbb5 100644
--- a/src/syn_event_handler.erl
+++ b/src/syn_event_handler.erl
@@ -30,7 +30,19 @@
%% ===================================================================
%% @doc Defines Syn's callbacks.
%%
-%% You can specify the callback module with {@link syn:set_event_handler/1}.
+%% You can specify a callback module for the whole node with {@link syn:set_event_handler/1}, and a callback module
+%% for a single Scope with {@link syn:set_event_handler/2} or with the `scope_event_handlers' key of the `syn'
+%% application environment. Syn selects the module for each Scope as a whole:
+%%
+%% - If the Scope has its own module, Syn uses only that module for all of the Scope's callbacks.
+%% If the module does not export a callback, Syn skips that callback for the Scope and does not fall back to the
+%% node-wide handler. Without `resolve_registry_conflict/4', Syn applies its default rule: it keeps the later
+%% registration and kills the other process.
+%% - Otherwise Syn uses the node-wide module.
+%% - Without either, Syn uses its defaults.
+%%
+%% A node-wide module therefore receives no callbacks from Scopes that have their own module.
+%%
%% In your module you need to specify the behavior `syn_event_handler' and implement the callbacks.
%% All callbacks are optional, so you just need to define the ones you need.
%%
@@ -74,8 +86,9 @@
%% one of the two Pids, or if the method crashes, none of the Pids will be killed and the conflicting name will be freed.
%%
%% Important Note: the conflict resolution method will be called on the two nodes where the conflicting processes are running on.
-%% Therefore, this method MUST be defined in the same way across all nodes of the cluster and have the same effect
-%% regardless of the node it is run on, or you will experience unexpected results.
+%% Therefore, every node must resolve a conflict in a Scope the same way, which in practice means the same module and
+%% version. This method MUST be defined in the same way across all nodes and have the same effect regardless of the
+%% node it is run on, or you will experience unexpected results.
%%
%% Examples
%% The following callback module implements the `on_process_unregistered/5' and the `on_process_left/5' callbacks.
@@ -130,6 +143,8 @@
%% API
-export([ensure_event_handler_loaded/0]).
+-export([set_scope_event_handler/2]).
+-export([set_configured_scope_event_handlers/0]).
-export([call_event_handler/2]).
-export([do_resolve_registry_conflict/4]).
@@ -217,18 +232,46 @@
-spec ensure_event_handler_loaded() -> ok.
ensure_event_handler_loaded() ->
%% get handler
- CustomEventHandler = get_custom_event_handler(),
- %% ensure that is it loaded (not using code:ensure_loaded/1 to support embedded mode)
- catch CustomEventHandler:module_info(exports),
- ok.
+ CustomEventHandler = get_node_wide_event_handler(),
+ ensure_module_loaded(CustomEventHandler).
+
+%% @private
+-spec set_scope_event_handler(Scope :: atom(), Module :: module()) -> ok.
+set_scope_event_handler(Scope, Module) ->
+ %% in interactive mode this loads the module before events can reach it;
+ %% in embedded mode the release has already loaded it, and nothing here can load it
+ ensure_module_loaded(Module),
+ persistent_term:put({syn_event_handler, Scope}, Module).
+
+%% @private
+-spec set_configured_scope_event_handlers() -> ok.
+set_configured_scope_event_handlers() ->
+ set_configured_scope_event_handlers(application:get_env(syn, scope_event_handlers, [])).
+
+-spec set_configured_scope_event_handlers(Entries :: [{Scope :: atom(), Module :: module()}]) -> ok.
+set_configured_scope_event_handlers(Entries) when is_list(Entries) ->
+ %% a malformed value or entry crashes with function_clause, so that syn fails to start
+ lists:foreach(fun({Scope, Module}) when is_atom(Scope), is_atom(Module), Module =/= undefined ->
+ %% in interactive mode this loads the module if it can;
+ %% in embedded mode the boot script has already loaded every module of the release
+ ensure_module_loaded(Module),
+ case erlang:module_loaded(Module) of
+ true ->
+ persistent_term:put({syn_event_handler, Scope}, Module);
+
+ false ->
+ %% fail the start: the scope would skip every callback and resolve its conflicts with the defaults
+ error({scope_event_handler_not_loaded, Scope, Module})
+ end
+ end, Entries).
%% @private
-spec call_event_handler(
CallbackMethod :: atom(),
- Args :: [term()]
+ Args :: [term(), ...]
) -> any().
-call_event_handler(CallbackMethod, Args) ->
- CustomEventHandler = get_custom_event_handler(),
+call_event_handler(CallbackMethod, [Scope | _] = Args) ->
+ CustomEventHandler = get_custom_event_handler(Scope),
case erlang:function_exported(CustomEventHandler, CallbackMethod, length(Args)) of
true ->
try apply(CustomEventHandler, CallbackMethod, Args)
@@ -251,7 +294,7 @@ call_event_handler(CallbackMethod, Args) ->
{Pid2 :: pid(), Meta2 :: term(), Time2 :: non_neg_integer()}
) -> {PidToKeep :: pid() | undefined, KillOtherPid :: boolean()}.
do_resolve_registry_conflict(Scope, Name, {Pid1, Meta1, Time1}, {Pid2, Meta2, Time2}) ->
- CustomEventHandler = get_custom_event_handler(),
+ CustomEventHandler = get_custom_event_handler(Scope),
case erlang:function_exported(CustomEventHandler, resolve_registry_conflict, 4) of
true ->
try CustomEventHandler:resolve_registry_conflict(Scope, Name, {Pid1, Meta1, Time1}, {Pid2, Meta2, Time2}) of
@@ -283,6 +326,20 @@ do_resolve_registry_conflict(Scope, Name, {Pid1, Meta1, Time1}, {Pid2, Meta2, Ti
%% ===================================================================
%% Internal
%% ===================================================================
--spec get_custom_event_handler() -> undefined | module().
-get_custom_event_handler() ->
+-spec get_custom_event_handler(Scope :: atom()) -> undefined | module().
+get_custom_event_handler(Scope) ->
+ %% a scope handler replaces the node-wide handler for all callbacks of its scope
+ case persistent_term:get({syn_event_handler, Scope}, undefined) of
+ undefined -> get_node_wide_event_handler();
+ Module -> Module
+ end.
+
+-spec get_node_wide_event_handler() -> undefined | module().
+get_node_wide_event_handler() ->
application:get_env(syn, event_handler, undefined).
+
+-spec ensure_module_loaded(Module :: undefined | module()) -> ok.
+ensure_module_loaded(Module) ->
+ %% not using code:ensure_loaded/1 to support embedded mode
+ catch Module:module_info(exports),
+ ok.
diff --git a/test/syn_pg_SUITE.erl b/test/syn_pg_SUITE.erl
index fafc40d3..b8e3b210 100644
--- a/test/syn_pg_SUITE.erl
+++ b/test/syn_pg_SUITE.erl
@@ -33,7 +33,8 @@
%% tests
-export([
- one_node_strict_mode/1
+ one_node_strict_mode/1,
+ one_node_scope_event_handler_joined_left/1
]).
-export([
two_nodes_member_count/1
@@ -98,7 +99,8 @@ all() ->
groups() ->
[
{one_node_pg, [shuffle], [
- one_node_strict_mode
+ one_node_strict_mode,
+ one_node_scope_event_handler_joined_left
]},
{two_nodes_pg, [shuffle], [
two_nodes_member_count
@@ -223,6 +225,38 @@ one_node_strict_mode(_Config) ->
[{Self, undefined}] = syn:members(scope, "strict"),
ok = syn:leave(scope, "strict", Self).
+one_node_scope_event_handler_joined_left(_Config) ->
+ %% start syn
+ ok = syn:start(),
+
+ %% node-wide handler, and an own handler for one scope
+ ok = syn:set_event_handler(syn_test_event_handler_callbacks),
+ ok = syn:set_event_handler(scope_with_handler, syn_test_event_handler_scope_callbacks),
+ ok = syn:add_node_to_scopes([scope_with_handler, scope_without_handler]),
+
+ %% init
+ LocalNode = node(),
+ Pid = syn_test_suite_helper:start_process(),
+
+ %% join, update & leave in both scopes
+ %% (the scope handler does not implement the update callbacks, the node-wide handler does)
+ ok = syn:join(scope_with_handler, "group-handler", Pid, {recipient, self(), <<"meta">>}),
+ ok = syn:join(scope_without_handler, "group-handler", Pid, {recipient, self(), <<"meta">>}),
+ ok = syn:join(scope_with_handler, "group-handler", Pid, {recipient, self(), <<"new-meta">>}),
+ ok = syn:join(scope_without_handler, "group-handler", Pid, {recipient, self(), <<"new-meta">>}),
+ ok = syn:leave(scope_with_handler, "group-handler", Pid),
+ ok = syn:leave(scope_without_handler, "group-handler", Pid),
+
+ %% check that each scope's callbacks reach only one handler
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_joined, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "group-handler", Pid, <<"meta">>, normal},
+ {on_process_left, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "group-handler", Pid, <<"new-meta">>, normal},
+ {on_process_joined, LocalNode, scope_without_handler, "group-handler", Pid, <<"meta">>, normal},
+ {on_group_process_updated, LocalNode, scope_without_handler, "group-handler", Pid, <<"new-meta">>, normal},
+ {on_group_process_updated, LocalNode, scope_without_handler, "group-handler", Pid, <<"meta">>, <<"new-meta">>, normal},
+ {on_process_left, LocalNode, scope_without_handler, "group-handler", Pid, <<"new-meta">>, normal}
+ ]).
+
two_nodes_member_count(Config) ->
%% get slave
SlaveNode1 = proplists:get_value(syn_slave_1, Config),
diff --git a/test/syn_registry_SUITE.erl b/test/syn_registry_SUITE.erl
index 63651203..af2e23eb 100644
--- a/test/syn_registry_SUITE.erl
+++ b/test/syn_registry_SUITE.erl
@@ -36,7 +36,16 @@
one_node_via_register_unregister/1,
one_node_via_register_unregister_with_metadata/1,
one_node_strict_mode/1,
- one_node_repeated_restart/1
+ one_node_repeated_restart/1,
+ one_node_scope_event_handler_reg_unreg/1,
+ one_node_scope_event_handler_loads_module/1,
+ one_node_scope_event_handler_set_before_start/1,
+ one_node_scope_event_handler_invalid_args/1,
+ one_node_scope_event_handlers_config/1,
+ one_node_scope_event_handlers_config_loads_module/1,
+ one_node_scope_event_handlers_config_replaces_handler/1,
+ one_node_scope_event_handlers_config_invalid_entry/1,
+ one_node_scope_event_handlers_config_module_not_loaded/1
]).
-export([
three_nodes_discover/1,
@@ -46,6 +55,8 @@
three_nodes_cluster_conflicts/1,
three_nodes_custom_event_handler_reg_unreg/1,
three_nodes_custom_event_handler_conflict_resolution/1,
+ three_nodes_scope_event_handler_conflict_resolution/1,
+ three_nodes_scope_event_handlers_config_boot_sync/1,
three_nodes_update/1,
three_nodes_ack_sync_reconciles_registry_snapshot/1,
three_nodes_ack_sync_ordered_delivery/1
@@ -94,7 +105,16 @@ groups() ->
one_node_via_register_unregister,
one_node_via_register_unregister_with_metadata,
one_node_strict_mode,
- one_node_repeated_restart
+ one_node_repeated_restart,
+ one_node_scope_event_handler_reg_unreg,
+ one_node_scope_event_handler_loads_module,
+ one_node_scope_event_handler_set_before_start,
+ one_node_scope_event_handler_invalid_args,
+ one_node_scope_event_handlers_config,
+ one_node_scope_event_handlers_config_loads_module,
+ one_node_scope_event_handlers_config_replaces_handler,
+ one_node_scope_event_handlers_config_invalid_entry,
+ one_node_scope_event_handlers_config_module_not_loaded
]},
{three_nodes_registry, [shuffle], [
three_nodes_discover,
@@ -104,6 +124,8 @@ groups() ->
three_nodes_cluster_conflicts,
three_nodes_custom_event_handler_reg_unreg,
three_nodes_custom_event_handler_conflict_resolution,
+ three_nodes_scope_event_handler_conflict_resolution,
+ three_nodes_scope_event_handlers_config_boot_sync,
three_nodes_update,
three_nodes_ack_sync_reconciles_registry_snapshot,
three_nodes_ack_sync_ordered_delivery
@@ -299,6 +321,195 @@ one_node_repeated_restart(_Config) ->
end,
lists:foreach(StartStop, lists:duplicate(RepeatCount, 0)).
+one_node_scope_event_handler_reg_unreg(_Config) ->
+ %% start syn
+ ok = syn:start(),
+
+ %% node-wide handler, and an own handler for one scope
+ ok = syn:set_event_handler(syn_test_event_handler_callbacks),
+ ok = syn:set_event_handler(scope_with_handler, syn_test_event_handler_scope_callbacks),
+ ok = syn:add_node_to_scopes([scope_with_handler, scope_without_handler]),
+
+ %% init
+ LocalNode = node(),
+ Pid = syn_test_suite_helper:start_process(),
+
+ %% register, update & unregister in both scopes
+ %% (the scope handler does not implement the update callbacks, the node-wide handler does)
+ ok = syn:register(scope_with_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+ ok = syn:register(scope_without_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+ ok = syn:register(scope_with_handler, "proc-handler", Pid, {recipient, self(), <<"new-meta">>}),
+ ok = syn:register(scope_without_handler, "proc-handler", Pid, {recipient, self(), <<"new-meta">>}),
+ ok = syn:unregister(scope_with_handler, "proc-handler"),
+ ok = syn:unregister(scope_without_handler, "proc-handler"),
+
+ %% check that each scope's callbacks reach only one handler
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_registered, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "proc-handler", Pid, <<"meta">>, normal},
+ {on_process_unregistered, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "proc-handler", Pid, <<"new-meta">>, normal},
+ {on_process_registered, LocalNode, scope_without_handler, "proc-handler", Pid, <<"meta">>, normal},
+ {on_registry_process_updated, LocalNode, scope_without_handler, "proc-handler", Pid, <<"new-meta">>, normal},
+ {on_registry_process_updated, LocalNode, scope_without_handler, "proc-handler", Pid, <<"meta">>, <<"new-meta">>, normal},
+ {on_process_unregistered, LocalNode, scope_without_handler, "proc-handler", Pid, <<"new-meta">>, normal}
+ ]).
+
+one_node_scope_event_handler_loads_module(_Config) ->
+ %% unload handler module
+ code:purge(syn_test_event_handler_scope_callbacks),
+ code:delete(syn_test_event_handler_scope_callbacks),
+ code:purge(syn_test_event_handler_scope_callbacks),
+ false = erlang:module_loaded(syn_test_event_handler_scope_callbacks),
+
+ %% start syn
+ ok = syn:start(),
+ ok = syn:add_node_to_scopes([scope_with_handler]),
+
+ %% set handler
+ ok = syn:set_event_handler(scope_with_handler, syn_test_event_handler_scope_callbacks),
+ true = erlang:module_loaded(syn_test_event_handler_scope_callbacks),
+
+ %% register
+ LocalNode = node(),
+ Pid = syn_test_suite_helper:start_process(),
+ ok = syn:register(scope_with_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+
+ %% check callbacks called
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_registered, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "proc-handler", Pid, <<"meta">>, normal}
+ ]).
+
+one_node_scope_event_handler_set_before_start(_Config) ->
+ %% set handler before syn and the scope exist
+ ok = syn:set_event_handler(scope_with_handler, syn_test_event_handler_scope_callbacks),
+
+ %% start syn
+ ok = syn:start(),
+ ok = syn:add_node_to_scopes([scope_with_handler]),
+
+ %% register
+ LocalNode = node(),
+ Pid = syn_test_suite_helper:start_process(),
+ ok = syn:register(scope_with_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+
+ %% check callbacks called
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_registered, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "proc-handler", Pid, <<"meta">>, normal}
+ ]).
+
+one_node_scope_event_handler_invalid_args(_Config) ->
+ {'EXIT', {function_clause, _}} = (catch syn:set_event_handler(<<"scope">>, syn_test_event_handler_scope_callbacks)),
+ {'EXIT', {function_clause, _}} = (catch syn:set_event_handler(scope_with_handler, "syn_test_event_handler_scope_callbacks")),
+ {'EXIT', {function_clause, _}} = (catch syn:set_event_handler(scope_with_handler, undefined)),
+ %% a test case that returns {'EXIT', _} counts as failed
+ ok.
+
+one_node_scope_event_handlers_config(_Config) ->
+ %% configure scopes and handlers before syn starts
+ application:set_env(syn, event_handler, syn_test_event_handler_callbacks),
+ application:set_env(syn, scopes, [scope_with_handler, scope_without_handler]),
+ application:set_env(syn, scope_event_handlers, [{scope_with_handler, syn_test_event_handler_scope_callbacks}]),
+
+ %% start syn
+ ok = syn:start(),
+
+ %% init
+ LocalNode = node(),
+ Pid = syn_test_suite_helper:start_process(),
+
+ %% register & unregister in both scopes
+ ok = syn:register(scope_with_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+ ok = syn:register(scope_without_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+ ok = syn:unregister(scope_with_handler, "proc-handler"),
+ ok = syn:unregister(scope_without_handler, "proc-handler"),
+
+ %% check that each scope's callbacks reach only one handler
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_registered, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "proc-handler", Pid, <<"meta">>, normal},
+ {on_process_unregistered, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "proc-handler", Pid, <<"meta">>, normal},
+ {on_process_registered, LocalNode, scope_without_handler, "proc-handler", Pid, <<"meta">>, normal},
+ {on_process_unregistered, LocalNode, scope_without_handler, "proc-handler", Pid, <<"meta">>, normal}
+ ]).
+
+one_node_scope_event_handlers_config_loads_module(_Config) ->
+ %% unload handler module
+ code:purge(syn_test_event_handler_scope_callbacks),
+ code:delete(syn_test_event_handler_scope_callbacks),
+ code:purge(syn_test_event_handler_scope_callbacks),
+ false = erlang:module_loaded(syn_test_event_handler_scope_callbacks),
+
+ %% configure handler
+ application:set_env(syn, scope_event_handlers, [{scope_with_handler, syn_test_event_handler_scope_callbacks}]),
+
+ %% start syn
+ ok = syn:start(),
+ true = erlang:module_loaded(syn_test_event_handler_scope_callbacks).
+
+one_node_scope_event_handlers_config_replaces_handler(_Config) ->
+ %% set handlers before syn starts, then configure another handler for one of the scopes
+ ok = syn:set_event_handler(scope_with_handler, syn_test_event_handler_callbacks),
+ ok = syn:set_event_handler(scope_with_runtime_handler, syn_test_event_handler_callbacks),
+ application:set_env(syn, scope_event_handlers, [{scope_with_handler, syn_test_event_handler_scope_callbacks}]),
+
+ %% start syn
+ ok = syn:start(),
+ ok = syn:add_node_to_scopes([scope_with_handler, scope_with_runtime_handler]),
+
+ %% register in both scopes
+ LocalNode = node(),
+ Pid = syn_test_suite_helper:start_process(),
+ ok = syn:register(scope_with_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+ ok = syn:register(scope_with_runtime_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+
+ %% the configured scope uses the configured handler, the other scope keeps its handler
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_registered, syn_test_event_handler_scope_callbacks, LocalNode, scope_with_handler, "proc-handler", Pid, <<"meta">>, normal},
+ {on_process_registered, LocalNode, scope_with_runtime_handler, "proc-handler", Pid, <<"meta">>, normal}
+ ]).
+
+one_node_scope_event_handlers_config_invalid_entry(_Config) ->
+ lists:foreach(fun(BadEntry) ->
+ application:set_env(syn, scope_event_handlers, [BadEntry]),
+ {'EXIT', {{badmatch, {error, {syn, StartError}}}, _}} = (catch syn:start()),
+ %% the start fails on the bad entry itself
+ {bad_return, {_, {'EXIT', {function_clause, [{syn_event_handler, _, [BadEntry], _} | _]}}}} = StartError
+ end, [
+ {<<"scope">>, syn_test_event_handler_scope_callbacks},
+ {scope_with_handler, "syn_test_event_handler_scope_callbacks"},
+ {scope_with_handler, undefined},
+ scope_with_handler,
+ {scope_with_handler, syn_test_event_handler_scope_callbacks, extra}
+ ]),
+
+ %% a value that is not a list fails in syn as well, on the value itself
+ BadValue = #{scope_with_handler => syn_test_event_handler_scope_callbacks},
+ application:set_env(syn, scope_event_handlers, BadValue),
+ {'EXIT', {{badmatch, {error, {syn, StartError}}}, _}} = (catch syn:start()),
+ {bad_return, {_, {'EXIT', {function_clause, [{syn_event_handler, _, [BadValue], _} | _]}}}} = StartError.
+
+one_node_scope_event_handlers_config_module_not_loaded(_Config) ->
+ %% configure a node-wide handler, and a scope handler module that does not exist
+ application:set_env(syn, event_handler, syn_test_event_handler_callbacks),
+ application:set_env(syn, scope_event_handlers, [{scope_with_handler, syn_test_event_handler_missing}]),
+
+ %% the start fails on the module that cannot be loaded
+ {'EXIT', {{badmatch, {error, {syn, StartError}}}, _}} = (catch syn:start()),
+ {bad_return, {_, {'EXIT', {{scope_event_handler_not_loaded, scope_with_handler, syn_test_event_handler_missing}, _}}}} = StartError,
+
+ %% start syn without the entry
+ application:unset_env(syn, scope_event_handlers),
+ ok = syn:start(),
+ ok = syn:add_node_to_scopes([scope_with_handler]),
+
+ %% register
+ LocalNode = node(),
+ Pid = syn_test_suite_helper:start_process(),
+ ok = syn:register(scope_with_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+
+ %% the failed entry left no handler behind, so the callback reaches the node-wide handler
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_registered, LocalNode, scope_with_handler, "proc-handler", Pid, <<"meta">>, normal}
+ ]).
+
three_nodes_discover(Config) ->
%% get slaves
SlaveNode1 = proplists:get_value(syn_slave_1, Config),
@@ -1528,6 +1739,145 @@ three_nodes_custom_event_handler_conflict_resolution(Config) ->
fun() -> rpc:call(SlaveNode2, erlang, is_process_alive, [PidOn2]) end
).
+three_nodes_scope_event_handler_conflict_resolution(Config) ->
+ %% get slaves
+ SlaveNode1 = proplists:get_value(syn_slave_1, Config),
+ SlaveNode2 = proplists:get_value(syn_slave_2, Config),
+ Nodes = [node(), SlaveNode1, SlaveNode2],
+ Scopes = [scope_with_resolver, scope_without_handler, scope_without_resolver],
+
+ %% start syn on nodes
+ ok = syn:start(),
+ ok = rpc:call(SlaveNode1, syn, start, []),
+ ok = rpc:call(SlaveNode2, syn, start, []),
+
+ %% set handlers and add scopes on all nodes (scope_without_handler has no handler of its own)
+ lists:foreach(fun(Node) ->
+ ok = rpc:call(Node, syn, set_event_handler, [syn_test_event_handler_node_wide_resolution]),
+ ok = rpc:call(Node, syn, set_event_handler, [scope_with_resolver, syn_test_event_handler_scope_resolution]),
+ ok = rpc:call(Node, syn, set_event_handler, [scope_without_resolver, syn_test_event_handler_scope_callbacks]),
+ ok = rpc:call(Node, syn, add_node_to_scopes, [Scopes])
+ end, Nodes),
+
+ %% wait for full scope discovery
+ lists:foreach(fun(Scope) ->
+ lists:foreach(fun(Node) ->
+ syn_test_suite_helper:assert_scope_subcluster(registry, Node, Scope, Nodes -- [Node])
+ end, Nodes)
+ end, Scopes),
+
+ %% current node
+ TestPid = self(),
+ LocalNode = node(),
+
+ %% partial netsplit (1 cannot see 2)
+ rpc:call(SlaveNode1, syn_test_suite_helper, disconnect_node, [SlaveNode2]),
+ syn_test_suite_helper:assert_cluster(node(), [SlaveNode1, SlaveNode2]),
+ syn_test_suite_helper:assert_cluster(SlaveNode1, [node()]),
+ syn_test_suite_helper:assert_cluster(SlaveNode2, [node()]),
+
+ %% start conflict processes
+ PidOn1 = syn_test_suite_helper:start_process(SlaveNode1),
+ PidOn2 = syn_test_suite_helper:start_process(SlaveNode2),
+ DefaultRulePidOn1 = syn_test_suite_helper:start_process(SlaveNode1),
+ DefaultRulePidOn2 = syn_test_suite_helper:start_process(SlaveNode2),
+ monitor(process, PidOn1),
+ DefaultRulePidOn1MRef = monitor(process, DefaultRulePidOn1),
+
+ %% --> conflict by netsplit, in every scope the registration on node 2 is the later one
+ %% the marker in the metadata names the resolver that keeps the process, so the two resolvers disagree in every scope
+ ok = rpc:call(SlaveNode1, syn, register, [scope_with_resolver, "proc-conflict", PidOn1, {recipient, TestPid, node_wide_keeps}]),
+ ok = rpc:call(SlaveNode1, syn, register, [scope_without_handler, "proc-conflict", PidOn1, {recipient, TestPid, scope_keeps}]),
+ ok = rpc:call(SlaveNode1, syn, register, [scope_without_resolver, "proc-conflict", DefaultRulePidOn1, {recipient, TestPid, node_wide_keeps}]),
+ %% the local node must get the registrations on node 1 before those on node 2, or it drops them as older
+ syn_test_suite_helper:assert_wait(
+ {PidOn1, {recipient, TestPid, node_wide_keeps}},
+ fun() -> syn:lookup(scope_with_resolver, "proc-conflict") end
+ ),
+ syn_test_suite_helper:assert_wait(
+ {PidOn1, {recipient, TestPid, scope_keeps}},
+ fun() -> syn:lookup(scope_without_handler, "proc-conflict") end
+ ),
+ syn_test_suite_helper:assert_wait(
+ {DefaultRulePidOn1, {recipient, TestPid, node_wide_keeps}},
+ fun() -> syn:lookup(scope_without_resolver, "proc-conflict") end
+ ),
+ ok = rpc:call(SlaveNode2, syn, register, [scope_with_resolver, "proc-conflict", PidOn2, {recipient, TestPid, scope_keeps}]),
+ ok = rpc:call(SlaveNode2, syn, register, [scope_without_handler, "proc-conflict", PidOn2, {recipient, TestPid, node_wide_keeps}]),
+ ok = rpc:call(SlaveNode2, syn, register, [scope_without_resolver, "proc-conflict", DefaultRulePidOn2, {recipient, TestPid, scope_keeps}]),
+
+ %% check callbacks (only the handler of scope_without_resolver implements them)
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_registered, syn_test_event_handler_scope_callbacks, LocalNode, scope_without_resolver, "proc-conflict", DefaultRulePidOn1, node_wide_keeps, normal},
+ {on_process_unregistered, syn_test_event_handler_scope_callbacks, LocalNode, scope_without_resolver, "proc-conflict", DefaultRulePidOn1, node_wide_keeps, normal},
+ {on_process_registered, syn_test_event_handler_scope_callbacks, LocalNode, scope_without_resolver, "proc-conflict", DefaultRulePidOn2, scope_keeps, normal},
+ {on_process_registered, syn_test_event_handler_scope_callbacks, SlaveNode1, scope_without_resolver, "proc-conflict", DefaultRulePidOn1, node_wide_keeps, normal},
+ {on_process_registered, syn_test_event_handler_scope_callbacks, SlaveNode2, scope_without_resolver, "proc-conflict", DefaultRulePidOn2, scope_keeps, normal}
+ ]),
+
+ %% re-join
+ rpc:call(SlaveNode1, syn_test_suite_helper, connect_node, [SlaveNode2]),
+ syn_test_suite_helper:assert_cluster(node(), [SlaveNode1, SlaveNode2]),
+ syn_test_suite_helper:assert_cluster(SlaveNode1, [node(), SlaveNode2]),
+ syn_test_suite_helper:assert_cluster(SlaveNode2, [node(), SlaveNode1]),
+
+ %% retrieve: the scope resolver, the node-wide resolver and the default rule each keep the process on node 2
+ lists:foreach(fun(Node) ->
+ syn_test_suite_helper:assert_wait(
+ {PidOn2, {recipient, TestPid, scope_keeps}},
+ fun() -> rpc:call(Node, syn, lookup, [scope_with_resolver, "proc-conflict"]) end
+ ),
+ syn_test_suite_helper:assert_wait(
+ {PidOn2, {recipient, TestPid, node_wide_keeps}},
+ fun() -> rpc:call(Node, syn, lookup, [scope_without_handler, "proc-conflict"]) end
+ ),
+ syn_test_suite_helper:assert_wait(
+ {DefaultRulePidOn2, {recipient, TestPid, scope_keeps}},
+ fun() -> rpc:call(Node, syn, lookup, [scope_without_resolver, "proc-conflict"]) end
+ )
+ end, Nodes),
+
+ %% each resolver runs once on each node that owns a conflicting process, and only in the scope it is selected for;
+ %% the default rule kills the discarded process, a custom resolver does not (no 'DOWN' for PidOn1)
+ syn_test_suite_helper:assert_received_messages([
+ {resolve_registry_conflict, syn_test_event_handler_scope_resolution, SlaveNode1, scope_with_resolver, "proc-conflict"},
+ {resolve_registry_conflict, syn_test_event_handler_scope_resolution, SlaveNode2, scope_with_resolver, "proc-conflict"},
+ {resolve_registry_conflict, syn_test_event_handler_node_wide_resolution, SlaveNode1, scope_without_handler, "proc-conflict"},
+ {resolve_registry_conflict, syn_test_event_handler_node_wide_resolution, SlaveNode2, scope_without_handler, "proc-conflict"},
+ {on_process_unregistered, syn_test_event_handler_scope_callbacks, SlaveNode1, scope_without_resolver, "proc-conflict", DefaultRulePidOn1, node_wide_keeps, syn_conflict_resolution},
+ {on_process_registered, syn_test_event_handler_scope_callbacks, SlaveNode1, scope_without_resolver, "proc-conflict", DefaultRulePidOn2, scope_keeps, syn_conflict_resolution},
+ {'DOWN', DefaultRulePidOn1MRef, process, DefaultRulePidOn1, {syn_resolve_kill, "proc-conflict", {recipient, TestPid, node_wide_keeps}}}
+ ]),
+ true = rpc:call(SlaveNode1, erlang, is_process_alive, [PidOn1]).
+
+three_nodes_scope_event_handlers_config_boot_sync(Config) ->
+ %% covers the boot-time sync of a scope that syn starts with a configured handler. A wrong start order (scopes
+ %% started before the handlers are stored) fails this test only when the sync lands before the handlers are stored,
+ %% so the test is not a deterministic guard of that order.
+
+ %% get slaves
+ SlaveNode1 = proplists:get_value(syn_slave_1, Config),
+
+ %% start syn on the local node and register a process
+ ok = syn:start(),
+ ok = syn:add_node_to_scopes([scope_with_handler]),
+ LocalNode = node(),
+ Pid = syn_test_suite_helper:start_process(),
+ ok = syn:register(scope_with_handler, "proc-handler", Pid, {recipient, self(), <<"meta">>}),
+
+ %% configure the scope, its own handler and a node-wide handler on node 1
+ ok = rpc:call(SlaveNode1, application, set_env, [syn, event_handler, syn_test_event_handler_callbacks]),
+ ok = rpc:call(SlaveNode1, application, set_env, [syn, scopes, [scope_with_handler]]),
+ ok = rpc:call(SlaveNode1, application, set_env, [syn, scope_event_handlers, [{scope_with_handler, syn_test_event_handler_scope_callbacks}]]),
+
+ %% start syn on node 1, its scope gets the registration from the local node when it syncs
+ ok = rpc:call(SlaveNode1, syn, start, []),
+
+ %% check that the callback reaches only the scope handler
+ syn_test_suite_helper:assert_received_messages([
+ {on_process_registered, syn_test_event_handler_scope_callbacks, SlaveNode1, scope_with_handler, "proc-handler", Pid, <<"meta">>, {syn_remote_scope_node_up, scope_with_handler, LocalNode}}
+ ]).
+
three_nodes_update(Config) ->
%% get slaves
SlaveNode1 = proplists:get_value(syn_slave_1, Config),
diff --git a/test/syn_test_event_handler_node_wide_resolution.erl b/test/syn_test_event_handler_node_wide_resolution.erl
new file mode 100644
index 00000000..0ae0c94e
--- /dev/null
+++ b/test/syn_test_event_handler_node_wide_resolution.erl
@@ -0,0 +1,37 @@
+%% ==========================================================================================================
+%% Syn - A global Process Registry and Process Group manager.
+%%
+%% The MIT License (MIT)
+%%
+%% Copyright (c) 2015-2026 Roberto Ostinelli and Neato Robotics, Inc.
+%%
+%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%% of this software and associated documentation files (the "Software"), to deal
+%% in the Software without restriction, including without limitation the rights
+%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the Software is
+%% furnished to do so, subject to the following conditions:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%% THE SOFTWARE.
+%% ==========================================================================================================
+-module(syn_test_event_handler_node_wide_resolution).
+-behaviour(syn_event_handler).
+
+-export([resolve_registry_conflict/4]).
+
+%% keeps the process registered with the marker `node_wide_keeps' and tells the recipient that it was called
+resolve_registry_conflict(Scope, Name, {Pid1, {recipient, RecipientPid, Marker1}, _Time1}, {Pid2, _Meta2, _Time2}) ->
+ RecipientPid ! {resolve_registry_conflict, ?MODULE, node(), Scope, Name},
+ case Marker1 of
+ node_wide_keeps -> Pid1;
+ _ -> Pid2
+ end.
diff --git a/test/syn_test_event_handler_scope_callbacks.erl b/test/syn_test_event_handler_scope_callbacks.erl
new file mode 100644
index 00000000..2f9c5850
--- /dev/null
+++ b/test/syn_test_event_handler_scope_callbacks.erl
@@ -0,0 +1,44 @@
+%% ==========================================================================================================
+%% Syn - A global Process Registry and Process Group manager.
+%%
+%% The MIT License (MIT)
+%%
+%% Copyright (c) 2015-2026 Roberto Ostinelli and Neato Robotics, Inc.
+%%
+%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%% of this software and associated documentation files (the "Software"), to deal
+%% in the Software without restriction, including without limitation the rights
+%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the Software is
+%% furnished to do so, subject to the following conditions:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%% THE SOFTWARE.
+%% ==========================================================================================================
+-module(syn_test_event_handler_scope_callbacks).
+-behaviour(syn_event_handler).
+
+-export([on_process_registered/5]).
+-export([on_process_unregistered/5]).
+-export([on_process_joined/5]).
+-export([on_process_left/5]).
+
+on_process_registered(Scope, Name, Pid, {recipient, RecipientPid, AdditionalMeta}, Reason) ->
+ RecipientPid ! {on_process_registered, ?MODULE, node(), Scope, Name, Pid, AdditionalMeta, Reason}.
+
+on_process_unregistered(Scope, Name, Pid, {recipient, RecipientPid, AdditionalMeta}, Reason) ->
+ RecipientPid ! {on_process_unregistered, ?MODULE, node(), Scope, Name, Pid, AdditionalMeta, Reason}.
+
+on_process_joined(Scope, GroupName, Pid, {recipient, RecipientPid, AdditionalMeta}, Reason) ->
+ RecipientPid ! {on_process_joined, ?MODULE, node(), Scope, GroupName, Pid, AdditionalMeta, Reason}.
+
+on_process_left(Scope, GroupName, Pid, {recipient, RecipientPid, AdditionalMeta}, Reason) ->
+ RecipientPid ! {on_process_left, ?MODULE, node(), Scope, GroupName, Pid, AdditionalMeta, Reason}.
diff --git a/test/syn_test_event_handler_scope_resolution.erl b/test/syn_test_event_handler_scope_resolution.erl
new file mode 100644
index 00000000..3ba9325b
--- /dev/null
+++ b/test/syn_test_event_handler_scope_resolution.erl
@@ -0,0 +1,37 @@
+%% ==========================================================================================================
+%% Syn - A global Process Registry and Process Group manager.
+%%
+%% The MIT License (MIT)
+%%
+%% Copyright (c) 2015-2026 Roberto Ostinelli and Neato Robotics, Inc.
+%%
+%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%% of this software and associated documentation files (the "Software"), to deal
+%% in the Software without restriction, including without limitation the rights
+%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the Software is
+%% furnished to do so, subject to the following conditions:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%% THE SOFTWARE.
+%% ==========================================================================================================
+-module(syn_test_event_handler_scope_resolution).
+-behaviour(syn_event_handler).
+
+-export([resolve_registry_conflict/4]).
+
+%% keeps the process registered with the marker `scope_keeps' and tells the recipient that it was called
+resolve_registry_conflict(Scope, Name, {Pid1, {recipient, RecipientPid, Marker1}, _Time1}, {Pid2, _Meta2, _Time2}) ->
+ RecipientPid ! {resolve_registry_conflict, ?MODULE, node(), Scope, Name},
+ case Marker1 of
+ scope_keeps -> Pid1;
+ _ -> Pid2
+ end.
diff --git a/test/syn_test_suite_helper.erl b/test/syn_test_suite_helper.erl
index 1d7f50ea..f6338bc8 100755
--- a/test/syn_test_suite_helper.erl
+++ b/test/syn_test_suite_helper.erl
@@ -43,6 +43,7 @@
%% internal
-export([process_main/0]).
+-export([erase_scope_event_handlers/0]).
%% macro
-define(DEFAULT_WAIT_TIMEOUT, 5000).
@@ -139,6 +140,9 @@ clean_after_test() ->
rpc:call(Node, application, unset_env, [syn, event_handler]),
rpc:call(Node, application, unset_env, [syn, strict_mode]),
rpc:call(Node, application, unset_env, [syn, scopes]),
+ rpc:call(Node, application, unset_env, [syn, scope_event_handlers]),
+ %% clean scope event handlers (persistent terms survive the application stop)
+ rpc:call(Node, syn_test_suite_helper, erase_scope_event_handlers, []),
%% messages
flush_inbox()
end, Nodes).
@@ -335,6 +339,12 @@ process_main() ->
process_main()
end.
+erase_scope_event_handlers() ->
+ lists:foreach(fun
+ ({{syn_event_handler, _} = Key, _}) -> persistent_term:erase(Key);
+ (_) -> ok
+ end, persistent_term:get()).
+
do_assert_scope_subcluster(Type, Node, Scope, ExpectedNodes) ->
do_assert_scope_subcluster(Type, Node, Scope, ExpectedNodes, os:system_time(millisecond)).
do_assert_scope_subcluster(Type, Node, Scope, ExpectedNodes, StartAt) ->