Skip to content

Commit 552b21e

Browse files
authored
Merge pull request #49 from zmstone/251214-fail-fast-for-unreachable-peer
251214 fail fast for unreachable peer
2 parents 6a6164e + 418167e commit 552b21e

6 files changed

Lines changed: 240 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
Below is a non-exhaustive list of changes between `gen_rpc` versions.
44

5+
## 3.4.4
6+
7+
- Refactor client initialization to use async connection setup via `handle_continue/2`, preventing dispatcher blocking during client startup
8+
- Fix supervisor crash reports by using `{shutdown, Reason}` for graceful client termination instead of abnormal reasons
9+
- Improve error handling and logging for connection failures
10+
11+
## 3.4.3
12+
13+
- Performance: Use lazy tracepoints in hot code paths to reduce overhead
14+
15+
## 3.4.2
16+
17+
- Chore: Reduce debug level logging
18+
- CI: Update GitHub action versions
19+
20+
## 3.4.1
21+
22+
- Fix: Don't crash the server due to client problems
23+
524
## 3.4.0
625

726
- Back-pressure towards sender by `{active, 100}` socket option in acceptor side.

src/driver/gen_rpc_driver_ssl.erl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,9 @@ connect(Node, Port) when is_atom(Node) ->
5656
[Node, Port, gen_rpc_helper:socket_to_string(Socket)]),
5757
{ok, Socket};
5858
{error, Reason} ->
59-
?log(error, "event=connect_to_remote_server peer=~s, port=~p, reason=~0p", [Node, Port, Reason]),
6059
{error, {badtcp,Reason}}
6160
end.
6261

63-
6462
-spec listen(inet:port_number()) -> {ok, ssl:sslsocket()} | {error, term()}.
6563
listen(Port) when is_integer(Port) ->
6664
SslOpts = merge_ssl_options(server),

src/driver/gen_rpc_driver_tcp.erl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ connect(Node, Port) when is_atom(Node) ->
5353
[Node, Port, gen_rpc_helper:socket_to_string(Socket)]),
5454
{ok, Socket};
5555
{error, Reason} ->
56-
?log(error, "event=connect_to_remote_server, peer=~s, port=~p, reason=~0p", [Node, Port, Reason]),
5756
{error, {badtcp,Reason}}
5857
end.
5958

src/gen_rpc_client.erl

Lines changed: 121 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@
2626
-define(NAME(NODE_OR_TUPLE), {client, NODE_OR_TUPLE}).
2727

2828
%%% Local state
29-
-record(state, {socket :: port(),
29+
-record(state, {socket :: port() | undefined,
3030
driver :: atom(),
3131
driver_mod :: atom(),
3232
driver_closed :: atom(),
3333
driver_error :: atom(),
3434
max_batch_size :: integer(),
35-
keepalive :: tuple()}).
35+
keepalive :: tuple() | undefined}).
3636

3737
%%% Supervisor functions
3838
-export([start_link/1, stop/1]).
@@ -50,7 +50,7 @@
5050

5151
%%% Behaviour callbacks
5252
-export([init/1, handle_call/3, handle_cast/2,
53-
handle_info/2, terminate/2, code_change/3]).
53+
handle_info/2, handle_continue/2, terminate/2, code_change/3]).
5454

