Current section
Files
Jump to
Current section
Files
src/ecpool_worker.erl
%%--------------------------------------------------------------------
%% Copyright (c) 2019 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(ecpool_worker).
-behaviour(gen_server).
-export([start_link/4]).
%% API Function Exports
-export([client/1, is_connected/1, set_reconnect_callback/2]).
%% gen_server Function Exports
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
-record(state, {pool, id, client, mod, on_reconnect, on_disconnect, supervisees = [], opts}).
%%--------------------------------------------------------------------
%% Callback
%%--------------------------------------------------------------------
-ifdef(use_specs).
-callback(connect(ConnOpts :: list())
-> {ok, pid()} | {error, Reason :: term()}).
-else.
-export([behaviour_info/1]).
behaviour_info(callbacks) ->
[{connect, 1}];
behaviour_info(_Other) ->
undefined.
-endif.
%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
%% @doc Start a pool worker.
-spec(start_link(atom(), pos_integer(), module(), list()) ->
{ok, pid()} | ignore | {error, any()}).
start_link(Pool, Id, Mod, Opts) ->
gen_server:start_link(?MODULE, [Pool, Id, Mod, Opts], []).
%% @doc Get client/connection.
-spec(client(pid()) -> {ok, Client :: pid()} | {error, Reason :: term()}).
client(Pid) ->
gen_server:call(Pid, client, infinity).
%% @doc Is client connected?
-spec(is_connected(pid()) -> boolean()).
is_connected(Pid) ->
gen_server:call(Pid, is_connected, infinity).
-spec(set_reconnect_callback(pid(), ecpool:reconn_callback()) -> ok).
set_reconnect_callback(Pid, OnReconnect) ->
gen_server:cast(Pid, {set_reconn_callbk, OnReconnect}).
%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([Pool, Id, Mod, Opts]) ->
process_flag(trap_exit, true),
State = #state{pool = Pool, id = Id, mod = Mod, opts = Opts,
on_reconnect = proplists:get_value(on_reconnect, Opts),
on_disconnect = proplists:get_value(on_disconnect, Opts)},
case connect_internal(State) of
{ok, NewState} ->
gproc_pool:connect_worker(ecpool:name(Pool), {Pool, Id}),
{ok, NewState};
{error, Error} ->
{stop, Error}
end.
handle_call(is_connected, _From, State = #state{client = Client}) ->
IsAlive = Client =/= undefined andalso is_process_alive(Client),
{reply, IsAlive, State};
handle_call(client, _From, State = #state{client = undefined}) ->
{reply, {error, disconnected}, State};
handle_call(client, _From, State = #state{client = Client}) ->
{reply, {ok, Client}, State};
handle_call(Req, _From, State) ->
logger:error("[PoolWorker] unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast({set_reconn_callbk, OnReconnect}, State) ->
{noreply, State#state{on_reconnect = OnReconnect}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({'EXIT', Pid, Reason}, State = #state{opts = Opts, supervisees = SupPids}) ->
case lists:member(Pid, SupPids) of
true ->
case proplists:get_value(auto_reconnect, Opts, false) of
false -> {stop, Reason, State};
Secs -> reconnect(Secs, State)
end;
false ->
logger:debug("~p received unexpected exit:~0p from ~p. Supervisees: ~p",
[?MODULE, Reason, Pid, SupPids]),
{noreply, State}
end;
handle_info(reconnect, State = #state{opts = Opts, on_reconnect = OnReconnect}) ->
case connect_internal(State) of
{ok, NewState = #state{client = Client}} ->
handle_reconnect(Client, OnReconnect),
{noreply, NewState};
{Err, _Reason} when Err =:= error orelse Err =:= 'EXIT' ->
reconnect(proplists:get_value(auto_reconnect, Opts), State)
end;
handle_info(Info, State) ->
logger:error("[PoolWorker] unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #state{pool = Pool, id = Id,
client = Client,
on_disconnect = Disconnect}) ->
handle_disconnect(Client, Disconnect),
gproc_pool:disconnect_worker(ecpool:name(Pool), {Pool, Id}).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%% Internal Functions
%%--------------------------------------------------------------------
connect(#state{mod = Mod, opts = Opts, id = Id}) ->
Mod:connect([{ecpool_worker_id, Id} | connopts(Opts, [])]).
connopts([], Acc) ->
Acc;
connopts([{pool_size, _}|Opts], Acc) ->
connopts(Opts, Acc);
connopts([{pool_type, _}|Opts], Acc) ->
connopts(Opts, Acc);
connopts([{auto_reconnect, _}|Opts], Acc) ->
connopts(Opts, Acc);
connopts([{bind, _}|Opts], Acc) ->
connopts(Opts, Acc);
connopts([{unbind, _}|Opts], Acc) ->
connopts(Opts, Acc);
connopts([Opt|Opts], Acc) ->
connopts(Opts, [Opt|Acc]).
reconnect(Secs, State = #state{client = Client, on_disconnect = Disconnect, supervisees = SubPids}) ->
[erlang:unlink(P) || P <- SubPids, is_pid(P)],
handle_disconnect(Client, Disconnect),
erlang:send_after(timer:seconds(Secs), self(), reconnect),
{noreply, State#state{client = undefined}}.
handle_reconnect(_, undefined) ->
ok;
handle_reconnect(Client, OnReconnect) ->
OnReconnect(Client).
handle_disconnect(undefined, _) ->
ok;
handle_disconnect(_, undefined) ->
ok;
handle_disconnect(Client, Disconnect) ->
Disconnect(Client).
connect_internal(State) ->
try connect(State) of
{ok, Client} when is_pid(Client) ->
erlang:link(Client),
{ok, State#state{client = Client, supervisees = [Client]}};
{ok, Client, #{supervisees := SupPids} = _SupOpts} when is_list(SupPids) ->
[erlang:link(P) || P <- SupPids],
{ok, State#state{client = Client, supervisees = SupPids}};
{error, Error} ->
{error, Error}
catch
_C:Reason -> {error, Reason}
end.