Browse Source

Add strict_mode config option to enforce metadata best practices.

Roberto Ostinelli 3 years ago
parent
commit
8f67d20ecb
10 changed files with 356 additions and 95 deletions
  1. 141 0
      OPTIONS.md
  2. 1 1
      docs.config
  3. 22 8
      src/syn.erl
  4. 5 0
      src/syn_backbone.erl
  5. 3 0
      src/syn_event_handler.erl
  6. 27 14
      src/syn_pg.erl
  7. 25 12
      src/syn_registry.erl
  8. 69 37
      test/syn_pg_SUITE.erl
  9. 52 22
      test/syn_registry_SUITE.erl
  10. 11 1
      test/syn_test_suite_helper.erl

+ 141 - 0
OPTIONS.md

@@ -0,0 +1,141 @@
+# Options
+Syn accepts the following options that can be set as keys in the environment variable `syn`.
+
+## event_handler
+Please see [`syn_event_handler`](syn_event_handler.html) for information on callbacks.
+
+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.
+
+#### Elixir
+
+```elixir
+config :syn,
+  event_handler: MyCustomEventHandler
+```
+
+#### Erlang
+
+```erlang
+{syn, [
+  {event_handler, my_custom_event_handler}
+]}
+```
+
+## scopes
+Please see [`here`](syn.html) for information on Scopes.
+
+Scopes can be defined either by using the [`add_node_to_scopes/1`](syn.html#add_node_to_scopes/1) method, or
+by setting the `scopes` configuration variable.
+
+#### Elixir
+
+```elixir
+config :syn,
+  scopes: [:devices, :users]
+```
+
+#### Erlang
+
+```erlang
+{syn, [
+  {scopes, [devices, users]}
+]}
+```
+
+## strict_mode
+By default, Syn doesn't enforce which process performs the Registry and Process Groups operations:
+for instance, a process can be registered by a call running in another process.
+While this is fine in standard operations, those that would end up updating a process' metadata
+return a `{error, non_strict_update}` tuple when `strict_mode` is not enabled (the default).
+
+This is because enabling any process to update another process' metadata could lead to potential race conditions.
+
+Operations are serialized by the [authority node](internals.html#node-authority) responsible for a process, however
+simultaneous requests coming from different processes to update a specific process' metadata would result
+in having the metadata of the first incoming request to be overwritten by the second request.
+While this doesn't compromise Syn's strict eventual consistency (i.e. the data across the cluster will eventually converge),
+an application might overwrite metadata and have unexpected logical consequences.
+
+Imagine for instance that a process A has a metadata of attributes, such as `[{color, "blue"}]`. A different process
+might request to update process A's metadata to include size, by setting its metadata to `[{color, "blue"}, {size, 100}]`.
+At almost the same time another process issues the request to update process A's metadata to include weight, by setting
+its metadata to `[{color, "blue"}, {weight, 1.0}]`. The first request to set size will then be overwritten shortly after
+by the second incoming request to set the weight, thus resulting in the loss of the size information,
+and the process A' metadata will be propagated to `[{color, "blue"}, {weight, 1.0}]` in all the cluster.
+
+For this reason, if an application needs to update process' metadata, `strict_mode` needs to be turned on.
+When enabled, only a process can perform Registry and Process Groups operations for itself.
+This basically means that the `Pid` parameter of most methods must be `self()`.
+
+This is a global setting that cannot be specified on a per-scope basis. The same setting SHOULD be set
+on every Erlang cluster node running Syn.
+
+#### Elixir
+With `strict_mode` turned off (the default):
+
+```elixir
+iex> pid = spawn(fn -> receive do _ -> :ok end end)
+#PID<0.108.0>
+iex> :syn.register(:users, "hedy", pid).
+:ok
+iex> :syn.register(:users, "hedy", pid).
+ok
+iex> :syn.register(:users, "hedy", pid, :new_metadata).
+{:error, :non_strict_update}
+```
+
+With `strict_mode` turned on:
+
+```elixir
+iex> pid = spawn(fn -> receive do _ -> :ok end end)
+#PID<0.108.0>
+iex> :syn.register(:users, "hedy", pid).
+{:error, :not_self}
+iex> :syn.register(:users, "hedy", self()).
+:ok
+iex> :syn.register(:users, "hedy", self(), :new_metadata).
+:ok
+```
+
+#### Erlang
+With `strict_mode` turned off (the default):
+
+```erlang
+1> Pid = spawn(fun() -> receive _ -> ok end end).
+<0.83.0>
+2> syn:register(users, "hedy", Pid).
+ok
+3> syn:register(users, "hedy", Pid).
+ok
+4> syn:register(users, "hedy", Pid, new_metadata).
+{error, non_strict_update}
+```
+
+With `strict_mode` turned on:
+
+```erlang
+1> Pid = spawn(fun() -> receive _ -> ok end end).
+<0.83.0>
+2> syn:register(users, "hedy", Pid).
+{error, not_self}
+3> syn:register(users, "hedy", self()).
+ok
+4> syn:register(users, "hedy", self(), new_metadata).
+ok
+```
+
+`strict_mode` can be turned on by setting the `strict_mode` configuration variable.
+
+#### Elixir
+```elixir
+config :syn,
+  strict_mode: true
+```
+
+#### Erlang
+```erlang
+{syn, [
+  {strict_mode, true}
+]}
+```

+ 1 - 1
docs.config

@@ -1,4 +1,4 @@
 {source_url, <<"https://github.com/ostinelli/syn">>}.
 {source_url, <<"https://github.com/ostinelli/syn">>}.
-{extras, [<<"README.md">>, <<"INTERNALS.md">>]}.
+{extras, [<<"README.md">>, <<"OPTIONS.md">>, <<"INTERNALS.md">>]}.
 {main, <<"readme">>}.
 {main, <<"readme">>}.
 {proglang, erlang}.
 {proglang, erlang}.

+ 22 - 8
src/syn.erl

@@ -286,15 +286,22 @@ register(Scope, Name, Pid) ->
 
 
 %% @doc Registers a process with metadata in the specified Scope.
 %% @doc Registers a process with metadata in the specified Scope.
 %%
 %%
+%% You may re-register a process multiple times, and you may also register the same process with different names.
+%%
+%% To update a process' metadata, <a href="options.html#strict_mode">`strict_mode'</a> must be enabled.
+%%
+%% When a process gets registered, Syn will automatically monitor it.
+%%
 %% Possible error reasons:
 %% Possible error reasons:
 %% <ul>
 %% <ul>
-%% <li>`taken': name is already registered with another `pid()'.</li>
 %% <li>`not_alive': The `pid()' being registered is not alive.</li>
 %% <li>`not_alive': The `pid()' being registered is not alive.</li>
+%% <li>`taken': name is already registered with another `pid()'.</li>
+%% <li>`non_strict_update': process is already registered with different metadata,
+%% but <a href="options.html#strict_mode">`strict_mode'</a> is not enabled.</li>
+%% <li>`not_self': the method is called from a process other than `self()',
+%% but <a href="options.html#strict_mode">`strict_mode'</a> is enabled.</li>
 %% </ul>
 %% </ul>
 %%
 %%
-%% You may re-register a process multiple times, for example if you need to update its metadata.
-%% When a process gets registered, Syn will automatically monitor it. You may also register the same process with different names.
-%%
 %% <h2>Examples</h2>
 %% <h2>Examples</h2>
 %% <h3>Elixir</h3>
 %% <h3>Elixir</h3>
 %% ```
 %% ```
@@ -462,15 +469,22 @@ join(Scope, GroupName, Pid) ->
 
 
 %% @doc Adds a `pid()' with metadata to GroupName in the specified Scope.
 %% @doc Adds a `pid()' with metadata to GroupName in the specified Scope.
 %%
 %%
+%% A process can join multiple groups. A process may join the same group multiple times, though it will still be
+%% listed only once in it.
+%%
+%% To update a process' metadata, <a href="options.html#strict_mode">`strict_mode'</a> must be enabled.
+%%
+%% When a process joins a group, Syn will automatically monitor it.
+%%
 %% Possible error reasons:
 %% Possible error reasons:
 %% <ul>
 %% <ul>
 %% <li>`not_alive': The `pid()' being added is not alive.</li>
 %% <li>`not_alive': The `pid()' being added is not alive.</li>
+%% <li>`non_strict_update': process is already in group with different metadata,
+%% but <a href="options.html#strict_mode">`strict_mode'</a> is not enabled.</li>
+%% <li>`not_self': the method is called from a process other than `self()',
+%% but <a href="options.html#strict_mode">`strict_mode'</a> is enabled.</li>
 %% </ul>
 %% </ul>
 %%
 %%
-%% A process can join multiple groups. When a process joins a group, Syn will automatically monitor it.
-%% A process may join the same group multiple times, for example if you need to update its metadata,
-%% though it will still be listed only once in it.
-%%
 %% <h2>Examples</h2>
 %% <h2>Examples</h2>
 %% <h3>Elixir</h3>
 %% <h3>Elixir</h3>
 %% ```
 %% ```

+ 5 - 0
src/syn_backbone.erl

@@ -32,6 +32,7 @@
 -export([create_tables_for_scope/1]).
 -export([create_tables_for_scope/1]).
 -export([get_table_name/2]).
 -export([get_table_name/2]).
 -export([save_process_name/2, get_process_name/1]).
 -export([save_process_name/2, get_process_name/1]).
+-export([is_strict_mode/0]).
 
 
 %% gen_server callbacks
 %% gen_server callbacks
 -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
 -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
@@ -75,6 +76,10 @@ get_table_name(TableId, Scope) ->
         [] -> undefined
         [] -> undefined
     end.
     end.
 
 
+-spec is_strict_mode() -> boolean().
+is_strict_mode() ->
+    application:get_env(syn, strict_mode, false).
+
 %% ===================================================================
 %% ===================================================================
 %% Callbacks
 %% Callbacks
 %% ===================================================================
 %% ===================================================================

+ 3 - 0
src/syn_event_handler.erl

@@ -37,6 +37,9 @@
 %% All of the callbacks, except for `resolve_registry_conflict/4', are called on all the nodes of the cluster.
 %% All of the callbacks, except for `resolve_registry_conflict/4', are called on all the nodes of the cluster.
 %% This allows you to receive events for the processes running on nodes that get shut down, or in case of net splits.
 %% This allows you to receive events for the processes running on nodes that get shut down, or in case of net splits.
 %%
 %%
+%% For the `on_registry_process_updated/5' and `on_group_process_updated/5' callbacks to be called,
+%% <a href="options.html#strict_mode">`strict_mode'</a> must be enabled.
+%%
 %% The argument `Reason' in the callbacks can be:
 %% The argument `Reason' in the callbacks can be:
 %% <ul>
 %% <ul>
 %% <li> `normal' for expected operations.</li>
 %% <li> `normal' for expected operations.</li>

+ 27 - 14
src/syn_pg.erl

@@ -128,18 +128,24 @@ is_local_member(Scope, GroupName, Pid) ->
 
 
 -spec join(Scope :: atom(), GroupName :: term(), Pid :: pid(), Meta :: term()) -> ok.
 -spec join(Scope :: atom(), GroupName :: term(), Pid :: pid(), Meta :: term()) -> ok.
 join(Scope, GroupName, Pid, Meta) ->
 join(Scope, GroupName, Pid, Meta) ->
-    Node = node(Pid),
-    case syn_gen_scope:call(?MODULE, Node, Scope, {'3.0', join_on_node, node(), GroupName, Pid, Meta}) of
-        {ok, {CallbackMethod, Time, TableByName, TableByPid}} when Node =/= node() ->
-            %% update table on caller node immediately so that subsequent calls have an updated pg
-            add_to_local_table(GroupName, Pid, Meta, Time, undefined, TableByName, TableByPid),
-            %% callback
-            syn_event_handler:call_event_handler(CallbackMethod, [Scope, GroupName, Pid, Meta, normal]),
-            %% return
-            ok;
+    case syn_backbone:is_strict_mode() of
+        true when Pid =/= self() ->
+            {error, not_self};
 
 
-        {Response, _} ->
-            Response
+        _ ->
+            Node = node(Pid),
+            case syn_gen_scope:call(?MODULE, Node, Scope, {'3.0', join_on_node, node(), GroupName, Pid, Meta}) of
+                {ok, {CallbackMethod, Time, TableByName, TableByPid}} when Node =/= node() ->
+                    %% update table on caller node immediately so that subsequent calls have an updated pg
+                    add_to_local_table(GroupName, Pid, Meta, Time, undefined, TableByName, TableByPid),
+                    %% callback
+                    syn_event_handler:call_event_handler(CallbackMethod, [Scope, GroupName, Pid, Meta, normal]),
+                    %% return
+                    ok;
+
+                {Response, _} ->
+                    Response
+            end
     end.
     end.
 
 
 -spec leave(Scope :: atom(), GroupName :: term(), Pid :: pid()) -> ok | {error, Reason :: term()}.
 -spec leave(Scope :: atom(), GroupName :: term(), Pid :: pid()) -> ok | {error, Reason :: term()}.
@@ -279,12 +285,19 @@ handle_call({'3.0', join_on_node, RequesterNode, GroupName, Pid, Meta}, _From, #
                     end,
                     end,
                     do_join_on_node(GroupName, Pid, Meta, MRef, normal, RequesterNode, on_process_joined, State);
                     do_join_on_node(GroupName, Pid, Meta, MRef, normal, RequesterNode, on_process_joined, State);
 
 
-                {{_, Meta}, _, _, _, _} ->
+                {{_, _}, Meta, _, _, _} ->
                     %% re-joined with same meta
                     %% re-joined with same meta
-                    {ok, noop};
+                    {reply, {ok, noop}, State};
 
 
                 {{_, _}, _, _, MRef, _} ->
                 {{_, _}, _, _, MRef, _} ->
-                    do_join_on_node(GroupName, Pid, Meta, MRef, normal, RequesterNode, on_group_process_updated, State)
+                    %% re-joined with different meta
+                    case syn_backbone:is_strict_mode() of
+                        true ->
+                            do_join_on_node(GroupName, Pid, Meta, MRef, normal, RequesterNode, on_group_process_updated, State);
+
+                        false ->
+                            {reply, {{error, non_strict_update}, undefined}, State}
+                    end
             end;
             end;
 
 
         false ->
         false ->

+ 25 - 12
src/syn_registry.erl

@@ -83,18 +83,24 @@ lookup(Scope, Name) ->
 
 
 -spec register(Scope :: atom(), Name :: term(), Pid :: pid(), Meta :: term()) -> ok | {error, Reason :: term()}.
 -spec register(Scope :: atom(), Name :: term(), Pid :: pid(), Meta :: term()) -> ok | {error, Reason :: term()}.
 register(Scope, Name, Pid, Meta) ->
 register(Scope, Name, Pid, Meta) ->
-    Node = node(Pid),
-    case syn_gen_scope:call(?MODULE, Node, Scope, {'3.0', register_on_node, node(), Name, Pid, Meta}) of
-        {ok, {CallbackMethod, Time, TableByName, TableByPid}} when Node =/= node() ->
-            %% update table on caller node immediately so that subsequent calls have an updated registry
-            add_to_local_table(Name, Pid, Meta, Time, undefined, TableByName, TableByPid),
-            %% callback
-            syn_event_handler:call_event_handler(CallbackMethod, [Scope, Name, Pid, Meta, normal]),
-            %% return
-            ok;
+    case syn_backbone:is_strict_mode() of
+        true when Pid =/= self() ->
+            {error, not_self};
 
 
-        {Response, _} ->
-            Response
+        _ ->
+            Node = node(Pid),
+            case syn_gen_scope:call(?MODULE, Node, Scope, {'3.0', register_on_node, node(), Name, Pid, Meta}) of
+                {ok, {CallbackMethod, Time, TableByName, TableByPid}} when Node =/= node() ->
+                    %% update table on caller node immediately so that subsequent calls have an updated registry
+                    add_to_local_table(Name, Pid, Meta, Time, undefined, TableByName, TableByPid),
+                    %% callback
+                    syn_event_handler:call_event_handler(CallbackMethod, [Scope, Name, Pid, Meta, normal]),
+                    %% return
+                    ok;
+
+                {Response, _} ->
+                    Response
+            end
     end.
     end.
 
 
 -spec unregister(Scope :: atom(), Name :: term()) -> ok | {error, Reason :: term()}.
 -spec unregister(Scope :: atom(), Name :: term()) -> ok | {error, Reason :: term()}.
@@ -198,7 +204,14 @@ handle_call({'3.0', register_on_node, RequesterNode, Name, Pid, Meta}, _From, #s
                     {reply, {ok, noop}, State};
                     {reply, {ok, noop}, State};
 
 
                 {Name, Pid, _, _, MRef, _} ->
                 {Name, Pid, _, _, MRef, _} ->
-                    do_register_on_node(Name, Pid, Meta, MRef, normal, RequesterNode, on_registry_process_updated, State);
+                    %% same pid, different meta
+                    case syn_backbone:is_strict_mode() of
+                        true ->
+                            do_register_on_node(Name, Pid, Meta, MRef, normal, RequesterNode, on_registry_process_updated, State);
+
+                        false ->
+                            {reply, {{error, non_strict_update}, undefined}, State}
+                    end;
 
 
                 _ ->
                 _ ->
                     {reply, {{error, taken}, undefined}, State}
                     {reply, {{error, taken}, undefined}, State}

+ 69 - 37
test/syn_pg_SUITE.erl

@@ -33,6 +33,9 @@
 
 
 %% tests
 %% tests
 -export([
 -export([
+    one_node_strict_mode/1
+]).
+-export([
     three_nodes_discover/1,
     three_nodes_discover/1,
     three_nodes_join_leave_and_monitor/1,
     three_nodes_join_leave_and_monitor/1,
     three_nodes_join_filter_unknown_node/1,
     three_nodes_join_filter_unknown_node/1,
@@ -69,6 +72,7 @@
 %% -------------------------------------------------------------------
 %% -------------------------------------------------------------------
 all() ->
 all() ->
     [
     [
+        {group, one_node_pg},
         {group, three_nodes_pg},
         {group, three_nodes_pg},
         {group, four_nodes_pg}
         {group, four_nodes_pg}
     ].
     ].
@@ -87,6 +91,9 @@ all() ->
 %% -------------------------------------------------------------------
 %% -------------------------------------------------------------------
 groups() ->
 groups() ->
     [
     [
+        {one_node_pg, [shuffle], [
+            one_node_strict_mode
+        ]},
         {three_nodes_pg, [shuffle], [
         {three_nodes_pg, [shuffle], [
             three_nodes_discover,
             three_nodes_discover,
             three_nodes_join_leave_and_monitor,
             three_nodes_join_leave_and_monitor,
@@ -185,6 +192,33 @@ end_per_testcase(_, _Config) ->
 %% ===================================================================
 %% ===================================================================
 %% Tests
 %% Tests
 %% ===================================================================
 %% ===================================================================
+one_node_strict_mode(_Config) ->
+    %% start syn
+    ok = syn:start(),
+    syn:add_node_to_scopes([scope]),
+
+    %% start process
+    Pid = syn_test_suite_helper:start_process(),
+
+    %% strict mode disabled
+    ok = syn:join(scope, "strict-false", Pid, metadata),
+    ok = syn:join(scope, "strict-false", Pid, metadata),
+    {error, non_strict_update} = syn:join(scope, "strict-false", Pid, new_metadata),
+    ok = syn:join(scope, "strict-false", Pid, metadata),
+    [{Pid, metadata}] = syn:members(scope, "strict-false"),
+    ok = syn:leave(scope, "strict-false", Pid),
+
+    %% strict mode enabled
+    application:set_env(syn, strict_mode, true),
+    {error, not_self} = syn:join(scope, "strict-true", Pid, metadata),
+    Self = self(),
+    ok = syn:join(scope, "strict-true", Self, metadata),
+    ok = syn:join(scope, "strict-true", Self, new_metadata),
+    [{Self, new_metadata}] = syn:members(scope, "strict-true"),
+    ok = syn:join(scope, "strict-true", Self),
+    [{Self, undefined}] = syn:members(scope, "strict-true"),
+    ok = syn:leave(scope, "strict-true", Self).
+
 three_nodes_discover(Config) ->
 three_nodes_discover(Config) ->
     %% get slaves
     %% get slaves
     SlaveNode1 = proplists:get_value(syn_slave_1, Config),
     SlaveNode1 = proplists:get_value(syn_slave_1, Config),
@@ -478,9 +512,13 @@ three_nodes_join_leave_and_monitor(Config) ->
     1 = rpc:call(SlaveNode2, syn, group_count, [scope_bc, SlaveNode1]),
     1 = rpc:call(SlaveNode2, syn, group_count, [scope_bc, SlaveNode1]),
     0 = rpc:call(SlaveNode2, syn, group_count, [scope_bc, SlaveNode2]),
     0 = rpc:call(SlaveNode2, syn, group_count, [scope_bc, SlaveNode2]),
 
 
-    %% re-join to edit meta
-    ok = syn:join(scope_ab, {group, "one"}, PidWithMeta, <<"with updated meta">>),
-    ok = rpc:call(SlaveNode2, syn, join, [scope_bc, {group, "two"}, PidRemoteOn1, added_meta]), %% updated on slave 2
+    %% enable strict to allow for updates
+    application:set_env(syn, strict_mode, true),
+    rpc:call(SlaveNode1, application, set_env, [syn, strict_mode, true]),
+
+    %% re-join to edit meta (strict is enabled, so send message)
+    PidWithMeta ! {pg_update_meta, scope_ab, {group, "one"}, <<"with updated meta">>},
+    PidRemoteOn1 ! {pg_update_meta, scope_bc, {group, "two"}, added_meta},
 
 
     %% retrieve
     %% retrieve
     syn_test_suite_helper:assert_wait(
     syn_test_suite_helper:assert_wait(
@@ -568,7 +606,11 @@ three_nodes_join_leave_and_monitor(Config) ->
     1 = rpc:call(SlaveNode2, syn, group_count, [scope_bc, SlaveNode1]),
     1 = rpc:call(SlaveNode2, syn, group_count, [scope_bc, SlaveNode1]),
     0 = rpc:call(SlaveNode2, syn, group_count, [scope_bc, SlaveNode2]),
     0 = rpc:call(SlaveNode2, syn, group_count, [scope_bc, SlaveNode2]),
 
 
-    syn:join(scope_ab, {group, "two"}, PidRemoteOn1),
+    %% disable strict
+    application:set_env(syn, strict_mode, false),
+    rpc:call(SlaveNode1, application, set_env, [syn, strict_mode, false]),
+
+    ok = syn:join(scope_ab, {group, "two"}, PidRemoteOn1),
     syn_test_suite_helper:assert_wait(
     syn_test_suite_helper:assert_wait(
         lists:sort([{Pid, undefined}, {PidWithMeta, "with-meta-2"}, {PidRemoteOn1, undefined}]),
         lists:sort([{Pid, undefined}, {PidWithMeta, "with-meta-2"}, {PidRemoteOn1, undefined}]),
         fun() -> lists:sort(syn:members(scope_ab, {group, "two"})) end
         fun() -> lists:sort(syn:members(scope_ab, {group, "two"})) end
@@ -1123,8 +1165,11 @@ three_nodes_custom_event_handler_joined_left(Config) ->
         {on_process_joined, SlaveNode2, scope_all, "my-group", Pid2, <<"meta-for-2">>, normal}
         {on_process_joined, SlaveNode2, scope_all, "my-group", Pid2, <<"meta-for-2">>, normal}
     ]),
     ]),
 
 
-    %% ---> on meta update
-    ok = syn:join(scope_all, "my-group", Pid, {recipient, self(), <<"new-meta-0">>}),
+    %% enable strict to allow for updates
+    application:set_env(syn, strict_mode, true),
+
+    %% ---> on meta update (strict is enabled, so send message)
+    Pid ! {pg_update_meta, scope_all, "my-group", {recipient, self(), <<"new-meta-0">>}},
 
 
     %% check callbacks called
     %% check callbacks called
     syn_test_suite_helper:assert_received_messages([
     syn_test_suite_helper:assert_received_messages([
@@ -1133,24 +1178,17 @@ three_nodes_custom_event_handler_joined_left(Config) ->
         {on_group_process_updated, SlaveNode2, scope_all, "my-group", Pid, <<"new-meta-0">>, normal}
         {on_group_process_updated, SlaveNode2, scope_all, "my-group", Pid, <<"new-meta-0">>, normal}
     ]),
     ]),
 
 
-    %% update meta from another node
-    ok = rpc:call(SlaveNode1, syn, join, [scope_all, "my-group", Pid, {recipient, self(), <<"new-meta">>}]),
-
-    %% check callbacks called
-    syn_test_suite_helper:assert_received_messages([
-        {on_group_process_updated, LocalNode, scope_all, "my-group", Pid, <<"new-meta">>, normal},
-        {on_group_process_updated, SlaveNode1, scope_all, "my-group", Pid, <<"new-meta">>, normal},
-        {on_group_process_updated, SlaveNode2, scope_all, "my-group", Pid, <<"new-meta">>, normal}
-    ]),
+    %% disable strict
+    application:set_env(syn, strict_mode, false),
 
 
     %% ---> on left
     %% ---> on left
     ok = syn:leave(scope_all, "my-group", Pid),
     ok = syn:leave(scope_all, "my-group", Pid),
 
 
     %% check callbacks called
     %% check callbacks called
     syn_test_suite_helper:assert_received_messages([
     syn_test_suite_helper:assert_received_messages([
-        {on_process_left, LocalNode, scope_all, "my-group", Pid, <<"new-meta">>, normal},
-        {on_process_left, SlaveNode1, scope_all, "my-group", Pid, <<"new-meta">>, normal},
-        {on_process_left, SlaveNode2, scope_all, "my-group", Pid, <<"new-meta">>, normal}
+        {on_process_left, LocalNode, scope_all, "my-group", Pid, <<"new-meta-0">>, normal},
+        {on_process_left, SlaveNode1, scope_all, "my-group", Pid, <<"new-meta-0">>, normal},
+        {on_process_left, SlaveNode2, scope_all, "my-group", Pid, <<"new-meta-0">>, normal}
     ]),
     ]),
 
 
     %% leave from another node
     %% leave from another node
@@ -1169,7 +1207,7 @@ three_nodes_custom_event_handler_joined_left(Config) ->
 
 
     %% ---> after a netsplit
     %% ---> after a netsplit
     PidRemoteOn1 = syn_test_suite_helper:start_process(SlaveNode1),
     PidRemoteOn1 = syn_test_suite_helper:start_process(SlaveNode1),
-    syn:join(scope_all, remote_on_1, PidRemoteOn1, {recipient, self(), <<"netsplit">>}),
+    ok = syn:join(scope_all, remote_on_1, PidRemoteOn1, {recipient, self(), <<"netsplit">>}),
 
 
     %% check callbacks called
     %% check callbacks called
     syn_test_suite_helper:assert_received_messages([
     syn_test_suite_helper:assert_received_messages([
@@ -1221,7 +1259,7 @@ three_nodes_custom_event_handler_joined_left(Config) ->
 
 
     %% ---> call if process died during the scope process crash
     %% ---> call if process died during the scope process crash
     TransientPid = syn_test_suite_helper:start_process(),
     TransientPid = syn_test_suite_helper:start_process(),
-    syn:join(scope_all, "transient-group", TransientPid, {recipient, self(), "transient-meta"}),
+    ok = syn:join(scope_all, "transient-group", TransientPid, {recipient, self(), "transient-meta"}),
 
 
     %% check callbacks called
     %% check callbacks called
     syn_test_suite_helper:assert_received_messages([
     syn_test_suite_helper:assert_received_messages([
@@ -1631,6 +1669,12 @@ four_nodes_concurrency(Config) ->
     ok = rpc:call(SlaveNode2, syn, start, []),
     ok = rpc:call(SlaveNode2, syn, start, []),
     ok = rpc:call(SlaveNode3, syn, start, []),
     ok = rpc:call(SlaveNode3, syn, start, []),
 
 
+    %% enable strict to allow for updates
+    application:set_env(syn, strict_mode, true),
+    rpc:call(SlaveNode1, application, set_env, [syn, strict_mode, true]),
+    rpc:call(SlaveNode2, application, set_env, [syn, strict_mode, true]),
+    rpc:call(SlaveNode3, application, set_env, [syn, strict_mode, true]),
+
     %% add scopes
     %% add scopes
     ok = syn:add_node_to_scopes([scope_all]),
     ok = syn:add_node_to_scopes([scope_all]),
     ok = rpc:call(SlaveNode1, syn, add_node_to_scopes, [[scope_all]]),
     ok = rpc:call(SlaveNode1, syn, add_node_to_scopes, [[scope_all]]),
@@ -1641,30 +1685,16 @@ four_nodes_concurrency(Config) ->
     TestPid = self(),
     TestPid = self(),
     Iterations = 250,
     Iterations = 250,
 
 
-    %% pids
-    PidLocal = syn_test_suite_helper:start_process(),
-    PidOn1 = syn_test_suite_helper:start_process(SlaveNode1),
-    PidOn2 = syn_test_suite_helper:start_process(SlaveNode2),
-    PidOn3 = syn_test_suite_helper:start_process(SlaveNode3),
-
-    LocalNode = node(),
-    PidMap = #{
-        LocalNode => PidLocal,
-        SlaveNode1 => PidOn1,
-        SlaveNode2 => PidOn2,
-        SlaveNode3 => PidOn3
-    },
-
     %% concurrent test
     %% concurrent test
     WorkerFun = fun() ->
     WorkerFun = fun() ->
-        Pid = maps:get(node(), PidMap),
+        Self = self(),
         lists:foreach(fun(_) ->
         lists:foreach(fun(_) ->
             %% loop
             %% loop
             RandomMeta = rand:uniform(99999),
             RandomMeta = rand:uniform(99999),
-            ok = syn:join(scope_all, <<"concurrent-scope">>, Pid, RandomMeta),
+            ok = syn:join(scope_all, <<"concurrent-scope">>, Self, RandomMeta),
             %% random leave
             %% random leave
             case rand:uniform(5) of
             case rand:uniform(5) of
-                1 -> syn:leave(scope_all, <<"concurrent-scope">>, Pid);
+                1 -> syn:leave(scope_all, <<"concurrent-scope">>, Self);
                 _ -> ok
                 _ -> ok
             end,
             end,
             %% random sleep
             %% random sleep
@@ -1674,6 +1704,8 @@ four_nodes_concurrency(Config) ->
         TestPid ! {done, node()}
         TestPid ! {done, node()}
     end,
     end,
 
 
+    LocalNode = node(),
+
     %% spawn concurrent
     %% spawn concurrent
     spawn(LocalNode, WorkerFun),
     spawn(LocalNode, WorkerFun),
     spawn(SlaveNode1, WorkerFun),
     spawn(SlaveNode1, WorkerFun),

+ 52 - 22
test/syn_registry_SUITE.erl

@@ -33,7 +33,8 @@
 
 
 %% tests
 %% tests
 -export([
 -export([
-    one_node_via_register_unregister/1
+    one_node_via_register_unregister/1,
+    one_node_strict_mode/1
 ]).
 ]).
 -export([
 -export([
     three_nodes_discover/1,
     three_nodes_discover/1,
@@ -85,7 +86,8 @@ all() ->
 groups() ->
 groups() ->
     [
     [
         {one_node_registry, [shuffle], [
         {one_node_registry, [shuffle], [
-            one_node_via_register_unregister
+            one_node_via_register_unregister,
+            one_node_strict_mode
         ]},
         ]},
         {three_nodes_registry, [shuffle], [
         {three_nodes_registry, [shuffle], [
             three_nodes_discover,
             three_nodes_discover,
@@ -215,6 +217,31 @@ one_node_via_register_unregister(_Config) ->
     %% send via syn
     %% send via syn
     {badarg, {GenServerNameCustom, anything}} = (catch syn:send(GenServerNameCustom, anything)).
     {badarg, {GenServerNameCustom, anything}} = (catch syn:send(GenServerNameCustom, anything)).
 
 
+one_node_strict_mode(_Config) ->
+    %% start syn
+    ok = syn:start(),
+    syn:add_node_to_scopes([scope]),
+
+    %% start process
+    Pid = syn_test_suite_helper:start_process(),
+
+    %% strict mode disabled
+    ok = syn:register(scope, "strict-false", Pid, metadata),
+    ok = syn:register(scope, "strict-false", Pid, metadata),
+    {error, non_strict_update} = syn:register(scope, "strict-false", Pid, new_metadata),
+    {Pid, metadata} = syn:lookup(scope, "strict-false"),
+    ok = syn:register(scope, "strict-false", Pid, metadata),
+
+    %% strict mode enabled
+    application:set_env(syn, strict_mode, true),
+    {error, not_self} = syn:register(scope, "strict-true", Pid, metadata),
+    Self = self(),
+    ok = syn:register(scope, "strict-true", Self, metadata),
+    ok = syn:register(scope, "strict-true", Self, new_metadata),
+    {Self, new_metadata} = syn:lookup(scope, "strict-true"),
+    ok = syn:register(scope, "strict-true", Self),
+    {Self, undefined} = syn:lookup(scope, "strict-true").
+
 three_nodes_discover(Config) ->
 three_nodes_discover(Config) ->
     %% get slaves
     %% get slaves
     SlaveNode1 = proplists:get_value(syn_slave_1, Config),
     SlaveNode1 = proplists:get_value(syn_slave_1, Config),
@@ -427,8 +454,12 @@ three_nodes_register_unregister_and_monitor(Config) ->
     1 = rpc:call(SlaveNode2, syn, registry_count, [scope_bc, SlaveNode1]),
     1 = rpc:call(SlaveNode2, syn, registry_count, [scope_bc, SlaveNode1]),
     0 = rpc:call(SlaveNode2, syn, registry_count, [scope_bc, SlaveNode2]),
     0 = rpc:call(SlaveNode2, syn, registry_count, [scope_bc, SlaveNode2]),
 
 
-    %% re-register to edit meta
-    ok = syn:register(scope_ab, "scope_a_alias", PidWithMeta, <<"with_meta_updated">>),
+    %% enable strict to allow for updates
+    application:set_env(syn, strict_mode, true),
+
+    %% re-register to edit meta (strict is enabled, so send message)
+    PidWithMeta ! {registry_update_meta, scope_ab, "scope_a_alias", <<"with_meta_updated">>},
+
     syn_test_suite_helper:assert_wait(
     syn_test_suite_helper:assert_wait(
         {PidWithMeta, <<"with_meta_updated">>},
         {PidWithMeta, <<"with_meta_updated">>},
         fun() -> syn:lookup(scope_ab, "scope_a_alias") end
         fun() -> syn:lookup(scope_ab, "scope_a_alias") end
@@ -439,8 +470,11 @@ three_nodes_register_unregister_and_monitor(Config) ->
     ),
     ),
     {badrpc, {'EXIT', {{invalid_scope, scope_ab}, _}}} = (catch rpc:call(SlaveNode2, syn, lookup, [scope_ab, "scope_a_alias"])),
     {badrpc, {'EXIT', {{invalid_scope, scope_ab}, _}}} = (catch rpc:call(SlaveNode2, syn, lookup, [scope_ab, "scope_a_alias"])),
 
 
+    %% disable strict
+    application:set_env(syn, strict_mode, false),
+
     %% register remote
     %% register remote
-    syn:register(scope_ab, "ab_on_1", PidRemoteWithMetaOn1, <<"ab-on-1">>),
+    ok = syn:register(scope_ab, "ab_on_1", PidRemoteWithMetaOn1, <<"ab-on-1">>),
     syn_test_suite_helper:assert_wait(
     syn_test_suite_helper:assert_wait(
         {PidRemoteWithMetaOn1, <<"ab-on-1">>},
         {PidRemoteWithMetaOn1, <<"ab-on-1">>},
         fun() -> syn:lookup(scope_ab, "ab_on_1") end
         fun() -> syn:lookup(scope_ab, "ab_on_1") end
@@ -966,8 +1000,11 @@ three_nodes_custom_event_handler_reg_unreg(Config) ->
         {on_process_registered, SlaveNode2, scope_all, "proc-handler-2", Pid2, <<"meta-for-2">>, normal}
         {on_process_registered, SlaveNode2, scope_all, "proc-handler-2", Pid2, <<"meta-for-2">>, normal}
     ]),
     ]),
 
 
-    %% ---> on meta update
-    ok = syn:register(scope_all, "proc-handler", Pid, {recipient, self(), <<"new-meta">>}),
+    %% enable strict to allow for updates
+    application:set_env(syn, strict_mode, true),
+
+    %% ---> on meta update (strict is enabled, so send message)
+    Pid ! {registry_update_meta, scope_all, "proc-handler", {recipient, self(), <<"new-meta">>}},
 
 
     %% check callbacks called
     %% check callbacks called
     syn_test_suite_helper:assert_received_messages([
     syn_test_suite_helper:assert_received_messages([
@@ -976,16 +1013,6 @@ three_nodes_custom_event_handler_reg_unreg(Config) ->
         {on_registry_process_updated, SlaveNode2, scope_all, "proc-handler", Pid, <<"new-meta">>, normal}
         {on_registry_process_updated, SlaveNode2, scope_all, "proc-handler", Pid, <<"new-meta">>, normal}
     ]),
     ]),
 
 
-    %% meta update from another node
-    ok = rpc:call(SlaveNode1, syn, register, [scope_all, "proc-handler-2", Pid2, {recipient, self(), <<"meta-for-2-update">>}]),
-
-    %% check callbacks called
-    syn_test_suite_helper:assert_received_messages([
-        {on_registry_process_updated, LocalNode, scope_all, "proc-handler-2", Pid2, <<"meta-for-2-update">>, normal},
-        {on_registry_process_updated, SlaveNode1, scope_all, "proc-handler-2", Pid2, <<"meta-for-2-update">>, normal},
-        {on_registry_process_updated, SlaveNode2, scope_all, "proc-handler-2", Pid2, <<"meta-for-2-update">>, normal}
-    ]),
-
     %% ---> on unregister
     %% ---> on unregister
     ok = syn:unregister(scope_all, "proc-handler"),
     ok = syn:unregister(scope_all, "proc-handler"),
 
 
@@ -1001,9 +1028,9 @@ three_nodes_custom_event_handler_reg_unreg(Config) ->
 
 
     %% check callbacks called
     %% check callbacks called
     syn_test_suite_helper:assert_received_messages([
     syn_test_suite_helper:assert_received_messages([
-        {on_process_unregistered, LocalNode, scope_all, "proc-handler-2", Pid2, <<"meta-for-2-update">>, normal},
-        {on_process_unregistered, SlaveNode1, scope_all, "proc-handler-2", Pid2, <<"meta-for-2-update">>, normal},
-        {on_process_unregistered, SlaveNode2, scope_all, "proc-handler-2", Pid2, <<"meta-for-2-update">>, normal}
+        {on_process_unregistered, LocalNode, scope_all, "proc-handler-2", Pid2, <<"meta-for-2">>, normal},
+        {on_process_unregistered, SlaveNode1, scope_all, "proc-handler-2", Pid2, <<"meta-for-2">>, normal},
+        {on_process_unregistered, SlaveNode2, scope_all, "proc-handler-2", Pid2, <<"meta-for-2">>, normal}
     ]),
     ]),
 
 
     %% clean & check
     %% clean & check
@@ -1011,9 +1038,12 @@ three_nodes_custom_event_handler_reg_unreg(Config) ->
     %% no messages
     %% no messages
     syn_test_suite_helper:assert_empty_queue(),
     syn_test_suite_helper:assert_empty_queue(),
 
 
+    %% disable strict
+    application:set_env(syn, strict_mode, false),
+
     %% ---> after a netsplit
     %% ---> after a netsplit
     PidRemoteOn1 = syn_test_suite_helper:start_process(SlaveNode1),
     PidRemoteOn1 = syn_test_suite_helper:start_process(SlaveNode1),
-    syn:register(scope_all, remote_on_1, PidRemoteOn1, {recipient, self(), <<"netsplit">>}),
+    ok = syn:register(scope_all, remote_on_1, PidRemoteOn1, {recipient, self(), <<"netsplit">>}),
 
 
     %% check callbacks called
     %% check callbacks called
     syn_test_suite_helper:assert_received_messages([
     syn_test_suite_helper:assert_received_messages([
@@ -1111,7 +1141,7 @@ three_nodes_custom_event_handler_reg_unreg(Config) ->
 
 
     %% ---> call if process died during the scope process crash
     %% ---> call if process died during the scope process crash
     TransientPid = syn_test_suite_helper:start_process(),
     TransientPid = syn_test_suite_helper:start_process(),
-    syn:register(scope_all, "transient-pid", TransientPid, {recipient, self(), "transient-meta"}),
+    ok = syn:register(scope_all, "transient-pid", TransientPid, {recipient, self(), "transient-meta"}),
 
 
     %% check callbacks called
     %% check callbacks called
     syn_test_suite_helper:assert_received_messages([
     syn_test_suite_helper:assert_received_messages([

+ 11 - 1
test/syn_test_suite_helper.erl

@@ -125,6 +125,7 @@ clean_after_test() ->
         rpc:call(Node, application, stop, [syn]),
         rpc:call(Node, application, stop, [syn]),
         %% clean env
         %% clean env
         rpc:call(Node, application, unset_env, [syn, event_handler]),
         rpc:call(Node, application, unset_env, [syn, event_handler]),
+        rpc:call(Node, application, unset_env, [syn, strict_mode]),
         %% messages
         %% messages
         flush_inbox()
         flush_inbox()
     end, Nodes).
     end, Nodes).
@@ -315,7 +316,16 @@ send_error_logger_to_disk() ->
 %% ===================================================================
 %% ===================================================================
 process_main() ->
 process_main() ->
     receive
     receive
-        _ -> process_main()
+        {registry_update_meta, Scope, Name, NewMeta} ->
+            ok = syn:register(Scope, Name, self(), NewMeta),
+            process_main();
+
+        {pg_update_meta, Scope, GroupName, NewMeta} ->
+            ok = syn:join(Scope, GroupName, self(), NewMeta),
+            process_main();
+
+        _ ->
+            process_main()
     end.
     end.
 
 
 do_assert_scope_subcluster(Type, Node, Scope, ExpectedNodes) ->
 do_assert_scope_subcluster(Type, Node, Scope, ExpectedNodes) ->