From f986565050ac30075ef3c0a451bf6dad91c7c446 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 13 Sep 2016 11:15:32 +0200 Subject: Implement call/3 dirty_timeout --- lib/stdlib/doc/src/gen_statem.xml | 32 ++++++++-- lib/stdlib/src/gen_statem.erl | 110 ++++++++++++++++++++++------------- lib/stdlib/test/gen_statem_SUITE.erl | 46 ++++++++++++++- 3 files changed, 140 insertions(+), 48 deletions(-) diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 3322571b2c..17f1526a21 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -919,18 +919,40 @@ handle_event(_, _, State, Data) ->

- For Timeout =/= infinity, + For Timeout < infinity, to avoid getting a late reply in the caller's - inbox, this function spawns a proxy process that + inbox if the caller should catch exceptions, + this function spawns a proxy process that does the call. A late reply gets delivered to the dead proxy process, hence gets discarded. This is less efficient than using - Timeout =:= infinity. + Timeout == infinity.

- The call can fail, for example, if the gen_statem dies - before or during this function call. + Timeout can also be a tuple + {clean_timeout,T} or + {dirty_timeout,T}, where + T is the timeout time. + {clean_timeout,T} works like + just T described in the note above + and uses a proxy process for T < infinity, + while {dirty_timeout,T} + bypasses the proxy process which is more lightweight. +

+ +

+ If you combine catching exceptions from this function + with {dirty_timeout,T} + to avoid that the calling process dies when the call + times out, you will have to be prepared to handle + a late reply. + So why not just allow the calling process to die? +

+
+

+ The call can also fail, for example, if the gen_statem + dies before or during this function call.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 3b3477b282..46c0e92a9b 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -385,53 +385,79 @@ call(ServerRef, Request) -> -spec call( ServerRef :: server_ref(), Request :: term(), - Timeout :: timeout()) -> + Timeout :: + timeout() | + {'clean_timeout',T :: timeout()} | + {'dirty_timeout',T :: timeout()}) -> Reply :: term(). -call(ServerRef, Request, infinity) -> - try gen:call(ServerRef, '$gen_call', Request, infinity) of - {ok,Reply} -> - Reply - catch - Class:Reason -> - erlang:raise( - Class, - {Reason,{?MODULE,call,[ServerRef,Request,infinity]}}, - erlang:get_stacktrace()) - end; call(ServerRef, Request, Timeout) -> - %% Call server through proxy process to dodge any late reply - Ref = make_ref(), - Self = self(), - Pid = spawn( - fun () -> - Self ! - try gen:call( - ServerRef, '$gen_call', Request, Timeout) of - Result -> - {Ref,Result} - catch Class:Reason -> - {Ref,Class,Reason,erlang:get_stacktrace()} - end - end), - Mref = monitor(process, Pid), - receive - {Ref,Result} -> - demonitor(Mref, [flush]), - case Result of + case parse_timeout(Timeout) of + {dirty_timeout,T} -> + try gen:call(ServerRef, '$gen_call', Request, T) of {ok,Reply} -> Reply + catch + Class:Reason -> + erlang:raise( + Class, + {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}}, + erlang:get_stacktrace()) + end; + {clean_timeout,T} -> + %% Call server through proxy process to dodge any late reply + Ref = make_ref(), + Self = self(), + Pid = spawn( + fun () -> + Self ! + try gen:call( + ServerRef, '$gen_call', Request, T) of + Result -> + {Ref,Result} + catch Class:Reason -> + {Ref,Class,Reason, + erlang:get_stacktrace()} + end + end), + Mref = monitor(process, Pid), + receive + {Ref,Result} -> + demonitor(Mref, [flush]), + case Result of + {ok,Reply} -> + Reply + end; + {Ref,Class,Reason,Stacktrace} -> + demonitor(Mref, [flush]), + erlang:raise( + Class, + {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}}, + Stacktrace); + {'DOWN',Mref,_,_,Reason} -> + %% There is a theoretical possibility that the + %% proxy process gets killed between try--of and ! + %% so this clause is in case of that + exit(Reason) end; - {Ref,Class,Reason,Stacktrace} -> - demonitor(Mref, [flush]), - erlang:raise( - Class, - {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}}, - Stacktrace); - {'DOWN',Mref,_,_,Reason} -> - %% There is a theoretical possibility that the - %% proxy process gets killed between try--of and ! - %% so this clause is in case of that - exit(Reason) + Error when is_atom(Error) -> + erlang:error(Error, [ServerRef,Request,Timeout]) + end. + +parse_timeout(Timeout) -> + case Timeout of + {clean_timeout,infinity} -> + {dirty_timeout,infinity}; + {clean_timeout,_} -> + Timeout; + {dirty_timeout,_} -> + Timeout; + {_,_} -> + %% Be nice and throw a badarg for speling errors + badarg; + infinity -> + {dirty_timeout,infinity}; + T -> + {clean_timeout,T} end. %% Reply from a state machine callback to whom awaits in call/2 diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 1d1417c2e6..e092940174 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -57,7 +57,7 @@ tcs(start) -> tcs(stop) -> [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]; tcs(abnormal) -> - [abnormal1, abnormal2]; + [abnormal1, abnormal1clean, abnormal1dirty, abnormal2]; tcs(sys) -> [sys1, call_format_status, error_format_status, terminate_crash_format, @@ -451,8 +451,52 @@ abnormal1(Config) -> gen_statem:call(Name, {delayed_answer,1000}, 10), Reason), ok = gen_statem:stop(Name), + ?t:sleep(1100), ok = verify_empty_msgq(). +%% Check that time outs in calls work +abnormal1clean(Config) -> + Name = abnormal1clean, + LocalSTM = {local,Name}, + + {ok, _Pid} = + gen_statem:start(LocalSTM, ?MODULE, start_arg(Config, []), []), + + %% timeout call. + delayed = + gen_statem:call(Name, {delayed_answer,1}, {clean_timeout,100}), + {timeout,_} = + ?EXPECT_FAILURE( + gen_statem:call( + Name, {delayed_answer,1000}, {clean_timeout,10}), + Reason), + ok = gen_statem:stop(Name), + ?t:sleep(1100), + ok = verify_empty_msgq(). + +%% Check that time outs in calls work +abnormal1dirty(Config) -> + Name = abnormal1dirty, + LocalSTM = {local,Name}, + + {ok, _Pid} = + gen_statem:start(LocalSTM, ?MODULE, start_arg(Config, []), []), + + %% timeout call. + delayed = + gen_statem:call(Name, {delayed_answer,1}, {dirty_timeout,100}), + {timeout,_} = + ?EXPECT_FAILURE( + gen_statem:call( + Name, {delayed_answer,1000}, {dirty_timeout,10}), + Reason), + ok = gen_statem:stop(Name), + ?t:sleep(1100), + case flush() of + [{Ref,delayed}] when is_reference(Ref) -> + ok + end. + %% Check that bad return values makes the stm crash. Note that we must %% trap exit since we must link to get the real bad_return_ error abnormal2(Config) -> -- cgit v1.2.3