Skip to content

Commit

Permalink
Support external node driver/port discovery
Browse files Browse the repository at this point in the history
- Support consulting an M:F external to gen_rpc to discover assignment of
  protocol and port for remote nodes.

- Update Makefile targets for travis
  • Loading branch information
priestjim committed Oct 5, 2016
1 parent 22c671f commit db027b9
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ cache:
- $HOME/.cache/rebar3
- _plt
install: "true"
script: "make testclean dist coveralls"
script: "make travis"
branches:
only:
- master
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

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

## 2.0.1

- Support external node driver/port discovery using an `{M, F}` tuple instead of
a map in `client_config_per_node`.

## 2.0.0

This release boasts a major rengineer/refactor of `gen_rpc` that includes quite a few new features:
Expand Down
11 changes: 3 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# distclean: rebar3 as dev do clean -a
# and explicitly delete other build artifacts
# test: rebar3 as test do ct -v, cover
# coveralls: Send coverage to coveralls.io
# travis: Run the proper tests/coveralls in travis
# dialyzer: rebar3 as test do dialyzer
# xref: rebar3 as dev do xref
# dist: rebar3 as test do compile, ct -v -c, xref, dialyzer, cover
Expand All @@ -26,7 +26,7 @@
.DEFAULT_GOAL := all

# Build targets
.PHONY: all test dialyzer xref spec dist coveralls
.PHONY: all test dialyzer xref spec dist travis

# Run targets
.PHONY: shell shell-master shell-slave
Expand Down Expand Up @@ -62,8 +62,6 @@ OTP_RELEASE = $(shell escript otp-release.escript)

PLT_FILE = $(CURDIR)/_plt/rebar3_$(OTP_RELEASE)_plt

COVERDATA = $(CURDIR)/_build/test/ct.coverdata

# ======================
# Integration test logic
# ======================
Expand Down Expand Up @@ -103,7 +101,7 @@ spec: dialyzer
dist: $(REBAR) test
@REBAR_PROFILE=dev $(REBAR) do dialyzer, xref

coveralls: $(COVERDATA)
travis: testclean dist
@REBAR_PROFILE=test $(REBAR) do coveralls send || true

# =============================================================================
Expand Down Expand Up @@ -146,8 +144,5 @@ $(REBAR):
tags:
find src _build/default/lib -name "*.[he]rl" -print | etags -

$(COVERDATA):
@$(MAKE) test

$(PLT_FILE):
@REBAR_PROFILE=dev $(REBAR) do dialyzer || true
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ Finally, start a couple of nodes to test it out:
It should be either `tcp` or `ssl`.

- `client_config_per_node`: A map of `Node => {Driver, Port}` or `Node => Port` that instructs `gen_rpc` on the `Port`
and/or `Driver` to use when connecting to a `Node`.
and/or `Driver` to use when connecting to a `Node`. If you prefer to use an external discovery service to map `Nodes`
to `{Driver, Port}` tuples, instead of the map, you'll need to define a `{Module, Function}` tuple instead with a function
that takes the `Node` as its single argument, consumes the external discovery service and returns a `{Driver, Port}` tuple.

