Browse Source

Add groups vasic registration.

Roberto Ostinelli 5 years ago
parent
commit
31d2abab0f
6 changed files with 592 additions and 3 deletions
  1. 1 0
      src/syn.app.src
  2. 31 1
      src/syn.erl
  3. 3 2
      src/syn.hrl
  4. 329 0
      src/syn_groups.erl
  5. 1 0
      src/syn_sup.erl
  6. 227 0
      test/syn_groups_SUITE.erl

+ 1 - 0
src/syn.app.src

@@ -3,6 +3,7 @@
         {description, "A global Process Registry and Process Group manager."},
         {vsn, "2.0.0"},
         {registered, [
+            syn_groups,
             syn_registry,
             syn_sup
         ]},

+ 31 - 1
src/syn.erl

@@ -31,6 +31,10 @@
 -export([unregister/1]).
 -export([whereis/1, whereis/2]).
 -export([registry_count/0, registry_count/1]).
+-export([join/2, join/3]).
+-export([leave/2]).
+-export([get_members/1, get_members/2]).
+-export([member/2]).
 
 %% gen_server via interface
 -export([register_name/2, unregister_name/1, whereis_name/1, send/2]).
@@ -47,6 +51,7 @@ start() ->
 stop() ->
     ok = application:stop(syn).
 
+%% ----- \/ registry -------------------------------------------------
 -spec register(Name :: term(), Pid :: pid()) -> ok | {error, Reason :: term()}.
 register(Name, Pid) ->
     syn_registry:register(Name, Pid).
@@ -75,7 +80,7 @@ registry_count() ->
 registry_count(Node) ->
     syn_registry:count(Node).
 
-%% gen_server via interface
+%% ----- \/ gen_server via module interface --------------------------
 -spec register_name(Name :: term(), Pid :: pid()) -> yes | no.
 register_name(Name, Pid) ->
     case syn_registry:register(Name, Pid) of
@@ -103,3 +108,28 @@ send(Name, Message) ->
             Pid ! Message,
             Pid
     end.
+
+%% ----- \/ groups ---------------------------------------------------
+-spec join(GroupName :: term(), Pid :: pid()) -> ok.
+join(GroupName, Pid) ->
+    syn_groups:join(GroupName, Pid).
+
+-spec join(GroupName :: term(), Pid :: pid(), Meta :: term()) -> ok.
+join(GroupName, Pid, Meta) ->
+    syn_groups:join(GroupName, Pid, Meta).
+
+-spec leave(GroupName :: term(), Pid :: pid()) -> ok | {error, Reason :: term()}.
+leave(GroupName, Pid) ->
+    syn_groups:leave(GroupName, Pid).
+
+-spec get_members(GroupName :: term()) -> [pid()].
+get_members(GroupName) ->
+    syn_groups:get_members(GroupName).
+
+-spec get_members(GroupName :: term(), with_meta) -> [{pid(), Meta :: term()}].
+get_members(GroupName, with_meta) ->
+    syn_groups:get_members(GroupName, with_meta).
+
+-spec member(GroupName :: term(), Pid :: pid()) -> boolean().
+member(GroupName, Pid) ->
+    syn_groups:member(GroupName, Pid).

+ 3 - 2
src/syn.hrl

@@ -29,13 +29,14 @@
     pid = undefined :: atom() | pid(),
     node = undefined :: atom(),
     meta = undefined :: any(),
-    monitor_ref = undefined :: undefined | '_' | reference()
+    monitor_ref = undefined :: atom() | reference()
 }).
 -record(syn_groups_table, {
     name = undefined :: any(),
     pid = undefined :: undefined | pid(),
     node = undefined :: atom(),
-    meta = undefined :: any()
+    meta = undefined :: any(),
+    monitor_ref = undefined :: atom() | reference()
 }).
 
 %% types

+ 329 - 0
src/syn_groups.erl