5555
%%% Process exports
5656
-export([async_call_worker/5, cast_worker/4]).
@@ -104,6 +104,12 @@ call(NodeOrTuple, M, F, A, RecvTimeout, SendTimeout) when ?is_node_or_tuple(Node
104104
gen_server:call(Pid, {{call,M,F,A}, SendTimeout}, gen_rpc_helper:get_call_receive_timeout(RecvTimeout))
105105
catch
106106
exit:{timeout,_Reason} -> {badrpc,timeout};
107+
exit:{{shutdown, {badrpc, Reason}}, {gen_server, call, _}} ->
108+
%% the client gen_server stopped in handle_continue
109+
{badrpc, Reason};
110+
exit:{{shutdown, Reason}, {gen_server, call, _}} ->
111+
%% the client gen_server stopped with shutdown reason (non-badrpc)
112+
{badrpc, Reason};
107113
exit:OtherReason -> {badrpc, {unknown_error, OtherReason}}
108114
end;
109115
{error, Reason} ->
@@ -230,12 +236,15 @@ where_is(NodeOrTuple) ->
230236
%%% ===================================================
231237
%%% Behaviour callbacks
232238
%%% ===================================================
233-
%% If we're called with a key, remove it, the key is only relevant
234-
%% at the process name level
235-
init({{Node,_Key}}) ->
236-
init({Node});
237-
238-
init({Node}) ->
239+
init({NodeOrTuple}) ->
240+
%% Set process label for OTP >= 27
241+
ok = set_process_label_if_supported({?MODULE, NodeOrTuple}),
242+
{Node, Key} = case is_atom(NodeOrTuple) of
243+
true ->
244+
{NodeOrTuple, undefined};
245+
false ->
246+
NodeOrTuple
247+
end,
239248
ok = gen_rpc_helper:set_optimal_process_flags(),
240249
case gen_rpc_helper:get_client_config_per_node(Node) of
241250
{error, Reason} ->
@@ -244,38 +253,51 @@ init({Node}) ->
244253
{Driver, Port} ->
245254
{DriverMod, DriverClosed, DriverError} = gen_rpc_helper:get_client_driver_options(Driver),
246255
?log(info, "event=initializing_client driver=~s node=\"~s\" port=~B", [Driver, Node, Port]),
247-
case gen_rpc_auth:connect_with_auth(DriverMod, Node, Port) of
248-
{ok, Socket} ->
249-
Interval = application:get_env(?APP, keepalive_interval, 60), % 60s
250-
StatFun = fun() ->
251-
case DriverMod:getstat(Socket, [recv_oct]) of
252-
{ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct};
253-
{error, Error} -> {error, Error}
254-
end
255-
end,
256-
case gen_rpc_keepalive:start(StatFun, Interval, {keepalive, check}) of
257-
{ok, KeepAlive} ->
258-
MaxBatchSize = application:get_env(?APP, max_batch_size, 0),
259-
{ok, #state{socket=Socket,
260-
driver=Driver,
261-
driver_mod=DriverMod,
262-
driver_closed=DriverClosed,
263-
driver_error=DriverError,
264-
max_batch_size=MaxBatchSize,
265-
keepalive=KeepAlive},
266-
gen_rpc_helper:get_inactivity_timeout(?MODULE)};
267-
{error, Error} ->
268-
?log(error, "event=start_keepalive_failed driver=~p, reason=\"~p\"", [Driver, Error]),
269-
{stop, Error}
270-
end;
271-
{error, ReasonTuple} ->
272-
?log(error, "event=client_authentication_failed driver=~s reason=\"~p\"", [Driver, ReasonTuple]),
273-
{stop, ReasonTuple};
274-
{unreachable, Reason} ->
275-
%% This should be badtcp but to conform with
276-
%% the RPC library we return badrpc
277-
{stop, {badrpc, Reason}}
278-
end
256+
MaxBatchSize = application:get_env(?APP, max_batch_size, 0),
257+
InitialState = #state{socket=undefined,
258+
driver=Driver,
259+
driver_mod=DriverMod,
260+
driver_closed=DriverClosed,
261+
driver_error=DriverError,
262+
max_batch_size=MaxBatchSize,
263+
keepalive=undefined},
264+
%% Return immediately, connection happens in handle_continue
265+
{ok, InitialState, {continue, {connect, Node, Port, Key}}}
266+
end.
267+
268+
%% Handle async connection initialization
269+
handle_continue({connect, Node, Port, Key}, #state{driver_mod = DriverMod, driver = Driver} = State) ->
270+
IdForLog = case Key =:= undefined of
271+
true ->
272+
io_lib:format("peer=~s://~s:~p", [Driver, Node, Port]);
273+
false ->
274+
io_lib:format("peer=~s://~s:~p, key=~p", [Driver, Node, Port, Key])
275+
end,
276+
case gen_rpc_auth:connect_with_auth(DriverMod, Node, Port) of
277+
{ok, Socket} ->
278+
Interval = application:get_env(?APP, keepalive_interval, 60), % 60s
279+
StatFun = fun() ->
280+
case DriverMod:getstat(Socket, [recv_oct]) of
281+
{ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct};
282+
{error, Error} -> {error, Error}
283+
end
284+
end,
285+
case gen_rpc_keepalive:start(StatFun, Interval, {keepalive, check}) of
286+
{ok, KeepAlive} ->
287+
ConnectedState = State#state{socket=Socket, keepalive=KeepAlive},
288+
{noreply, ConnectedState, gen_rpc_helper:get_inactivity_timeout(?MODULE)};
289+
{error, Error} ->
290+
?log(error, "event=start_keepalive_failed ~s reason=\"~0p\"", [IdForLog, Error]),
291+
{stop, {shutdown, Error}, State}
292+
end;
293+
{error, ReasonTuple} ->
294+
?log(error, "event=client_authentication_failed ~s reason=\"~0p\"", [IdForLog, ReasonTuple]),
295+
{stop, {shutdown, ReasonTuple}, State};
296+
{unreachable, Reason} ->
297+
%% This should be badtcp but to conform with
298+
%% the RPC library we return badrpc
299+
?log(error, "event=connect_to_remote_server, ~s, reason=\"~0p\"", [IdForLog, Reason]),
300+
{stop, {shutdown, {badrpc, Reason}}, State}
279301
end.
280302

281303
%% This is the actual CALL handler
@@ -288,7 +310,7 @@ handle_call({{call,_M,_F,_A} = PacketTuple, SendTimeout}, Caller, #state{socket=
288310
{error, Reason} ->
289311
?log(error, "message=call event=transmission_failed driver=~s socket=\"~s\" caller=\"~p\" reason=\"~p\"",
290312
[Driver, gen_rpc_helper:socket_to_string(Socket), Caller, Reason]),
291-
{stop, Reason, Reason, State};
313+
{stop, {shutdown, Reason}, Reason, State};
292314
ok ->
293315
?log(debug, "message=call event=transmission_succeeded driver=~s socket=\"~s\" caller=\"~p\"",
294316
[Driver, gen_rpc_helper:socket_to_string(Socket), Caller]),
@@ -313,7 +335,7 @@ handle_cast({{async_call,_M,_F,_A} = PacketTuple, Caller, Ref}, #state{socket=So
313335
{error, Reason} ->
314336
?log(error, "message=async_call event=transmission_failed driver=~s socket=\"~s\" worker_pid=\"~p\" call_ref=\"~p\" reason=\"~p\"",
315337
[Driver, gen_rpc_helper:socket_to_string(Socket), Caller, Ref, Reason]),
316-
{stop, Reason, Reason, State};
338+
{stop, {shutdown, Reason}, Reason, State};
317339
ok ->
318340
?log(debug, "message=async_call event=transmission_succeeded driver=~s socket=\"~s\" worker_pid=\"~p\" call_ref=\"~p\"",
319341
[Driver, gen_rpc_helper:socket_to_string(Socket), Caller, Ref]),
@@ -394,7 +416,7 @@ handle_info({keepalive, check}, #state{driver=Driver, keepalive=KeepAlive} = Sta
394416
{error, Reason} ->
395417
?log(error, "event=keepalive_check_failed driver=~p, reason=\"~p\" action=stopping",
396418
[Driver, Reason]),
397-
{stop, Reason, State}
419+
{stop, {shutdown, Reason}, State}
398420
end;
399421

400422
handle_info({inet_reply, _Socket, ok}, State) ->
@@ -413,10 +435,25 @@ handle_info(Msg, #state{socket=Socket, driver=Driver} = State) ->
413435
code_change(_OldVsn, State, _Extra) ->
414436
{ok, State}.
415437

438+
terminate(_Reason, #state{keepalive=undefined}) ->
439+
ok;
416440
terminate(_Reason, #state{keepalive=KeepAlive}) ->
417441
gen_rpc_keepalive:cancel(KeepAlive),
418442
ok.
419443

444+
%%% ===================================================
445+
%%% Helper functions
446+
%%% ===================================================
447+
448+
%% Set process label for OTP >= 27
449+
-if(?OTP_RELEASE >= 27).
450+
set_process_label_if_supported(Label) ->
451+
proc_lib:set_label(Label).
452+
-else.
453+
set_process_label_if_supported(_Label) ->
454+
ok.
455+
-endif.
456+
420457
%%% ===================================================
421458
%%% Private functions
422459
%%% ===================================================
@@ -437,7 +474,7 @@ send_cast(PacketTuple, #state{socket=Socket, driver=Driver, driver_mod=DriverMod
437474
, driver => Driver
438475
, reason => Reason
439476
}),
440-
{stop, Reason, State};
477+
{stop, {shutdown, Reason}, State};
441478
ok ->
442479
ok = case Activate of
443480
true -> DriverMod:activate_socket(Socket);
@@ -455,7 +492,7 @@ send_ping(#state{socket=Socket, driver=Driver, driver_mod=DriverMod} = State) ->
455492
{error, Reason} ->
456493
?log(error, "message=ping event=transmission_failed driver=~s socket=\"~s\" reason=\"~p\"",
457494
[Driver, gen_rpc_helper:socket_to_string(Socket), Reason]),
458-
{stop, Reason, State};
495+
{stop, {shutdown, Reason}, State};
459496
ok ->
460497
?log(debug, "message=ping event=transmission_succeeded driver=~s socket=\"~s\"",
461498
[Driver, gen_rpc_helper:socket_to_string(Socket)]),
@@ -496,47 +533,54 @@ cast_worker(NodeOrTuple, Cast, Ret, SendTimeout) ->
496533
async_call_worker(NodeOrTuple, M, F, A, Ref) ->
497534
TTL = gen_rpc_helper:get_async_call_inactivity_timeout(),
498535
PidName = ?NAME(NodeOrTuple),
499-
SrvPid = case gen_rpc_registry:whereis_name(PidName) of
536+
case gen_rpc_registry:whereis_name(PidName) of
500537
undefined ->
501538
?log(info, "event=client_process_not_found target=\"~p\" action=spawning_client", [NodeOrTuple]),
502539
case gen_rpc_dispatcher:start_client(NodeOrTuple) of
503540
{ok, NewPid} ->
541+
%% Monitor the client process in case it dies before handling the cast
542+
MRef = erlang:monitor(process, NewPid),
504543
ok = gen_server:cast(NewPid, {{async_call,M,F,A}, self(), Ref}),
505-
NewPid;
544+
wait_for_async_reply(NewPid, MRef, Ref, TTL);
506545
{error, {badrpc,_} = RpcError} ->
507-
RpcError
546+
wait_for_yield_and_send(RpcError, Ref, TTL)
508547
end;
509548
Pid ->
510549
?log(debug, "event=client_process_found pid=\"~p\" target=\"~p\"", [Pid, NodeOrTuple]),
550+
%% Monitor the client process in case it dies before handling the cast
551+
MRef = erlang:monitor(process, Pid),
511552
ok = gen_server:cast(Pid, {{async_call,M,F,A}, self(), Ref}),
512-
Pid
513-
end,
514-
case SrvPid of
515-
SrvPid when is_pid(SrvPid) ->
516-
receive
517-
%% Wait for the reply from the node's gen_rpc client process
518-
{SrvPid,Ref,async_call,Reply} ->
519-
%% Wait for a yield request from the caller
520-
receive
521-
{YieldPid,Ref,yield} ->
522-
YieldPid ! {self(), Ref, async_call, Reply}
523-
after
524-
TTL ->
525-
exit({error, async_call_cleanup_timeout_reached})
526-
end
527-
after
528-
TTL ->
529-
exit({error, async_call_cleanup_timeout_reached})
530-
end;
531-
TRpcError ->
532-
%% Wait for a yield request from the caller
533-
receive
534-
{YieldPid,Ref,yield} ->
535-
YieldPid ! {self(), Ref, async_call, TRpcError}
536-
after
537-
TTL ->
538-
exit({error, async_call_cleanup_timeout_reached})
539-
end
553+
wait_for_async_reply(Pid, MRef, Ref, TTL)
554+
end.
555+
556+
wait_for_async_reply(Pid, MRef, Ref, TTL) ->
557+
receive
558+
%% Wait for the reply from the node's gen_rpc client process
559+
{Pid,Ref,async_call,Reply} ->
560+
erlang:demonitor(MRef, [flush]),
561+
wait_for_yield_and_send(Reply, Ref, TTL);
562+
{'DOWN', MRef, process, Pid, Reason} ->
563+
%% Client process died before handling the cast
564+
ErrorReply = case Reason of
565+
{shutdown, {badrpc, _} = BadRpc} -> BadRpc;
566+
{shutdown, ShutdownReason} -> {badrpc, ShutdownReason};
567+
_ -> {badrpc, Reason}
568+
end,
569+
wait_for_yield_and_send(ErrorReply, Ref, TTL)
570+
after
571+
TTL ->
572+
erlang:demonitor(MRef, [flush]),
573+
exit({error, async_call_cleanup_timeout_reached})
574+
end.
575+
576+
wait_for_yield_and_send(Result, Ref, TTL) ->
577+
%% Wait for a yield request from the caller and send the result
578+
receive
579+
{YieldPid,Ref,yield} ->
580+
YieldPid ! {self(), Ref, async_call, Result}
581+
after
582+
TTL ->
583+
exit({error, async_call_cleanup_timeout_reached})
540584
end.
541585

542586
parse_multicall_results(Keys, Nodes, undefined) ->

src/gen_rpc_registry.erl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ start_link() ->
4242
-spec nodes() -> [node()].
4343
nodes() ->
4444
ThisNode = node(),
45-
lists:usort(
46-
ets:select(?TAB,
47-
ets:fun2ms(fun({{client, {Node, _}}, _}) when Node =/= ThisNode -> Node;
48-
({{client, Node}, _}) when Node =/= ThisNode -> Node
49-
end))).
50-
45+
%% Select all client entries with their Pids, then filter by process liveness
46+
AllEntries = ets:select(?TAB,
47+
ets:fun2ms(fun({{client, {Node, _}}, Pid}) when Node =/= ThisNode -> {Node, Pid};
48+
({{client, Node}, Pid}) when Node =/= ThisNode -> {Node, Pid}
49+
end)),
50+
%% Filter out entries where the process is not alive
51+
AliveNodes = [Node || {Node, Pid} <- AllEntries, is_process_alive(Pid)],
52+
lists:usort(AliveNodes).
5153

5254
-spec all_processes(_Tag) -> [{_Key, pid()}].
5355
all_processes(Tag) ->

0 commit comments

Comments
 (0)