Skip to content

Commit cd2a01e

Browse files
authored
Merge pull request #58 from emqx/feat/server-port-precheck-api
feat: add server port precheck api and tests
2 parents 09b1927 + 1ea0896 commit cd2a01e

7 files changed

Lines changed: 292 additions & 14 deletions

File tree

CHANGELOG.md

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

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

5+
## 3.6.0
6+
7+
- Support compression when serializing Erlang term to binary.
8+
- Add `gen_rpc:check_server_ports_available/0` to help checking if `Address:Port` is in use.
9+
510
## 3.5.1
611

712
- Fix log 'domain' metadata.

src/gen_rpc.erl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
-export([sbcast/2, sbcast/3]).
3333

3434
%% Misc functions
35-
-export([nodes/0]).
35+
-export([nodes/0, check_server_ports_available/0]).
3636

3737
%% Set logger module
3838
-export([set_logger/1]).
@@ -148,6 +148,10 @@ sbcast(Nodes, Name, Msg) when is_list(Nodes), is_atom(Name) ->
148148
nodes() ->
149149
gen_rpc_registry:nodes().
150150

151+
-spec check_server_ports_available() -> ok | {error, [map()]}.
152+
check_server_ports_available() ->
153+
gen_rpc_helper:check_server_ports_available().
154+
151155
-spec set_logger(module()) -> ok.
152156
set_logger(Module) ->
153157
gen_rpc_logger:set_logger(Module).

src/gen_rpc_client.erl

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,13 @@ call(NodeOrTuple, M, F, A, RecvTimeout, SendTimeout) when ?is_node_or_tuple(Node
107107
catch
108108
exit:{timeout,_Reason} -> {badrpc,timeout};
109109
exit:{{shutdown, {badrpc, Reason}}, {gen_server, call, _}} ->
110-
%% the client gen_server stopped in handle_continue
110+
%% The client gen_server stopped in handle_continue.
111111
{badrpc, Reason};
112112
exit:{{shutdown, Reason}, {gen_server, call, _}} ->
113-
%% the client gen_server stopped with shutdown reason (non-badrpc)
113+
%% The client gen_server stopped with shutdown reason (non-badrpc).
114114
{badrpc, Reason};
115-
exit:OtherReason -> {badrpc, {unknown_error, OtherReason}}
115+
exit:OtherReason ->
116+
{badrpc, {unknown_error, OtherReason}}
116117
end;
117118
{error, Reason} ->
118119
Reason
@@ -296,12 +297,14 @@ handle_continue({connect, Node, Port, Key}, #state{driver_mod = DriverMod, drive
296297
{stop, {shutdown, Error}, State}
297298
end;
298299
{error, ReasonTuple} ->
299-
?log(error, "client_authentication_failed",
300-
#{driver => Driver,
301-
node => Node,
302-
port => Port,
303-
key => Key,
304-
cause => ReasonTuple}),
300+
?tp(error, client_authentication_failed, #{
301+
driver => Driver,
302+
node => Node,
303+
port => Port,
304+
key => Key,
305+
cause => ReasonTuple,
306+
domain => ?D_CLIENT
307+
}),
305308
{stop, {shutdown, ReasonTuple}, State};
306309
{unreachable, Reason} ->
307310
%% This should be badtcp but to conform with

src/gen_rpc_helper.erl

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
get_inactivity_timeout/1,
4545
get_async_call_inactivity_timeout/0,
4646
get_listen_ip_config/0,
47-
term_to_iovec/1]).
47+
term_to_iovec/1,
48+
check_server_ports_available/0]).
4849

4950
%%% ===================================================
5051
%%% Public API
@@ -231,6 +232,14 @@ get_async_call_inactivity_timeout() ->
231232
get_user_tcp_opts(Type) ->
232233
get_user_tcp_opts(?USER_TCP_OPTS, Type).
233234

