Roberto Ostinelli 3 лет назад
Родитель
Сommit
ec85cb9b64
10 измененных файлов с 784 добавлено и 299 удалено
  1. 1 1
      LICENSE.md
  2. 88 5
      README.md
  3. 2 2
      docs.config
  4. 1 1
      docs.sh
  5. 8 4
      rebar.config
  6. 339 30
      src/syn.erl
  7. 92 3
      src/syn_event_handler.erl
  8. 21 21
      src/syn_groups.erl
  9. 1 1
      src/syn_sup.erl
  10. 231 231
      test/syn_groups_SUITE.erl

+ 1 - 1
LICENSE.md

@@ -1,4 +1,4 @@
-# The MIT License (MIT)
+# License
 
 Copyright (c) 2015-2021 Roberto Ostinelli and Neato Robotics, Inc.
 

+ 88 - 5
README.md

@@ -1,7 +1,90 @@
-[![Build Status](https://travis-ci.com/ostinelli/syn.svg?branch=master)](https://travis-ci.com/ostinelli/syn) [![Hex pm](https://img.shields.io/hexpm/v/syn.svg)](https://hex.pm/packages/syn)
-
-# Syn (v3)
+# Syn
 **Syn** (short for _synonym_) is a global Process Registry and Process Group manager for Erlang and Elixir.
-Syn automatically manages addition / removal of nodes from the cluster, and is also able to recover from net splits.
+Syn automatically manages dynamic clusters (addition / removal of nodes), and is also able to recover from net splits.
+
+## Introduction
+
+### What is a Process Registry?
+A global Process Registry allows registering a process on all the nodes of a cluster with a single Key.
+Consider this the process equivalent of a DNS server: in the same way you can retrieve an IP address from a domain name,
+you can retrieve a process from its Key.
+
+Typical Use Case: registering on a system a process that handles a physical device (using its serial number).
+
+### What is a Process Group?
+A global Process Group is a named group which contains many processes, possibly running on different nodes.
+With the group Name, you can retrieve on any cluster node the list of these processes, or publish a message to all of them.
+This mechanism allows for Publish / Subscribe patterns.
+
+Typical Use Case: a chatroom.
+
+### What is Syn?
+Syn is a Process Registry and Process Group manager that has the following features:
+
+* Global Process Registry (i.e. a process is uniquely identified with a Key across all the nodes of a cluster).
+* Global Process Group manager (i.e. a group is uniquely identified with a Name across all the nodes of a cluster).
+* Any term can be used as Key and Name.
+* PubSub mechanism: messages can be published to all members of a Process Group (_globally_ on all the cluster or _locally_ on a single node). 
+* Sub-clusters by using Scopes allows great scalability.
+* Dynamically sized clusters (addition / removal of nodes is handled automatically).
+* Net Splits automatic resolution.
+* Fast writes.
+* Configurable callbacks.
+* Processes are automatically monitored and removed from the Process Registry and Process Groups if they die.
+
+## Notes
+In any distributed system you are faced with a consistency challenge, which is often resolved by having one master arbiter
+performing all write operations (chosen with a mechanism of [leader election](http://en.wikipedia.org/wiki/Leader_election)),
+or through [atomic transactions](http://en.wikipedia.org/wiki/Atomicity_(database_systems)).
+
+Syn was born for applications of the [IoT](http://en.wikipedia.org/wiki/Internet_of_Things) field. In this context,
+Keys used to identify a process are often the physical object's unique identifier (for instance, its serial or MAC address),
+and are therefore already defined and unique _before_ hitting the system. The consistency challenge is less of a problem in this case,
+since the likelihood of concurrent incoming requests that would register processes with the same Key is extremely low and, in most cases, acceptable.
+
+In addition, write speeds were a determining factor in the architecture of Syn.
+
+Therefore, Availability has been chosen over Consistency and Syn is [eventually consistent](http://en.wikipedia.org/wiki/Eventual_consistency).
+
+## Installation
+
+### For Elixir
+Add it to your deps:
+
+```elixir
+defp deps do
+  [{:syn, "~> 3.0"}]
+end
+```
+
+### For Erlang
+If you're using [rebar3](https://github.com/erlang/rebar3), add `syn` as a dependency in your project's `rebar.config` file:
+
+```erlang
+{deps, [
+  {syn, {git, "git://github.com/ostinelli/syn.git", {tag, "3.0.0"}}}
+]}.
+```
+Or, if you're using [Hex.pm](https://hex.pm/) as package manager (with the [rebar3_hex](https://github.com/hexpm/rebar3_hex) plugin):
+
+```erlang
+{deps, [
+  {syn, "3.0.0"}
+]}.
+```
+
+Ensure that `syn` is started with your application, for example by adding it in your `.app` file to the list of `applications`:
 
-V3 is a WIP.
+```erlang
+{application, my_app, [
+    %% ...
+    {applications, [
+        kernel,
+        stdlib,
+        sasl,
+        syn,
+        %% ...
+    ]},
+    %% ...
+]}.
+```

+ 2 - 2
docs.config

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

+ 1 - 1
docs.sh

@@ -11,5 +11,5 @@ rebar3 compile
 rebar3 as docs edoc
 version=3.0.0
 ex_doc "syn" $version "_build/default/lib/syn/ebin" \
-  --source-ref v${version} \
+  --source-ref ${version} \
   --config docs.config $@

+ 8 - 4
rebar.config

@@ -1,9 +1,13 @@
 {erl_opts, [debug_info]}.
 {deps, []}.
+
 {profiles, [
-    {docs, [{edoc_opts, [{preprocess, true},
-        {doclet, edoc_doclet_chunks},
-        {layout, edoc_layout_chunks},
-        {dir, "_build/default/lib/syn/doc"}]}
+    {docs, [
+        {edoc_opts, [
+            {preprocess, true},
+            {doclet, edoc_doclet_chunks},
+            {layout, edoc_layout_chunks},
+            {dir, "_build/default/lib/syn/doc"}]
+        }
     ]}
 ]}.

+ 339 - 30
src/syn.erl

@@ -23,6 +23,25 @@
 %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 %% THE SOFTWARE.
 %% ==========================================================================================================
+
+%% ===================================================================
+%% @doc `syn' exposes all of the global Process Registry and Process Group APIs.
+%%
+%% Syn implement Scopes. A Scope is a way to create a logical overlay network running on top of the Erlang distribution cluster.
+%% Nodes that belong to the same Scope will form a "sub-cluster", and will synchronize data between themselves and themselves only.
+%%
+%% This allows for improved scalability, as it is possible to divide an Erlang cluster into sub-clusters which
+%% hold specific portions of data.
+%%
+%% Every node in an Erlang cluster is automatically added to the Scope `default'. It is therefore not mandatory
+%% to use scopes, but it is advisable to do so when scalability is a concern.
+%%
+%% Please note that most of the methods documented here that allow to specify a Scope will raise a
+%% `error({invalid_scope, Scope})' if the local node has not been added to the specified Scope or if the Pids
+%% passed in as variables are running on a node that has not been added to the specified Scope.
+%%
+%% @end
+%% ===================================================================
 -module(syn).
 
 %% API
@@ -45,90 +64,254 @@
 -export([is_local_member/2, is_local_member/3]).
 -export([join/2, join/3, join/4]).
 -export([leave/2, leave/3]).
--export([group_count/0, group_count/1, group_count/2]).
--export([local_group_count/0, local_group_count/1]).
--export([group_names/0, group_names/1, group_names/2]).
--export([local_group_names/0, local_group_names/1]).
+-export([groups_count/0, groups_count/1, groups_count/2]).
+-export([local_groups_count/0, local_groups_count/1]).
+-export([groups_names/0, groups_names/1, groups_names/2]).
+-export([local_groups_names/0, local_groups_names/1]).
 -export([publish/2, publish/3]).
 -export([local_publish/2, local_publish/3]).
 -export([multi_call/2, multi_call/3, multi_call/4, multi_call_reply/2]).
 
-%% ===================================================================
+
 %% API
 %% ===================================================================
+%% @doc Starts Syn manually.
+%%
+%% In most cases Syn will be started as one of your application's dependencies,
+%% however you may use this helper method to start it manually.
 -spec start() -> ok.
 start() ->
     {ok, _} = application:ensure_all_started(syn),
     ok.
 
+%% @doc Stops Syn manually.
 -spec stop() -> ok | {error, Reason :: any()}.
 stop() ->
     application:stop(syn).
 
 %% ----- \/ scopes ---------------------------------------------------
+%% @doc Retrieves the Scopes that the node has been added to.
 -spec node_scopes() -> [atom()].
 node_scopes() ->
     syn_sup:node_scopes().
 
+%% @doc Add the local node to the specified Scope.
+%%
+%% <h2>Examples</h2>
+%% The following adds the local node to the scope "devices" and then register a process handling a device in that scope:
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.add_node_to_scope(:devices)
+%% :ok
+%% iex> :syn.register(:devices, "hedy", self())
+%% :ok
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:add_node_to_scope(devices).
+%% ok
+%% 2> syn:register(devices, "SN-123-456789", self()).
+%% ok
+%% '''
 -spec add_node_to_scope(Scope :: atom()) -> ok.
 add_node_to_scope(Scope) ->
     syn_sup:add_node_to_scope(Scope).
 
+%% @doc Add the local node to the specified Scopes.
 -spec add_node_to_scopes(Scopes :: [atom()]) -> ok.
 add_node_to_scopes(Scopes) ->
     lists:foreach(fun(Scope) ->
         syn_sup:add_node_to_scope(Scope)
     end, Scopes).
 
+%% @doc Sets the handler module.
+%%
+%% Please see {@link syn_event_handler} for information on callbacks.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.set_event_handler(MyCustomEventHandler)
+%% ok
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:set_event_handler(my_custom_event_handler).
+%% ok
+%% '''
 -spec set_event_handler(module()) -> ok.
 set_event_handler(Module) ->
     application:set_env(syn, event_handler, Module).
 
 %% ----- \/ registry -------------------------------------------------
+%% @doc Looks up a registry entry in the `default' scope.
+%%
+%% Same as `lookup(default, Name)'.
 -spec lookup(Name :: any()) -> {pid(), Meta :: any()} | undefined.
 lookup(Name) ->
     syn_registry:lookup(Name).
 
+%% @doc Looks up a registry entry in the specified Scope.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.lookup(:devices, "SN-123-456789")
+%% {#PID<0.105.0>, undefined}
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:lookup(devices, "SN-123-456789").
+%% {<0.79.0>, undefined}
+%% '''
 -spec lookup(Scope :: atom(), Name :: any()) -> {pid(), Meta :: any()} | undefined.
 lookup(Scope, Name) ->
     syn_registry:lookup(Scope, Name).
 
+%% @doc Registers a process in the `default' scope.
+%%
+%% Same as `register(default, Name, Pid, undefined)'.
 -spec register(Name :: any(), Pid :: pid()) -> ok | {error, Reason :: any()}.
 register(Name, Pid) ->
     syn_registry:register(Name, Pid).
 
+%% @doc Registers a process with metadata in the `default' scope OR with undefined metadata in the specified Scope.
+%%
+%% Same as `register(default, Name, Pid, Meta)' or `register(Scope, Name, Pid, undefined)'
+%% depending on the position of the `pid()' value.
 -spec register(NameOrScope :: any(), PidOrName :: any(), MetaOrPid :: any()) -> ok | {error, Reason :: any()}.
 register(NameOrScope, PidOrName, MetaOrPid) ->
     syn_registry:register(NameOrScope, PidOrName, MetaOrPid).
 
+%% @doc Registers a process with metadata in the specified Scope.
+%%
+%% Possible error reasons:
+%% <ul>
+%% <li>`taken': name is already registered with another `pid()'.</li>
+%% <li>`not_alive': The `pid()' being registered is not alive.</li>
+%% </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.
+%%
+%% Processes can also be registered as `gen_server' names, by usage of via-tuples. This way, you can use the `gen_server'
+%% API with these tuples without referring to the Pid directly.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.register(:devices, "SN-123-456789", self(), [meta: :one])
+%% :ok
+%% '''
+%% ```
+%% iex> tuple = {:via, :syn, <<"your process name">>}.
+%% :ok
+%% iex> GenServer.start_link(__MODULE__, [], name: tuple)
+%% {ok, #PID<0.105.0>}
+%% iex> GenServer.call(tuple, your_message).
+%% your_message
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:register(devices, "SN-123-456789", self(), [{meta, one}]).
+%% ok
+%% '''
+%% ```
+%% 1> Tuple = {via, syn, <<"your process name">>}.
+%% ok
+%% 2> gen_server:start_link(Tuple, your_module, []).
+%% {ok, <0.79.0>}
+%% 3> gen_server:call(Tuple, your_message).
+%% your_message
+%% '''
 -spec register(Scope :: atom(), Name :: any(), Pid :: pid(), Meta :: any()) -> ok | {error, Reason :: any()}.
 register(Scope, Name, Pid, Meta) ->
     syn_registry:register(Scope, Name, Pid, Meta).
 
+%% @doc Unregisters a process.
+%%
+%% Same as `unregister(default, Name)'.
 -spec unregister(Name :: any()) -> ok | {error, Reason :: any()}.
 unregister(Name) ->
     syn_registry:unregister(Name).
 
+%% @doc Unregisters a process.
+%%
+%% Possible error reasons:
+%% <ul>
+%% <li>`undefined': name is not registered.</li>
+%% <li>`race_condition': the local `pid()' does not correspond to the cluster value. This is a rare occasion.</li>
+%% </ul>
+%%
+%% You don't need to unregister names of processes that are about to die, since they are monitored by Syn
+%% and they will be removed automatically. If you manually unregister a process before it dies, the Syn callbacks will not be called.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.unregister(:devices, "SN-123-456789")
+%% :ok
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:unregister(devices, "SN-123-456789").
+%% ok
+%% '''
 -spec unregister(Scope :: atom(), Name :: any()) -> ok | {error, Reason :: any()}.
 unregister(Scope, Name) ->
     syn_registry:unregister(Scope, Name).
 
+%% @doc Returns the count of all registered processes for the `default' scope.
+%%
+%% Same as `registry_count(default)'.
 -spec registry_count() -> non_neg_integer().
 registry_count() ->
     syn_registry:count().
 
+%% @doc Returns the count of all registered processes for the specified Scope.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.registry_count(:devices)
+%% 512473
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:registry_count(devices).
+%% 512473
+%% '''
 -spec registry_count(Scope :: atom()) -> non_neg_integer().
 registry_count(Scope) ->
     syn_registry:count(Scope).
 
+%% @doc Returns the count of all registered processes for the specified Scope running on a node.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.registry_count(:devices, :"two@example.com")
+%% 128902
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:registry_count(devices, 'two@example.com').
+%% 128902
+%% '''
 -spec registry_count(Scope :: atom(), Node :: node()) -> non_neg_integer().
 registry_count(Scope, Node) ->
     syn_registry:count(Scope, Node).
 
+%% @doc Returns the count of all registered processes for the `default' scope running on the local node.
+%%
+%% Same as `registry_count(default, node())'.
 -spec local_registry_count() -> non_neg_integer().
 local_registry_count() ->
     syn_registry:local_count().
 
+%% @doc Returns the count of all registered processes for the specified Scope running on the local node.
+%%
+%% Same as `registry_count(Scope, node())'.
 -spec local_registry_count(Scope :: atom()) -> non_neg_integer().
 local_registry_count(Scope) ->
     syn_registry:local_count(Scope).
@@ -166,97 +349,223 @@ send(Name, Message) ->
     end.
 
 %% ----- \/ groups ---------------------------------------------------
+%% @doc Returns the list of all members for GroupName in the `default' Scope.
+%%
+%% Same as `members(default, GroupName)'.
 -spec members(GroupName :: term()) -> [{Pid :: pid(), Meta :: term()}].
 members(GroupName) ->
     syn_groups:members(GroupName).
 
+%% @doc Returns the list of all members for GroupName in the specified Scope.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.join(:devices, "area-1", self()).
+%% :ok
+%% iex> :syn.members(:devices, "area-1").
+%% [{#PID<0.105.0>, :undefined}]
+%%%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:join(devices, "area-1", self()).
+%% ok
+%% 2> syn:members(devices, "area-1").
+%% [{<0.69.0>, undefined}]
+%% '''
 -spec members(Scope :: atom(), GroupName :: term()) -> [{Pid :: pid(), Meta :: term()}].
 members(Scope, GroupName) ->
     syn_groups:members(Scope, GroupName).
 
+%% @doc Returns whether a `pid()' is a member of GroupName in the `default' scope.
+%%
+%% Same as `is_member(default, GroupName, Pid)'.
 -spec is_member(GroupName :: any(), Pid :: pid()) -> boolean().
 is_member(GroupName, Pid) ->
     syn_groups:is_member(GroupName, Pid).
 
+%% @doc Returns whether a `pid()' is a member of GroupName in the specified Scope.
+%%
+%% This method will raise a `error({invalid_scope, Scope})' if the node has not been added to the specified Scope.
 -spec is_member(Scope :: atom(), GroupName :: any(), Pid :: pid()) -> boolean().
 is_member(Scope, GroupName, Pid) ->
     syn_groups:is_member(Scope, GroupName, Pid).
 
+%% @doc Returns the list of all members for GroupName in the `default' scope running on the local node.
+%%
+%% Same as `local_members(default, GroupName)'.
 -spec local_members(GroupName :: term()) -> [{Pid :: pid(), Meta :: term()}].
 local_members(GroupName) ->
     syn_groups:local_members(GroupName).
 
+%% @doc Returns the list of all members for GroupName in the specified Scope running on the local node.
+%%
+%% This method will raise a `error({invalid_scope, Scope})' if the node has not been added to the specified Scope.
 -spec local_members(Scope :: atom(), GroupName :: term()) -> [{Pid :: pid(), Meta :: term()}].
 local_members(Scope, GroupName) ->
     syn_groups:local_members(Scope, GroupName).
 
+%% @doc Returns whether a `pid()' is a member of GroupName in the `default' scope running on the local node.
+%%
+%% Same as `is_local_member(default, GroupName, Pid)'.
 -spec is_local_member(GroupName :: any(), Pid :: pid()) -> boolean().
 is_local_member(GroupName, Pid) ->
     syn_groups:is_local_member(GroupName, Pid).
 
+%% @doc Returns whether a `pid()' is a member of GroupName in the specified Scope running on the local node.
+%%
+%% This method will raise a `error({invalid_scope, Scope})' if the node has not been added to the specified Scope.
 -spec is_local_member(Scope :: atom(), GroupName :: any(), Pid :: pid()) -> boolean().
 is_local_member(Scope, GroupName, Pid) ->
     syn_groups:is_local_member(Scope, GroupName, Pid).
 
+%% @doc Adds a `pid()' to GroupName in the `default' scope.
+%%
+%% Same as `join(default, GroupName, Pid)'.
 -spec join(GroupName :: any(), Pid :: pid()) -> ok | {error, Reason :: any()}.
 join(GroupName, Pid) ->
     syn_groups:join(GroupName, Pid).
 
+%% @doc Adds a `pid()' with metadata to GroupName in the `default' scope OR with undefined metadata in the specified Scope.
 -spec join(GroupNameOrScope :: any(), PidOrGroupName :: any(), MetaOrPid :: any()) -> ok | {error, Reason :: any()}.
 join(GroupNameOrScope, PidOrGroupName, MetaOrPid) ->
     syn_groups:join(GroupNameOrScope, PidOrGroupName, MetaOrPid).
 
+%% @doc Adds a `pid()' with metadata to GroupName in the specified Scope.
+%%
+%% Possible error reasons:
+%% <ul>
+%% <li>`not_alive': The `pid()' being added is not alive.</li>
+%% </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>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.join(:devices, "area-1", self(), [meta: :one]).
+%% :ok
+%%%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:join(devices, "area-1", self(), [{meta, one}]).
+%% ok
+%% '''
 -spec join(Scope :: atom(), GroupName :: any(), Pid :: pid(), Meta :: any()) -> ok | {error, Reason :: any()}.
 join(Scope, GroupName, Pid, Meta) ->
     syn_groups:join(Scope, GroupName, Pid, Meta).
 
+%% @doc Removes a `pid()' from GroupName in the `default' Scope.
+%%
+%% Same as `leave(default, GroupName, Pid)'.
 -spec leave(GroupName :: any(), Pid :: pid()) -> ok | {error, Reason :: any()}.
 leave(GroupName, Pid) ->
     syn_groups:leave(GroupName, Pid).
 
+%% @doc Removes a `pid()' from GroupName in the specified Scope.
+%%
+%% Possible error reasons:
+%% <ul>
+%% <li>`not_in_group': The `pid()' is not in GroupName for the specified Scope.</li>
+%% </ul>
+%%
+%% You don't need to remove processes that are about to die, since they are monitored by Syn and they will be removed
+%% automatically from their groups.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.leave(:devices, "area-1", self()).
+%% :ok
+%%%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:leave(devices, "area-1", self()).
+%% ok
+%% '''
 -spec leave(Scope :: atom(), GroupName :: any(), Pid :: pid()) -> ok | {error, Reason :: any()}.
 leave(Scope, GroupName, Pid) ->
     syn_groups:leave(Scope, GroupName, Pid).
 
--spec group_count() -> non_neg_integer().
-group_count() ->
+%% @doc Returns the count of all the groups for the `default' scope.
+%%
+%% Same as `groups_count(default)'.
+-spec groups_count() -> non_neg_integer().
+groups_count() ->
     syn_groups:count().
 
--spec group_count(Scope :: atom()) -> non_neg_integer().
-group_count(Scope) ->
+%% @doc Returns the count of all the groups for the specified Scope.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.groups_count(:devices)
+%% 321778
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:groups_count(devices).
+%% 321778
+%% '''
+-spec groups_count(Scope :: atom()) -> non_neg_integer().
+groups_count(Scope) ->
     syn_groups:count(Scope).
 
--spec group_count(Scope :: atom(), Node :: node()) -> non_neg_integer().
-group_count(Scope, Node) ->
+%% @doc Returns the count of all the groups for the specified Scope which have at least 1 process running
+%% on Node.
+%%
+%% <h2>Examples</h2>
+%% <h3>Elixir</h3>
+%% ```
+%% iex> :syn.groups_count(:devices, :"two@example.com")
+%% 15422
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% 1> syn:groups_count(devices, 'two@example.com').
+%% 15422
+%% '''
+-spec groups_count(Scope :: atom(), Node :: node()) -> non_neg_integer().
+groups_count(Scope, Node) ->
     syn_groups:count(Scope, Node).
 
--spec local_group_count() -> non_neg_integer().
-local_group_count() ->
+%% @doc Returns the count of all the groups for the `default' scope which have at least 1 process running
+%% on Node.
+%%
+%% Same as `groups_count(default, node())'.
+-spec local_groups_count() -> non_neg_integer().
+local_groups_count() ->
     syn_groups:local_count().
 
--spec local_group_count(Scope :: atom()) -> non_neg_integer().
-local_group_count(Scope) ->
+%% @doc Returns the count of all the groups for the specified Scope which have at least 1 process running
+%% on Node.
+%%
+%% Same as `groups_count(Scope, node())'.
+-spec local_groups_count(Scope :: atom()) -> non_neg_integer().
+local_groups_count(Scope) ->
     syn_groups:local_count(Scope).
 
--spec group_names() -> [GroupName :: term()].
-group_names() ->
-    syn_groups:group_names().
+-spec groups_names() -> [GroupName :: term()].
+groups_names() ->
+    syn_groups:groups_names().
 
--spec group_names(Scope :: atom()) -> [GroupName :: term()].
-group_names(Scope) ->
-    syn_groups:group_names(Scope).
+-spec groups_names(Scope :: atom()) -> [GroupName :: term()].
+groups_names(Scope) ->
+    syn_groups:groups_names(Scope).
 
--spec group_names(Scope :: atom(), Node :: node()) -> [GroupName :: term()].
-group_names(Scope, Node) ->
-    syn_groups:group_names(Scope, Node).
+-spec groups_names(Scope :: atom(), Node :: node()) -> [GroupName :: term()].
+groups_names(Scope, Node) ->
+    syn_groups:groups_names(Scope, Node).
 
--spec local_group_names() -> [GroupName :: term()].
-local_group_names() ->
-    syn_groups:local_group_names().
+-spec local_groups_names() -> [GroupName :: term()].
+local_groups_names() ->
+    syn_groups:local_groups_names().
 
--spec local_group_names(Scope :: atom()) -> [GroupName :: term()].
-local_group_names(Scope) ->
-    syn_groups:local_group_names(Scope).
+-spec local_groups_names(Scope :: atom()) -> [GroupName :: term()].
+local_groups_names(Scope) ->
+    syn_groups:local_groups_names(Scope).
 
 -spec publish(GroupName :: any(), Message :: any()) -> {ok, RecipientCount :: non_neg_integer()}.
 publish(GroupName, Message) ->

+ 92 - 3
src/syn_event_handler.erl

@@ -26,6 +26,95 @@
 %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 %% THE SOFTWARE.
 %% ==========================================================================================================
+
+%% ===================================================================
+%% @doc `syn_event_handler' defines Syn's callbacks.
+%%
+%% You can specify the callback module with {@link syn:set_event_handler/1}.
+%% 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.
+%%
+%% While all callbacks do not have a direct effect on Syn (their return value is ignored), a special case is the callback
+%% `resolve_registry_conflict/3'. If specified, this is the method that will be used to resolve registry conflicts when detected.
+%%
+%% In case of net splits or race conditions, a specific name might get registered simultaneously on two different nodes.
+%% When this happens, the cluster experiences a registry naming conflict.
+%%
+%% Syn will resolve this Process Registry conflict by choosing a single process. By default, Syn keeps track of the time
+%% when a registration takes place with {@link erlang:system_time/0},
+%% compares values between conflicting processes and:
+%% <ul>
+%% <li>Keeps the one with the higher value (the process that was registered more recently).</li>
+%% <li>Kills the other process by sending a kill signal with `exit(Pid, {syn_resolve_kill, Name, Meta})'.</li>
+%% </ul>
+%% This is a very simple mechanism that can be imprecise, as system clocks are not perfectly aligned in a cluster.
+%% If something more elaborate is desired you MAY specify a custom handler that implements the `resolve_registry_conflict/3' callback.
+%% To this effect, you may also store additional data to resolve conflicts in the `Meta' value, since it will be passed
+%% into the callback for both of the conflicting processes.
+%%
+%% If implemented, this method MUST return the `pid()' of the process that you wish to keep. The other process will be killed.
+%%
+%% 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.
+%%
+%% <h2>Examples</h2>
+%% The following callback module implements the `on_process_unregistered/4' and the `on_process_left/4' callbacks.
+%% <h3>Elixir</h3>
+%% ```
+%% defmodule MyCustomEventHandler do
+%%   ‎@behaviour :syn_event_handler
+%%
+%%   ‎@impl true
+%%   ‎@spec on_process_unregistered(
+%%     scope :: atom(),
+%%     name :: any(),
+%%     pid :: pid(),
+%%     meta :: any()
+%%   ) :: any()
+%%   def on_process_unregistered(scope, name, pid, meta) do
+%%   end
+%%
+%%   ‎@impl true
+%%   ‎@spec on_process_left(
+%%     scope :: atom(),
+%%     group_name :: any(),
+%%     pid :: pid(),
+%%     meta :: any()
+%%   ) :: any()
+%%   def on_process_left(scope, group_name, pid, meta) do
+%%   end
+%% end
+%% '''
+%% <h3>Erlang</h3>
+%% ```
+%% -module(my_custom_event_handler).
+%% -behaviour(syn_event_handler).
+%% -export([on_process_unregistered/4]).
+%% -export([on_group_process_exit/4]).
+%%
+%% -spec on_process_unregistered(
+%%     Scope :: atom(),
+%%     Name :: any(),
+%%     Pid :: pid(),
+%%     Meta :: any()
+%% ) -> any().
+%% on_process_unregistered(Scope, Name, Pid, Meta) ->
+%%     ok.
+%%
+%% -spec on_process_left(
+%%     Scope :: atom(),
+%%     GroupName :: any(),
+%%     Pid :: pid(),
+%%     Meta :: any()
+%% ) -> any().
+%% on_process_left(Scope, GroupName, Pid, Meta) ->
+%%     ok.
+%% '''
+%%
+%% @end
+%% ===================================================================
+
 -module(syn_event_handler).
 
 %% API
@@ -77,9 +166,9 @@
 
 -callback resolve_registry_conflict(
     Name :: any(),
-    {Pid1 :: pid(), Meta1 :: any()},
-    {Pid2 :: pid(), Meta2 :: any()}
-) -> PidToKeep :: pid() | undefined.
+    {Pid1 :: pid(), Meta1 :: any(), Time1 :: non_neg_integer()},
+    {Pid2 :: pid(), Meta2 :: any(), Time2 :: non_neg_integer()}
+) -> PidToKeep :: pid().
 
 -optional_callbacks([on_process_registered/4, on_registry_process_updated/4, on_process_unregistered/4]).
 -optional_callbacks([on_process_joined/4, on_group_process_updated/4, on_process_left/4]).

+ 21 - 21
src/syn_groups.erl

@@ -37,8 +37,8 @@
 -export([is_local_member/2, is_local_member/3]).
 -export([count/0, count/1, count/2]).
 -export([local_count/0, local_count/1]).
--export([group_names/0, group_names/1, group_names/2]).
--export([local_group_names/0, local_group_names/1]).
+-export([groups_names/0, groups_names/1, groups_names/2]).
+-export([local_groups_names/0, local_groups_names/1]).
 -export([publish/2, publish/3]).
 -export([local_publish/2, local_publish/3]).
 -export([multi_call/2, multi_call/3, multi_call/4, multi_call_reply/2]).
@@ -197,12 +197,12 @@ count() ->
 
 -spec count(Scope :: atom()) -> non_neg_integer().
 count(Scope) ->
-    Set = group_names_ordset(Scope, '_'),
+    Set = groups_names_ordset(Scope, '_'),
     ordsets:size(Set).
 
 -spec count(Scope :: atom(), Node :: node()) -> non_neg_integer().
 count(Scope, Node) ->
-    Set = group_names_ordset(Scope, Node),
+    Set = groups_names_ordset(Scope, Node),
     ordsets:size(Set).
 
 -spec local_count() -> non_neg_integer().
@@ -213,30 +213,30 @@ local_count() ->
 local_count(Scope) ->
     count(Scope, node()).
 
--spec group_names() -> [GroupName :: term()].
-group_names() ->
-    group_names(?DEFAULT_SCOPE).
+-spec groups_names() -> [GroupName :: term()].
+groups_names() ->
+    groups_names(?DEFAULT_SCOPE).
 
--spec group_names(Scope :: atom()) -> [GroupName :: term()].
-group_names(Scope) ->
-    Set = group_names_ordset(Scope, '_'),
+-spec groups_names(Scope :: atom()) -> [GroupName :: term()].
+groups_names(Scope) ->
+    Set = groups_names_ordset(Scope, '_'),
     ordsets:to_list(Set).
 
--spec group_names(Scope :: atom(), Node :: node()) -> [GroupName :: term()].
-group_names(Scope, Node) ->
-    Set = group_names_ordset(Scope, Node),
+-spec groups_names(Scope :: atom(), Node :: node()) -> [GroupName :: term()].
+groups_names(Scope, Node) ->
+    Set = groups_names_ordset(Scope, Node),
     ordsets:to_list(Set).
 
--spec local_group_names() -> [GroupName :: term()].
-local_group_names() ->
-    group_names(?DEFAULT_SCOPE, node()).
+-spec local_groups_names() -> [GroupName :: term()].
+local_groups_names() ->
+    groups_names(?DEFAULT_SCOPE, node()).
 
--spec local_group_names(Scope :: atom()) -> [GroupName :: term()].
-local_group_names(Scope) ->
-    group_names(Scope, node()).
+-spec local_groups_names(Scope :: atom()) -> [GroupName :: term()].
+local_groups_names(Scope) ->
+    groups_names(Scope, node()).
 
--spec group_names_ordset(Scope :: atom(), Node :: node()) -> ordsets:ordset(GroupName :: term()).
-group_names_ordset(Scope, NodeParam) ->
+-spec groups_names_ordset(Scope :: atom(), Node :: node()) -> ordsets:ordset(GroupName :: term()).
+groups_names_ordset(Scope, NodeParam) ->
     case syn_backbone:get_table_name(syn_groups_by_name, Scope) of
         undefined ->
             error({invalid_scope, Scope});

+ 1 - 1
src/syn_sup.erl

@@ -52,7 +52,7 @@ node_scopes() ->
     end.
 
 -spec add_node_to_scope(Scope :: atom()) -> ok.
-add_node_to_scope(Scope) ->
+add_node_to_scope(Scope) when is_atom(Scope) ->
     error_logger:info_msg("SYN[~s] Adding node to scope", [Scope]),
     %% save to ENV (failsafe if sup is restarted)
     CustomScopes0 = case application:get_env(syn, custom_scopes) of

+ 231 - 231
test/syn_groups_SUITE.erl

@@ -43,8 +43,8 @@
     three_nodes_publish_custom_scope/1,
     three_nodes_multi_call_default_scope/1,
     three_nodes_multi_call_custom_scope/1,
-    three_nodes_group_names_default_scope/1,
-    three_nodes_group_names_custom_scope/1
+    three_nodes_groups_names_default_scope/1,
+    three_nodes_groups_names_custom_scope/1
 ]).
 
 %% internals
@@ -98,8 +98,8 @@ groups() ->
             three_nodes_publish_custom_scope,
             three_nodes_multi_call_default_scope,
             three_nodes_multi_call_custom_scope,
-            three_nodes_group_names_default_scope,
-            three_nodes_group_names_custom_scope
+            three_nodes_groups_names_default_scope,
+            three_nodes_groups_names_custom_scope
         ]}
     ].
 %% -------------------------------------------------------------------
@@ -432,10 +432,10 @@ three_nodes_join_leave_and_monitor_default_scope(Config) ->
     false = rpc:call(SlaveNode2, syn, is_local_member, [{group, "two"}, PidWithMeta]),
     false = rpc:call(SlaveNode2, syn, is_local_member, [{group, "two"}, PidRemoteOn1]),
 
-    0 = syn:group_count(),
-    0 = syn:group_count(default, node()),
-    0 = syn:group_count(default, SlaveNode1),
-    0 = syn:group_count(default, SlaveNode2),
+    0 = syn:groups_count(),
+    0 = syn:groups_count(default, node()),
+    0 = syn:groups_count(default, SlaveNode1),
+    0 = syn:groups_count(default, SlaveNode2),
 
     %% join
     ok = syn:join({group, "one"}, Pid),
@@ -537,10 +537,10 @@ three_nodes_join_leave_and_monitor_default_scope(Config) ->
     false = rpc:call(SlaveNode2, syn, is_local_member, [{group, "two"}, PidWithMeta]),
     false = rpc:call(SlaveNode2, syn, is_local_member, [{group, "two"}, PidRemoteOn1]),
 
-    2 = syn:group_count(),
-    2 = syn:group_count(default, node()),
-    1 = syn:group_count(default, SlaveNode1),
-    0 = syn:group_count(default, SlaveNode2),
+    2 = syn:groups_count(),
+    2 = syn:groups_count(default, node()),
+    1 = syn:groups_count(default, SlaveNode1),
+    0 = syn:groups_count(default, SlaveNode2),
 
     %% re-join to edit meta
     ok = syn:join({group, "one"}, PidWithMeta, <<"with updated meta">>),
@@ -599,10 +599,10 @@ three_nodes_join_leave_and_monitor_default_scope(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [{group, "two"}])) end
     ),
 
-    2 = syn:group_count(),
-    2 = syn:group_count(default, node()),
-    1 = syn:group_count(default, SlaveNode1),
-    0 = syn:group_count(default, SlaveNode2),
+    2 = syn:groups_count(),
+    2 = syn:groups_count(default, node()),
+    1 = syn:groups_count(default, SlaveNode1),
+    0 = syn:groups_count(default, SlaveNode2),
 
     %% crash scope process to ensure that monitors get recreated
     exit(whereis(syn_groups_default), kill),
@@ -703,10 +703,10 @@ three_nodes_join_leave_and_monitor_default_scope(Config) ->
     false = rpc:call(SlaveNode2, syn, is_local_member, [{group, "two"}, PidWithMeta]),
     false = rpc:call(SlaveNode2, syn, is_local_member, [{group, "two"}, PidRemoteOn1]),
 
-    1 = syn:group_count(),
-    1 = syn:group_count(default, node()),
-    0 = syn:group_count(default, SlaveNode1),
-    0 = syn:group_count(default, SlaveNode2),
+    1 = syn:groups_count(),
+    1 = syn:groups_count(default, node()),
+    0 = syn:groups_count(default, SlaveNode1),
+    0 = syn:groups_count(default, SlaveNode2),
 
     %% errors
     {error, not_in_group} = syn:leave({group, "one"}, PidWithMeta).
@@ -894,30 +894,30 @@ three_nodes_join_leave_and_monitor_custom_scope(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [custom_scope_bc, {group, "two"}])) end
     ),
 
-    2 = syn:group_count(custom_scope_ab),
-    2 = syn:group_count(custom_scope_ab, node()),
-    0 = syn:group_count(custom_scope_ab, SlaveNode1),
-    0 = syn:group_count(custom_scope_ab, SlaveNode2),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, node()),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode1),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode2),
-    2 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab]),
-    2 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, node()]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, SlaveNode1]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, SlaveNode2]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode2]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, node()]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, SlaveNode1]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, SlaveNode2]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode2]),
+    2 = syn:groups_count(custom_scope_ab),
+    2 = syn:groups_count(custom_scope_ab, node()),
+    0 = syn:groups_count(custom_scope_ab, SlaveNode1),
+    0 = syn:groups_count(custom_scope_ab, SlaveNode2),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, node()),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode1),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode2),
+    2 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab]),
+    2 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, node()]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, SlaveNode1]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, SlaveNode2]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode2]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, node()]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, SlaveNode1]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, SlaveNode2]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode2]),
 
     %% re-join to edit meta
     ok = syn:join(custom_scope_ab, {group, "one"}, PidWithMeta, <<"with updated meta">>),
@@ -984,30 +984,30 @@ three_nodes_join_leave_and_monitor_custom_scope(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [custom_scope_bc, {group, "two"}])) end
     ),
 
-    2 = syn:group_count(custom_scope_ab),
-    2 = syn:group_count(custom_scope_ab, node()),
-    0 = syn:group_count(custom_scope_ab, SlaveNode1),
-    0 = syn:group_count(custom_scope_ab, SlaveNode2),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, node()),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode1),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode2),
-    2 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab]),
-    2 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, node()]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, SlaveNode1]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, SlaveNode2]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode2]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, node()]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, SlaveNode1]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, SlaveNode2]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode2]),
+    2 = syn:groups_count(custom_scope_ab),
+    2 = syn:groups_count(custom_scope_ab, node()),
+    0 = syn:groups_count(custom_scope_ab, SlaveNode1),
+    0 = syn:groups_count(custom_scope_ab, SlaveNode2),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, node()),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode1),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode2),
+    2 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab]),
+    2 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, node()]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, SlaveNode1]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, SlaveNode2]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode2]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, node()]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, SlaveNode1]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, SlaveNode2]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode2]),
 
     %% crash scope process to ensure that monitors get recreated
     exit(whereis(syn_groups_custom_scope_ab), kill),
@@ -1106,30 +1106,30 @@ three_nodes_join_leave_and_monitor_custom_scope(Config) ->
         [], fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [custom_scope_bc, {group, "two"}])) end
     ),
 
-    1 = syn:group_count(custom_scope_ab),
-    1 = syn:group_count(custom_scope_ab, node()),
-    0 = syn:group_count(custom_scope_ab, SlaveNode1),
-    0 = syn:group_count(custom_scope_ab, SlaveNode2),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, node()),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode1),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode2),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, node()]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, SlaveNode1]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_ab, SlaveNode2]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, node()]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode2]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, node()]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, SlaveNode1]),
-    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, group_count, [custom_scope_ab, SlaveNode2]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, node()]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode2]),
+    1 = syn:groups_count(custom_scope_ab),
+    1 = syn:groups_count(custom_scope_ab, node()),
+    0 = syn:groups_count(custom_scope_ab, SlaveNode1),
+    0 = syn:groups_count(custom_scope_ab, SlaveNode2),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, node()),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode1),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode2),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, node()]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, SlaveNode1]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_ab, SlaveNode2]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, node()]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode2]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, node()]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, SlaveNode1]),
+    {badrpc, {'EXIT', {{invalid_scope, custom_scope_ab}, _}}} = catch rpc:call(SlaveNode2, syn, groups_count, [custom_scope_ab, SlaveNode2]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, node()]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode2]),
 
     %% errors
     {error, not_in_group} = syn:leave(custom_scope_ab, {group, "one"}, PidWithMeta).
@@ -1225,18 +1225,18 @@ three_nodes_cluster_changes(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [<<"group-2">>])) end
     ),
 
-    2 = syn:group_count(),
-    0 = syn:group_count(default, node()),
-    1 = syn:group_count(default, SlaveNode1),
-    2 = syn:group_count(default, SlaveNode2),
-    2 = rpc:call(SlaveNode1, syn, group_count, []),
-    0 = rpc:call(SlaveNode1, syn, group_count, [default, node()]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [default, SlaveNode1]),
-    2 = rpc:call(SlaveNode1, syn, group_count, [default, SlaveNode2]),
-    2 = rpc:call(SlaveNode2, syn, group_count, []),
-    0 = rpc:call(SlaveNode2, syn, group_count, [default, node()]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [default, SlaveNode1]),
-    2 = rpc:call(SlaveNode2, syn, group_count, [default, SlaveNode2]),
+    2 = syn:groups_count(),
+    0 = syn:groups_count(default, node()),
+    1 = syn:groups_count(default, SlaveNode1),
+    2 = syn:groups_count(default, SlaveNode2),
+    2 = rpc:call(SlaveNode1, syn, groups_count, []),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [default, node()]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [default, SlaveNode1]),
+    2 = rpc:call(SlaveNode1, syn, groups_count, [default, SlaveNode2]),
+    2 = rpc:call(SlaveNode2, syn, groups_count, []),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [default, node()]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [default, SlaveNode1]),
+    2 = rpc:call(SlaveNode2, syn, groups_count, [default, SlaveNode2]),
 
     {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:members(custom_scope_bc, <<"scoped-on-bc">>),
     syn_test_suite_helper:assert_wait(
@@ -1258,18 +1258,18 @@ three_nodes_cluster_changes(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [custom_scope_bc, <<"scoped-on-bc">>])) end
     ),
 
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, node()),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode1),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode2),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode2]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode2]),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, node()),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode1),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode2),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode2]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode2]),
 
     %% partial netsplit (1 cannot see 2)
     rpc:call(SlaveNode1, syn_test_suite_helper, disconnect_node, [SlaveNode2]),
@@ -1330,18 +1330,18 @@ three_nodes_cluster_changes(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [<<"group-2">>])) end
     ),
 
-    2 = syn:group_count(),
-    0 = syn:group_count(default, node()),
-    1 = syn:group_count(default, SlaveNode1),
-    2 = syn:group_count(default, SlaveNode2),
-    1 = rpc:call(SlaveNode1, syn, group_count, []),
-    0 = rpc:call(SlaveNode1, syn, group_count, [default, node()]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [default, SlaveNode1]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [default, SlaveNode2]),
-    2 = rpc:call(SlaveNode2, syn, group_count, []),
-    0 = rpc:call(SlaveNode2, syn, group_count, [default, node()]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [default, SlaveNode1]),
-    2 = rpc:call(SlaveNode2, syn, group_count, [default, SlaveNode2]),
+    2 = syn:groups_count(),
+    0 = syn:groups_count(default, node()),
+    1 = syn:groups_count(default, SlaveNode1),
+    2 = syn:groups_count(default, SlaveNode2),
+    1 = rpc:call(SlaveNode1, syn, groups_count, []),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [default, node()]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [default, SlaveNode1]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [default, SlaveNode2]),
+    2 = rpc:call(SlaveNode2, syn, groups_count, []),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [default, node()]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [default, SlaveNode1]),
+    2 = rpc:call(SlaveNode2, syn, groups_count, [default, SlaveNode2]),
 
     {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:members(custom_scope_bc, <<"scoped-on-bc">>),
     syn_test_suite_helper:assert_wait(
@@ -1363,18 +1363,18 @@ three_nodes_cluster_changes(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [custom_scope_bc, <<"scoped-on-bc">>])) end
     ),
 
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, node()),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode1),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode2),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode2]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, node()]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode2]),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, node()),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode1),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode2),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode2]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, node()]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode2]),
 
     %% re-join
     rpc:call(SlaveNode1, syn_test_suite_helper, connect_node, [SlaveNode2]),
@@ -1435,18 +1435,18 @@ three_nodes_cluster_changes(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [<<"group-2">>])) end
     ),
 
-    2 = syn:group_count(),
-    0 = syn:group_count(default, node()),
-    1 = syn:group_count(default, SlaveNode1),
-    2 = syn:group_count(default, SlaveNode2),
-    2 = rpc:call(SlaveNode1, syn, group_count, []),
-    0 = rpc:call(SlaveNode1, syn, group_count, [default, node()]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [default, SlaveNode1]),
-    2 = rpc:call(SlaveNode1, syn, group_count, [default, SlaveNode2]),
-    2 = rpc:call(SlaveNode2, syn, group_count, []),
-    0 = rpc:call(SlaveNode2, syn, group_count, [default, node()]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [default, SlaveNode1]),
-    2 = rpc:call(SlaveNode2, syn, group_count, [default, SlaveNode2]),
+    2 = syn:groups_count(),
+    0 = syn:groups_count(default, node()),
+    1 = syn:groups_count(default, SlaveNode1),
+    2 = syn:groups_count(default, SlaveNode2),
+    2 = rpc:call(SlaveNode1, syn, groups_count, []),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [default, node()]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [default, SlaveNode1]),
+    2 = rpc:call(SlaveNode1, syn, groups_count, [default, SlaveNode2]),
+    2 = rpc:call(SlaveNode2, syn, groups_count, []),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [default, node()]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [default, SlaveNode1]),
+    2 = rpc:call(SlaveNode2, syn, groups_count, [default, SlaveNode2]),
 
     {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:members(custom_scope_bc, <<"scoped-on-bc">>),
     syn_test_suite_helper:assert_wait(
@@ -1468,18 +1468,18 @@ three_nodes_cluster_changes(Config) ->
         fun() -> lists:sort(rpc:call(SlaveNode2, syn, local_members, [custom_scope_bc, <<"scoped-on-bc">>])) end
     ),
 
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, node()),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode1),
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_count(custom_scope_bc, SlaveNode2),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    1 = rpc:call(SlaveNode1, syn, group_count, [custom_scope_bc, SlaveNode2]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc]),
-    0 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, node()]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode1]),
-    1 = rpc:call(SlaveNode2, syn, group_count, [custom_scope_bc, SlaveNode2]).
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, node()),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode1),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_count(custom_scope_bc, SlaveNode2),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    1 = rpc:call(SlaveNode1, syn, groups_count, [custom_scope_bc, SlaveNode2]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc]),
+    0 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, node()]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode1]),
+    1 = rpc:call(SlaveNode2, syn, groups_count, [custom_scope_bc, SlaveNode2]).
 
 three_nodes_custom_event_handler_joined_left(Config) ->
     %% get slaves
@@ -1843,7 +1843,7 @@ three_nodes_multi_call_custom_scope(Config) ->
     ]),
     BadRepliesBC = [].
 
-three_nodes_group_names_default_scope(Config) ->
+three_nodes_groups_names_default_scope(Config) ->
     %% get slaves
     SlaveNode1 = proplists:get_value(slave_node_1, Config),
     SlaveNode2 = proplists:get_value(slave_node_2, Config),
@@ -1870,53 +1870,53 @@ three_nodes_group_names_default_scope(Config) ->
     %% retrieve
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(syn:group_names()) end
+        fun() -> lists:sort(syn:groups_names()) end
     ),
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(syn:group_names(default, node())) end
+        fun() -> lists:sort(syn:groups_names(default, node())) end
     ),
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers-2">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(syn:group_names(default, SlaveNode1)) end
+        fun() -> lists:sort(syn:groups_names(default, SlaveNode1)) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-2">>],
-        fun() -> lists:sort(syn:group_names(default, SlaveNode2)) end
+        fun() -> lists:sort(syn:groups_names(default, SlaveNode2)) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [])) end
     ),
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers-2">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-2">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, SlaveNode2])) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [])) end
     ),
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers-2">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-2">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, SlaveNode2])) end
     ),
 
     %% leave
@@ -1927,53 +1927,53 @@ three_nodes_group_names_default_scope(Config) ->
     %% retrieve
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(syn:group_names()) end
+        fun() -> lists:sort(syn:groups_names()) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(syn:group_names(default, node())) end
+        fun() -> lists:sort(syn:groups_names(default, node())) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-3">>],
-        fun() -> lists:sort(syn:group_names(default, SlaveNode1)) end
+        fun() -> lists:sort(syn:groups_names(default, SlaveNode1)) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(syn:group_names(default, SlaveNode2)) end
+        fun() -> lists:sort(syn:groups_names(default, SlaveNode2)) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-3">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, SlaveNode2])) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-3">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, SlaveNode2])) end
     ),
 
     %% partial netsplit (1 cannot see 2)
@@ -1985,53 +1985,53 @@ three_nodes_group_names_default_scope(Config) ->
     %% retrieve
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(syn:group_names()) end
+        fun() -> lists:sort(syn:groups_names()) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(syn:group_names(default, node())) end
+        fun() -> lists:sort(syn:groups_names(default, node())) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-3">>],
-        fun() -> lists:sort(syn:group_names(default, SlaveNode1)) end
+        fun() -> lists:sort(syn:groups_names(default, SlaveNode1)) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(syn:group_names(default, SlaveNode2)) end
+        fun() -> lists:sort(syn:groups_names(default, SlaveNode2)) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-3">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, SlaveNode2])) end
     ),
 
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, SlaveNode2])) end
     ),
 
     %% re-join
@@ -2043,56 +2043,56 @@ three_nodes_group_names_default_scope(Config) ->
     %% retrieve
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(syn:group_names()) end
+        fun() -> lists:sort(syn:groups_names()) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(syn:group_names(default, node())) end
+        fun() -> lists:sort(syn:groups_names(default, node())) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-3">>],
-        fun() -> lists:sort(syn:group_names(default, SlaveNode1)) end
+        fun() -> lists:sort(syn:groups_names(default, SlaveNode1)) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(syn:group_names(default, SlaveNode2)) end
+        fun() -> lists:sort(syn:groups_names(default, SlaveNode2)) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-3">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [default, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [default, SlaveNode2])) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-3">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-3">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [default, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [default, SlaveNode2])) end
     ).
 
-three_nodes_group_names_custom_scope(Config) ->
+three_nodes_groups_names_custom_scope(Config) ->
     %% get slaves
     SlaveNode1 = proplists:get_value(slave_node_1, Config),
     SlaveNode2 = proplists:get_value(slave_node_2, Config),
@@ -2122,75 +2122,75 @@ three_nodes_group_names_custom_scope(Config) ->
     ok = rpc:call(SlaveNode2, syn, join, [custom_scope_bc, <<"subscribers-2">>, PidRemoteOn2]),
 
     %% errors
-    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:group_names(custom_scope_bc),
+    {'EXIT', {{invalid_scope, custom_scope_bc}, _}} = catch syn:groups_names(custom_scope_bc),
 
     %% retrieve
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(syn:group_names(custom_scope_ab)) end
+        fun() -> lists:sort(syn:groups_names(custom_scope_ab)) end
     ),
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(syn:group_names(custom_scope_ab, node())) end
+        fun() -> lists:sort(syn:groups_names(custom_scope_ab, node())) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(syn:group_names(custom_scope_ab, SlaveNode1)) end
+        fun() -> lists:sort(syn:groups_names(custom_scope_ab, SlaveNode1)) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(syn:group_names(custom_scope_ab, SlaveNode2)) end
+        fun() -> lists:sort(syn:groups_names(custom_scope_ab, SlaveNode2)) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [custom_scope_ab])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [custom_scope_ab])) end
     ),
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [custom_scope_ab, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [custom_scope_ab, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [custom_scope_ab, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [custom_scope_ab, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [custom_scope_ab, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [custom_scope_ab, SlaveNode2])) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [custom_scope_bc])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [custom_scope_bc])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [custom_scope_bc, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [custom_scope_bc, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [custom_scope_bc, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [custom_scope_bc, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-2">>],
-        fun() -> lists:sort(rpc:call(SlaveNode1, syn, group_names, [custom_scope_bc, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode1, syn, groups_names, [custom_scope_bc, SlaveNode2])) end
     ),
 
     syn_test_suite_helper:assert_wait(
         lists:sort([<<"subscribers">>, <<"subscribers-2">>]),
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [custom_scope_bc])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [custom_scope_bc])) end
     ),
     syn_test_suite_helper:assert_wait(
         [],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [custom_scope_bc, node()])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [custom_scope_bc, node()])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [custom_scope_bc, SlaveNode1])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [custom_scope_bc, SlaveNode1])) end
     ),
     syn_test_suite_helper:assert_wait(
         [<<"subscribers-2">>],
-        fun() -> lists:sort(rpc:call(SlaveNode2, syn, group_names, [custom_scope_bc, SlaveNode2])) end
+        fun() -> lists:sort(rpc:call(SlaveNode2, syn, groups_names, [custom_scope_bc, SlaveNode2])) end
     ).
 
 %% ===================================================================