- `rpc_module_control`: Set it to `blacklist` to define a list of modules that will not be exposed to `gen_rpc` or to `whitelist`
to define the list of modules that will be exposed to `gen_rpc`. Set it to `disabled` to disable this feature.
Expand Down
4 changes: 4 additions & 0 deletions src/gen_rpc.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
{files, [
"LICENSE",
"README.md",
"CHANGELOG.md",
"include",
"package.exs",
"rebar.config",
Expand All @@ -41,6 +42,9 @@
%% Describe it as node_name => {driver, port}
%% Or node_name => driver to use the default port
%% for the specified client driver
%% If you have an external service that allows discovery
%% of port/node mappings, you can replace the map
%% with an {M,F} tuple and it will be called instead
{client_config_per_node, #{}},
%% List of modules available for RPC
%% This is either whitelist, blacklist or disabled
Expand Down
45 changes: 25 additions & 20 deletions src/gen_rpc_client.erl
Original file line number Diff line number Diff line change
Expand Up @@ -227,26 +227,31 @@ sbcast(Nodes, Name, Msg) when is_list(Nodes), is_atom(Name) ->
%%% ===================================================
init({Node}) ->
ok = gen_rpc_helper:set_optimal_process_flags(),
{Driver, Port} = gen_rpc_helper:get_client_config_per_node(Node),
{DriverMod, DriverClosed, DriverError} = gen_rpc_helper:get_client_driver_options(Driver),
?log(info, "event=initializing_client driver=~s node=\"~s\" port=~B", [Driver, Node, Port]),
case DriverMod:connect(Node, Port) of
{ok, Socket} ->
case DriverMod:authenticate_server(Socket) of
ok ->
{ok, #state{socket=Socket,
driver=Driver,
driver_mod=DriverMod,
driver_closed=DriverClosed,
driver_error=DriverError}, gen_rpc_helper:get_inactivity_timeout(?MODULE)};
{error, ReasonTuple} ->
?log(error, "event=client_authentication_failed driver=~s reason=\"~p\"", [Driver, ReasonTuple]),
{stop, ReasonTuple}
end;
{error, {_Class,Reason}} ->
%% This should be badtcp but to conform with
%% the RPC library we return badrpc
{stop, {badrpc,Reason}}
case gen_rpc_helper:get_client_config_per_node(Node) of
{error, {Class,Reason}} ->
?log(error, "event=external_source_error action=falling_back_to_local reason=\"~s:~p\"", [Class, Reason]),
{stop, {badrpc, {external_source_error, Reason}}};
{Driver, Port} ->
{DriverMod, DriverClosed, DriverError} = gen_rpc_helper:get_client_driver_options(Driver),
?log(info, "event=initializing_client driver=~s node=\"~s\" port=~B", [Driver, Node, Port]),
case DriverMod:connect(Node, Port) of
{ok, Socket} ->
case DriverMod:authenticate_server(Socket) of
ok ->
{ok, #state{socket=Socket,
driver=Driver,
driver_mod=DriverMod,
driver_closed=DriverClosed,
driver_error=DriverError}, gen_rpc_helper:get_inactivity_timeout(?MODULE)};
{error, ReasonTuple} ->
?log(error, "event=client_authentication_failed driver=~s reason=\"~p\"", [Driver, ReasonTuple]),
{stop, ReasonTuple}
end;
{error, {_Class,Reason}} ->
%% This should be badtcp but to conform with
%% the RPC library we return badrpc
{stop, {badrpc,Reason}}
end
end.

%% This is the actual CALL handler
Expand Down
40 changes: 27 additions & 13 deletions src/gen_rpc_helper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -139,21 +139,20 @@ get_client_driver_options(Driver) when is_atom(Driver) ->
ErrorMsg = list_to_atom(lists:flatten([DriverStr, "_error"])),
{DriverMod, ClosedMsg, ErrorMsg}.

-spec get_client_config_per_node(atom()) -> {atom(), inet:port_number()}.
-spec get_client_config_per_node(atom()) -> {atom(), inet:port_number()} | {error, {atom(), term()}}.
get_client_config_per_node(Node) when is_atom(Node) ->
{ok, NodeConfig} = application:get_env(?APP, client_config_per_node),
case maps:find(Node, NodeConfig) of
error ->
{ok, Driver} = application:get_env(?APP, default_client_driver),
DriverStr = erlang:atom_to_list(Driver),
PortSetting = list_to_atom(lists:flatten([DriverStr, "_client_port"])),
{ok, Port} = application:get_env(?APP, PortSetting),
{Driver, Port};
{ok, Port} when is_integer(Port) ->
{ok, Driver} = application:get_env(?APP, default_client_driver),
{Driver, Port};
{ok, {Driver,Port}} ->
{Driver, Port}
case NodeConfig of
{M, F} ->
try
{Driver, Port} = M:F(Node),
{Driver, Port}
catch
Class:Reason ->
{error, {Class,Reason}}
end;
NodeConfig ->
get_client_config_from_map(Node, NodeConfig)
end.

-spec get_connect_timeout() -> timeout().
Expand Down Expand Up @@ -224,6 +223,21 @@ get_async_call_inactivity_timeout() ->
%%% ===================================================
%%% Private functions
%%% ===================================================
get_client_config_from_map(Node, NodeConfig) ->
case maps:find(Node, NodeConfig) of
error ->
{ok, Driver} = application:get_env(?APP, default_client_driver),
DriverStr = erlang:atom_to_list(Driver),
PortSetting = list_to_atom(lists:flatten([DriverStr, "_client_port"])),
{ok, Port} = application:get_env(?APP, PortSetting),
{Driver, Port};
{ok, {Driver,Port}} ->
{Driver, Port};
{ok, Port} ->
{ok, Driver} = application:get_env(?APP, default_client_driver),
{Driver, Port}
end.

hybrid_proplist_compare({K1,_V1}, {K2,_V2}) ->
K1 =< K2;

Expand Down
21 changes: 21 additions & 0 deletions test/ct/remote_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ init_per_testcase(rpc_module_blacklist, Config) ->
ok = rpc:call(?SLAVE, application, set_env, [?APP, rpc_module_control, blacklist]),
Config;

init_per_testcase(external_client_config_source, Config) ->
ok = gen_rpc_test_helper:restart_application(),
ok = gen_rpc_test_helper:start_master(ssl),
ok = gen_rpc_test_helper:start_slave(tcp),
%% No need to restore original setting with an
%% end_per_testcase since this setting gets overwritten
%% upon every application restart
ok = application:set_env(?APP, client_config_per_node, {?MODULE, external_client_config_fun}),
Config;

init_per_testcase(_OtherTest, Config) ->
ok = gen_rpc_test_helper:restart_application(),
Driver = gen_rpc_test_helper:get_driver_from_config(Config),
Expand Down Expand Up @@ -266,6 +276,9 @@ rpc_module_blacklist(_Config) ->
{badrpc, unauthorized} = gen_rpc:call(?SLAVE, erlang, node),
60000 = gen_rpc:call(?SLAVE, timer, seconds, [60]).

external_client_config_source(_Config) ->
{_Mega, _Sec, _Micro} = gen_rpc:call(?SLAVE, os, timestamp).

wrong_cookie(_Config) ->
OrigCookie = erlang:get_cookie(),
RandCookie = list_to_atom(atom_to_list(OrigCookie) ++ "123"),
Expand Down Expand Up @@ -313,3 +326,11 @@ interleaved_call_executor(Num) when is_integer(Num) ->
ok = timer:sleep((3 - Num) * 1000),
%% Then return the number
Num.

%% Enable TCP only communication to the slave when testing
%% the external client config source
%% This should force communication with the slave
%% over TCP even on the SSL group
external_client_config_fun(?SLAVE) ->
{tcp, ?SLAVE_PORT}.

1 change: 1 addition & 0 deletions test/gen_rpc_test_helper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ get_test_functions(Module) ->
({interleaved_call_proc,_}) -> false;
({interleaved_call_executor,_}) -> false;
({interleaved_call_loop,_}) -> false;
({external_client_config_fun,_}) -> false;
%% Multi RPC
({spawn_listener,_}) -> false;
({spawn_listener2,_}) -> false;
Expand Down

0 comments on commit db027b9

Please sign in to comment.