235+
-spec check_server_ports_available() -> ok | {error, [map()]}.
236+
check_server_ports_available() ->
237+
Endpoints = server_listen_endpoints(),
238+
case find_port_conflicts(Endpoints) of
239+
[] -> ok;
240+
Conflicts -> {error, Conflicts}
241+
end.
242+
234243
%%% ===================================================
235244
%%% Private functions
236245
%%% ===================================================
@@ -250,10 +259,59 @@ get_client_config_from_map(Node, NodeConfig) ->
250259
{ok, {Driver,Port}} ->
251260
{Driver, Port};
252261
{ok, Port} ->
253-
{ok, Driver} = application:get_env(?APP, default_client_driver),
262+
{ok, Driver} = application:get_env(?APP, default_client_driver),
254263
{Driver, Port}
255264
end.
256265

266+
server_listen_endpoints() ->
267+
IP = probe_ip(),
268+
lists:filtermap(
269+
fun(Driver) ->
270+
case is_driver_enabled(Driver) of
271+
false ->
272+
false;
273+
true ->
274+
{_DriverMod, Port, _ClosedMsg, _ErrorMsg} = get_server_driver_options(Driver),
275+
{true, #{driver => Driver, port => Port, ip => IP}}
276+
end
277+
end,
278+
[tcp, ssl]
279+
).
280+
281+
probe_ip() ->
282+
case get_socket_ip() of
283+
{_, IP} -> IP;
284+
undefined -> undefined
285+
end.
286+
287+
find_port_conflicts(Endpoints) ->
288+
lists:filtermap(
289+
fun(Endpoint) ->
290+
case probe_endpoint(Endpoint) of
291+
ok -> false;
292+
{error, Reason} -> {true, Endpoint#{reason => Reason}}
293+
end
294+
end,
295+
Endpoints
296+
).
297+
298+
probe_endpoint(#{port := Port, ip := IP}) ->
299+
Opts = [binary, {reuseaddr, false}, {active, false}] ++ probe_ip_opts(IP),
300+
case gen_tcp:listen(Port, Opts) of
301+
{ok, Socket} ->
302+
ok = gen_tcp:close(Socket),
303+
ok;
304+
{error, Reason} ->
305+
{error, Reason}
306+
end.
307+
308+
probe_ip_opts(undefined) ->
309+
[];
310+
probe_ip_opts(IP) when is_tuple(IP), tuple_size(IP) =:= 8 ->
311+
[inet6, {ip, IP}];
312+
probe_ip_opts(IP) ->
313+
[{ip, IP}].
314+
257315
hybrid_proplist_compare({K1,_V1}, {K2,_V2}) ->
258316
K1 =< K2;
259317

test/auth_SUITE.erl

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,15 @@ t_auth_server_fail(Config) ->
131131
end),
132132
ok = gen_rpc_test_helper:start_master(Driver),
133133
ok = gen_rpc_test_helper:start_slave(Driver),
134-
?assertMatch({badrpc, invalid_cookie}, gen_rpc:call(?SLAVE, ?MODULE, canary, []))
134+
?assert(
135+
begin
136+
Res = gen_rpc:call(?SLAVE, ?MODULE, canary, []),
137+
Res =:= {badrpc, invalid_cookie} orelse match_unknown_call_error(Res)
138+
end
139+
)
135140
end,
136141
[ fun ?MODULE:prop_canary/1
142+
, fun ?MODULE:prop_client_authentication_failed_trace/1
137143
]).
138144

139145
%% In this testcase we don't test auth, but the rest of the gen_rpc library.
@@ -287,6 +293,21 @@ canary() ->
287293
prop_canary(Trace) ->
288294
?assertMatch([], ?of_kind(gen_rpc_canary, Trace)).
289295

296+
prop_client_authentication_failed_trace(Trace) ->
297+
Events = ?of_kind(client_authentication_failed, Trace),
298+
?assertMatch([_ | _], Events),
299+
?assert(
300+
lists:any(
301+
fun
302+
(#{cause := {badrpc, invalid_cookie}}) ->
303+
true;
304+
(_) ->
305+
false
306+
end,
307+
Events
308+
)
309+
).
310+
290311
prop_no_fallback(Trace) ->
291312
?assertMatch([], ?of_kind([gen_rpc_insecure_fallback, gen_rpc_auth_cr_v1_fallback], Trace)).
292313

@@ -336,3 +357,8 @@ has_fallback(Config) ->
336357
%% Insecure fallback can only be triggered by very ancient
337358
%% versions, skip it for 3.0+:
338359
atom_to_list(proplists:get_value(old_tag, Config)) < "2.99999999".
360+
361+
match_unknown_call_error({badrpc, {unknown_error, _}}) ->
362+
true;
363+
match_unknown_call_error(_) ->
364+
false.

test/gen_rpc_port_check_tests.erl

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
%%--------------------------------------------------------------------
2+
%% Copyright (c) 2026 EMQ Technologies Co., Ltd. All Rights Reserved.
3+
%%--------------------------------------------------------------------
4+
5+
-module(gen_rpc_port_check_tests).
6+
7+
-include_lib("eunit/include/eunit.hrl").
8+
9+
-define(APP, gen_rpc).
10+
11+
check_server_ports_available_stateless_conflict_test() ->
12+
Port = gen_rpc_helper:port(node()),
13+
maybe_with_blocker(
14+
Port,
15+
fun() ->
16+
with_env(
17+
#{
18+
port_discovery => stateless,
19+
tcp_server_port => 5369,
20+
ssl_server_port => false
21+
},
22+
fun() ->
23+
?assertMatch(
24+
{error, [#{driver := tcp, port := Port, reason := eaddrinuse}]},
25+
gen_rpc:check_server_ports_available()
26+
)
27+
end
28+
)
29+
end
30+
).
31+
32+
check_server_ports_available_manual_conflict_test() ->
33+
{ok, Blocker} = gen_tcp:listen(0, [binary, {reuseaddr, true}, {active, false}, {ip, {127, 0, 0, 1}}]),
34+
{ok, {_, Port}} = inet:sockname(Blocker),
35+
try
36+
with_env(
37+
#{
38+
port_discovery => manual,
39+
tcp_server_port => Port,
40+
ssl_server_port => false
41+
},
42+
fun() ->
43+
?assertMatch(
44+
{error, [#{driver := tcp, port := Port, reason := eaddrinuse}]},
45+
gen_rpc:check_server_ports_available()
46+
)
47+
end
48+
)
49+
after
50+
ok = gen_tcp:close(Blocker)
51+
end.
52+
53+
check_server_ports_available_manual_conflict_ipv6_test() ->
54+
with_ipv6_support(
55+
fun() ->
56+
{ok, Blocker} = gen_tcp:listen(
57+
0,
58+
[binary, inet6, {reuseaddr, true}, {active, false}, {ip, {0, 0, 0, 0, 0, 0, 0, 1}}]
59+
),
60+
{ok, {_, Port}} = inet:sockname(Blocker),
61+
try
62+
with_env(
63+
#{
64+
port_discovery => manual,
65+
tcp_server_port => Port,
66+
ssl_server_port => false,
67+
socket_ip => {0, 0, 0, 0, 0, 0, 0, 1}
68+
},
69+
fun() ->
70+
?assertMatch(
71+
{error, [#{
72+
driver := tcp,
73+
ip := {0, 0, 0, 0, 0, 0, 0, 1},
74+
port := Port,
75+
reason := eaddrinuse
76+
}]},
77+
gen_rpc:check_server_ports_available()
78+
)
79+
end
80+
)
81+
after
82+
ok = gen_tcp:close(Blocker)
83+
end
84+
end
85+
).
86+
87+
check_server_ports_available_manual_different_ipv4_ip_no_conflict_test() ->
88+
with_ipv4_alias_support(
89+
{127, 0, 0, 2},
90+
fun() ->
91+
{ok, Blocker} = gen_tcp:listen(
92+
0,
93+
[binary, {reuseaddr, true}, {active, false}, {ip, {127, 0, 0, 2}}]
94+
),
95+
{ok, {_, Port}} = inet:sockname(Blocker),
96+
try
97+
with_env(
98+
#{
99+
port_discovery => manual,
100+
tcp_server_port => Port,
101+
ssl_server_port => false,
102+
socket_ip => {127, 0, 0, 1}
103+
},
104+
fun() ->
105+
?assertEqual(ok, gen_rpc:check_server_ports_available())
106+
end
107+
)
108+
after
109+
ok = gen_tcp:close(Blocker)
110+
end
111+
end
112+
).
113+
114+
maybe_with_blocker(Port, Fun) ->
115+
case gen_tcp:listen(Port, [binary, {reuseaddr, true}, {active, false}, {ip, {127, 0, 0, 1}}]) of
116+
{ok, Blocker} ->
117+
try
118+
Fun()
119+
after
120+
ok = gen_tcp:close(Blocker)
121+
end;
122+
{error, eaddrinuse} ->
123+
%% The port is already occupied by another process; this is enough for this test.
124+
Fun();
125+
{error, Reason} ->
126+
error({failed_to_prepare_port_blocker, Port, Reason})
127+
end.
128+
129+
with_env(Overrides, Fun) ->
130+
Saved = save_env(maps:keys(Overrides)),
131+
try
132+
maps:foreach(
133+
fun(Key, Value) ->
134+
ok = application:set_env(?APP, Key, Value)
135+
end,
136+
Overrides
137+
),
138+
Fun()
139+
after
140+
restore_env(Saved)
141+
end.
142+
143+
save_env(Keys) ->
144+
maps:from_list([{Key, application:get_env(?APP, Key)} || Key <- Keys]).
145+
146+
restore_env(Saved) ->
147+
maps:foreach(
148+
fun
149+
(Key, undefined) ->
150+
application:unset_env(?APP, Key);
151+
(Key, {ok, Value}) ->
152+
ok = application:set_env(?APP, Key, Value)
153+
end,
154+
Saved
155+
).
156+
157+
with_ipv6_support(Fun) ->
158+
case gen_tcp:listen(
159+
0,
160+
[binary, inet6, {reuseaddr, true}, {active, false}, {ip, {0, 0, 0, 0, 0, 0, 0, 1}}]
161+
) of
162+
{ok, Probe} ->
163+
ok = gen_tcp:close(Probe),
164+
Fun();
165+
{error, eafnosupport} ->
166+
ok;
167+
{error, einval} ->
168+
ok;
169+
{error, Reason} ->
170+
error({unexpected_ipv6_probe_error, Reason})
171+
end.
172+
173+
with_ipv4_alias_support(IP, Fun) ->
174+
case gen_tcp:listen(0, [binary, {reuseaddr, true}, {active, false}, {ip, IP}]) of
175+
{ok, Probe} ->
176+
ok = gen_tcp:close(Probe),
177+
Fun();
178+
{error, eaddrnotavail} ->
179+
ok;
180+
{error, Reason} ->
181+
error({unexpected_ipv4_alias_probe_error, IP, Reason})
182+
end.

test/remote_SUITE.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ call_unreachable_peer(_Config) ->
286286
Mref = monitor(process, Pid),
287287
receive
288288
{'DOWN', Mref, process, Pid, _Reason} ->
289-
?assertEqual(undefined, gen_rpc_client:where_is(UnreachableNode)),
289+
?retry(20, 50, ?assertEqual(undefined, gen_rpc_client:where_is(UnreachableNode))),
290290
ok
291291
after
292292
5000 ->

0 commit comments

Comments
 (0)