Browse Source

Add callback for process groups.

[ostinelli/syn#10]
Roberto Ostinelli 8 years ago
parent
commit
e524c03b00
4 changed files with 123 additions and 14 deletions
  1. 45 4
      README.md
  2. 29 5
      src/syn_groups.erl
  3. 6 1
      test/syn-test.config
  4. 43 4
      test/syn_groups_SUITE.erl

+ 45 - 4
README.md

@@ -261,7 +261,7 @@ Types:
 	Error = pid_not_in_group
 ```
 
-> 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.
+> 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. If you manually remove a process from a group just before it dies, the callback on process exit (see here below) might not get called.
 
 To get a list of the members of a group:
 
@@ -383,7 +383,7 @@ Types:
 	Meta = any()
 	Reason = any()
 ```
-The `Key` and `Pid` are the ones of the process that exited with `Reason`.
+The `Key`, `Pid` and `Meta` are the ones of the process that exited with `Reason`.
 
 For instance, if you want to print a log when a registered process exited:
 
@@ -449,7 +449,48 @@ Set it in the options:
 > Important Note: The conflict resolution method SHOULD be defined in the same way across all nodes of the cluster. Having different conflict resolution options on different nodes can have unexpected results.
 
 ### Process Groups options
-There currently are none.
+These allow setting the Process Groups options, and are:
+
+ * `process_groups_process_exit_callback`
+
+#### Callback on process exit
+The `process_groups_process_exit_callback` option allows you to specify the `module` and the `function` of the callback that will be triggered when a member process of a group exits. This callback will be called only on the node where the process was running.
+
+The callback function is defined as:
+```erlang
+CallbackFun = fun(Names, Pid, Meta, Reason) -> any().
+
+Types:
+	Name = any()
+	Pid = pid()
+	Meta = any()
+	Reason = any()
+```
+`Name` is the Process Group that the process with `Pid` and `Meta` that exited with `Reason` was a member of.
+
+For instance, if you want to print a log when a member process of a group exited:
+
+```erlang
+-module(my_callback).
+-export([callback_on_process_exit/4]).
+
+callback_on_process_exit(Name, Pid, Meta, Reason) ->
+	error_logger:info_msg(
+		"Process with Pid ~p and Meta ~p of Group ~p exited with reason ~p~n",
+		[Pid, Meta, Name, Reason]
+	)
+```
+
+Set it in the options:
+```erlang
+{syn, [
+    %% define callback function
+    {process_groups_process_exit_callback, [my_callback, callback_on_process_exit]}
+]}
+```
+If you don't set this option, no callback will be triggered.
+
+> This callback will be called for every Process Group that the process was a member of.
 
 
 ## Internals
@@ -469,4 +510,4 @@ Ensure that  proper testing is included. To run Syn tests you simply have to be
 
 ```bash
 $ make tests
-```
+```

+ 29 - 5
src/syn_groups.erl

@@ -43,7 +43,10 @@
 -export([multi_call_and_receive/4]).
 
 %% records
--record(state, {}).
+-record(state, {
+    process_groups_process_exit_callback_module = undefined :: atom(),
+    process_groups_process_exit_callback_function = undefined :: atom()
+}).
 
 %% macros
 -define(DEFAULT_MULTI_CALL_TIMEOUT_MS, 5000).
@@ -130,8 +133,17 @@ init([]) ->
     %% trap linked processes signal
     process_flag(trap_exit, true),
     
+    %% get options
+    {ok, [ProcessExitCallbackModule, ProcessExitCallbackFunction]} = syn_utils:get_env_value(
+        process_groups_process_exit_callback,
+        [undefined, undefined]
+    ),
+    
     %% build state
-    {ok, #state{}}.
+    {ok, #state{
+        process_groups_process_exit_callback_module = ProcessExitCallbackModule,
+        process_groups_process_exit_callback_function = ProcessExitCallbackFunction
+    }}.
 
 %% ----------------------------------------------------------------------------------------------------------
 %% Call messages
@@ -202,7 +214,10 @@ handle_cast(Msg, State) ->
     {noreply, #state{}, Timeout :: non_neg_integer()} |
     {stop, Reason :: any(), #state{}}.
 
-handle_info({'EXIT', Pid, Reason}, State) ->
+handle_info({'EXIT', Pid, Reason}, #state{
+    process_groups_process_exit_callback_module = ProcessExitCallbackModule,
+    process_groups_process_exit_callback_function = ProcessExitCallbackFunction
+} = State) ->
     %% check if pid is in table
     case find_groups_by_pid(Pid) of
         [] ->
@@ -216,8 +231,9 @@ handle_info({'EXIT', Pid, Reason}, State) ->
         
         Processes ->
             F = fun(Process) ->
-                %% get group
+                %% get group & meta
                 Name = Process#syn_groups_table.name,
+                Meta = Process#syn_groups_table.meta,
                 %% log
                 case Reason of
                     normal -> ok;
@@ -226,7 +242,15 @@ handle_info({'EXIT', Pid, Reason}, State) ->
                         error_logger:error_msg("Process of group ~p and pid ~p exited with reason: ~p", [Name, Pid, Reason])
                 end,
                 %% delete from table
-                remove_process(Process)
+                remove_process(Process),
+                
+                %% callback in separate process
+                spawn(fun() ->
+                    case ProcessExitCallbackModule of
+                        undefined -> ok;
+                        _ -> ProcessExitCallbackModule:ProcessExitCallbackFunction(Name, Pid, Meta, Reason)
+                    end
+                end)
             end,
             lists:foreach(F, Processes)
     end,

+ 6 - 1
test/syn-test.config

@@ -19,7 +19,12 @@
         %% If this is not desired, you can set the registry_conflicting_process_callback option here below to instruct Syn
         %% to trigger a callback, so that you can perform custom operations (such as a graceful shutdown).
 
-        {registry_conflicting_process_callback, [syn_registry_consistency_SUITE, registry_conflicting_process_callback_dummy]}
+        {registry_conflicting_process_callback, [syn_registry_consistency_SUITE, registry_conflicting_process_callback_dummy]},
+    
+        %% You can set a callback to be triggered when a member process of a group exits.
+        %% This callback will be called only on the node where the process was running.
+    
+        {process_groups_process_exit_callback, [syn_groups_SUITE, process_groups_process_exit_callback_dummy]}
 
     ]}
 

+ 43 - 4
test/syn_groups_SUITE.erl

@@ -39,7 +39,8 @@
     single_node_publish/1,
     single_node_multi_call/1,
     single_node_multi_call_when_recipient_crashes/1,
-    single_node_meta/1
+    single_node_meta/1,
+    single_node_callback_on_process_exit/1
 ]).
 -export([
     two_nodes_kill/1,
@@ -47,9 +48,10 @@
     two_nodes_multi_call/1
 ]).
 
-%% internal
+%% internals
 -export([recipient_loop/1]).
 -export([called_loop/1, called_loop_that_crashes/1]).
+-export([process_groups_process_exit_callback_dummy/4]).
 
 %% include
 -include_lib("common_test/include/ct.hrl").
@@ -93,7 +95,8 @@ groups() ->
             single_node_publish,
             single_node_multi_call,
             single_node_multi_call_when_recipient_crashes,
-            single_node_meta
+            single_node_meta,
+            single_node_callback_on_process_exit
         ]},
         {two_nodes_process_groups, [shuffle], [
             two_nodes_kill,
@@ -338,7 +341,7 @@ single_node_multi_call_when_recipient_crashes(_Config) ->
     syn_test_suite_helper:kill_process(Pid1),
     syn_test_suite_helper:kill_process(Pid2).
 
-single_node_meta(Config) ->
+single_node_meta(_Config) ->
     %% set schema location
     application:set_env(mnesia, schema_location, ram),
     %% start
@@ -365,6 +368,39 @@ single_node_meta(Config) ->
     %% kill process
     syn_test_suite_helper:kill_process(Pid).
 
+single_node_callback_on_process_exit(_Config) ->
+    CurrentNode = node(),
+    %% set schema location
+    application:set_env(mnesia, schema_location, ram),
+    %% load configuration variables from syn-test.config => this defines the callback
+    syn_test_suite_helper:set_environment_variables(),
+    %% start
+    ok = syn:start(),
+    ok = syn:init(),
+    %% register global process
+    ResultPid = self(),
+    global:register_name(syn_process_groups_SUITE_result, ResultPid),
+    %% start process
+    Pid = syn_test_suite_helper:start_process(),
+    %% register
+    ok = syn:join(<<"my group">>, Pid, {some, meta, 1}),
+    ok = syn:join(<<"my other group">>, Pid, {some, meta, 2}),
+    %% kill process
+    syn_test_suite_helper:kill_process(Pid),
+    %% check callback were triggered
+    receive
+        {exited, CurrentNode, <<"my group">>, Pid, {some, meta, 1}, killed} -> ok
+    after 2000 ->
+        ok = process_groups_exit_callback_was_not_called_from_local_node
+    end,
+    receive
+        {exited, CurrentNode, <<"my other group">>, Pid, {some, meta, 2}, killed} -> ok
+    after 2000 ->
+        ok = process_groups_exit_callback_was_not_called_from_local_node
+    end,
+    %% unregister
+    global:unregister_name(syn_process_groups_SUITE_result).
+    
 two_nodes_kill(Config) ->
     %% get slave
     SlaveNode = proplists:get_value(slave_node, Config),
@@ -492,3 +528,6 @@ called_loop_that_crashes(_PidName) ->
     receive
         {syn_multi_call, _CallerPid, get_pid_name} -> exit(recipient_crashed_on_purpose)
     end.
+
+process_groups_process_exit_callback_dummy(Name, Pid, Meta, Reason) ->
+    global:send(syn_process_groups_SUITE_result, {exited, node(), Name, Pid, Meta, Reason}).