Skip to content

Commit c8b88f6

Browse files
committed
feat: add server port precheck api and tests
1 parent a155ccb commit c8b88f6

3 files changed

Lines changed: 210 additions & 3 deletions

File tree

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_helper.erl

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
get_control_receive_timeout/0,
4242
get_inactivity_timeout/1,
4343
get_async_call_inactivity_timeout/0,
44-
get_listen_ip_config/0]).
44+
get_listen_ip_config/0,
45+
check_server_ports_available/0]).
4546

4647
%%% ===================================================
4748
%%% Public API
@@ -222,6 +223,15 @@ get_async_call_inactivity_timeout() ->
222223
get_user_tcp_opts(Type) ->
223224
get_user_tcp_opts(?USER_TCP_OPTS, Type).
224225

226+
-spec check_server_ports_available() -> ok | {error, [map()]}.
227+
check_server_ports_available() ->
228+
_ = application:load(?APP),
229+
Endpoints = server_listen_endpoints(),
230+
case find_port_conflicts(Endpoints) of
231+
[] -> ok;
232+
Conflicts -> {error, Conflicts}
233+
end.
234+
225235
%%% ===================================================
226236
%%% Private functions
227237
%%% ===================================================
@@ -241,10 +251,59 @@ get_client_config_from_map(Node, NodeConfig) ->
241251
{ok, {Driver,Port}} ->
242252
{Driver, Port};
243253
{ok, Port} ->
244-
{ok, Driver} = application:get_env(?APP, default_client_driver),
254+
{ok, Driver} = application:get_env(?APP, default_client_driver),
245255
{Driver, Port}
246256
end.
247257

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

test/gen_rpc_port_check_tests.erl

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
maybe_with_blocker(Port, Fun) ->
88+
case gen_tcp:listen(Port, [binary, {reuseaddr, true}, {active, false}, {ip, {127, 0, 0, 1}}]) of
89+
{ok, Blocker} ->
90+
try
91+
Fun()
92+
after
93+
ok = gen_tcp:close(Blocker)
94+
end;
95+
{error, eaddrinuse} ->
96+
%% The port is already occupied by another process; this is enough for this test.
97+
Fun();
98+
{error, Reason} ->
99+
error({failed_to_prepare_port_blocker, Port, Reason})
100+
end.
101+
102+
with_env(Overrides, Fun) ->
103+
Saved = save_env(maps:keys(Overrides)),
104+
try
105+
maps:foreach(
106+
fun(Key, Value) ->
107+
ok = application:set_env(?APP, Key, Value)
108+
end,
109+
Overrides
110+
),
111+
Fun()
112+
after
113+
restore_env(Saved)
114+
end.
115+
116+
save_env(Keys) ->
117+
maps:from_list([{Key, application:get_env(?APP, Key)} || Key <- Keys]).
118+
119+
restore_env(Saved) ->
120+
maps:foreach(
121+
fun
122+
(Key, undefined) ->
123+
application:unset_env(?APP, Key);
124+
(Key, {ok, Value}) ->
125+
ok = application:set_env(?APP, Key, Value)
126+
end,
127+
Saved
128+
).
129+
130+
with_ipv6_support(Fun) ->
131+
case gen_tcp:listen(
132+
0,
133+
[binary, inet6, {reuseaddr, true}, {active, false}, {ip, {0, 0, 0, 0, 0, 0, 0, 1}}]
134+
) of
135+
{ok, Probe} ->
136+
ok = gen_tcp:close(Probe),
137+
Fun();
138+
{error, eafnosupport} ->
139+
ok;
140+
{error, einval} ->
141+
ok;
142+
{error, Reason} ->
143+
error({unexpected_ipv6_probe_error, Reason})
144+
end.

0 commit comments

Comments
 (0)