diff options
author | Siri Hansen <[email protected]> | 2018-10-09 13:18:19 +0200 |
---|---|---|
committer | Siri Hansen <[email protected]> | 2018-10-19 11:57:46 +0200 |
commit | 35d0c2085cd07376157ac4bbe95c79db005e7a5c (patch) | |
tree | 63221a5719a52a3ee78c1d784097eac8216c667b /lib/kernel/src/logger_h_common.erl | |
parent | bc693e588be932663b75a7261ac19655ab597ae2 (diff) | |
download | otp-35d0c2085cd07376157ac4bbe95c79db005e7a5c.tar.gz otp-35d0c2085cd07376157ac4bbe95c79db005e7a5c.tar.bz2 otp-35d0c2085cd07376157ac4bbe95c79db005e7a5c.zip |
[logger] Further refactor built-in handlers
Diffstat (limited to 'lib/kernel/src/logger_h_common.erl')
-rw-r--r-- | lib/kernel/src/logger_h_common.erl | 389 |
1 files changed, 167 insertions, 222 deletions
diff --git a/lib/kernel/src/logger_h_common.erl b/lib/kernel/src/logger_h_common.erl index d01851b2b1..d290f51e34 100644 --- a/lib/kernel/src/logger_h_common.erl +++ b/lib/kernel/src/logger_h_common.erl @@ -24,7 +24,7 @@ -include("logger_internal.hrl"). %% API --export([start_link/3, info/2, filesync/2, reset/2]). +-export([start_link/1, info/2, filesync/2, reset/2]). %% gen_server and proc_lib callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, @@ -52,65 +52,37 @@ -define(READ_ONLY_KEYS,[handler_pid,mode_tab]). %%%----------------------------------------------------------------- -%%% -filesync(Module, Name) when is_atom(Name) -> - try - gen_server:call(?name_to_reg_name(Module,Name), - filesync, ?DEFAULT_CALL_TIMEOUT) - catch - _:{timeout,_} -> {error,handler_busy} - end; -filesync(_, Name) -> - {error,{badarg,{filesync,[Name]}}}. +%%% API -info(Module, Name) when is_atom(Name) -> - try - gen_server:call(?name_to_reg_name(Module,Name), - info, ?DEFAULT_CALL_TIMEOUT) - catch - _:{timeout,_} -> {error,handler_busy} - end; -info(_, Name) -> - {error,{badarg,{info,[Name]}}}. +%% This function is called by the logger_sup supervisor +start_link(Args) -> + proc_lib:start_link(?MODULE,init,[Args]). -reset(Module, Name) when is_atom(Name) -> - try - gen_server:call(?name_to_reg_name(Module,Name), - reset, ?DEFAULT_CALL_TIMEOUT) - catch - _:{timeout,_} -> {error,handler_busy} - end; -reset(_, Name) -> - {error,{badarg,{reset,[Name]}}}. +filesync(Module, Name) -> + call(Module, Name, filesync). +info(Module, Name) -> + call(Module, Name, info). +reset(Module, Name) -> + call(Module, Name, reset). %%%----------------------------------------------------------------- %%% Handler being added adding_handler(#{id:=Name,module:=Module}=Config) -> - HConfig = maps:get(config, Config, #{}), - HandlerConfig0 = maps:without(?CONFIG_KEYS,HConfig), + HConfig0 = maps:get(config, Config, #{}), + HandlerConfig0 = maps:without(?CONFIG_KEYS,HConfig0), case Module:check_config(Name,set,undefined,HandlerConfig0) of {ok,HandlerConfig} -> ModifiedCommon = maps:with(?CONFIG_KEYS,HandlerConfig), - CommonConfig0 = maps:with(?CONFIG_KEYS,HConfig), + CommonConfig0 = maps:with(?CONFIG_KEYS,HConfig0), CommonConfig = maps:merge( maps:merge(get_default_config(), CommonConfig0), ModifiedCommon), case check_config(CommonConfig) of ok -> - State = maps:merge(get_init_state(), CommonConfig), - HConfig1 = maps:merge(CommonConfig,HandlerConfig), - Config1 = Config#{config=>HConfig1}, - case overload_levels_ok(State) of - true -> - start(Name, Config1, State); - false -> - #{sync_mode_qlen := SMQL, - drop_mode_qlen := DMQL, - flush_qlen := FQL} = State, - {error,{invalid_levels,{SMQL,DMQL,FQL}}} - end; + HConfig = maps:merge(CommonConfig,HandlerConfig), + start(Config#{config => HConfig}); {error,Faulty} -> {error,{invalid_config,Module,Faulty}} end; @@ -141,7 +113,7 @@ removing_handler(#{id:=Name, module:=Module}) -> %%%----------------------------------------------------------------- %%% Updating handler config changing_config(SetOrUpdate, - OldConfig=#{id:=Name,config:=OldHConfig,module:=Module}, + #{id:=Name,config:=OldHConfig,module:=Module}, NewConfig0) -> NewHConfig0 = maps:get(config, NewConfig0, #{}), OldHandlerConfig = maps:without(?CONFIG_KEYS++?READ_ONLY_KEYS,OldHConfig), @@ -169,12 +141,9 @@ changing_config(SetOrUpdate, ReadOnly), NewConfig = NewConfig0#{config=>NewHConfig}, HPid = maps:get(handler_pid,OldHConfig), - try gen_server:call(HPid, {change_config,OldConfig,NewConfig}, - ?DEFAULT_CALL_TIMEOUT) of + case call(HPid, {change_config,NewConfig}) of ok -> {ok,NewConfig}; Error -> Error - catch - _:{timeout,_} -> {error,handler_busy} end; {error,Faulty} -> {error,{invalid_config,Module,Faulty}} @@ -204,61 +173,35 @@ filter_config(#{config:=HConfig}=Config) -> Config#{config=>maps:without(?READ_ONLY_KEYS,HConfig)}. %%%----------------------------------------------------------------- -%%% Add a standard handler to the logger. -%%% This starts a dedicated handler process which should always -%%% exist if the handler is registered with logger (and should not -%%% exist if the handler is not registered). -%%% -%%% Handler specific config should be provided with a sub map associated -%%% with a key named 'config', e.g: +%%% Start the handler process %%% -%%% Config = #{config => #{sync_mode_qlen => 50} +%%% The process must always exist if the handler is registered with +%%% logger (and must not exist if the handler is not registered). %%% -%%% The standard handler process is linked to logger_sup, which is -%%% part of the kernel application's supervision tree. -start(Name, Config, HandlerState) -> +%%% The handler process is linked to logger_sup, which is part of the +%%% kernel application's supervision tree. +start(#{id := Name} = Config0) -> ChildSpec = #{id => Name, - start => {?MODULE, start_link, [Name,Config,HandlerState]}, + start => {?MODULE, start_link, [Config0]}, restart => temporary, shutdown => 2000, type => worker, modules => [?MODULE]}, case supervisor:start_child(logger_sup, ChildSpec) of - {ok,Pid,Config1} -> + {ok,Pid,Config} -> ok = logger_handler_watcher:register_handler(Name,Pid), - {ok,Config1}; + {ok,Config}; Error -> Error end. -%%%----------------------------------------------------------------- -%%% Start a standard handler process and link to caller. -%%% This function is called by the kernel supervisor when this -%%% handler process gets added --spec start_link(Name, Config, HandlerState) -> {ok,Pid} | {error,Reason} when - Name :: atom(), - Config :: logger:handler_config(), - HandlerState :: map(), - Pid :: pid(), - Reason :: term(). - -start_link(Name, Config, HandlerState) -> - proc_lib:start_link(?MODULE,init,[[Name,Config,HandlerState]]). - -%%%----------------------------------------------------------------- -%%% -get_init_state() -> - #{ctrl_sync_int => ?CONTROLLER_SYNC_INTERVAL, - filesync_ok_qlen => ?FILESYNC_OK_QLEN}. - %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([Name, Config = #{config := HConfig, module := Module}, - State = #{filesync_repeat_interval := FSyncInt, - ctrl_sync_int := CtrlSyncInt}]) -> +init(#{id := Name, module := Module, + formatter := Formatter, config := HConfig0} = Config0) -> RegName = ?name_to_reg_name(Module,Name), register(RegName, self()), process_flag(trap_exit, true), @@ -267,43 +210,36 @@ init([Name, Config = #{config := HConfig, module := Module}, ?init_test_hooks(), ?start_observation(Name), - case Module:init(Name, HConfig) of + case Module:init(Name, HConfig0) of {ok,HState} -> try ets:new(Name, [public]) of ModeTab -> ?set_mode(ModeTab, async), T0 = ?timestamp(), - State1 = - ?merge_with_stats(State#{id => Name, - module => Module, - mode_tab => ModeTab, - mode => async, - ctrl_sync_count => CtrlSyncInt, - last_qlen => 0, - last_log_ts => T0, - last_op => sync, - burst_win_ts => T0, - burst_msg_count => 0, - handler_state => HState}), - Config1 = - Config#{config => HConfig#{handler_pid => self(), - mode_tab => ModeTab}}, - proc_lib:init_ack({ok,self(),Config1}), - if is_integer(FSyncInt) -> - gen_server:cast(self(), repeated_filesync); - true -> - ok - end, - case unset_restart_flag(Name, Module) of - true -> - %% inform about restart - gen_server:cast(self(), {log_handler_info, - "Handler ~p restarted", - [Name]}); - false -> - %% initial start - ok - end, + HConfig = HConfig0#{handler_pid => self(), + mode_tab => ModeTab}, + Config = Config0#{config => HConfig}, + proc_lib:init_ack({ok,self(),Config}), + %% Storing common config in state to avoid copying + %% (sending) the config data for each log message + CommonConfig = maps:with(?CONFIG_KEYS,HConfig), + State = + ?merge_with_stats( + CommonConfig#{id => Name, + module => Module, + mode_tab => ModeTab, + mode => async, + ctrl_sync_count => + ?CONTROLLER_SYNC_INTERVAL, + last_qlen => 0, + last_log_ts => T0, + last_op => sync, + burst_win_ts => T0, + burst_msg_count => 0, + formatter => Formatter, + handler_state => HState}), + State1 = set_repeated_filesync(State), + unset_restart_flag(State1), gen_server:enter_loop(?MODULE, [], State1) catch _:Error -> @@ -326,47 +262,33 @@ handle_call({log, Bin}, _From, State) -> handle_call(filesync, _From, State = #{id := Name, module := Module, handler_state := HandlerState}) -> - {Result,HandlerState1} = Module:sync_filesync(Name,HandlerState), + {Result,HandlerState1} = Module:filesync(Name,sync,HandlerState), {reply, Result, State#{handler_state=>HandlerState1, last_op=>sync}}; -handle_call({change_config,_OldConfig,NewConfig}, _From, +handle_call({change_config, #{formatter:=Formatter, config:=NewHConfig}}, _From, State = #{filesync_repeat_interval := FSyncInt0}) -> - HConfig = maps:get(config, NewConfig, #{}), - State1 = maps:merge(State, HConfig), - case overload_levels_ok(State1) of - true -> - _ = - case maps:get(filesync_repeat_interval, HConfig, undefined) of - undefined -> - ok; - no_repeat -> - _ = logger_h_common:cancel_timer(maps:get(rep_sync_tref, - State, - undefined)); - FSyncInt0 -> - ok; - _FSyncInt1 -> - _ = logger_h_common:cancel_timer(maps:get(rep_sync_tref, - State, - undefined)), - gen_server:cast(self(), repeated_filesync) - end, - {reply, ok, State1}; - false -> - #{sync_mode_qlen := SMQL, - drop_mode_qlen := DMQL, - flush_qlen := FQL} = State1, - {reply, {error,{invalid_levels,{SMQL,DMQL,FQL}}}, State} - end; + %% In the future, if handler_state must be updated due to config + %% change, then we need to add a callback to Module here. + CommonConfig = maps:with(?CONFIG_KEYS,NewHConfig), + State1 = maps:merge(State, CommonConfig), + State2 = + case maps:get(filesync_repeat_interval, NewHConfig) of + FSyncInt0 -> + State1; + _FSyncInt1 -> + set_repeated_filesync(cancel_repeated_filesync(State1)) + end, + {reply, ok, State2#{formatter:=Formatter}}; handle_call(info, _From, State) -> {reply, State, State}; -handle_call(reset, _From, #{module:=Module,handler_state:=HandlerState}=State) -> +handle_call(reset, _From, + #{id:=Name,module:=Module,handler_state:=HandlerState}=State) -> State1 = ?merge_with_stats(State), {reply, ok, State1#{last_qlen => 0, last_log_ts => ?timestamp(), - handler_state => Module:reset_state(HandlerState)}}; + handler_state => Module:reset_state(Name,HandlerState)}}; handle_call(stop, _From, State) -> {stop, {shutdown,stopped}, ok, State}. @@ -376,41 +298,37 @@ handle_cast({log, Bin}, State) -> {_,State1} = do_log(Bin, cast, State), {noreply, State1}; -handle_cast({log_handler_info, Format, Args}, State = #{id:=Name}) -> - log_handler_info(Name, Format, Args, State), - {noreply, State}; - %% If FILESYNC_REPEAT_INTERVAL is set to a millisec value, this %% clause gets called repeatedly by the handler. In order to %% guarantee that a filesync *always* happens after the last log %% event, the repeat operation must be active! handle_cast(repeated_filesync,State = #{filesync_repeat_interval := no_repeat}) -> + %% This clause handles a race condition which may occur when + %% config changes filesync_repeat_interval from an integer value + %% to no_repeat. {noreply,State}; handle_cast(repeated_filesync, State = #{id := Name, module := Module, handler_state := HandlerState, - filesync_repeat_interval := FSyncInt, last_op := LastOp}) -> - HandlerState1 = + State1 = if LastOp == sync -> - HandlerState; + State; true -> - Module:async_filesync(Name,HandlerState) + {_,HS} = Module:filesync(Name, async, HandlerState), + State#{handler_state => HS, last_op => sync} end, - {ok,TRef} = timer:apply_after(FSyncInt, gen_server,cast, - [self(),repeated_filesync]), - {noreply,State#{handler_state=>HandlerState1, - rep_sync_tref => TRef, - last_op => sync}}. + {noreply,set_repeated_filesync(State1)}. -handle_info(Info, #{module := Module, handler_state := HandlerState} = State) -> - {noreply,State#{handler_state => Module:handle_info(Info,HandlerState)}}. +handle_info(Info, #{id := Name, module := Module, + handler_state := HandlerState} = State) -> + {noreply,State#{handler_state => Module:handle_info(Name,Info,HandlerState)}}. terminate(Reason, State = #{id := Name, module := Module, handler_state := HandlerState}) -> - _ = cancel_timer(maps:get(rep_sync_tref, State, undefined)), + _ = cancel_repeated_filesync(State), _ = Module:terminate(Name, Reason, HandlerState), ok = stop_or_restart(Name, Reason, State), unregister(?name_to_reg_name(Module, Name)), @@ -420,10 +338,24 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. +%%%----------------------------------------------------------------- +%%% Internal functions +call(Module, Name, Op) when is_atom(Name) -> + call(?name_to_reg_name(Module,Name), Op); +call(_, Name, Op) -> + {error,{badarg,{Op,[Name]}}}. + +call(Server, Msg) -> + try + gen_server:call(Server, Msg, ?DEFAULT_CALL_TIMEOUT) + catch + _:{timeout,_} -> {error,handler_busy} + end. + %% check for overload between every event (and set Mode to async, %% sync or drop accordingly), but never flush the whole mailbox %% before LogWindowSize events have been handled -do_log(Bin, CallOrCast, State = #{id:=Name, module:=Module, mode:=Mode0}) -> +do_log(Bin, CallOrCast, State = #{id:=Name, mode:=Mode0}) -> T1 = ?timestamp(), %% check if the handler is getting overloaded, or if it's @@ -444,7 +376,7 @@ do_log(Bin, CallOrCast, State = #{id:=Name, module:=Module, mode:=Mode0}) -> end, %% kill the handler if it can't keep up with the load - kill_if_choked(Name, Module, QLen, Mem, State), + kill_if_choked(Name, QLen, Mem, State), if Mode1 == flush -> flush(Name, QLen, T1, State1); @@ -473,8 +405,9 @@ flush(Name, _QLen0, T1, State=#{last_log_ts := _T0, mode_tab := ModeTab}) -> ?observe(Name,{flushed,NewFlushed+1}), State1 = ?update_max_time(?diff_time(T1,_T0),State), + State2 = ?update_max_qlen(_QLen1,State1), {dropped,?update_other(flushed,FLUSHED,NewFlushed, - State1#{mode => ?set_mode(ModeTab,async), + State2#{mode => ?set_mode(ModeTab,async), last_qlen => 0, last_log_ts => T1})}. @@ -484,24 +417,23 @@ write(Name, Mode, T1, Bin, _CallOrCast, handler_state := HandlerState, mode_tab := ModeTab, ctrl_sync_count := CtrlSync, - ctrl_sync_int := CtrlSyncInt, last_qlen := LastQLen, last_log_ts := T0}) -> %% check if we need to limit the number of writes %% during a burst of log events {DoWrite,BurstWinT,BurstMsgCount} = limit_burst(State), - %% only log synhrounously every CtrlSyncInt time, to give the - %% handler time between writes so it can keep up with incoming - %% messages + %% only log synhrounously every ?CONTROLLER_SYNC_INTERVAL time, to + %% give the handler time between writes so it can keep up with + %% incoming messages {Result,LastQLen1,HandlerState1} = if DoWrite, CtrlSync == 0 -> ?observe(Name,{_CallOrCast,1}), - {_,HS1} = Module:sync_write(Name, Bin, HandlerState), + {_,HS1} = Module:write(Name, sync, Bin, HandlerState), {ok,element(2, process_info(self(), message_queue_len)),HS1}; DoWrite -> ?observe(Name,{_CallOrCast,1}), - HS1 = Module:async_write(Name, Bin, HandlerState), + {_,HS1} = Module:write(Name, async, Bin, HandlerState), {ok,LastQLen,HS1}; not DoWrite -> ?observe(Name,{flushed,1}), @@ -519,39 +451,37 @@ write(Name, Mode, T1, Bin, _CallOrCast, {Mode1,BurstMsgCount1,HandlerState2} = if (LastQLen1 < ?FILESYNC_OK_QLEN) andalso (Time > ?IDLE_DETECT_TIME_USEC) -> - HS2 = Module:async_filesync(Name,HandlerState), + {_,HS2} = Module:filesync(Name,async,HandlerState), {?change_mode(ModeTab, Mode, async),0,HS2}; true -> {Mode,BurstMsgCount,HandlerState1} end, - State1 = - ?update_calls_or_casts(_CallOrCast,1,State), - State2 = + State1 = ?update_calls_or_casts(_CallOrCast,1,State), + State2 = ?update_max_qlen(LastQLen1,State1), + State3 = ?update_max_time(Time, - State1#{mode => Mode1, + State2#{mode => Mode1, last_qlen := LastQLen1, last_log_ts => T1, last_op => write, burst_win_ts => BurstWinT, burst_msg_count => BurstMsgCount1, - ctrl_sync_count => if CtrlSync==0 -> CtrlSyncInt; - true -> CtrlSync-1 - end, + ctrl_sync_count => + if CtrlSync==0 -> ?CONTROLLER_SYNC_INTERVAL; + true -> CtrlSync-1 + end, handler_state => HandlerState2}), - {Result,State2}. + {Result,State3}. log_handler_info(Name, Format, Args, #{module:=Module, + formatter:=Formatter, handler_state:=HandlerState}) -> - Config = - case logger:get_handler_config(Name) of - {ok,Conf} -> Conf; - _ -> #{formatter=>{?DEFAULT_FORMATTER,?DEFAULT_FORMAT_CONFIG}} - end, + Config = #{formatter=>Formatter}, Meta = #{time=>erlang:system_time(microsecond)}, Bin = log_to_binary(#{level => notice, msg => {Format,Args}, meta => Meta}, Config), - _ = Module:async_write(Name, Bin, HandlerState), + _ = Module:write(Name, async, Bin, HandlerState), ok. %%%----------------------------------------------------------------- @@ -612,7 +542,20 @@ string_to_binary(String) -> %%%----------------------------------------------------------------- %%% Check that the configuration term is valid check_config(Config) when is_map(Config) -> - check_common_config(maps:to_list(Config)). + case check_common_config(maps:to_list(Config)) of + ok -> + case overload_levels_ok(Config) of + true -> + ok; + false -> + Faulty = maps:with([sync_mode_qlen, + drop_mode_qlen, + flush_qlen],Config), + {error,{invalid_levels,Faulty}} + end; + Error -> + Error + end. check_common_config([{sync_mode_qlen,N}|Config]) when is_integer(N) -> check_common_config(Config); @@ -667,15 +610,13 @@ call_cast_or_drop(_Name, HandlerPid, ModeTab, Bin) -> async -> gen_server:cast(HandlerPid, {log,Bin}); sync -> - try gen_server:call(HandlerPid, {log,Bin}, ?DEFAULT_CALL_TIMEOUT) of - %% if return value from call == dropped, the - %% message has been flushed by handler and should - %% therefore not be counted as dropped in stats - ok -> ok; - dropped -> ok - catch - _:{timeout,_} -> - ?observe(_Name,{dropped,1}) + case call(HandlerPid, {log,Bin}) of + ok -> + ok; + _Other -> + %% dropped or {error,handler_busy} + ?observe(_Name,{dropped,1}), + ok end; drop -> ?observe(_Name,{dropped,1}) @@ -686,10 +627,8 @@ call_cast_or_drop(_Name, HandlerPid, ModeTab, Bin) -> end, ok. -handler_exit(_Name, Reason) -> - exit(Reason). - -set_restart_flag(Name, Module) -> +set_restart_flag(#{id := Name, module := Module} = State) -> + log_handler_info(Name, "Handler ~p overloaded and stopping", [Name], State), Flag = list_to_atom(lists:concat([Module,"_",Name,"_restarting"])), spawn(fun() -> register(Flag, self()), @@ -697,14 +636,14 @@ set_restart_flag(Name, Module) -> end), ok. -unset_restart_flag(Name, Module) -> +unset_restart_flag(#{id := Name, module := Module} = State) -> Flag = list_to_atom(lists:concat([Module,"_",Name,"_restarting"])), case whereis(Flag) of undefined -> - false; + ok; Pid -> exit(Pid, kill), - true + log_handler_info(Name, "Handler ~p restarted", [Name], State) end. check_load(State = #{id:=_Name, mode_tab := ModeTab, mode := Mode, @@ -764,24 +703,17 @@ limit_burst(#{burst_win_ts := BurstWinT0, {true,BurstWinT0,BurstMsgCount+1} end. -kill_if_choked(Name, Module, QLen, Mem, - State = #{overload_kill_enable := KillIfOL, - overload_kill_qlen := OLKillQLen, - overload_kill_mem_size := OLKillMem}) -> +kill_if_choked(Name, QLen, Mem, State = #{overload_kill_enable := KillIfOL, + overload_kill_qlen := OLKillQLen, + overload_kill_mem_size := OLKillMem}) -> if KillIfOL andalso ((QLen > OLKillQLen) orelse (Mem > OLKillMem)) -> - log_handler_info(Name, - "Handler ~p overloaded and stopping", - [Name], State), - set_restart_flag(Name, Module), - handler_exit(Name, {shutdown,{overloaded,Name,QLen,Mem}}); + set_restart_flag(State), + exit({shutdown,{overloaded,Name,QLen,Mem}}); true -> ok end. -flush_log_events() -> - flush_log_events(-1). - flush_log_events(Limit) -> process_flag(priority, high), Flushed = flush_log_events(0, Limit), @@ -804,14 +736,27 @@ flush_log_events(N, Limit) -> 0 -> N end. -cancel_timer(TRef) when is_atom(TRef) -> ok; -cancel_timer(TRef) -> timer:cancel(TRef). - +set_repeated_filesync(#{filesync_repeat_interval:=FSyncInt} = State) + when is_integer(FSyncInt) -> + {ok,TRef} = timer:apply_after(FSyncInt, gen_server, cast, + [self(),repeated_filesync]), + State#{rep_sync_tref=>TRef}; +set_repeated_filesync(State) -> + State. + +cancel_repeated_filesync(State) -> + case maps:take(rep_sync_tref,State) of + {TRef,State1} -> + _ = timer:cancel(TRef), + State1; + error -> + State + end. stop_or_restart(Name, {shutdown,Reason={overloaded,_Name,_QLen,_Mem}}, #{overload_kill_restart_after := RestartAfter}) -> %% If we're terminating because of an overload situation (see - %% kill_if_choked/5), we need to remove the handler and set a + %% kill_if_choked/4), we need to remove the handler and set a %% restart timer. A separate process must perform this in order to %% avoid deadlock. HandlerPid = self(), |