diff --git a/apps/emqx/src/emqx_rpc.erl b/apps/emqx/src/emqx_rpc.erl index 86ec5937f..4675c3734 100644 --- a/apps/emqx/src/emqx_rpc.erl +++ b/apps/emqx/src/emqx_rpc.erl @@ -43,6 +43,10 @@ erpc_multicall/1 ]). +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + -compile( {inline, [ rpc_node/1, @@ -75,15 +79,15 @@ -spec call(node(), module(), atom(), list()) -> call_result(). call(Node, Mod, Fun, Args) -> - filter_result(gen_rpc:call(rpc_node(Node), Mod, Fun, Args)). + maybe_badrpc(gen_rpc:call(rpc_node(Node), Mod, Fun, Args)). -spec call(term(), node(), module(), atom(), list()) -> call_result(). call(Key, Node, Mod, Fun, Args) -> - filter_result(gen_rpc:call(rpc_node({Key, Node}), Mod, Fun, Args)). + maybe_badrpc(gen_rpc:call(rpc_node({Key, Node}), Mod, Fun, Args)). -spec call(term(), node(), module(), atom(), list(), timeout()) -> call_result(). call(Key, Node, Mod, Fun, Args, Timeout) -> - filter_result(gen_rpc:call(rpc_node({Key, Node}), Mod, Fun, Args, Timeout)). + maybe_badrpc(gen_rpc:call(rpc_node({Key, Node}), Mod, Fun, Args, Timeout)). -spec multicall([node()], module(), atom(), list()) -> multicall_result(). multicall(Nodes, Mod, Fun, Args) -> @@ -127,18 +131,15 @@ rpc_nodes([], Acc) -> rpc_nodes([Node | Nodes], Acc) -> rpc_nodes(Nodes, [rpc_node(Node) | Acc]). -filter_result({Error, Reason}) when - Error =:= badrpc; Error =:= badtcp --> +maybe_badrpc({Error, Reason}) when Error =:= badrpc; Error =:= badtcp -> {badrpc, Reason}; -filter_result(Delivery) -> +maybe_badrpc(Delivery) -> Delivery. max_client_num() -> emqx:get_config([rpc, tcp_client_num], ?DefaultClientNum). -spec unwrap_erpc(emqx_rpc:erpc(A) | [emqx_rpc:erpc(A)]) -> A | {error, _Err} | list(). - unwrap_erpc(Res) when is_list(Res) -> [unwrap_erpc(R) || R <- Res]; unwrap_erpc({ok, A}) -> @@ -151,3 +152,73 @@ unwrap_erpc({exit, Err}) -> {error, Err}; unwrap_erpc({error, {erpc, Err}}) -> {error, Err}. + +-ifdef(TEST). + +badrpc_call_test_() -> + application:ensure_all_started(gen_rpc), + Node = node(), + [ + {"throw", fun() -> + ?assertEqual(foo, call(Node, erlang, throw, [foo])) + end}, + {"error", fun() -> + ?assertMatch({badrpc, {'EXIT', {foo, _}}}, call(Node, erlang, error, [foo])) + end}, + {"exit", fun() -> + ?assertEqual({badrpc, {'EXIT', foo}}, call(Node, erlang, exit, [foo])) + end}, + {"timeout", fun() -> + ?assertEqual({badrpc, timeout}, call(key, Node, timer, sleep, [1000], 100)) + end}, + {"noconnection", fun() -> + %% mute crash report from gen_rpc + logger:set_primary_config(level, critical), + try + ?assertEqual( + {badrpc, nxdomain}, call(key, 'no@such.node', foo, bar, []) + ) + after + logger:set_primary_config(level, notice) + end + end} + ]. + +multicall_test() -> + application:ensure_all_started(gen_rpc), + logger:set_primary_config(level, critical), + BadNode = 'no@such.node', + ThisNode = node(), + Nodes = [ThisNode, BadNode], + Call4 = fun(M, F, A) -> multicall(Nodes, M, F, A) end, + Call5 = fun(Key, M, F, A) -> multicall(Key, Nodes, M, F, A) end, + try + ?assertMatch({[foo], [{BadNode, _}]}, Call4(erlang, throw, [foo])), + ?assertMatch({[], [{ThisNode, _}, {BadNode, _}]}, Call4(erlang, error, [foo])), + ?assertMatch({[], [{ThisNode, _}, {BadNode, _}]}, Call4(erlang, exit, [foo])), + ?assertMatch({[], [{ThisNode, _}, {BadNode, _}]}, Call5(key, foo, bar, [])) + after + logger:set_primary_config(level, notice) + end. + +unwrap_erpc_test_() -> + Nodes = [node()], + MultiC = fun(M, F, A) -> unwrap_erpc(erpc:multicall(Nodes, M, F, A, 100)) end, + [ + {"throw", fun() -> + ?assertEqual([{error, foo}], MultiC(erlang, throw, [foo])) + end}, + {"error", fun() -> + ?assertEqual([{error, foo}], MultiC(erlang, error, [foo])) + end}, + {"exit", fun() -> + ?assertEqual([{error, {exception, foo}}], MultiC(erlang, exit, [foo])) + end}, + {"noconnection", fun() -> + ?assertEqual( + [{error, noconnection}], unwrap_erpc(erpc:multicall(['no@such.node'], foo, bar, [])) + ) + end} + ]. + +-endif. diff --git a/apps/emqx/test/props/prop_emqx_rpc.erl b/apps/emqx/test/props/prop_emqx_rpc.erl deleted file mode 100644 index e544ae082..000000000 --- a/apps/emqx/test/props/prop_emqx_rpc.erl +++ /dev/null @@ -1,195 +0,0 @@ -%%-------------------------------------------------------------------- -%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved. -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. -%%-------------------------------------------------------------------- - --module(prop_emqx_rpc). - --include_lib("proper/include/proper.hrl"). --include_lib("eunit/include/eunit.hrl"). - --define(NODENAME, 'test@127.0.0.1'). - --define(ALL(Vars, Types, Exprs), - ?SETUP( - fun() -> - State = do_setup(), - fun() -> do_teardown(State) end - end, - ?FORALL(Vars, Types, Exprs) - ) -). - -%%-------------------------------------------------------------------- -%% Properties -%%-------------------------------------------------------------------- - -prop_node() -> - ?ALL( - Node0, - nodename(), - begin - Node = punch(Node0), - ?assert(emqx_rpc:cast(Node, erlang, system_time, [])), - case emqx_rpc:call(Node, erlang, system_time, []) of - {badrpc, _Reason} -> true; - Delivery when is_integer(Delivery) -> true; - _Other -> false - end - end - ). - -prop_node_with_key() -> - ?ALL( - {Node0, Key}, - nodename_with_key(), - begin - Node = punch(Node0), - ?assert(emqx_rpc:cast(Key, Node, erlang, system_time, [])), - case emqx_rpc:call(Key, Node, erlang, system_time, []) of - {badrpc, _Reason} -> true; - Delivery when is_integer(Delivery) -> true; - _Other -> false - end - end - ). - -prop_nodes() -> - ?ALL( - Nodes0, - nodesname(), - begin - Nodes = punch(Nodes0), - case emqx_rpc:multicall(Nodes, erlang, system_time, []) of - {RealResults, RealBadNodes} when - is_list(RealResults); - is_list(RealBadNodes) - -> - true; - _Other -> - false - end - end - ). - -prop_nodes_with_key() -> - ?ALL( - {Nodes0, Key}, - nodesname_with_key(), - begin - Nodes = punch(Nodes0), - case emqx_rpc:multicall(Key, Nodes, erlang, system_time, []) of - {RealResults, RealBadNodes} when - is_list(RealResults); - is_list(RealBadNodes) - -> - true; - _Other -> - false - end - end - ). - -%%-------------------------------------------------------------------- -%% Helper -%%-------------------------------------------------------------------- - -do_setup() -> - ensure_distributed_nodename(), - ok = logger:set_primary_config(#{level => warning}), - {ok, _Apps} = application:ensure_all_started(gen_rpc), - ok = application:set_env(gen_rpc, call_receive_timeout, 100), - ok = meck:new(gen_rpc, [passthrough, no_history]), - ok = meck:expect( - gen_rpc, - multicall, - fun(Nodes, Mod, Fun, Args) -> - gen_rpc:multicall(Nodes, Mod, Fun, Args, 100) - end - ). - -do_teardown(_) -> - ok = net_kernel:stop(), - ok = application:stop(gen_rpc), - ok = meck:unload(gen_rpc), - %% wait for tcp close - timer:sleep(2500). - -ensure_distributed_nodename() -> - case net_kernel:start([?NODENAME]) of - {ok, _} -> - ok; - {error, {already_started, _}} -> - net_kernel:stop(), - net_kernel:start([?NODENAME]); - {error, {{shutdown, {_, _, {'EXIT', nodistribution}}}, _}} -> - %% start epmd first - spawn_link(fun() -> os:cmd("epmd") end), - timer:sleep(100), - net_kernel:start([?NODENAME]) - end. - -%%-------------------------------------------------------------------- -%% Generator -%%-------------------------------------------------------------------- - -nodename() -> - ?LET( - {NodePrefix, HostName}, - {node_prefix(), hostname()}, - begin - Node = NodePrefix ++ "@" ++ HostName, - list_to_atom(Node) - end - ). - -nodename_with_key() -> - ?LET( - {NodePrefix, HostName, Key}, - {node_prefix(), hostname(), choose(0, 10)}, - begin - Node = NodePrefix ++ "@" ++ HostName, - {list_to_atom(Node), Key} - end - ). - -nodesname() -> - oneof([list(nodename()), [node()]]). - -nodesname_with_key() -> - oneof([{list(nodename()), choose(0, 10)}, {[node()], 1}]). - -node_prefix() -> - oneof(["emqxct", text_like()]). - -text_like() -> - ?SUCHTHAT(Text, list(range($a, $z)), (length(Text) =< 100 andalso length(Text) > 0)). - -hostname() -> - oneof(["127.0.0.1", "localhost"]). - -%%-------------------------------------------------------------------- -%% Utils -%%-------------------------------------------------------------------- - -%% After running the props, the `node()` () is only able to return an -%% incorrect node name - `nonode@nohost`, But we want a distributed nodename -%% So, just translate the `nonode@nohost` to ?NODENAME -punch(Nodes) when is_list(Nodes) -> - lists:map(fun punch/1, Nodes); -punch('nonode@nohost') -> - %% Equal to ?NODENAME - node(); -punch(GoodBoy) -> - GoodBoy.