syn_event_handler.erl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. %% ==========================================================================================================
  2. %% Syn - A global Process Registry and Process Group manager.
  3. %%
  4. %% The MIT License (MIT)
  5. %%
  6. %% Copyright (c) 2019-2022 Roberto Ostinelli <roberto@ostinelli.net> and Neato Robotics, Inc.
  7. %%
  8. %% Portions of code from Ulf Wiger's unsplit server module:
  9. %% <https://github.com/uwiger/unsplit/blob/master/src/unsplit_server.erl>
  10. %%
  11. %% Permission is hereby granted, free of charge, to any person obtaining a copy
  12. %% of this software and associated documentation files (the "Software"), to deal
  13. %% in the Software without restriction, including without limitation the rights
  14. %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. %% copies of the Software, and to permit persons to whom the Software is
  16. %% furnished to do so, subject to the following conditions:
  17. %%
  18. %% The above copyright notice and this permission notice shall be included in
  19. %% all copies or substantial portions of the Software.
  20. %%
  21. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. %% THE SOFTWARE.
  28. %% ==========================================================================================================
  29. %% ===================================================================
  30. %% @doc Defines Syn's callbacks.
  31. %%
  32. %% You can specify the callback module with {@link syn:set_event_handler/1}.
  33. %% In your module you need to specify the behavior `syn_event_handler' and implement the callbacks.
  34. %% All callbacks are optional, so you just need to define the ones you need.
  35. %%
  36. %% All of the callbacks, except for `resolve_registry_conflict/4', are called on all the nodes of the cluster.
  37. %% This allows you to receive events for the processes running on nodes that get shut down, or in case of net splits.
  38. %%
  39. %% The argument `Reason' in the callbacks can be:
  40. %% <ul>
  41. %% <li> `normal' for expected operations.</li>
  42. %% <li> Crash reasons when processes die (for `on_process_unregistered/5' and `on_process_left/5').</li>
  43. %% <li> `{syn_remote_scope_node_up, Scope, Node}' for `on_process_registered/5' and `on_process_joined/5'
  44. %% when the callbacks are called due to nodes syncing.</li>
  45. %% <li> `{syn_remote_scope_node_down, Scope, Node}' for `on_process_unregistered/5' and `on_process_left/5'
  46. %% when the callbacks are called due to nodes disconnecting.</li>
  47. %% <li> `syn_conflict_resolution' for `on_process_registered/5' and `on_process_unregistered/5'
  48. %% during registry conflict resolution.</li>
  49. %% <li> `undefined' for `on_process_unregistered/5' and `on_process_left/5' when the processes died while the
  50. %% scope process had crashed.</li>
  51. %% </ul>
  52. %% While all callbacks do not have a direct effect on Syn (their return value is ignored), a special case is the callback
  53. %% `resolve_registry_conflict/4'. If specified, this is the method that will be used to resolve registry conflicts when detected.
  54. %%
  55. %% In case of net splits or race conditions, a specific name might get registered simultaneously on two different nodes.
  56. %% When this happens, the cluster experiences a registry naming conflict.
  57. %%
  58. %% Syn will resolve this Process Registry conflict by choosing a single process. By default, Syn keeps track of the time
  59. %% when a registration takes place with {@link erlang:system_time/0}, compares values between conflicting processes and:
  60. %% <ul>
  61. %% <li>Keeps the one with the higher value (the process that was registered more recently).</li>
  62. %% <li>Kills the other process by sending a kill signal with `exit(Pid, {syn_resolve_kill, Name, Meta})'.</li>
  63. %% </ul>
  64. %% This is a very simple mechanism that can be imprecise, as system clocks are not perfectly aligned in a cluster.
  65. %% If something more elaborate is desired, or if you do not want the discarded process to be killed (i.e. to perform
  66. %% a graceful shutdown), you MAY specify a custom handler that implements the `resolve_registry_conflict/4' callback.
  67. %% To this effect, you may also store additional data to resolve conflicts in the `Meta' value, since it will be passed
  68. %% into the callback for both of the conflicting processes.
  69. %%
  70. %% If implemented, this method MUST return the `pid()' of the process that you wish to keep. The other process will not
  71. %% be killed, so you will have to decide what to do with it. If the custom conflict resolution method does not return
  72. %% one of the two Pids, or if the method crashes, none of the Pids will be killed and the conflicting name will be freed.
  73. %%
  74. %% Important Note: the conflict resolution method will be called on the two nodes where the conflicting processes are running on.
  75. %% Therefore, this method MUST be defined in the same way across all nodes of the cluster and have the same effect
  76. %% regardless of the node it is run on, or you will experience unexpected results.
  77. %%
  78. %% <h3>Examples</h3>
  79. %% The following callback module implements the `on_process_unregistered/4' and the `on_process_left/4' callbacks.
  80. %% <h4>Elixir</h4>
  81. %% ```
  82. %% defmodule MyCustomEventHandler do
  83. %% ‎@behaviour :syn_event_handler
  84. %%
  85. %% ‎@impl true
  86. %% def on_process_unregistered(scope, name, pid, meta, reason) do
  87. %% end
  88. %%
  89. %% ‎@impl true
  90. %% def on_process_left(scope, group_name, pid, meta, reason) do
  91. %% end
  92. %% end
  93. %% '''
  94. %% <h4>Erlang</h4>
  95. %% ```
  96. %% -module(my_custom_event_handler).
  97. %% -behaviour(syn_event_handler).
  98. %% -export([on_process_unregistered/4]).
  99. %% -export([on_group_process_exit/4]).
  100. %%
  101. %% -spec on_process_unregistered(
  102. %% Scope :: atom(),
  103. %% Name :: term(),
  104. %% Pid :: pid(),
  105. %% Meta :: term(),
  106. %% Reason :: atom()
  107. %% ) -> term().
  108. %% on_process_unregistered(Scope, Name, Pid, Meta, Reason) ->
  109. %% ok.
  110. %%
  111. %% -spec on_process_left(
  112. %% Scope :: atom(),
  113. %% GroupName :: term(),
  114. %% Pid :: pid(),
  115. %% Meta :: term(),
  116. %% Reason :: atom()
  117. %% ) -> term().
  118. %% on_process_left(Scope, GroupName, Pid, Meta, Reason) ->
  119. %% ok.
  120. %% '''
  121. %%
  122. %% @end
  123. %% ===================================================================
  124. -module(syn_event_handler).
  125. %% API
  126. -export([ensure_event_handler_loaded/0]).
  127. -export([call_event_handler/2]).
  128. -export([do_resolve_registry_conflict/4]).
  129. -callback on_process_registered(
  130. Scope :: atom(),
  131. Name :: term(),
  132. Pid :: pid(),
  133. Meta :: term(),
  134. Reason :: atom()
  135. ) -> any().
  136. -callback on_registry_process_updated(
  137. Scope :: atom(),
  138. Name :: term(),
  139. Pid :: pid(),
  140. Meta :: term(),
  141. Reason :: atom()
  142. ) -> any().
  143. -callback on_process_unregistered(
  144. Scope :: atom(),
  145. Name :: term(),
  146. Pid :: pid(),
  147. Meta :: term(),
  148. Reason :: atom()
  149. ) -> any().
  150. -callback on_process_joined(
  151. Scope :: atom(),
  152. GroupName :: term(),
  153. Pid :: pid(),
  154. Meta :: term(),
  155. Reason :: atom()
  156. ) -> any().
  157. -callback on_group_process_updated(
  158. Scope :: atom(),
  159. GroupName :: term(),
  160. Pid :: pid(),
  161. Meta :: term(),
  162. Reason :: atom()
  163. ) -> any().
  164. -callback on_process_left(
  165. Scope :: atom(),
  166. GroupName :: term(),
  167. Pid :: pid(),
  168. Meta :: term(),
  169. Reason :: atom()
  170. ) -> any().
  171. -callback resolve_registry_conflict(
  172. Scope :: atom(),
  173. Name :: term(),
  174. {Pid1 :: pid(), Meta1 :: term(), Time1 :: non_neg_integer()},
  175. {Pid2 :: pid(), Meta2 :: term(), Time2 :: non_neg_integer()}
  176. ) -> PidToKeep :: pid().
  177. -optional_callbacks([on_process_registered/5, on_registry_process_updated/5, on_process_unregistered/5]).
  178. -optional_callbacks([on_process_joined/5, on_group_process_updated/5, on_process_left/5]).
  179. -optional_callbacks([resolve_registry_conflict/4]).
  180. %% ===================================================================
  181. %% API
  182. %% ===================================================================
  183. %% @private
  184. -spec ensure_event_handler_loaded() -> ok.
  185. ensure_event_handler_loaded() ->
  186. %% get handler
  187. CustomEventHandler = get_custom_event_handler(),
  188. %% ensure that is it loaded (not using code:ensure_loaded/1 to support embedded mode)
  189. catch CustomEventHandler:module_info(exports),
  190. ok.
  191. %% @private
  192. -spec call_event_handler(
  193. CallbackMethod :: atom(),
  194. Args :: [term()]
  195. ) -> any().
  196. call_event_handler(CallbackMethod, Args) ->
  197. CustomEventHandler = get_custom_event_handler(),
  198. case erlang:function_exported(CustomEventHandler, CallbackMethod, 5) of
  199. true ->
  200. try apply(CustomEventHandler, CallbackMethod, Args)
  201. catch Class:Reason:Stacktrace ->
  202. error_logger:error_msg(
  203. "SYN[~s] Error ~p:~p in custom handler ~p: ~p",
  204. [node(), Class, Reason, CallbackMethod, Stacktrace]
  205. )
  206. end;
  207. _ ->
  208. ok
  209. end.
  210. %% @private
  211. -spec do_resolve_registry_conflict(
  212. Scope :: atom(),
  213. Name :: term(),
  214. {Pid1 :: pid(), Meta1 :: term(), Time1 :: non_neg_integer()},
  215. {Pid2 :: pid(), Meta2 :: term(), Time2 :: non_neg_integer()}
  216. ) -> {PidToKeep :: pid() | undefined, KillOtherPid :: boolean()}.
  217. do_resolve_registry_conflict(Scope, Name, {Pid1, Meta1, Time1}, {Pid2, Meta2, Time2}) ->
  218. CustomEventHandler = get_custom_event_handler(),
  219. case erlang:function_exported(CustomEventHandler, resolve_registry_conflict, 4) of
  220. true ->
  221. try CustomEventHandler:resolve_registry_conflict(Scope, Name, {Pid1, Meta1, Time1}, {Pid2, Meta2, Time2}) of
  222. PidToKeep when is_pid(PidToKeep) -> {PidToKeep, false};
  223. _ -> {undefined, false}
  224. catch Class:Reason ->
  225. error_logger:error_msg(
  226. "SYN[~s] Error ~p in custom handler resolve_registry_conflict: ~p",
  227. [node(), Class, Reason]
  228. ),
  229. {undefined, false}
  230. end;
  231. _ ->
  232. %% by default, keep pid registered more recently
  233. %% this is a simple mechanism that can be imprecise, as system clocks are not perfectly aligned in a cluster
  234. %% if something more elaborate is desired (such as vector clocks) use Meta to store data and a custom event handler
  235. PidToKeep = case Time1 > Time2 of
  236. true -> Pid1;
  237. _ -> Pid2
  238. end,
  239. {PidToKeep, true}
  240. end.
  241. %% ===================================================================
  242. %% Internal
  243. %% ===================================================================
  244. -spec get_custom_event_handler() -> undefined | {ok, CustomEventHandler :: atom()}.
  245. get_custom_event_handler() ->
  246. application:get_env(syn, event_handler, undefined).