@@ -0,0 +1,329 @@
+%% ==========================================================================================================
+%% Syn - A global Process Registry and Process Group manager.
+%%
+%% The MIT License (MIT)
+%%
+%% Copyright (c) 2015-2019 Roberto Ostinelli <roberto@ostinelli.net> and Neato Robotics, Inc.
+%%
+%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%% of this software and associated documentation files (the "Software"), to deal
+%% in the Software without restriction, including without limitation the rights
+%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the Software is
+%% furnished to do so, subject to the following conditions:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%% THE SOFTWARE.
+%% ==========================================================================================================
+-module(syn_groups).
+-behaviour(gen_server).
+
+%% API
+-export([start_link/0]).
+-export([join/2, join/3]).
+-export([leave/2]).
+-export([get_members/1, get_members/2]).
+-export([member/2]).
+
+%% sync API
+-export([sync_join/3, sync_leave/2]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+%% records
+-record(state, {}).
+
+%% includes
+-include("syn.hrl").
+
+%% ===================================================================
+%% API
+%% ===================================================================
+-spec start_link() -> {ok, pid()} | {error, any()}.
+start_link() ->
+    Options = [],
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], Options).
+
+-spec join(GroupName :: term(), Pid :: pid()) -> ok.
+join(GroupName, Pid) ->
+    join(GroupName, Pid, undefined).
+
+-spec join(GroupName :: any(), Pid :: pid(), Meta :: any()) -> ok.
+join(GroupName, Pid, Meta) when is_pid(Pid) ->
+    Node = node(Pid),
+    gen_server:call({?MODULE, Node}, {join_on_node, GroupName, Pid, Meta}).
+
+-spec leave(GroupName :: term(), Pid :: pid()) -> ok | {error, Reason :: term()}.
+leave(GroupName, Pid) ->
+    case find_process_entry_by_name_and_pid(GroupName, Pid) of
+        undefined ->
+            {error, not_in_group};
+        _ ->
+            Node = node(Pid),
+            gen_server:call({?MODULE, Node}, {leave_on_node, GroupName, Pid})
+    end.
+
+-spec get_members(Name :: any()) -> [pid()].
+get_members(GroupName) ->
+    Entries = mnesia:dirty_read(syn_groups_table, GroupName),
+    Pids = [Entry#syn_groups_table.pid || Entry <- Entries],
+    lists:sort(Pids).
+
+-spec get_members(GroupName :: any(), with_meta) -> [{pid(), Meta :: any()}].
+get_members(GroupName, with_meta) ->
+    Entries = mnesia:dirty_read(syn_groups_table, GroupName),
+    Pids = [{Entry#syn_groups_table.pid, Entry#syn_groups_table.meta} || Entry <- Entries],
+    lists:sort(Pids).
+
+-spec member(Pid :: pid(), GroupName :: term()) -> boolean().
+member(Pid, GroupName) ->
+    case find_process_entry_by_name_and_pid(GroupName, Pid) of
+        undefined -> false;
+        _ -> true
+    end.
+
+-spec sync_join(GroupName :: term(), Pid :: pid(), Meta :: term()) -> ok.
+sync_join(GroupName, Pid, Meta) ->
+    gen_server:cast(?MODULE, {sync_join, GroupName, Pid, Meta}).
+
+-spec sync_leave(GroupName :: term(), Pid :: pid()) -> ok.
+sync_leave(GroupName, Pid) ->
+    gen_server:cast(?MODULE, {sync_leave, GroupName, Pid}).
+
+%% ===================================================================
+%% Callbacks
+%% ===================================================================
+
+%% ----------------------------------------------------------------------------------------------------------
+%% Init
+%% ----------------------------------------------------------------------------------------------------------
+-spec init([]) ->
+    {ok, #state{}} |
+    {ok, #state{}, Timeout :: non_neg_integer()} |
+    ignore |
+    {stop, Reason :: any()}.
+init([]) ->
+    %% wait for table
+    case mnesia:wait_for_tables([syn_groups_table], 10000) of
+        ok ->
+            %% monitor nodes
+            ok = net_kernel:monitor_nodes(true),
+            %% init
+            {ok, #state{}};
+        Reason ->
+            {stop, {error_waiting_for_groups_table, Reason}}
+    end.
+
+%% ----------------------------------------------------------------------------------------------------------
+%% Call messages
+%% ----------------------------------------------------------------------------------------------------------
+-spec handle_call(Request :: any(), From :: any(), #state{}) ->
+    {reply, Reply :: any(), #state{}} |
+    {reply, Reply :: any(), #state{}, Timeout :: non_neg_integer()} |
+    {noreply, #state{}} |
+    {noreply, #state{}, Timeout :: non_neg_integer()} |
+    {stop, Reason :: any(), Reply :: any(), #state{}} |
+    {stop, Reason :: any(), #state{}}.
+
+handle_call({join_on_node, GroupName, Pid, Meta}, _From, State) ->
+    join_on_node(GroupName, Pid, Meta),
+    %% multicast
+    rpc:eval_everywhere(nodes(), ?MODULE, sync_join, [GroupName, Pid, Meta]),
+    %% return
+    {reply, ok, State};
+
+handle_call({leave_on_node, GroupName, Pid}, _From, State) ->
+    case leave_on_node(GroupName, Pid) of
+        ok ->
+            %% multicast
+            rpc:eval_everywhere(nodes(), ?MODULE, sync_leave, [GroupName, Pid]),
+            %% return
+            {reply, ok, State};
+        {error, Reason} ->
+            %% return
+            {reply, {error, Reason}, State}
+    end;
+
+handle_call(Request, From, State) ->
+    error_logger:warning_msg("Syn(~p): Received from ~p an unknown call message: ~p~n", [node(), Request, From]),
+    {reply, undefined, State}.
+
+%% ----------------------------------------------------------------------------------------------------------
+%% Cast messages
+%% ----------------------------------------------------------------------------------------------------------
+-spec handle_cast(Msg :: any(), #state{}) ->
+    {noreply, #state{}} |
+    {noreply, #state{}, Timeout :: non_neg_integer()} |
+    {stop, Reason :: any(), #state{}}.
+
+handle_cast({sync_join, GroupName, Pid, Meta}, State) ->
+    %% add to table
+    add_to_local_table(GroupName, Pid, Meta, undefined),
+    %% return
+    {noreply, State};
+
+handle_cast({sync_leave, GroupName, Pid}, State) ->
+    %% remove entry
+    remove_from_local_table(GroupName, Pid),
+    %% return
+    {noreply, State};
+
+handle_cast(Msg, State) ->
+    error_logger:warning_msg("Syn(~p): Received an unknown cast message: ~p~n", [node(), Msg]),
+    {noreply, State}.
+
+%% ----------------------------------------------------------------------------------------------------------
+%% All non Call / Cast messages
+%% ----------------------------------------------------------------------------------------------------------
+-spec handle_info(Info :: any(), #state{}) ->
+    {noreply, #state{}} |
+    {noreply, #state{}, Timeout :: non_neg_integer()} |
+    {stop, Reason :: any(), #state{}}.
+
+handle_info({'DOWN', _MonitorRef, process, Pid, Reason}, State) ->
+    case find_processes_entry_by_pid(Pid) of
+        [] ->
+            %% log
+            log_process_exit(undefined, Pid, Reason);
+
+        Entries ->
+            lists:foreach(fun(Entry) ->
+                %% get process info
+                GroupName = Entry#syn_groups_table.name,
+                %% log
+                log_process_exit(GroupName, Pid, Reason),
+                %% remove from table
+                remove_from_local_table(Entry),
+                %% multicast
+                rpc:eval_everywhere(nodes(), ?MODULE, sync_leave, [GroupName, Pid])
+            end, Entries)
+    end,
+    %% return
+    {noreply, State};
+
+handle_info(Info, State) ->
+    error_logger:warning_msg("Syn(~p): Received an unknown info message: ~p~n", [node(), Info]),
+    {noreply, State}.
+
+%% ----------------------------------------------------------------------------------------------------------
+%% Terminate
+%% ----------------------------------------------------------------------------------------------------------
+-spec terminate(Reason :: any(), #state{}) -> terminated.
+terminate(Reason, _State) ->
+    error_logger:info_msg("Syn(~p): Terminating with reason: ~p~n", [node(), Reason]),
+    terminated.
+
+%% ----------------------------------------------------------------------------------------------------------
+%% Convert process state when code is changed.
+%% ----------------------------------------------------------------------------------------------------------
+-spec code_change(OldVsn :: any(), #state{}, Extra :: any()) -> {ok, #state{}}.
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%% ===================================================================
+%% Internal
+%% ===================================================================
+-spec join_on_node(GroupName :: any(), Pid :: pid(), Meta :: any()) -> ok.
+join_on_node(GroupName, Pid, Meta) ->
+    MonitorRef = case find_processes_entry_by_pid(Pid) of
+        [] ->
+            %% process is not monitored yet, add
+            erlang:monitor(process, Pid);
+        [Entry | _] ->
+            Entry#syn_groups_table.monitor_ref
+    end,
+    %% add to table
+    add_to_local_table(GroupName, Pid, Meta, MonitorRef).
+
+-spec leave_on_node(GroupName :: any(), Pid :: pid()) -> ok | {error, Reason :: term()}.
+leave_on_node(GroupName, Pid) ->
+    case find_process_entry_by_name_and_pid(GroupName, Pid) of
+        undefined ->
+            {error, not_in_group};
+
+        Entry when Entry#syn_groups_table.monitor_ref =/= undefined ->
+            %% is this the last group process is in?
+            case find_processes_entry_by_pid(Pid) of
+                [Entry] ->
+                    %% demonitor
+                    erlang:demonitor(Entry#syn_groups_table.monitor_ref);
+                _ ->
+                    ok
+            end,
+            %% remove from table
+            remove_from_local_table(Entry)
+    end.
+
+-spec add_to_local_table(GroupName :: any(), Pid :: pid(), Meta :: any(), MonitorRef :: undefined | reference()) -> ok.
+add_to_local_table(GroupName, Pid, Meta, MonitorRef) ->
+    %% clean if any
+    remove_from_local_table(GroupName, Pid),
+    %% write
+    mnesia:dirty_write(#syn_groups_table{
+        name = GroupName,
+        pid = Pid,
+        node = node(Pid),
+        meta = Meta,
+        monitor_ref = MonitorRef
+    }).
+
+-spec remove_from_local_table(GroupName :: term(), Pid :: pid()) -> ok | {error, Reason :: term()}.
+remove_from_local_table(GroupName, Pid) ->
+    case find_process_entry_by_name_and_pid(GroupName, Pid) of
+        undefined ->
+            {error, not_in_group};
+        Entry ->
+            %% remove from table
+            remove_from_local_table(Entry)
+    end.
+
+-spec remove_from_local_table(Entry :: #syn_groups_table{}) -> ok.
+remove_from_local_table(Entry) ->
+    mnesia:dirty_delete_object(syn_groups_table, Entry).
+
+-spec find_processes_entry_by_pid(Pid :: pid()) -> Entries :: list(#syn_groups_table{}).
+find_processes_entry_by_pid(Pid) when is_pid(Pid) ->
+    mnesia:dirty_index_read(syn_groups_table, Pid, #syn_groups_table.pid).
+
+-spec find_process_entry_by_name_and_pid(GroupName :: term(), Pid :: pid()) -> Entry :: #syn_groups_table{} | undefined.
+find_process_entry_by_name_and_pid(GroupName, Pid) ->
+    %% build match specs
+    MatchHead = #syn_groups_table{name = GroupName, pid = Pid, _ = '_'},
+    Guards = [],
+    Result = '$_',
+    %% select
+    case mnesia:dirty_select(syn_groups_table, [{MatchHead, Guards, [Result]}]) of
+        [Entry] -> Entry;
+        [] -> undefined
+    end.
+
+-spec log_process_exit(Name :: term(), Pid :: pid(), Reason :: term()) -> ok.
+log_process_exit(GroupName, Pid, Reason) ->
+    case Reason of
+        normal -> ok;
+        shutdown -> ok;
+        {shutdown, _} -> ok;
+        killed -> ok;
+        _ ->
+            case GroupName of
+                undefined ->
+                    error_logger:error_msg(
+                        "Syn(~p): Received a DOWN message from an unmonitored process ~p with reason: ~p~n",
+                        [node(), Pid, Reason]
+                    );
+                _ ->
+                    error_logger:error_msg(
+                        "Syn(~p): Process in group ~p and pid ~p exited with reason: ~p~n",
+                        [node(), GroupName, Pid, Reason]
+                    )
+            end
+    end.

+ 1 - 0
src/syn_sup.erl

@@ -50,6 +50,7 @@ start_link() ->
     {ok, {{supervisor:strategy(), non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}.
 init([]) ->
     Children = [
+        ?CHILD(syn_groups, worker),
         ?CHILD(syn_registry, worker)
     ],
     {ok, {{one_for_one, 10, 10}, Children}}.

+ 227 - 0
test/syn_groups_SUITE.erl

@@ -0,0 +1,227 @@
+%% ==========================================================================================================
+%% Syn - A global Process Registry and Process Group manager.
+%%
+%% The MIT License (MIT)
+%%
+%% Copyright (c) 2015-2019 Roberto Ostinelli <roberto@ostinelli.net> and Neato Robotics, Inc.
+%%
+%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%% of this software and associated documentation files (the "Software"), to deal
+%% in the Software without restriction, including without limitation the rights
+%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the Software is
+%% furnished to do so, subject to the following conditions:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%% THE SOFTWARE.
+%% ==========================================================================================================
+-module(syn_groups_SUITE).
+
+%% callbacks
+-export([all/0]).
+-export([init_per_suite/1, end_per_suite/1]).
+-export([groups/0, init_per_group/2, end_per_group/2]).
+-export([init_per_testcase/2, end_per_testcase/2]).
+
+%% tests
+-export([
+    single_node_join_and_monitor/1,
+    single_node_join_and_leave/1
+]).
+
+%% include
+-include_lib("common_test/include/ct.hrl").
+
+
+%% ===================================================================
+%% Callbacks
+%% ===================================================================
+
+%% -------------------------------------------------------------------
+%% Function: all() -> GroupsAndTestCases | {skip,Reason}
+%% GroupsAndTestCases = [{group,GroupName} | TestCase]
+%% GroupName = atom()
+%% TestCase = atom()
+%% Reason = term()
+%% -------------------------------------------------------------------
+all() ->
+    [
+        {group, single_node_groups}
+    ].
+
+%% -------------------------------------------------------------------
+%% Function: groups() -> [Group]
+%% Group = {GroupName,Properties,GroupsAndTestCases}
+%% GroupName = atom()
+%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}]
+%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase]
+%% TestCase = atom()
+%% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}}
+%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail |
+%%			   repeat_until_any_ok | repeat_until_any_fail
+%% N = integer() | forever
+%% -------------------------------------------------------------------
+groups() ->
+    [
+        {single_node_groups, [shuffle], [
+            single_node_join_and_monitor,
+            single_node_join_and_leave
+        ]}
+    ].
+%% -------------------------------------------------------------------
+%% Function: init_per_suite(Config0) ->
+%%				Config1 | {skip,Reason} |
+%%              {skip_and_save,Reason,Config1}
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%% -------------------------------------------------------------------
+init_per_suite(Config) ->
+    Config.
+
+%% -------------------------------------------------------------------
+%% Function: end_per_suite(Config0) -> void() | {save_config,Config1}
+%% Config0 = Config1 = [tuple()]
+%% -------------------------------------------------------------------
+end_per_suite(_Config) ->
+    ok.
+
+%% -------------------------------------------------------------------
+%% Function: init_per_group(GroupName, Config0) ->
+%%				Config1 | {skip,Reason} |
+%%              {skip_and_save,Reason,Config1}
+%% GroupName = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%% -------------------------------------------------------------------
+init_per_group(two_nodes_process_registration, Config) ->
+    %% start slave
+    {ok, SlaveNode} = syn_test_suite_helper:start_slave(syn_slave),
+    %% config
+    [{slave_node, SlaveNode} | Config];
+init_per_group(three_nodes_process_registration, Config) ->
+    %% start slave
+    {ok, SlaveNode1} = syn_test_suite_helper:start_slave(syn_slave_1),
+    {ok, SlaveNode2} = syn_test_suite_helper:start_slave(syn_slave_2),
+    %% config
+    [{slave_node_1, SlaveNode1}, {slave_node_2, SlaveNode2} | Config];
+init_per_group(_GroupName, Config) ->
+    Config.
+
+%% -------------------------------------------------------------------
+%% Function: end_per_group(GroupName, Config0) ->
+%%				void() | {save_config,Config1}
+%% GroupName = atom()
+%% Config0 = Config1 = [tuple()]
+%% -------------------------------------------------------------------
+end_per_group(two_nodes_process_registration, Config) ->
+    SlaveNode = proplists:get_value(slave_node, Config),
+    syn_test_suite_helper:connect_node(SlaveNode),
+    syn_test_suite_helper:stop_slave(syn_slave),
+    timer:sleep(1000);
+end_per_group(three_nodes_process_registration, Config) ->
+    SlaveNode1 = proplists:get_value(slave_node_1, Config),
+    syn_test_suite_helper:connect_node(SlaveNode1),
+    SlaveNode2 = proplists:get_value(slave_node_2, Config),
+    syn_test_suite_helper:connect_node(SlaveNode2),
+    syn_test_suite_helper:stop_slave(syn_slave_1),
+    syn_test_suite_helper:stop_slave(syn_slave_2),
+    timer:sleep(1000);
+end_per_group(_GroupName, _Config) ->
+    ok.
+
+%% -------------------------------------------------------------------
+%% Function: init_per_testcase(TestCase, Config0) ->
+%%				Config1 | {skip,Reason} | {skip_and_save,Reason,Config1}
+%% TestCase = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%% -------------------------------------------------------------------
+init_per_testcase(_TestCase, Config) ->
+    Config.
+
+%% -------------------------------------------------------------------
+%% Function: end_per_testcase(TestCase, Config0) ->
+%%				void() | {save_config,Config1} | {fail,Reason}
+%% TestCase = atom()
+%% Config0 = Config1 = [tuple()]
+%% Reason = term()
+%% -------------------------------------------------------------------
+end_per_testcase(_, _Config) ->
+    syn_test_suite_helper:clean_after_test().
+
+%% ===================================================================
+%% Tests
+%% ===================================================================
+single_node_join_and_monitor(_Config) ->
+    GroupName = "my group",
+    %% start
+    ok = syn:start(),
+    %% start processes
+    Pid = syn_test_suite_helper:start_process(),
+    PidWithMeta = syn_test_suite_helper:start_process(),
+    %% retrieve
+    [] = syn:get_members(GroupName),
+    [] = syn:get_members(GroupName, with_meta),
+    false = syn:member(Pid, GroupName),
+    false = syn:member(PidWithMeta, GroupName),
+    %% join
+    ok = syn:join(GroupName, Pid),
+    ok = syn:join(GroupName, PidWithMeta, {with, meta}),
+    %% retrieve
+    true = syn:member(Pid, GroupName),
+    true = syn:member(PidWithMeta, GroupName),
+    true = lists:sort([Pid, PidWithMeta]) =:= lists:sort(syn:get_members(GroupName)),
+    true = lists:sort([{Pid, undefined}, {PidWithMeta, {with, meta}}]) =:= lists:sort(syn:get_members(GroupName, with_meta)),
+    %% re-join
+    ok = syn:join(GroupName, PidWithMeta, {with2, meta2}),
+    ct:pal("HERE ~p",[lists:sort(syn:get_members(GroupName, with_meta))]),
+    true = lists:sort([{Pid, undefined}, {PidWithMeta, {with2, meta2}}]) =:= lists:sort(syn:get_members(GroupName, with_meta)),
+    %% kill process
+    syn_test_suite_helper:kill_process(Pid),
+    syn_test_suite_helper:kill_process(PidWithMeta),
+    timer:sleep(100),
+    %% retrieve
+    [] = syn:get_members(GroupName),
+    [] = syn:get_members(GroupName, with_meta),
+    false = syn:member(Pid, GroupName),
+    false = syn:member(PidWithMeta, GroupName).
+
+single_node_join_and_leave(_Config) ->
+    GroupName = "my group",
+    %% start
+    ok = syn:start(),
+    %% start processes
+    Pid = syn_test_suite_helper:start_process(),
+    PidWithMeta = syn_test_suite_helper:start_process(),
+    %% retrieve
+    [] = syn:get_members(GroupName),
+    [] = syn:get_members(GroupName, with_meta),
+    false = syn:member(Pid, GroupName),
+    false = syn:member(PidWithMeta, GroupName),
+    %% join
+    ok = syn:join(GroupName, Pid),
+    ok = syn:join(GroupName, PidWithMeta, {with, meta}),
+    %% retrieve
+    true = syn:member(Pid, GroupName),
+    true = syn:member(PidWithMeta, GroupName),
+    true = lists:sort([Pid, PidWithMeta]) =:= lists:sort(syn:get_members(GroupName)),
+    true = lists:sort([{Pid, undefined}, {PidWithMeta, {with, meta}}]) =:= lists:sort(syn:get_members(GroupName, with_meta)),
+    %% leave
+    ok = syn:leave(GroupName, Pid),
+    ok = syn:leave(GroupName, PidWithMeta),
+    timer:sleep(100),
+    {error, not_in_group} = syn:leave(GroupName, Pid),
+    {error, not_in_group} = syn:leave(GroupName, PidWithMeta),
+    %% retrieve
+    [] = syn:get_members(GroupName),
+    [] = syn:get_members(GroupName, with_meta),
+    false = syn:member(Pid, GroupName),
+    false = syn:member(PidWithMeta, GroupName).