@ -0,0 +1,124 @@ | |||
%% beam cache 模块名 | |||
-define(agBeamPool, agBeamPool). | |||
-define(agBeamAgency, agBeamAgency). | |||
%% 默认值定义 | |||
-define(DEFAULT_BACKLOG_SIZE, 1024). | |||
-define(DEFAULT_INIT_OPTS, undefined). | |||
-define(DEFAULT_CONNECT_TIMEOUT, 500). | |||
-define(DEFAULT_PORT, 80). | |||
-define(DEFAULT_IP, <<"127.0.0.1">>). | |||
-define(DEFAULT_POOL_SIZE, 16). | |||
-define(DEFAULT_POOL_STRATEGY, random). | |||
-define(DEFAULT_POOL_OPTIONS, []). | |||
-define(DEFAULT_IS_RECONNECT, true). | |||
-define(DEFAULT_RECONNECT_MAX, 120000). | |||
-define(DEFAULT_RECONNECT_MIN, 500). | |||
-define(DEFAULT_SOCKET_OPTS, []). | |||
-define(DEFAULT_TIMEOUT, 1000). | |||
-define(DEFAULT_BODY, undefined). | |||
-define(DEFAULT_HEADERS, []). | |||
-define(DEFAULT_PID, self()). | |||
-define(DEFAULT_PROTOCOL, tcp). | |||
-define(GET_FROM_LIST(Key, List), case lists:keyfind(Key, 1, List) of false -> undefined; {_, Value} -> Value end). | |||
-define(GET_FROM_LIST(Key, List, Default), case lists:keyfind(Key, 1, List) of false -> Default; {_, Value} -> Value end). | |||
-define(WARN(PoolName, Format, Data), agMiscUtils:warnMsg(PoolName, Format, Data)). | |||
-record(dbUrl, { | |||
host :: host(), | |||
path :: path(), | |||
port :: 0..65535, | |||
hostname :: hostname(), | |||
protocol :: httpType() | |||
}). | |||
-record(requestRet, { | |||
state :: body | done, | |||
body :: undefined | binary(), | |||
content_length :: undefined | non_neg_integer() | chunked, | |||
headers :: undefined | [binary()], | |||
reason :: undefined | binary(), | |||
status_code :: undefined | 100..505 | |||
}). | |||
-record(request, { | |||
requestId :: requestId(), | |||
pid :: pid() | undefined, | |||
timeout :: timeout(), | |||
timestamp :: erlang:timestamp() | |||
}). | |||
-record(poolOpts, { | |||
poolSize :: poolSize(), | |||
backlogSize :: backlogSize(), | |||
poolStrategy :: poolStrategy() | |||
}). | |||
-record(reconnectState, { | |||
min :: time(), | |||
max :: time() | infinity, | |||
current :: time() | undefined | |||
}). | |||
-type requestRet() :: #requestRet {}. | |||
-type dbUrl() :: #dbUrl {}. | |||
-type error() :: {error, term()}. | |||
-type headers() :: [{iodata(), iodata()}, ...]. | |||
-type host() :: binary(). | |||
-type hostname() :: binary(). | |||
-type path() :: binary(). | |||
-type method() :: binary(). | |||
-type httpType() :: http | https. | |||
-type body() :: iodata() | undefined. | |||
-type options() :: [option(), ...]. | |||
-type option() :: | |||
{backlogSize, pos_integer()} | | |||
{poolSize, pos_integer()} | | |||
{poolStrategy, random | round_robin} | | |||
{reconnect, boolean()} | | |||
{reconnectTimeMin, pos_integer()} | | |||
{reconnectTimeMax, pos_integer() | infinity}. | |||
-type buoy_opts() :: | |||
#{ | |||
headers => headers(), | |||
body => body(), | |||
pid => pid(), | |||
timeout => non_neg_integer() | |||
}. | |||
-type backlogSize() :: pos_integer() | infinity. | |||
-type request() :: #request{}. | |||
-type clientOpt() :: | |||
{initOpts, term()} | | |||
{ip, inet:ip_address() | inet:hostname()} | | |||
{port, inet:port_number()} | | |||
{protocol, protocol()} | | |||
{reconnect, boolean()} | | |||
{reconnectTimeMin, time()} | | |||
{reconnectTimeMax, time() | infinity} | | |||
{socketOpts, [gen_tcp:connect_option(), ...]}. | |||
-type clientOpts() :: [clientOpt(), ...]. | |||
-type clientState() :: term(). | |||
-type externalRequestId() :: term(). | |||
-type poolName() :: atom(). | |||
-type poolOpt() :: | |||
{poolSize, pool_size()} | | |||
{backlogSize, backlog_size()} | | |||
{poolstrategy, poolStrategy()}. | |||
-type poolOpts() :: [poolOpt()]. | |||
-type poolOptsRec() :: #poolOpts{}. | |||
-type poolSize() :: pos_integer(). | |||
-type poolStrategy() :: random | round_robin. | |||
-type protocol() :: ssl | tcp. | |||
-type reconnectState() :: #reconnectState{}. | |||
-type requestId() :: {server_name(), reference()}. | |||
-type response() :: {external_request_id(), term()}. | |||
-type server_name() :: atom(). | |||
-type socket() :: inet:socket() | ssl:sslsocket(). | |||
-type socketType() :: inet | ssl. | |||
-type time() :: pos_integer(). |
@ -0,0 +1,34 @@ | |||
%%% ---------------------------------------------------------------------------- | |||
%%% Copyright (c) 2009, Erlang Training and Consulting Ltd. | |||
%%% All rights reserved. | |||
%%% | |||
%%% Redistribution and use in source and binary forms, with or without | |||
%%% modification, are permitted provided that the following conditions are met: | |||
%%% * Redistributions of source code must retain the above copyright | |||
%%% notice, this list of conditions and the following disclaimer. | |||
%%% * Redistributions in binary form must reproduce the above copyright | |||
%%% notice, this list of conditions and the following disclaimer in the | |||
%%% documentation and/or other materials provided with the distribution. | |||
%%% * Neither the name of Erlang Training and Consulting Ltd. nor the | |||
%%% names of its contributors may be used to endorse or promote products | |||
%%% derived from this software without specific prior written permission. | |||
%%% | |||
%%% THIS SOFTWARE IS PROVIDED BY Erlang Training and Consulting Ltd. ''AS IS'' | |||
%%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||
%%% ARE DISCLAIMED. IN NO EVENT SHALL Erlang Training and Consulting Ltd. BE | |||
%%% LIABLE SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR | |||
%%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | |||
%%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR | |||
%%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | |||
%%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
%%% ---------------------------------------------------------------------------- | |||
-record(lhttpc_url, { | |||
host :: string(), | |||
port :: integer(), | |||
path :: string(), | |||
is_ssl:: boolean(), | |||
user = "" :: string(), | |||
password = "" :: string() | |||
}). |
@ -0,0 +1,140 @@ | |||
%%% ---------------------------------------------------------------------------- | |||
%%% Copyright (c) 2009, Erlang Training and Consulting Ltd. | |||
%%% All rights reserved. | |||
%%% | |||
%%% Redistribution and use in source and binary forms, with or without | |||
%%% modification, are permitted provided that the following conditions are met: | |||
%%% * Redistributions of source code must retain the above copyright | |||
%%% notice, this list of conditions and the following disclaimer. | |||
%%% * Redistributions in binary form must reproduce the above copyright | |||
%%% notice, this list of conditions and the following disclaimer in the | |||
%%% documentation and/or other materials provided with the distribution. | |||
%%% * Neither the name of Erlang Training and Consulting Ltd. nor the | |||
%%% names of its contributors may be used to endorse or promote products | |||
%%% derived from this software without specific prior written permission. | |||
%%% | |||
%%% THIS SOFTWARE IS PROVIDED BY Erlang Training and Consulting Ltd. ''AS IS'' | |||
%%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||
%%% ARE DISCLAIMED. IN NO EVENT SHALL Erlang Training and Consulting Ltd. BE | |||
%%% LIABLE SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR | |||
%%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, | |||
%%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR | |||
%%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | |||
%%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||
%%% ---------------------------------------------------------------------------- | |||
-type header() :: | |||
'Cache-Control' | | |||
'Connection' | | |||
'Date' | | |||
'Pragma'| | |||
'Transfer-Encoding' | | |||
'Upgrade' | | |||
'Via' | | |||
'Accept' | | |||
'Accept-Charset'| | |||
'Accept-Encoding' | | |||
'Accept-Language' | | |||
'Authorization' | | |||
'From' | | |||
'Host' | | |||
'If-Modified-Since' | | |||
'If-Match' | | |||
'If-None-Match' | | |||
'If-Range'| | |||
'If-Unmodified-Since' | | |||
'Max-Forwards' | | |||
'Proxy-Authorization' | | |||
'Range'| | |||
'Referer' | | |||
'User-Agent' | | |||
'Age' | | |||
'Location' | | |||
'Proxy-Authenticate'| | |||
'Public' | | |||
'Retry-After' | | |||
'Server' | | |||
'Vary' | | |||
'Warning'| | |||
'Www-Authenticate' | | |||
'Allow' | | |||
'Content-Base' | | |||
'Content-Encoding'| | |||
'Content-Language' | | |||
'Content-Length' | | |||
'Content-Location'| | |||
'Content-Md5' | | |||
'Content-Range' | | |||
'Content-Type' | | |||
'Etag'| | |||
'Expires' | | |||
'Last-Modified' | | |||
'Accept-Ranges' | | |||
'Set-Cookie'| | |||
'Set-Cookie2' | | |||
'X-Forwarded-For' | | |||
'Cookie' | | |||
'Keep-Alive' | | |||
'Proxy-Connection' | | |||
binary() | | |||
string(). | |||
-type headers() :: [{header(), iodata()}]. | |||
-type method() :: string() | atom(). | |||
-type pos_timeout() :: pos_integer() | 'infinity'. | |||
-type bodypart() :: iodata() | 'http_eob'. | |||
-type socket() :: _. | |||
-type port_num() :: 1..65535. | |||
-type poolsize() :: non_neg_integer() | atom(). | |||
-type invalid_option() :: any(). | |||
-type pool_id() :: pid() | atom(). | |||
-type destination() :: {string(), pos_integer(), boolean()}. | |||
-type raw_headers() :: [{atom() | binary() | string(), binary() | string()}]. | |||
-type partial_download_option() :: | |||
{'window_size', window_size()} | | |||
{'part_size', non_neg_integer() | 'infinity'} | | |||
invalid_option(). | |||
-type option() :: | |||
{'connect_timeout', timeout()} | | |||
{'send_retry', non_neg_integer()} | | |||
{'partial_upload', non_neg_integer() | 'infinity'} | | |||
{'partial_download', [partial_download_option()]} | | |||
{'connect_options', socket_options()} | | |||
{'proxy', string()} | | |||
{'proxy_ssl_options', socket_options()} | | |||
{'pool', pid() | atom()} | | |||
invalid_option(). | |||
-type options() :: [option()]. | |||
-type host() :: string() | {integer(), integer(), integer(), integer()}. | |||
-type http_status() :: {integer(), string() | binary()} | {'nil','nil'}. | |||
-type socket_options() :: [{atom(), term()} | atom()]. | |||
-type window_size() :: non_neg_integer() | 'infinity'. | |||
-type upload_state() :: {pid(), window_size()}. | |||
-type body() :: binary() | | |||
'undefined' | % HEAD request. | |||
pid(). % When partial_download option is used. | |||
-type result() :: | |||
{ok, {{pos_integer(), string()}, headers(), body()}} | | |||
{ok, upload_state()} | | |||
{error, atom()}. |
@ -0,0 +1,15 @@ | |||
{application, erlArango, | |||
[{description, "An OTP application"}, | |||
{vsn, "0.1.0"}, | |||
{registered, []}, | |||
{mod, {erlArango_app, []}}, | |||
{applications, | |||
[kernel, | |||
stdlib | |||
]}, | |||
{env,[]}, | |||
{modules, []}, | |||
{licenses, ["Apache 2.0"]}, | |||
{links, []} | |||
]}. |
@ -0,0 +1,18 @@ | |||
%%%------------------------------------------------------------------- | |||
%% @doc erlArango public API | |||
%% @end | |||
%%%------------------------------------------------------------------- | |||
-module(erlArango_app). | |||
-behaviour(application). | |||
-export([start/2, stop/1]). | |||
start(_StartType, _StartArgs) -> | |||
erlArango_sup:start_link(). | |||
stop(_State) -> | |||
ok. | |||
%% internal functions |
@ -0,0 +1,35 @@ | |||
%%%------------------------------------------------------------------- | |||
%% @doc erlArango top level supervisor. | |||
%% @end | |||
%%%------------------------------------------------------------------- | |||
-module(erlArango_sup). | |||
-behaviour(supervisor). | |||
-export([start_link/0]). | |||
-export([init/1]). | |||
-define(SERVER, ?MODULE). | |||
start_link() -> | |||
supervisor:start_link({local, ?SERVER}, ?MODULE, []). | |||
%% sup_flags() = #{strategy => strategy(), % optional | |||
%% intensity => non_neg_integer(), % optional | |||
%% period => pos_integer()} % optional | |||
%% child_spec() = #{id => child_id(), % mandatory | |||
%% start => mfargs(), % mandatory | |||
%% restart => restart(), % optional | |||
%% shutdown => shutdown(), % optional | |||
%% type => worker(), % optional | |||
%% modules => modules()} % optional | |||
init([]) -> | |||
SupFlags = #{strategy => one_for_all, | |||
intensity => 0, | |||
period => 1}, | |||
ChildSpecs = [], | |||
{ok, {SupFlags, ChildSpecs}}. | |||
%% internal functions |
@ -0,0 +1,229 @@ | |||
-module(agAgencyPoolMgr). | |||
-include("agHttpCli.hrl"). | |||
-export([ | |||
startPool/2, | |||
startPool/3, | |||
stopPool/1, | |||
getOneAgency/1, | |||
start_link/3, | |||
init_it/3, | |||
system_code_change/4, | |||
system_continue/3, | |||
system_get_state/1, | |||
system_terminate/4, | |||
init/1, | |||
handleMsg/2, | |||
terminate/2 | |||
]). | |||
%% k-v缓存表 | |||
-define(ETS_AG_Pool, ets_ag_Pool). | |||
-define(ETS_AG_Agency, ets_ag_Agency). | |||
-record(state, {}). | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor start %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
-spec start_link(module(), term(), [proc_lib:spawn_option()]) -> {ok, pid()}. | |||
start_link(Name, Args, SpawnOpts) -> | |||
proc_lib:start_link(?MODULE, init_it, [Name, self(), Args], infinity, SpawnOpts). | |||
init_it(Name, Parent, Args) -> | |||
case safeRegister(Name) of | |||
true -> | |||
process_flag(trap_exit, true), | |||
moduleInit(Parent, Args); | |||
{false, Pid} -> | |||
proc_lib:init_ack(Parent, {error, {already_started, Pid}}) | |||
end. | |||
%% sys callbacks | |||
-spec system_code_change(term(), module(), undefined | term(), term()) -> {ok, term()}. | |||
system_code_change(State, _Module, _OldVsn, _Extra) -> | |||
{ok, State}. | |||
-spec system_continue(pid(), [], {module(), atom(), pid(), term()}) -> ok. | |||
system_continue(_Parent, _Debug, {Parent, State}) -> | |||
loop(Parent, State). | |||
-spec system_get_state(term()) -> {ok, term()}. | |||
system_get_state(State) -> | |||
{ok, State}. | |||
-spec system_terminate(term(), pid(), [], term()) -> none(). | |||
system_terminate(Reason, _Parent, _Debug, _State) -> | |||
exit(Reason). | |||
safeRegister(Name) -> | |||
try register(Name, self()) of | |||
true -> true | |||
catch | |||
_:_ -> {false, whereis(Name)} | |||
end. | |||
moduleInit(Parent, Args) -> | |||
case ?MODULE:init(Args) of | |||
{ok, State} -> | |||
proc_lib:init_ack(Parent, {ok, self()}), | |||
loop(Parent, State); | |||
{stop, Reason} -> | |||
proc_lib:init_ack(Parent, {error, Reason}), | |||
exit(Reason) | |||
end. | |||
loop(Parent, State) -> | |||
receive | |||
{system, From, Request} -> | |||
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Parent, State}); | |||
{'EXIT', Parent, Reason} -> | |||
terminate(Reason, State); | |||
Msg -> | |||
{ok, NewState} = ?MODULE:handleMsg(Msg, State), | |||
loop(Parent, NewState) | |||
end. | |||
terminate(Reason, State) -> | |||
?MODULE:terminate(Reason, State), | |||
exit(Reason). | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
-spec init(Args :: term()) -> ok. | |||
init(_Args) -> | |||
ets:new(?ETS_AG_Pool, [named_table, set, public]), | |||
{ok, #state{}}. | |||
handleMsg({'$gen_call', From, {startPool, Name, ClientOpts, PoolOpts}}, State) -> | |||
dealStart(Name, ClientOpts, PoolOpts), | |||
gen_server:reply(From, ok), | |||
{ok, State}; | |||
handleMsg({'$gen_call', From, {stopPool, Name}}, State) -> | |||
delaStop(Name), | |||
gen_server:reply(From, ok), | |||
{ok, State}; | |||
handleMsg(_Msg, State) -> | |||
?WARN(agAgencyPoolMgr, "receive unexpected msg: ~p", [_Msg]), | |||
{ok, State}. | |||
%% public | |||
-spec startPool(poolName(), clientOpts()) -> ok | {error, pool_name_used}. | |||
startPool(Name, ClientOpts) -> | |||
startPool(Name, ClientOpts, []). | |||
-spec startPool(poolName(), clientOpts(), poolOpts()) -> ok | {error, pool_name_used}. | |||
startPool(Name, ClientOpts, PoolOpts) -> | |||
case ?agBeamPool:get(Name) of | |||
undefined -> | |||
gen_server:call(?MODULE, {startPool, Name, ClientOpts, PoolOpts}); | |||
_ -> | |||
{error, pool_name_used} | |||
end. | |||
-spec stopPool(poolName()) -> ok | {error, pool_not_started}. | |||
stopPool(Name) -> | |||
case ?agBeamPool:get(Name) of | |||
undefined -> | |||
{error, pool_not_started}; | |||
_ -> | |||
gen_server:call(?MODULE, {stopPool, Name}) | |||
end. | |||
dealStart(Name, ClientOpts, PoolOpts) -> | |||
#poolOpts{poolSize = PoolSize} = PoolOptsRec = poolOptsToRec(PoolOpts), | |||
startChildren(Name, ClientOpts, PoolOptsRec), | |||
cacheAddPool(Name, PoolSize), | |||
cacheAddAgency(Name, PoolSize), | |||
ok. | |||
delaStop(Name) -> | |||
case ?agBeamPool:get(Name) of | |||
undefined -> | |||
{error, pool_not_started}; | |||
PoolSize -> | |||
stopChildren(agencyNames(Name, PoolSize)), | |||
cacheDelPool(Name), | |||
cacheDelAgency(Name), | |||
ok | |||
end. | |||
poolOptsToRec(Options) -> | |||
PoolSize = ?GET_FROM_LIST(poolSize, ?KEY_FIND(Options), ?DEFAULT_POOL_SIZE), | |||
BacklogSize = ?GET_FROM_LIST(backlogSize, ?KEY_FIND(Options), ?DEFAULT_BACKLOG_SIZE), | |||
PoolStrategy = ?GET_FROM_LIST(poolStrategy, ?KEY_FIND(Options), ?DEFAULT_POOL_STRATEGY), | |||
#poolOpts{poolSize = PoolSize, backlogSize = BacklogSize, poolStrategy = PoolStrategy}. | |||
agencyName(Name, Index) -> | |||
list_to_atom(atom_to_list(Name) ++ "_" ++ integer_to_list(Index)). | |||
agencyNames(Name, PoolSize) -> | |||
[agencyName(Name, N) || N <- lists:seq(1, PoolSize)]. | |||
agencyMod(tcp) -> | |||
agTcpAgency; | |||
agencyMod(ssl) -> | |||
agSslAgency; | |||
agencyMod(_) -> | |||
agTcpAgency. | |||
agencySpec(ServerMod, ServerName, Name, ClientOptions) -> | |||
StartFunc = {ServerMod, start_link, [ServerName, Name, ClientOptions]}, | |||
{ServerName, StartFunc, permanent, 5000, worker, [ServerMod]}. | |||
-spec startChildren(atom(), clientOpts(), poolOpts()) -> ok. | |||
startChildren(Name, ClientOpts, #poolOpts{poolSize = PoolSize}) -> | |||
Protocol = ?GET_FROM_LIST(protocol, ?KEY_FIND(ClientOpts), ?DEFAULT_PROTOCOL), | |||
AgencyMod = agencyMod(Protocol), | |||
AgencyNames = agencyNames(Name, PoolSize), | |||
AgencySpecs = [agencySpec(AgencyMod, AgencyName, Name, ClientOpts) || AgencyName <- AgencyNames], | |||
[supervisor:start_child(agHttpCli_sup, ServerSpec) || ServerSpec <- AgencySpecs], | |||
ok. | |||
stopChildren([ServerName | T]) -> | |||
supervisor:terminate_child(agHttpCli_sup, ServerName), | |||
supervisor:delete_child(agHttpCli_sup, ServerName), | |||
stopChildren(T); | |||
stopChildren([]) -> | |||
ok. | |||
cacheAddPool(Key, Value) -> | |||
ets:insert(?ETS_AG_Pool, {Key, Value}), | |||
KVS = ets:tab2list(?ETS_AG_Pool), | |||
agKvsToBeam:load(?agBeamPool, KVS), | |||
ok. | |||
cacheAddAgency(Name, PoolSize) -> | |||
NameList = [{{Name, N}, agencyName(Name, N)} || N <- lists:seq(1, PoolSize)], | |||
ets:insert(?ETS_AG_Agency, NameList), | |||
KVS = ets:tab2list(?ETS_AG_Agency), | |||
agKvsToBeam:load(?agBeamAgency, KVS), | |||
ok. | |||
cacheDelPool(Key) -> | |||
ets:delete(?ETS_AG_Pool, Key), | |||
KVS = ets:tab2list(?ETS_AG_Pool), | |||
agKvsToBeam:load(?agBeamPool, KVS), | |||
ok. | |||
cacheDelAgency(Name) -> | |||
ets:match_delete(?ETS_AG_Agency, {{Name, '_'}, '_'}), | |||
KVS = ets:tab2list(?ETS_AG_Agency), | |||
agKvsToBeam:load(?agBeamAgency, KVS), | |||
ok. | |||
getOneAgency(Name) -> | |||
case ?agBeamPool:get(Name) of | |||
undefined -> | |||
{error, pool_not_found}; | |||
PoolSize -> | |||
Ref = persistent_term:get(Name), | |||
AgencyIdx = atomics:add_get(Ref, 1, 1), | |||
case AgencyIdx >= PoolSize of | |||
true -> | |||
atomics:put(Ref, 1, 0), | |||
?ETS_AG_Agency:get({Name, PoolSize}); | |||
_ -> | |||
?ETS_AG_Agency:get({Name, AgencyIdx}) | |||
end | |||
end. |
@ -0,0 +1,81 @@ | |||
-module(agAgencyUtils). | |||
-include("agHttpCli.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
-export([ | |||
cancelTimer/1, | |||
agencyResponses/2, | |||
initReconnectState/1, | |||
resetReconnectState/1, | |||
reply/3, | |||
reply_all/2 | |||
]). | |||
-spec cancelTimer(undefined | reference()) -> ok. | |||
cancelTimer(undefined) -> ok; | |||
cancelTimer(TimerRef) -> | |||
case erlang:cancel_timer(TimerRef) of | |||
false -> | |||
%% 找不到计时器,我们还没有看到超时消息 | |||
receive | |||
{timeout, TimerRef, _Msg} -> | |||
%% 丢弃该超时消息 | |||
ok | |||
end; | |||
_ -> | |||
%% Timer 已经运行了 | |||
ok | |||
end. | |||
-spec agencyResponses([response()], server_name()) -> ok. | |||
agencyResponses([], _Name) -> | |||
ok; | |||
agencyResponses([{ExtRequestId, Reply} | T], Name) -> | |||
case shackle_queue:remove(Name, ExtRequestId) of | |||
{ok, Cast, TimerRef} -> | |||
erlang:cancel_timer(TimerRef), | |||
reply(Name, Reply, Cast); | |||
{error, not_found} -> | |||
ok | |||
end, | |||
agencyResponses(T, Name). | |||
-spec initReconnectState(client_options()) -> reconnect_state() | undefined. | |||
initReconnectState(Options) -> | |||
IsReconnect = ?GET_FROM_LIST(reconnect, ?KEY_FIND(Options), ?DEFAULT_IS_RECONNECT), | |||
case IsReconnect of | |||
true -> | |||
Max = ?GET_FROM_LIST(reconnectTimeMax, ?KEY_FIND(Options), ?DEFAULT_RECONNECT_MAX), | |||
Min = ?GET_FROM_LIST(reconnectTimeMin, ?KEY_FIND(Options), ?DEFAULT_RECONNECT_MIN), | |||
#reconnectState{min = Min, max = Max}; | |||
false -> | |||
undefined | |||
end. | |||
-spec resetReconnectState(undefined | reconnect_state()) -> reconnect_state() | undefined. | |||
resetReconnectState(ReconnectState) -> | |||
ReconnectState#reconnectState{current = undefined}. | |||
-spec reply(server_name(), term(), undefined | cast()) -> ok. | |||
reply(Name, _Reply, #request{pid = undefined}) -> | |||
shackle_backlog:decrement(Name), | |||
ok; | |||
reply(Name, Reply, #request{pid = Pid} = Request) -> | |||
shackle_backlog:decrement(Name), | |||
Pid ! {Request, Reply}, | |||
ok. | |||
-spec reply_all(server_name(), term()) -> ok. | |||
reply_all(Name, Reply) -> | |||
reply_all(Name, Reply, shackle_queue:clear(Name)). | |||
reply_all([{Cast, TimerRef} | T], Name, Reply) -> | |||
cancelTimer(TimerRef), | |||
reply(Name, Reply, Cast), | |||
reply_all(Name, Reply, T); | |||
reply_all([], _Name, _Reply) -> | |||
ok. |
@ -0,0 +1,200 @@ | |||
-module(agHttpCli). | |||
-include("agHttpCli.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
-export([ | |||
async_custom/3, | |||
async_get/2, | |||
async_post/2, | |||
async_put/2, | |||
async_request/3, | |||
custom/3, | |||
get/2, | |||
post/2, | |||
put/2, | |||
receive_response/1, | |||
request/3 | |||
]). | |||
%% public | |||
-spec async_custom(binary(), buoy_url(), buoy_opts()) -> | |||
{ok, shackle:request_id()} | error(). | |||
async_custom(Verb, Url, BuoyOpts) -> | |||
async_request({custom, Verb}, Url, BuoyOpts). | |||
-spec async_get(buoy_url(), buoy_opts()) -> | |||
{ok, shackle:request_id()} | error(). | |||
async_get(Url, BuoyOpts) -> | |||
async_request(get, Url, BuoyOpts). | |||
-spec async_post(buoy_url(), buoy_opts()) -> | |||
{ok, shackle:request_id()} | error(). | |||
async_post(Url, BuoyOpts) -> | |||
async_request(post, Url, BuoyOpts). | |||
-spec async_put(buoy_url(), buoy_opts()) -> | |||
{ok, shackle:request_id()} | error(). | |||
async_put(Url, BuoyOpts) -> | |||
async_request(put, Url, BuoyOpts). | |||
-spec async_request(method(), buoy_url(), buoy_opts()) -> | |||
{ok, shackle:request_id()} | error(). | |||
async_request(Method, #dbUrl{ | |||
protocol = Protocol, | |||
host = Host, | |||
hostname = Hostname, | |||
port = Port, | |||
path = Path | |||
}, BuoyOpts) -> | |||
case buoy_pool:lookup(Protocol, Hostname, Port) of | |||
{ok, PoolName} -> | |||
Headers = buoy_opts(headers, BuoyOpts), | |||
Body = buoy_opts(body, BuoyOpts), | |||
Request = {request, Method, Path, Headers, Host, Body}, | |||
Pid = buoy_opts(pid, BuoyOpts), | |||
Timeout = buoy_opts(timeout, BuoyOpts), | |||
shackle:cast(PoolName, Request, Pid, Timeout); | |||
{error, _} = E -> | |||
E | |||
end. | |||
-spec custom(binary(), buoy_url(), buoy_opts()) -> | |||
{ok, buoy_resp()} | error(). | |||
custom(Verb, Url, BuoyOpts) -> | |||
request({custom, Verb}, Url, BuoyOpts). | |||
-spec get(buoy_url(), buoy_opts()) -> | |||
{ok, buoy_resp()} | error(). | |||
get(Url, BuoyOpts) -> | |||
request(get, Url, BuoyOpts). | |||
-spec post(buoy_url(), buoy_opts()) -> | |||
{ok, buoy_resp()} | error(). | |||
post(Url, BuoyOpts) -> | |||
request(post, Url, BuoyOpts). | |||
-spec put(buoy_url(), buoy_opts()) -> | |||
{ok, buoy_resp()} | error(). | |||
put(Url, BuoyOpts) -> | |||
request(put, Url, BuoyOpts). | |||
-spec receive_response(request_id()) -> | |||
{ok, term()} | error(). | |||
receive_response(RequestId) -> | |||
shackle:receive_response(RequestId). | |||
-spec request(method(), buoy_url(), buoy_opts()) -> | |||
{ok, buoy_resp()} | error(). | |||
request(Method, #dbUrl{ | |||
protocol = Protocol, | |||
host = Host, | |||
hostname = Hostname, | |||
port = Port, | |||
path = Path | |||
}, BuoyOpts) -> | |||
case buoy_pool:lookup(Protocol, Hostname, Port) of | |||
{ok, PoolName} -> | |||
Headers = buoy_opts(headers, BuoyOpts), | |||
Body = buoy_opts(body, BuoyOpts), | |||
Request = {request, Method, Path, Headers, Host, Body}, | |||
Timeout = buoy_opts(timeout, BuoyOpts), | |||
shackle:call(PoolName, Request, Timeout); | |||
{error, _} = E -> | |||
E | |||
end. | |||
%% private | |||
buoy_opts(body, BuoyOpts) -> | |||
maps:get(body, BuoyOpts, ?DEFAULT_BODY); | |||
buoy_opts(headers, BuoyOpts) -> | |||
maps:get(headers, BuoyOpts, ?DEFAULT_HEADERS); | |||
buoy_opts(pid, BuoyOpts) -> | |||
maps:get(pid, BuoyOpts, ?DEFAULT_PID); | |||
buoy_opts(timeout, BuoyOpts) -> | |||
maps:get(timeout, BuoyOpts, ?DEFAULT_TIMEOUT). | |||
%% public | |||
-export([ | |||
call/2, | |||
call/3, | |||
cast/2, | |||
cast/3, | |||
cast/4, | |||
receive_response/1 | |||
]). | |||
%% public | |||
-spec call(pool_name(), term()) -> | |||
term() | {error, term()}. | |||
call(PoolName, Request) -> | |||
call(PoolName, Request, ?DEFAULT_TIMEOUT). | |||
-spec call(atom(), term(), timeout()) -> | |||
term() | {error, atom()}. | |||
call(PoolName, Request, Timeout) -> | |||
case cast(PoolName, Request, self(), Timeout) of | |||
{ok, RequestId} -> | |||
receive_response(RequestId); | |||
{error, Reason} -> | |||
{error, Reason} | |||
end. | |||
-spec cast(pool_name(), term()) -> | |||
{ok, request_id()} | {error, atom()}. | |||
cast(PoolName, Request) -> | |||
cast(PoolName, Request, self()). | |||
-spec cast(pool_name(), term(), pid()) -> | |||
{ok, request_id()} | {error, atom()}. | |||
cast(PoolName, Request, Pid) -> | |||
cast(PoolName, Request, Pid, ?DEFAULT_TIMEOUT). | |||
-spec cast(pool_name(), term(), pid(), timeout()) -> | |||
{ok, request_id()} | {error, atom()}. | |||
cast(PoolName, Request, Pid, Timeout) -> | |||
Timestamp = os:timestamp(), | |||
case agAgencyPoolMgr:server(PoolName) of | |||
{ok, Client, Server} -> | |||
RequestId = {Server, make_ref()}, | |||
Server ! {Request, #cast{ | |||
client = Client, | |||
pid = Pid, | |||
request_id = RequestId, | |||
timeout = Timeout, | |||
timestamp = Timestamp | |||
}}, | |||
{ok, RequestId}; | |||
{error, Reason} -> | |||
{error, Reason} | |||
end. | |||
-spec receive_response(request_id()) -> | |||
term() | {error, term()}. | |||
receive_response(RequestId) -> | |||
receive | |||
{#cast{request_id = RequestId}, Reply} -> | |||
Reply | |||
end. | |||
@ -0,0 +1,125 @@ | |||
%% beam cache 模块名 | |||
-define(agBeamPool, agBeamPool). | |||
-define(agBeamAgency, agBeamAgency). | |||
%% 默认值定义 | |||
-define(DEFAULT_BACKLOG_SIZE, 1024). | |||
-define(DEFAULT_INIT_OPTS, undefined). | |||
-define(DEFAULT_CONNECT_TIMEOUT, 500). | |||
-define(DEFAULT_PORT, 80). | |||
-define(DEFAULT_IP, <<"127.0.0.1">>). | |||
-define(DEFAULT_POOL_SIZE, 16). | |||
-define(DEFAULT_POOL_STRATEGY, random). | |||
-define(DEFAULT_POOL_OPTIONS, []). | |||
-define(DEFAULT_IS_RECONNECT, true). | |||
-define(DEFAULT_RECONNECT_MAX, 120000). | |||
-define(DEFAULT_RECONNECT_MIN, 500). | |||
-define(DEFAULT_SOCKET_OPTS, []). | |||
-define(DEFAULT_TIMEOUT, 1000). | |||
-define(DEFAULT_BODY, undefined). | |||
-define(DEFAULT_HEADERS, []). | |||
-define(DEFAULT_PID, self()). | |||
-define(DEFAULT_PROTOCOL, tcp). | |||
-define(KEY_FIND(List), lists:keyfind(Key, 1, List)). | |||
-define(GET_FROM_LIST(Key, ?KEY_FIND(List)), case Value of false -> undefined; _ -> element(2, Value) end). | |||
-define(GET_FROM_LIST(Key, ?KEY_FIND(List), Default), case Value of false -> Default; _ -> element(2, Value) end). | |||
-define(WARN(PoolName, Format, Data), agMiscUtils:warnMsg(PoolName, Format, Data)). | |||
-record(dbUrl, { | |||
host :: host(), | |||
path :: path(), | |||
port :: 0..65535, | |||
hostname :: hostname(), | |||
protocol :: httpType() | |||
}). | |||
-record(requestRet, { | |||
state :: body | done, | |||
body :: undefined | binary(), | |||
content_length :: undefined | non_neg_integer() | chunked, | |||
headers :: undefined | [binary()], | |||
reason :: undefined | binary(), | |||
status_code :: undefined | 100..505 | |||
}). | |||
-record(request, { | |||
requestId :: requestId(), | |||
pid :: pid() | undefined, | |||
timeout :: timeout(), | |||
timestamp :: erlang:timestamp() | |||
}). | |||
-record(poolOpts, { | |||
poolSize :: poolSize(), | |||
backlogSize :: backlogSize(), | |||
poolStrategy :: poolStrategy() | |||
}). | |||
-record(reconnectState, { | |||
min :: time(), | |||
max :: time() | infinity, | |||
current :: time() | undefined | |||
}). | |||
-type requestRet() :: #requestRet {}. | |||
-type dbUrl() :: #dbUrl {}. | |||
-type error() :: {error, term()}. | |||
-type headers() :: [{iodata(), iodata()}, ...]. | |||
-type host() :: binary(). | |||
-type hostname() :: binary(). | |||
-type path() :: binary(). | |||
-type method() :: binary(). | |||
-type httpType() :: http | https. | |||
-type body() :: iodata() | undefined. | |||
-type options() :: [option(), ...]. | |||
-type option() :: | |||
{backlogSize, pos_integer()} | | |||
{poolSize, pos_integer()} | | |||
{poolStrategy, random | round_robin} | | |||
{reconnect, boolean()} | | |||
{reconnectTimeMin, pos_integer()} | | |||
{reconnectTimeMax, pos_integer() | infinity}. | |||
-type buoy_opts() :: | |||
#{ | |||
headers => headers(), | |||
body => body(), | |||
pid => pid(), | |||
timeout => non_neg_integer() | |||
}. | |||
-type backlogSize() :: pos_integer() | infinity. | |||
-type request() :: #request{}. | |||
-type clientOpt() :: | |||
{initOpts, term()} | | |||
{ip, inet:ip_address() | inet:hostname()} | | |||
{port, inet:port_number()} | | |||
{protocol, protocol()} | | |||
{reconnect, boolean()} | | |||
{reconnectTimeMin, time()} | | |||
{reconnectTimeMax, time() | infinity} | | |||
{socketOpts, [gen_tcp:connect_option(), ...]}. | |||
-type clientOpts() :: [clientOpt(), ...]. | |||
-type clientState() :: term(). | |||
-type externalRequestId() :: term(). | |||
-type poolName() :: atom(). | |||
-type poolOpt() :: | |||
{poolSize, poolSize()} | | |||
{backlogSize, backlogSize()} | | |||
{poolstrategy, poolStrategy()}. | |||
-type poolOpts() :: [poolOpt()]. | |||
-type poolOptsRec() :: #poolOpts{}. | |||
-type poolSize() :: pos_integer(). | |||
-type poolStrategy() :: random | round_robin. | |||
-type protocol() :: ssl | tcp. | |||
-type reconnectState() :: #reconnectState{}. | |||
-type requestId() :: {server_name(), reference()}. | |||
-type response() :: {externalRequestId(), term()}. | |||
-type server_name() :: atom(). | |||
-type socket() :: inet:socket() | ssl:sslsocket(). | |||
-type socketType() :: inet | ssl. | |||
-type time() :: pos_integer(). |
@ -0,0 +1,74 @@ | |||
-module(agHttpCli_app). | |||
-include("buoy_internal.hrl"). | |||
-export([ | |||
start/0, | |||
stop/0 | |||
]). | |||
-behaviour(application). | |||
-export([ | |||
start/2, | |||
stop/1 | |||
]). | |||
%% public | |||
-spec start() -> | |||
{ok, [atom()]}. | |||
start() -> | |||
application:ensure_all_started(?APP). | |||
-spec stop() -> | |||
ok | {error, {not_started, ?APP}}. | |||
stop() -> | |||
application:stop(?APP). | |||
%% application callbacks | |||
-spec start(application:start_type(), term()) -> | |||
{ok, pid()}. | |||
start(_StartType, _StartArgs) -> | |||
agHttpCli_sup:start_link(). | |||
-spec stop(term()) -> | |||
ok. | |||
stop(_State) -> | |||
agAgencyPoolMgr:terminate(), | |||
ok. | |||
-behaviour(application). | |||
-export([ | |||
start/2, | |||
stop/1 | |||
]). | |||
%% public | |||
-spec start() -> | |||
{ok, [atom()]} | {error, term()}. | |||
start() -> | |||
application:ensure_all_started(?APP). | |||
-spec stop() -> | |||
ok | {error, term()}. | |||
stop() -> | |||
application:stop(?APP). | |||
%% application callbacks | |||
-spec start(application:start_type(), term()) -> | |||
{ok, pid()}. | |||
start(_StartType, _StartArgs) -> | |||
shackle_sup:start_link(). | |||
-spec stop(term()) -> | |||
ok. | |||
stop(_State) -> | |||
agAgencyPoolMgr:terminate(), | |||
ok. | |||
@ -0,0 +1,50 @@ | |||
-module(agHttpCli_sup). | |||
-include("buoy_internal.hrl"). | |||
-export([ | |||
start_link/0 | |||
]). | |||
-behaviour(supervisor). | |||
-export([ | |||
init/1 | |||
]). | |||
%% public | |||
-spec start_link() -> | |||
{ok, pid()}. | |||
start_link() -> | |||
supervisor:start_link({local, ?MODULE}, ?MODULE, []). | |||
%% supervisor callbacks | |||
-spec init([]) -> | |||
{ok, {{one_for_one, 5, 10}, []}}. | |||
init([]) -> | |||
buoy_pool:init(), | |||
{ok, {{one_for_one, 5, 10}, []}}. | |||
-behaviour(supervisor). | |||
-export([ | |||
init/1 | |||
]). | |||
%% internal | |||
-spec start_link() -> | |||
{ok, pid()}. | |||
start_link() -> | |||
supervisor:start_link({local, shackle_sup}, shackle_sup, []). | |||
%% supervisor callbacks | |||
-spec init([]) -> | |||
{ok, {{one_for_one, 5, 10}, []}}. | |||
init([]) -> | |||
shackle_backlog:init(), | |||
agAgencyPoolMgr:init(), | |||
shackle_queue:init(), | |||
{ok, {{one_for_one, 5, 10}, []}}. |
@ -0,0 +1,276 @@ | |||
-module(agHttpProtocol). | |||
-include("buoy_internal.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
-export([ | |||
bin_patterns/0, | |||
headers/1, | |||
request/5, | |||
response/1, | |||
response/3 | |||
]). | |||
-record(bin_patterns, { | |||
rn :: binary:cp(), | |||
rnrn :: binary:cp() | |||
}). | |||
-type bin_patterns() :: #bin_patterns {}. | |||
%% public | |||
-spec bin_patterns() -> | |||
bin_patterns(). | |||
bin_patterns() -> | |||
#bin_patterns { | |||
rn = binary:compile_pattern(<<"\r\n">>), | |||
rnrn = binary:compile_pattern(<<"\r\n\r\n">>) | |||
}. | |||
-spec headers(buoy_resp()) -> | |||
{ok, headers()} | {error, invalid_headers}. | |||
headers(#buoy_resp {headers = Headers}) -> | |||
parse_headers(Headers, []). | |||
-spec request(method(), path(), headers(), host(), body()) -> | |||
iolist(). | |||
request(Method, Path, Headers, Host, undefined) -> | |||
[format_method(Method), <<" ">>, Path, | |||
<<" HTTP/1.1\r\n">>, | |||
<<"Host: ">>, Host, | |||
<<"\r\nConnection: Keep-alive\r\n">>, | |||
<<"User-Agent: buoy\r\n">>, | |||
format_headers(Headers), <<"\r\n">>]; | |||
request(Method, Path, Headers, Host, Body) -> | |||
ContentLength = integer_to_binary(iolist_size(Body)), | |||
Headers2 = [{<<"Content-Length">>, ContentLength} | Headers], | |||
[format_method(Method), <<" ">>, Path, | |||
<<" HTTP/1.1\r\n">>, | |||
<<"Host: ">>, Host, | |||
<<"\r\nConnection: Keep-alive\r\n">>, | |||
<<"User-Agent: buoy\r\n">>, | |||
format_headers(Headers2), <<"\r\n">>, | |||
Body]. | |||
-spec response(binary()) -> | |||
{ok, buoy_resp(), binary()} | error(). | |||
response(Data) -> | |||
response(Data, undefined, bin_patterns()). | |||
-spec response(binary(), undefined | buoy_resp(), bin_patterns()) -> | |||
{ok, buoy_resp(), binary()} | error(). | |||
response(Data, undefined, BinPatterns) -> | |||
case parse_status_line(Data, BinPatterns) of | |||
{StatusCode, Reason, Rest} -> | |||
case split_headers(Rest, BinPatterns) of | |||
{undefined, Headers, Rest2} -> | |||
{ok, #buoy_resp { | |||
state = done, | |||
status_code = StatusCode, | |||
reason = Reason, | |||
headers = Headers, | |||
content_length = undefined | |||
}, Rest2}; | |||
{0, Headers, Rest2} -> | |||
{ok, #buoy_resp { | |||
state = done, | |||
status_code = StatusCode, | |||
reason = Reason, | |||
headers = Headers, | |||
content_length = 0 | |||
}, Rest2}; | |||
{ContentLength, Headers, Rest2} -> | |||
response(Rest2, #buoy_resp { | |||
state = body, | |||
status_code = StatusCode, | |||
reason = Reason, | |||
headers = Headers, | |||
content_length = ContentLength | |||
}, BinPatterns); | |||
{error, Reason2} -> | |||
{error, Reason2} | |||
end; | |||
{error, Reason} -> | |||
{error, Reason} | |||
end; | |||
response(Data, #buoy_resp { | |||
state = body, | |||
content_length = chunked | |||
} = Response, BinPatterns) -> | |||
case parse_chunks(Data, BinPatterns) of | |||
{ok, Body, Rest} -> | |||
{ok, Response#buoy_resp { | |||
state = done, | |||
body = Body | |||
}, Rest}; | |||
{error, Reason} -> | |||
{error, Reason} | |||
end; | |||
response(Data, #buoy_resp { | |||
state = body, | |||
content_length = ContentLength | |||
} = Response, _BinPatterns) when size(Data) >= ContentLength -> | |||
<<Body:ContentLength/binary, Rest/binary>> = Data, | |||
{ok, Response#buoy_resp { | |||
state = done, | |||
body = Body | |||
}, Rest}; | |||
response(Data, #buoy_resp { | |||
state = body | |||
} = Response, _BinPatterns) -> | |||
{ok, Response, Data}. | |||
%% private | |||
binary_split_global(Bin, Pattern) -> | |||
case binary:split(Bin, Pattern) of | |||
[Split, Rest] -> | |||
[Split | binary_split_global(Rest, Pattern)]; | |||
Rest -> | |||
Rest | |||
end. | |||
content_length([]) -> | |||
undefined; | |||
content_length([<<"Content-Length: ", Rest/binary>> | _T]) -> | |||
binary_to_integer(Rest); | |||
content_length([<<"content-length: ", Rest/binary>> | _T]) -> | |||
binary_to_integer(Rest); | |||
content_length([<<"Transfer-Encoding: chunked">> | _T]) -> | |||
chunked; | |||
content_length([<<"transfer-encoding: chunked">> | _T]) -> | |||
chunked; | |||
content_length([_ | T]) -> | |||
content_length(T). | |||
format_method(get) -> | |||
<<"GET">>; | |||
format_method(head) -> | |||
<<"HEAD">>; | |||
format_method(post) -> | |||
<<"POST">>; | |||
format_method(put) -> | |||
<<"PUT">>; | |||
format_method({custom, Verb}) -> | |||
Verb. | |||
format_headers(Headers) -> | |||
[format_header(Header) || Header <- Headers]. | |||
format_header({Key, Value}) -> | |||
[Key, <<": ">>, Value, <<"\r\n">>]. | |||
parse_chunks(Data, BinPatterns) -> | |||
parse_chunks(Data, BinPatterns, []). | |||
parse_chunks(Data, BinPatterns, Acc) -> | |||
case parse_chunk(Data, BinPatterns) of | |||
{ok, <<>>, Rest} -> | |||
{ok, iolist_to_binary(lists:reverse(Acc)), Rest}; | |||
{ok, Body, Rest} -> | |||
parse_chunks(Rest, BinPatterns, [Body | Acc]); | |||
{error, Reason} -> | |||
{error, Reason} | |||
end. | |||
parse_chunk(Data, #bin_patterns {rn = Rn}) -> | |||
case binary:split(Data, Rn) of | |||
[Size, Rest] -> | |||
case parse_chunk_size(Size) of | |||
undefined -> | |||
{error, invalid_chunk_size}; | |||
Size2 -> | |||
parse_chunk_body(Rest, Size2) | |||
end; | |||
[Data] -> | |||
{error, not_enough_data} | |||
end. | |||
parse_chunk_body(Data, Size) -> | |||
case Data of | |||
<<Body:Size/binary, "\r\n", Rest/binary>> -> | |||
{ok, Body, Rest}; | |||
_ -> | |||
{error, not_enough_data} | |||
end. | |||
parse_chunk_size(Bin) -> | |||
try | |||
binary_to_integer(Bin, 16) | |||
catch | |||
error:badarg -> | |||
undefined | |||
end. | |||
parse_headers([], Acc) -> | |||
{ok, lists:reverse(Acc)}; | |||
parse_headers([Header | T], Acc) -> | |||
case binary:split(Header, <<":">>) of | |||
[Header] -> | |||
{error, invalid_headers}; | |||
[Key, <<>>] -> | |||
parse_headers(T, [{Key, undefined} | Acc]); | |||
[Key, <<" ", Value/binary>>] -> | |||
parse_headers(T, [{Key, Value} | Acc]) | |||
end. | |||
parse_status_line(Data, #bin_patterns {rn = Rn}) -> | |||
case binary:split(Data, Rn) of | |||
[Data] -> | |||
{error, not_enough_data}; | |||
[Line, Rest] -> | |||
case parse_status_reason(Line) of | |||
{ok, StatusCode, Reason} -> | |||
{StatusCode, Reason, Rest}; | |||
{error, Reason} -> | |||
{error, Reason} | |||
end | |||
end. | |||
parse_status_reason(<<"HTTP/1.1 200 OK">>) -> | |||
{ok, 200, <<"OK">>}; | |||
parse_status_reason(<<"HTTP/1.1 204 No Content">>) -> | |||
{ok, 204, <<"No Content">>}; | |||
parse_status_reason(<<"HTTP/1.1 301 Moved Permanently">>) -> | |||
{ok, 301, <<"Moved Permanently">>}; | |||
parse_status_reason(<<"HTTP/1.1 302 Found">>) -> | |||
{ok, 302, <<"Found">>}; | |||
parse_status_reason(<<"HTTP/1.1 403 Forbidden">>) -> | |||
{ok, 403, <<"Forbidden">>}; | |||
parse_status_reason(<<"HTTP/1.1 404 Not Found">>) -> | |||
{ok, 404, <<"Not Found">>}; | |||
parse_status_reason(<<"HTTP/1.1 500 Internal Server Error">>) -> | |||
{ok, 500, <<"Internal Server Error">>}; | |||
parse_status_reason(<<"HTTP/1.1 502 Bad Gateway">>) -> | |||
{ok, 502, <<"Bad Gateway">>}; | |||
parse_status_reason(<<"HTTP/1.1 ", N1, N2, N3, " ", Reason/bits >>) | |||
when $0 =< N1, N1 =< $9, | |||
$0 =< N2, N2 =< $9, | |||
$0 =< N3, N3 =< $9 -> | |||
StatusCode = (N1 - $0) * 100 + (N2 - $0) * 10 + (N3 - $0), | |||
{ok, StatusCode, Reason}; | |||
parse_status_reason(<<"HTTP/1.0 ", _/binary>>) -> | |||
{error, unsupported_feature}; | |||
parse_status_reason(_) -> | |||
{error, bad_request}. | |||
split_headers(Data, #bin_patterns {rn = Rn, rnrn = Rnrn}) -> | |||
case binary:split(Data, Rnrn) of | |||
[Data] -> | |||
{error, not_enough_data}; | |||
[Headers, Rest] -> | |||
Headers2 = binary_split_global(Headers, Rn), | |||
ContentLength = content_length(Headers2), | |||
{ContentLength, Headers2, Rest} | |||
end. |
@ -0,0 +1,56 @@ | |||
-module(agKvsToBeam). | |||
-export([ | |||
load/2 | |||
]). | |||
-spec load(namespace(), [{key(), value()}]) -> ok. | |||
load(Module, KVs) -> | |||
Forms = forms(Module, KVs), | |||
{ok, Module, Bin} = compile:forms(Forms), | |||
code:soft_purge(Module), | |||
{module, Module} = code:load_binary(Module, atom_to_list(Module), Bin), | |||
ok. | |||
forms(Module, KVs) -> | |||
Mod = erl_syntax:attribute(erl_syntax:atom(module), | |||
[erl_syntax:atom(Module)]), | |||
ExportList = [erl_syntax:arity_qualifier(erl_syntax:atom(get), | |||
erl_syntax:integer(1))], | |||
Export = erl_syntax:attribute(erl_syntax:atom(export), | |||
[erl_syntax:list(ExportList)]), | |||
Function = erl_syntax:function(erl_syntax:atom(get), | |||
lookup_clauses(KVs)), | |||
[erl_syntax:revert(X) || X <- [Mod, Export, Function]]. | |||
lookup_clause(Key, Value) -> | |||
Var = to_syntax(Key), | |||
Body = to_syntax(Value), | |||
erl_syntax:clause([Var], [], [Body]). | |||
lookup_clause_anon() -> | |||
Var = erl_syntax:variable("_"), | |||
Body = erl_syntax:atom(undefined), | |||
erl_syntax:clause([Var], [], [Body]). | |||
lookup_clauses(KVs) -> | |||
lookup_clauses(KVs, []). | |||
lookup_clauses([], Acc) -> | |||
lists:reverse(lists:flatten([lookup_clause_anon() | Acc])); | |||
lookup_clauses([{Key, Value} | T], Acc) -> | |||
lookup_clauses(T, [lookup_clause(Key, Value) | Acc]). | |||
to_syntax(Atom) when is_atom(Atom) -> | |||
erl_syntax:atom(Atom); | |||
to_syntax(Binary) when is_binary(Binary) -> | |||
String = erl_syntax:string(binary_to_list(Binary)), | |||
erl_syntax:binary([erl_syntax:binary_field(String)]); | |||
to_syntax(Float) when is_float(Float) -> | |||
erl_syntax:float(Float); | |||
to_syntax(Integer) when is_integer(Integer) -> | |||
erl_syntax:integer(Integer); | |||
to_syntax(List) when is_list(List) -> | |||
erl_syntax:list([to_syntax(X) || X <- List]); | |||
to_syntax(Tuple) when is_tuple(Tuple) -> | |||
erl_syntax:tuple([to_syntax(X) || X <- tuple_to_list(Tuple)]). |
@ -0,0 +1,75 @@ | |||
-module(agMiscUtils). | |||
-include("agHttpCli.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
-export([ | |||
parseUrl/1 | |||
, random/1 | |||
, randomElement/1 | |||
, warnMsg/3 | |||
]). | |||
-spec parseUrl(binary()) -> dbUrl() | {error, invalid_url}. | |||
parseUrl(<<"http://", Rest/binary>>) -> | |||
parseUrl(tcp, Rest); | |||
parseUrl(<<"https://", Rest/binary>>) -> | |||
parseUrl(ssl, Rest); | |||
parseUrl(_) -> | |||
{error, invalid_url}. | |||
parseUrl(Protocol, Rest) -> | |||
{Host, Path} = | |||
case binary:split(Rest, <<"/">>, [trim]) of | |||
[UrlHost] -> | |||
{UrlHost, <<"/">>}; | |||
[UrlHost, UrlPath] -> | |||
{UrlHost, <<"/", UrlPath/binary>>} | |||
end, | |||
{Hostname, Port} = | |||
case binary:split(Host, <<":">>, [trim]) of | |||
[Host] -> | |||
case Protocol of | |||
tcp -> | |||
{Host, 80}; | |||
ssl -> | |||
{Host, 443} | |||
end; | |||
[UrlHostname, UrlPort] -> | |||
{UrlHostname, binary_to_integer(UrlPort)} | |||
end, | |||
#dbUrl{ | |||
host = Host, | |||
path = Path, | |||
port = Port, | |||
hostname = Hostname, | |||
protocol = Protocol | |||
}. | |||
%% public | |||
-export([ | |||
]). | |||
-spec random(pos_integer()) -> non_neg_integer(). | |||
random(1) -> 1; | |||
random(N) -> | |||
rand:uniform(N). | |||
-spec randomElement([term()]) -> term(). | |||
randomElement([X]) -> | |||
X; | |||
randomElement([_ | _] = List) -> | |||
T = list_to_tuple(List), | |||
element(random(tuple_size(T)), T). | |||
-spec warnMsg(pool_name(), string(), [term()]) -> ok. | |||
warnMsg(Pool, Format, Data) -> | |||
error_logger:warning_msg("[~p] " ++ Format, [Pool | Data]). |
@ -0,0 +1,59 @@ | |||
-module(agNetCli). | |||
-include("agHttpCli.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
-export([ | |||
handleRequest/2, | |||
handleData/2, | |||
terminate/1 | |||
]). | |||
-record(state, { | |||
binPatterns :: tuple(), | |||
buffer = <<>> :: binary(), | |||
response :: undefined | requestRet(), | |||
requestsIn = 0 :: non_neg_integer(), | |||
requestsOut = 0 :: non_neg_integer() | |||
}). | |||
-type state() :: #state {}. | |||
-spec handleRequest(term(), state()) -> {ok, non_neg_integer(), iodata(), state()}. | |||
handleRequest({request, Method, Path, Headers, Host, Body}, #state{requestsOut = RequestsOut} = State) -> | |||
Request = agHttpProtocol:request(Method, Path, Headers, Host, Body), | |||
{ok, RequestsOut, Request, State#state{requestsOut = RequestsOut + 1}}. | |||
-spec handleData(binary(), state()) -> {ok, [{pos_integer(), term()}], state()} | {error, atom(), state()}. | |||
handleData(Data, #state{binPatterns = BinPatterns, buffer = Buffer, requestsIn = RequestsIn, response = Response} = State) -> | |||
Data2 = <<Buffer/binary, Data/binary>>, | |||
case responses(Data2, RequestsIn, Response, BinPatterns, []) of | |||
{ok, RequestsIn2, Response2, Responses, Rest} -> | |||
{ok, Responses, State#state{ | |||
buffer = Rest, | |||
requestsIn = RequestsIn2, | |||
response = Response2 | |||
}}; | |||
{error, Reason} -> | |||
{error, Reason, State} | |||
end. | |||
-spec terminate(state()) -> ok. | |||
terminate(_State) -> | |||
ok. | |||
responses(<<>>, RequestsIn, Response, _BinPatterns, Responses) -> | |||
{ok, RequestsIn, Response, Responses, <<>>}; | |||
responses(Data, RequestsIn, Response, BinPatterns, Responses) -> | |||
case agHttpProtocol:response(Data, Response, BinPatterns) of | |||
{ok, #requestRet{state = done} = Response2, Rest} -> | |||
Responses2 = [{RequestsIn, {ok, Response2}} | Responses], | |||
responses(Rest, RequestsIn + 1, undefined, BinPatterns, Responses2); | |||
{ok, #requestRet{} = Response2, Rest} -> | |||
{ok, RequestsIn, Response2, Responses, Rest}; | |||
{error, not_enough_data} -> | |||
{ok, RequestsIn, Response, Responses, Data}; | |||
{error, _Reason} = E -> | |||
E | |||
end. |
@ -0,0 +1,324 @@ | |||
-module(agSslAgency). | |||
-include("shackle_internal.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
-export([ | |||
start_link/3, | |||
init_it/3, | |||
system_code_change/4, | |||
system_continue/3, | |||
system_get_state/1, | |||
system_terminate/4, | |||
init/3, | |||
handleMsg/2, | |||
terminate/2 | |||
]). | |||
-record(state, { | |||
client :: client(), | |||
init_options :: init_options(), | |||
ip :: inet:ip_address() | inet:hostname(), | |||
name :: server_name(), | |||
parent :: pid(), | |||
pool_name :: pool_name(), | |||
port :: inet:port_number(), | |||
reconnect_state :: undefined | reconnect_state(), | |||
socket :: undefined | ssl:sslsocket(), | |||
socket_options :: [ssl:connect_option()], | |||
timer_ref :: undefined | reference() | |||
}). | |||
-type init_opts() :: {pool_name(), client(), client_options()}. | |||
-type state() :: #state {}. | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor start %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
-spec start_link(module(), atom(), term(), [proc_lib:spawn_option()]) -> {ok, pid()}. | |||
start_link(Name, Args, SpawnOpts) -> | |||
proc_lib:start_link(?MODULE, init_it, [Name, self(), Args], infinity, SpawnOpts). | |||
init_it(Name, Parent, Args) -> | |||
case safeRegister(Name) of | |||
true -> | |||
process_flag(trap_exit, true), | |||
moduleInit(Parent, Args); | |||
{false, Pid} -> | |||
proc_lib:init_ack(Parent, {error, {already_started, Pid}}) | |||
end. | |||
%% sys callbacks | |||
-spec system_code_change(term(), module(), undefined | term(), term()) -> {ok, term()}. | |||
system_code_change(State, _Module, _OldVsn, _Extra) -> | |||
{ok, State}. | |||
-spec system_continue(pid(), [], {module(), atom(), pid(), term()}) -> ok. | |||
system_continue(_Parent, _Debug, {Parent, State}) -> | |||
loop(Parent, State). | |||
-spec system_get_state(term()) -> {ok, term()}. | |||
system_get_state(State) -> | |||
{ok, State}. | |||
-spec system_terminate(term(), pid(), [], term()) -> none(). | |||
system_terminate(Reason, _Parent, _Debug, _State) -> | |||
exit(Reason). | |||
safeRegister(Name) -> | |||
try register(Name, self()) of | |||
true -> true | |||
catch | |||
_:_ -> {false, whereis(Name)} | |||
end. | |||
moduleInit(Parent, Args) -> | |||
case ?MODULE:init(Args) of | |||
{ok, State} -> | |||
proc_lib:init_ack(Parent, {ok, self()}), | |||
loop(Parent, State); | |||
{stop, Reason} -> | |||
proc_lib:init_ack(Parent, {error, Reason}), | |||
exit(Reason) | |||
end. | |||
loop(Parent, State) -> | |||
receive | |||
{system, From, Request} -> | |||
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Parent, State}); | |||
{'EXIT', Parent, Reason} -> | |||
terminate(Reason, State); | |||
Msg -> | |||
{ok, NewState} = ?MODULE:handleMsg(Msg, State), | |||
loop(Parent, NewState) | |||
end. | |||
terminate(Reason, State) -> | |||
?MODULE:terminate(Reason, State), | |||
exit(Reason). | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
%% metal callbacks | |||
-spec init(server_name(), pid(), init_opts()) -> | |||
no_return(). | |||
init(Name, Parent, Opts) -> | |||
{PoolName, Client, ClientOptions} = Opts, | |||
self() ! ?MSG_CONNECT, | |||
ok = shackle_backlog:new(Name), | |||
InitOptions = ?LOOKUP(init_options, ClientOptions, | |||
?DEFAULT_INIT_OPTS), | |||
Ip = ?LOOKUP(ip, ClientOptions, ?DEFAULT_IP), | |||
Port = ?LOOKUP(port, ClientOptions), | |||
ReconnectState = agAgencyUtils:initReconnectState(ClientOptions), | |||
SocketOptions = ?LOOKUP(socket_options, ClientOptions, | |||
?DEFAULT_SOCKET_OPTS), | |||
{ok, {#state { | |||
client = Client, | |||
init_options = InitOptions, | |||
ip = Ip, | |||
name = Name, | |||
parent = Parent, | |||
pool_name = PoolName, | |||
port = Port, | |||
reconnect_state = ReconnectState, | |||
socket_options = SocketOptions | |||
}, undefined}}. | |||
-spec handle_msg(term(), {state(), client_state()}) -> | |||
{ok, term()}. | |||
handle_msg({_, #cast {} = Cast}, {#state { | |||
socket = undefined, | |||
name = Name | |||
} = State, ClientState}) -> | |||
agAgencyUtils:reply(Name, {error, no_socket}, Cast), | |||
{ok, {State, ClientState}}; | |||
handle_msg({Request, #cast { | |||
timeout = Timeout | |||
} = Cast}, {#state { | |||
client = Client, | |||
name = Name, | |||
pool_name = PoolName, | |||
socket = Socket | |||
} = State, ClientState}) -> | |||
try agNetCli:handleRequest(Request, ClientState) of | |||
{ok, ExtRequestId, Data, ClientState2} -> | |||
case ssl:send(Socket, Data) of | |||
ok -> | |||
Msg = {timeout, ExtRequestId}, | |||
TimerRef = erlang:send_after(Timeout, self(), Msg), | |||
shackle_queue:add(ExtRequestId, Cast, TimerRef), | |||
{ok, {State, ClientState2}}; | |||
{error, Reason} -> | |||
?WARN(PoolName, "send error: ~p", [Reason]), | |||
ssl:close(Socket), | |||
agAgencyUtils:reply(Name, {error, socket_closed}, Cast), | |||
close(State, ClientState2) | |||
end | |||
catch | |||
?EXCEPTION(E, R, Stacktrace) -> | |||
?WARN(PoolName, "handleRequest crash: ~p:~p~n~p~n", | |||
[E, R, ?GET_STACK(Stacktrace)]), | |||
agAgencyUtils:reply(Name, {error, client_crash}, Cast), | |||
{ok, {State, ClientState}} | |||
end; | |||
handle_msg({ssl, Socket, Data}, {#state { | |||
client = Client, | |||
name = Name, | |||
pool_name = PoolName, | |||
socket = Socket | |||
} = State, ClientState}) -> | |||
try agNetCli:handleData(Data, ClientState) of | |||
{ok, Replies, ClientState2} -> | |||
agAgencyUtils:agencyResponses(Replies, Name), | |||
{ok, {State, ClientState2}}; | |||
{error, Reason, ClientState2} -> | |||
?WARN(PoolName, "handleData error: ~p", [Reason]), | |||
ssl:close(Socket), | |||
close(State, ClientState2) | |||
catch | |||
?EXCEPTION(E, R, Stacktrace) -> | |||
?WARN(PoolName, "handleData crash: ~p:~p~n~p~n", | |||
[E, R, ?GET_STACK(Stacktrace)]), | |||
ssl:close(Socket), | |||
close(State, ClientState) | |||
end; | |||
handle_msg({timeout, ExtRequestId}, {#state { | |||
name = Name | |||
} = State, ClientState}) -> | |||
case shackle_queue:remove(Name, ExtRequestId) of | |||
{ok, Cast, _TimerRef} -> | |||
agAgencyUtils:reply(Name, {error, timeout}, Cast); | |||
{error, not_found} -> | |||
ok | |||
end, | |||
{ok, {State, ClientState}}; | |||
handle_msg({ssl_closed, Socket}, {#state { | |||
socket = Socket, | |||
pool_name = PoolName | |||
} = State, ClientState}) -> | |||
?WARN(PoolName, "connection closed", []), | |||
close(State, ClientState); | |||
handle_msg({ssl_error, Socket, Reason}, {#state { | |||
socket = Socket, | |||
pool_name = PoolName | |||
} = State, ClientState}) -> | |||
?WARN(PoolName, "connection error: ~p", [Reason]), | |||
ssl:close(Socket), | |||
close(State, ClientState); | |||
handle_msg(?MSG_CONNECT, {#state { | |||
client = Client, | |||
init_options = Init, | |||
ip = Ip, | |||
pool_name = PoolName, | |||
port = Port, | |||
reconnect_state = ReconnectState, | |||
socket_options = SocketOptions | |||
} = State, ClientState}) -> | |||
case connect(PoolName, Ip, Port, SocketOptions) of | |||
{ok, Socket} -> | |||
ClientState2 = agHttpProtocol:bin_patterns(), | |||
ReconnectState2 = agAgencyUtils:resetReconnectState(ReconnectState), | |||
{ok, {State#state { | |||
reconnect_state = ReconnectState2, | |||
socket = Socket | |||
}, ClientState2}}; | |||
{error, _Reason} -> | |||
reconnect(State, ClientState) | |||
end; | |||
handle_msg(Msg, {#state { | |||
pool_name = PoolName | |||
} = State, ClientState}) -> | |||
?WARN(PoolName, "unknown msg: ~p", [Msg]), | |||
{ok, {State, ClientState}}. | |||
-spec terminate(term(), term()) -> | |||
ok. | |||
terminate(_Reason, {#state { | |||
client = Client, | |||
name = Name, | |||
pool_name = PoolName, | |||
timer_ref = TimerRef | |||
}, ClientState}) -> | |||
agAgencyUtils:cancel_timer(TimerRef), | |||
try agNetCli:terminate(ClientState) | |||
catch | |||
?EXCEPTION(E, R, Stacktrace) -> | |||
?WARN(PoolName, "terminate crash: ~p:~p~n~p~n", | |||
[E, R, ?GET_STACK(Stacktrace)]) | |||
end, | |||
agAgencyUtils:reply_all(Name, {error, shutdown}), | |||
shackle_backlog:delete(Name), | |||
ok. | |||
%% private | |||
close(#state {name = Name} = State, ClientState) -> | |||
agAgencyUtils:reply_all(Name, {error, socket_closed}), | |||
reconnect(State, ClientState). | |||
connect(PoolName, Ip, Port, SocketOptions) -> | |||
case inet:getaddrs(Ip, inet) of | |||
{ok, Addrs} -> | |||
Ip2 = agMiscUtils:randomElement(Addrs), | |||
case ssl:connect(Ip2, Port, SocketOptions, | |||
?DEFAULT_CONNECT_TIMEOUT) of | |||
{ok, Socket} -> | |||
{ok, Socket}; | |||
{error, Reason} -> | |||
?WARN(PoolName, "connect error: ~p", [Reason]), | |||
{error, Reason} | |||
end; | |||
{error, Reason} -> | |||
?WARN(PoolName, "getaddrs error: ~p", [Reason]), | |||
{error, Reason} | |||
end. | |||
reconnect(State, undefined) -> | |||
reconnect_timer(State, undefined); | |||
reconnect(#state { | |||
client = Client, | |||
pool_name = PoolName | |||
} = State, ClientState) -> | |||
try agNetCli:terminate(ClientState) | |||
catch | |||
?EXCEPTION(E, R, Stacktrace) -> | |||
?WARN(PoolName, "terminate crash: ~p:~p~n~p~n", | |||
[E, R, ?GET_STACK(Stacktrace)]) | |||
end, | |||
reconnect_timer(State, ClientState). | |||
reconnect_timer(#state { | |||
reconnect_state = undefined | |||
} = State, ClientState) -> | |||
{ok, {State#state { | |||
socket = undefined | |||
}, ClientState}}; | |||
reconnect_timer(#state { | |||
reconnect_state = ReconnectState | |||
} = State, ClientState) -> | |||
ReconnectState2 = shackle_backoff:timeout(ReconnectState), | |||
#reconnect_state {current = Current} = ReconnectState2, | |||
TimerRef = erlang:send_after(Current, self(), ?MSG_CONNECT), | |||
{ok, {State#state { | |||
reconnect_state = ReconnectState2, | |||
socket = undefined, | |||
timer_ref = TimerRef | |||
}, ClientState}}. |
@ -0,0 +1,326 @@ | |||
-module(agTcpAgency). | |||
-include("shackle_internal.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
-export([ | |||
start_link/3, | |||
init_it/3, | |||
system_code_change/4, | |||
system_continue/3, | |||
system_get_state/1, | |||
system_terminate/4, | |||
init/3, | |||
handle_msg/2, | |||
terminate/2 | |||
]). | |||
-record(state, { | |||
client :: client(), | |||
init_options :: init_options(), | |||
ip :: inet:ip_address() | inet:hostname(), | |||
name :: server_name(), | |||
parent :: pid(), | |||
pool_name :: pool_name(), | |||
port :: inet:port_number(), | |||
reconnect_state :: undefined | reconnect_state(), | |||
socket :: undefined | inet:socket(), | |||
socket_options :: [gen_tcp:connect_option()], | |||
timer_ref :: undefined | reference() | |||
}). | |||
-type init_opts() :: {pool_name(), client(), client_options()}. | |||
-type state() :: #state {}. | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor start %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
-spec start_link(module(), atom(), term(), [proc_lib:spawn_option()]) -> {ok, pid()}. | |||
start_link(Name, Args, SpawnOpts) -> | |||
proc_lib:start_link(?MODULE, init_it, [Name, self(), Args], infinity, SpawnOpts). | |||
init_it(Name, Parent, Args) -> | |||
case safeRegister(Name) of | |||
true -> | |||
process_flag(trap_exit, true), | |||
moduleInit(Parent, Args); | |||
{false, Pid} -> | |||
proc_lib:init_ack(Parent, {error, {already_started, Pid}}) | |||
end. | |||
%% sys callbacks | |||
-spec system_code_change(term(), module(), undefined | term(), term()) -> {ok, term()}. | |||
system_code_change(State, _Module, _OldVsn, _Extra) -> | |||
{ok, State}. | |||
-spec system_continue(pid(), [], {module(), atom(), pid(), term()}) -> ok. | |||
system_continue(_Parent, _Debug, {Parent, State}) -> | |||
loop(Parent, State). | |||
-spec system_get_state(term()) -> {ok, term()}. | |||
system_get_state(State) -> | |||
{ok, State}. | |||
-spec system_terminate(term(), pid(), [], term()) -> none(). | |||
system_terminate(Reason, _Parent, _Debug, _State) -> | |||
exit(Reason). | |||
safeRegister(Name) -> | |||
try register(Name, self()) of | |||
true -> true | |||
catch | |||
_:_ -> {false, whereis(Name)} | |||
end. | |||
moduleInit(Parent, Args) -> | |||
case ?MODULE:init(Args) of | |||
{ok, State} -> | |||
proc_lib:init_ack(Parent, {ok, self()}), | |||
loop(Parent, State); | |||
{stop, Reason} -> | |||
proc_lib:init_ack(Parent, {error, Reason}), | |||
exit(Reason) | |||
end. | |||
loop(Parent, State) -> | |||
receive | |||
{system, From, Request} -> | |||
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Parent, State}); | |||
{'EXIT', Parent, Reason} -> | |||
terminate(Reason, State); | |||
Msg -> | |||
{ok, NewState} = ?MODULE:handleMsg(Msg, State), | |||
loop(Parent, NewState) | |||
end. | |||
terminate(Reason, State) -> | |||
?MODULE:terminate(Reason, State), | |||
exit(Reason). | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
%% metal callbacks | |||
-spec init(server_name(), pid(), init_opts()) -> | |||
no_return(). | |||
init(Name, Parent, Opts) -> | |||
{PoolName, Client, ClientOptions} = Opts, | |||
self() ! ?MSG_CONNECT, | |||
ok = shackle_backlog:new(Name), | |||
InitOptions = ?LOOKUP(init_options, ClientOptions, | |||
?DEFAULT_INIT_OPTS), | |||
Ip = ?LOOKUP(ip, ClientOptions, ?DEFAULT_IP), | |||
Port = ?LOOKUP(port, ClientOptions), | |||
ReconnectState = agAgencyUtils:initReconnectState(ClientOptions), | |||
SocketOptions = ?LOOKUP(socket_options, ClientOptions, | |||
?DEFAULT_SOCKET_OPTS), | |||
{ok, {#state { | |||
client = Client, | |||
init_options = InitOptions, | |||
ip = Ip, | |||
name = Name, | |||
parent = Parent, | |||
pool_name = PoolName, | |||
port = Port, | |||
reconnect_state = ReconnectState, | |||
socket_options = SocketOptions | |||
}, undefined}}. | |||
-spec handle_msg(term(), {state(), client_state()}) -> | |||
{ok, term()}. | |||
handle_msg({_, #cast {} = Cast}, {#state { | |||
socket = undefined, | |||
name = Name | |||
} = State, ClientState}) -> | |||
agAgencyUtils:reply(Name, {error, no_socket}, Cast), | |||
{ok, {State, ClientState}}; | |||
handle_msg({Request, #cast { | |||
timeout = Timeout | |||
} = Cast}, {#state { | |||
client = Client, | |||
name = Name, | |||
pool_name = PoolName, | |||
socket = Socket | |||
} = State, ClientState}) -> | |||
try agNetCli:handleRequest(Request, ClientState) of | |||
{ok, ExtRequestId, Data, ClientState2} -> | |||
case gen_tcp:send(Socket, Data) of | |||
ok -> | |||
Msg = {timeout, ExtRequestId}, | |||
TimerRef = erlang:send_after(Timeout, self(), Msg), | |||
shackle_queue:add(ExtRequestId, Cast, TimerRef), | |||
{ok, {State, ClientState2}}; | |||
{error, Reason} -> | |||
?WARN(PoolName, "send error: ~p", [Reason]), | |||
gen_tcp:close(Socket), | |||
agAgencyUtils:reply(Name, {error, socket_closed}, Cast), | |||
close(State, ClientState2) | |||
end | |||
catch | |||
?EXCEPTION(E, R, Stacktrace) -> | |||
?WARN(PoolName, "handleRequest crash: ~p:~p~n~p~n", | |||
[E, R, ?GET_STACK(Stacktrace)]), | |||
agAgencyUtils:reply(Name, {error, client_crash}, Cast), | |||
{ok, {State, ClientState}} | |||
end; | |||
handle_msg({tcp, Socket, Data}, {#state { | |||
client = Client, | |||
name = Name, | |||
pool_name = PoolName, | |||
socket = Socket | |||
} = State, ClientState}) -> | |||
try agNetCli:handleData(Data, ClientState) of | |||
{ok, Replies, ClientState2} -> | |||
agAgencyUtils:agencyResponses(Replies, Name), | |||
{ok, {State, ClientState2}}; | |||
{error, Reason, ClientState2} -> | |||
?WARN(PoolName, "handleData error: ~p", [Reason]), | |||
gen_tcp:close(Socket), | |||
close(State, ClientState2) | |||
catch | |||
?EXCEPTION(E, R, Stacktrace) -> | |||
?WARN(PoolName, "handleData crash: ~p:~p~n~p~n", | |||
[E, R, ?GET_STACK(Stacktrace)]), | |||
gen_tcp:close(Socket), | |||
close(State, ClientState) | |||
end; | |||
handle_msg({timeout, ExtRequestId}, {#state { | |||
name = Name | |||
} = State, ClientState}) -> | |||
case shackle_queue:remove(Name, ExtRequestId) of | |||
{ok, Cast, _TimerRef} -> | |||
agAgencyUtils:reply(Name, {error, timeout}, Cast); | |||
{error, not_found} -> | |||
ok | |||
end, | |||
{ok, {State, ClientState}}; | |||
handle_msg({tcp_closed, Socket}, {#state { | |||
socket = Socket, | |||
pool_name = PoolName | |||
} = State, ClientState}) -> | |||
?WARN(PoolName, "connection closed", []), | |||
close(State, ClientState); | |||
handle_msg({tcp_error, Socket, Reason}, {#state { | |||
socket = Socket, | |||
pool_name = PoolName | |||
} = State, ClientState}) -> | |||
?WARN(PoolName, "connection error: ~p", [Reason]), | |||
gen_tcp:close(Socket), | |||
close(State, ClientState); | |||
handle_msg(?MSG_CONNECT, {#state { | |||
client = Client, | |||
init_options = Init, | |||
ip = Ip, | |||
pool_name = PoolName, | |||
port = Port, | |||
reconnect_state = ReconnectState, | |||
socket_options = SocketOptions | |||
} = State, ClientState}) -> | |||
case connect(PoolName, Ip, Port, SocketOptions) of | |||
{ok, Socket} -> | |||
ClientState2 = agHttpProtocol:bin_patterns(), | |||
ReconnectState2 = agAgencyUtils:resetReconnectState(ReconnectState), | |||
{ok, {State#state { | |||
reconnect_state = ReconnectState2, | |||
socket = Socket | |||
}, ClientState2}}; | |||
{error, _Reason} -> | |||
reconnect(State, ClientState) | |||
end; | |||
handle_msg(Msg, {#state { | |||
pool_name = PoolName | |||
} = State, ClientState}) -> | |||
?WARN(PoolName, "unknown msg: ~p", [Msg]), | |||
{ok, {State, ClientState}}. | |||
-spec terminate(term(), term()) -> | |||
ok. | |||
terminate(_Reason, {#state { | |||
client = Client, | |||
name = Name, | |||
pool_name = PoolName, | |||
timer_ref = TimerRef | |||
}, ClientState}) -> | |||
agAgencyUtils:cancel_timer(TimerRef), | |||
try agNetCli:terminate(ClientState) | |||
catch | |||
?EXCEPTION(E, R, Stacktrace) -> | |||
?WARN(PoolName, "terminate crash: ~p:~p~n~p~n", | |||
[E, R, ?GET_STACK(Stacktrace)]) | |||
end, | |||
agAgencyUtils:reply_all(Name, {error, shutdown}), | |||
shackle_backlog:delete(Name), | |||
ok. | |||
%% private | |||
close(#state {name = Name} = State, ClientState) -> | |||
agAgencyUtils:reply_all(Name, {error, socket_closed}), | |||
reconnect(State, ClientState). | |||
connect(PoolName, Ip, Port, SocketOptions) -> | |||
case inet:getaddrs(Ip, inet) of | |||
{ok, Addrs} -> | |||
Ip2 = agMiscUtils:randomElement(Addrs), | |||
case gen_tcp:connect(Ip2, Port, SocketOptions, | |||
?DEFAULT_CONNECT_TIMEOUT) of | |||
{ok, Socket} -> | |||
{ok, Socket}; | |||
{error, Reason} -> | |||
?WARN(PoolName, "connect error: ~p", [Reason]), | |||
{error, Reason} | |||
end; | |||
{error, Reason} -> | |||
?WARN(PoolName, "getaddrs error: ~p", [Reason]), | |||
{error, Reason} | |||
end. | |||
reconnect(State, undefined) -> | |||
reconnect_timer(State, undefined); | |||
reconnect(#state { | |||
client = Client, | |||
pool_name = PoolName | |||
} = State, ClientState) -> | |||
try agNetCli:terminate(ClientState) | |||
catch | |||
?EXCEPTION(E, R, Stacktrace) -> | |||
?WARN(PoolName, "terminate crash: ~p:~p~n~p~n", | |||
[E, R, ?GET_STACK(Stacktrace)]) | |||
end, | |||
reconnect_timer(State, ClientState). | |||
reconnect_timer(#state { | |||
reconnect_state = undefined | |||
} = State, ClientState) -> | |||
{ok, {State#state { | |||
socket = undefined | |||
}, ClientState}}; | |||
reconnect_timer(#state { | |||
reconnect_state = ReconnectState | |||
} = State, ClientState) -> | |||
ReconnectState2 = shackle_backoff:timeout(ReconnectState), | |||
#reconnect_state {current = Current} = ReconnectState2, | |||
TimerRef = erlang:send_after(Current, self(), ?MSG_CONNECT), | |||
{ok, {State#state { | |||
reconnect_state = ReconnectState2, | |||
socket = undefined, | |||
timer_ref = TimerRef | |||
}, ClientState}}. |
@ -0,0 +1,130 @@ | |||
-module(buoy_pool). | |||
-include("buoy_internal.hrl"). | |||
-export([ | |||
init/0, | |||
lookup/3, | |||
start/1, | |||
start/2, | |||
stop/1, | |||
terminate/0 | |||
]). | |||
%% public | |||
-spec init() -> | |||
ok. | |||
init() -> | |||
foil:new(?MODULE), | |||
foil:load(?MODULE). | |||
-spec lookup(protocol_http(), hostname(), inet:port_number()) -> | |||
{ok, atom()} | {error, pool_not_started | buoy_not_started}. | |||
lookup(Protocol, Hostname, Port) -> | |||
case foil:lookup(buoy_pool, {Protocol, Hostname, Port}) of | |||
{ok, _} = R -> | |||
R; | |||
{error, key_not_found} -> | |||
{error, pool_not_started}; | |||
{error, _} -> | |||
{error, buoy_not_started} | |||
end. | |||
-spec start(buoy_url()) -> | |||
ok | {error, pool_already_started | buoy_not_started}. | |||
start(Url) -> | |||
start(Url, ?DEFAULT_POOL_OPTIONS). | |||
-spec start(buoy_url(), options()) -> | |||
ok | {error, pool_already_started | buoy_not_started}. | |||
start(#buoy_url { | |||
protocol = Protocol, | |||
hostname = Hostname, | |||
port = Port | |||
}, Options) -> | |||
Name = name(Protocol, Hostname, Port), | |||
ClientOptions = client_options(Protocol, Hostname, Port, Options), | |||
PoolOptions = pool_options(Options), | |||
case agAgencyPoolMgr:start(Name, ?CLIENT, ClientOptions, PoolOptions) of | |||
ok -> | |||
Key = {Protocol, Hostname, Port}, | |||
ok = foil:insert(?MODULE, Key, Name), | |||
ok = foil:load(?MODULE); | |||
{error, pool_already_started} = E -> | |||
E; | |||
{error, shackle_not_started} -> | |||
{error, buoy_not_started} | |||
end. | |||
-spec stop(buoy_url()) -> | |||
ok | {error, pool_not_started | buoy_not_started}. | |||
stop(#buoy_url { | |||
protocol = Protocol, | |||
hostname = Hostname, | |||
port = Port | |||
}) -> | |||
Key = {Protocol, Hostname, Port}, | |||
case foil:lookup(?MODULE, Key) of | |||
{ok, Name} -> | |||
agAgencyPoolMgr:stop(Name), | |||
foil:delete(?MODULE, Key), | |||
foil:load(?MODULE); | |||
{error, key_not_found} -> | |||
{error, pool_not_started}; | |||
{error, _} -> | |||
{error, buoy_not_started} | |||
end. | |||
-spec terminate() -> | |||
ok. | |||
terminate() -> | |||
foil:delete(?MODULE). | |||
%% private | |||
client_options(Protocol, Hostname, Port, Options) -> | |||
Reconnect = ?LOOKUP(reconnect, Options, ?DEFAULT_IS_RECONNECT), | |||
ReconnectTimeMax = ?LOOKUP(reconnect_time_max, Options, | |||
?DEFAULT_RECONNECT_MAX), | |||
ReconnectTimeMin = ?LOOKUP(reconnect_time_min, Options, | |||
?DEFAULT_RECONNECT_MIN), | |||
[{ip, binary_to_list(Hostname)}, | |||
{port, Port}, | |||
{protocol, shackle_protocol(Protocol)}, | |||
{reconnect, Reconnect}, | |||
{reconnect_time_max, ReconnectTimeMax}, | |||
{reconnect_time_min, ReconnectTimeMin}, | |||
{socket_options, [ | |||
binary, | |||
{packet, line}, | |||
{packet, raw}, | |||
{send_timeout, 50}, | |||
{send_timeout_close, true} | |||
]}]. | |||
name(Protocol, Hostname, Port) -> | |||
list_to_atom(atom_to_list(Protocol) ++ "_" | |||
++ binary_to_list(Hostname) ++ "_" | |||
++ integer_to_list(Port)). | |||
pool_options(Options) -> | |||
BacklogSize = ?LOOKUP(backlog_size, Options, ?DEFAULT_BACKLOG_SIZE), | |||
PoolSize = ?LOOKUP(pool_size, Options, ?DEFAULT_POOL_SIZE), | |||
PoolStrategy = ?LOOKUP(pool_strategy, Options, ?DEFAULT_POOL_STRATEGY), | |||
[{backlog_size, BacklogSize}, | |||
{pool_size, PoolSize}, | |||
{pool_strategy, PoolStrategy}]. | |||
shackle_protocol(http) -> | |||
shackle_tcp; | |||
shackle_protocol(https) -> | |||
shackle_ssl. |
@ -0,0 +1,91 @@ | |||
-module(genActor). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
-export([ | |||
start_link/3, | |||
init_it/3, | |||
system_code_change/4, | |||
system_continue/3, | |||
system_get_state/1, | |||
system_terminate/4 | |||
]). | |||
-spec start_link(module(), atom(), term(), [proc_lib:spawn_option()]) -> {ok, pid()}. | |||
start_link(Name, Args, SpawnOpts) -> | |||
proc_lib:start_link(?MODULE, init_it, [Name, self(), Args], infinity, SpawnOpts). | |||
init_it(Name, Parent, Args) -> | |||
case safeRegister(Name) of | |||
true -> | |||
process_flag(trap_exit, true), | |||
moduleInit(Parent, Args); | |||
{false, Pid} -> | |||
proc_lib:init_ack(Parent, {error, {already_started, Pid}}) | |||
end. | |||
%% sys callbacks | |||
-spec system_code_change(term(), module(), undefined | term(), term()) -> {ok, term()}. | |||
system_code_change(State, _Module, _OldVsn, _Extra) -> | |||
{ok, State}. | |||
-spec system_continue(pid(), [], {module(), atom(), pid(), term()}) -> ok. | |||
system_continue(_Parent, _Debug, {Parent, State}) -> | |||
loop(Parent, State). | |||
-spec system_get_state(term()) -> {ok, term()}. | |||
system_get_state(State) -> | |||
{ok, State}. | |||
-spec system_terminate(term(), pid(), [], term()) -> none(). | |||
system_terminate(Reason, _Parent, _Debug, _State) -> | |||
exit(Reason). | |||
safeRegister(Name) -> | |||
try register(Name, self()) of | |||
true -> true | |||
catch | |||
_:_ -> {false, whereis(Name)} | |||
end. | |||
moduleInit(Parent, Args) -> | |||
case ?MODULE:init(Args) of | |||
{ok, State} -> | |||
proc_lib:init_ack(Parent, {ok, self()}), | |||
loop(Parent, State); | |||
{stop, Reason} -> | |||
proc_lib:init_ack(Parent, {error, Reason}), | |||
exit(Reason) | |||
end. | |||
loop(Parent, State) -> | |||
receive | |||
{system, From, Request} -> | |||
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Parent, State}); | |||
{'EXIT', Parent, Reason} -> | |||
terminate(Reason, State); | |||
Msg -> | |||
{ok, NewState} = ?MODULE:handleMsg(Msg, State), | |||
loop(Parent, NewState) | |||
end. | |||
terminate(Reason, State) -> | |||
?MODULE:terminate(Reason, State), | |||
exit(Reason). | |||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% need %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |||
-spec init(Args :: term()) -> {ok, term()}. | |||
init(Args) -> | |||
{ok, {}}. | |||
-spec handleMsg(Msg :: term(), State :: term()) -> {ok, term()}. | |||
handleMsg(Msg, State) -> | |||
{ok, term}. | |||
-spec terminate(Reason :: term(), State :: term()) -> ok. | |||
terminate(Reason, State) -> | |||
ok. |
@ -0,0 +1,81 @@ | |||
-module(shackle_backlog). | |||
-include("shackle_internal.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
%% internal | |||
-export([ | |||
check/2, | |||
check/3, | |||
decrement/1, | |||
decrement/2, | |||
delete/1, | |||
init/0, | |||
new/1 | |||
]). | |||
-define(DEFAULT_DECREMENT, -1). | |||
-define(DEFAULT_INCREMENT, 1). | |||
%% internal | |||
-spec check(server_name(), backlog_size()) -> | |||
boolean(). | |||
check(ServerName, BacklogSize) -> | |||
check(ServerName, BacklogSize, ?DEFAULT_INCREMENT). | |||
-spec check(server_name(), backlog_size(), pos_integer()) -> | |||
boolean(). | |||
check(_ServerName, infinity, _Increment) -> | |||
true; | |||
check(ServerName, BacklogSize, Increment) -> | |||
case increment(ServerName, BacklogSize, Increment) of | |||
[BacklogSize, BacklogSize] -> | |||
false; | |||
[_, Value] when Value =< BacklogSize -> | |||
true | |||
end. | |||
-spec decrement(server_name()) -> | |||
non_neg_integer(). | |||
decrement(ServerName) -> | |||
decrement(ServerName, ?DEFAULT_DECREMENT). | |||
-spec decrement(server_name(), neg_integer()) -> | |||
non_neg_integer(). | |||
decrement(ServerName, Decrement) -> | |||
ets:update_counter(?ETS_TABLE_BACKLOG, ServerName, {2, Decrement, 0, 0}). | |||
-spec delete(server_name()) -> | |||
ok. | |||
delete(ServerName) -> | |||
ets:delete(?ETS_TABLE_BACKLOG, ServerName), | |||
ok. | |||
-spec init() -> | |||
ok. | |||
init() -> | |||
ets:new(?ETS_TABLE_BACKLOG, [ | |||
named_table, | |||
public, | |||
{write_concurrency, true} | |||
]), | |||
ok. | |||
-spec new(server_name()) -> | |||
ok. | |||
new(ServerName) -> | |||
ets:insert(?ETS_TABLE_BACKLOG, {ServerName, 0}), | |||
ok. | |||
%% private | |||
increment(ServerName, BacklogSize, Increment) -> | |||
UpdateOps = [{2, 0}, {2, Increment, BacklogSize, BacklogSize}], | |||
ets:update_counter(?ETS_TABLE_BACKLOG, ServerName, UpdateOps). |
@ -0,0 +1,46 @@ | |||
-module(shackle_backoff). | |||
-include("shackle_internal.hrl"). | |||
-compile({no_auto_import, [min/2]}). | |||
%% public | |||
-export([ | |||
timeout/1 | |||
]). | |||
%% public | |||
-spec timeout(reconnect_state()) -> | |||
reconnect_state(). | |||
timeout(#reconnect_state { | |||
current = undefined, | |||
min = Min | |||
} = ReconnectState) -> | |||
timeout(ReconnectState#reconnect_state { | |||
current = Min | |||
}); | |||
timeout(#reconnect_state { | |||
current = Current, | |||
max = Max | |||
} = ReconnectState) when Max =/= infinity, Current >= Max -> | |||
ReconnectState; | |||
timeout(#reconnect_state { | |||
current = Current, | |||
max = Max | |||
} = ReconnectState) -> | |||
Current2 = Current + agMiscUtils:random(trunc(Current / 2) + 1) - 1, | |||
ReconnectState#reconnect_state { | |||
current = min(Current2, Max) | |||
}. | |||
%% private | |||
min(A, infinity) -> | |||
A; | |||
min(A, B) when B >= A -> | |||
A; | |||
min(_, B) -> | |||
B. |
@ -0,0 +1,20 @@ | |||
-module(shackle_client). | |||
-include("shackle_internal.hrl"). | |||
-callback init(Options :: term()) -> | |||
{ok, State :: term()} | | |||
{error, Reason :: term()}. | |||
-callback setup(Socket :: inet:socket(), State :: term()) -> | |||
{ok, State :: term()} | | |||
{error, Reason :: term(), State :: term()}. | |||
-callback handleRequest(Request :: term(), State :: term()) -> | |||
{ok, RequestId :: external_request_id(), Data :: iodata(), State :: term()}. | |||
-callback handleData(Data :: binary(), State :: term()) -> | |||
{ok, [Response :: response()], State :: term()} | | |||
{error, Reason :: term(), State :: term()}. | |||
-callback terminate(State :: term()) -> | |||
ok. |
@ -0,0 +1,92 @@ | |||
-module(shackle_queue). | |||
-include("shackle_internal.hrl"). | |||
-compile(inline). | |||
-compile({inline_size, 512}). | |||
%% internal | |||
-export([ | |||
add/3, | |||
clear/1, | |||
init/0, | |||
remove/2 | |||
]). | |||
%% internal | |||
-spec add(external_request_id(), cast(), reference()) -> | |||
ok. | |||
add(ExtRequestId, #cast { | |||
request_id = {ServerName, _} | |||
} = Cast, TimerRef) -> | |||
Object = {{ServerName, ExtRequestId}, {Cast, TimerRef}}, | |||
ets:insert(?ETS_TABLE_QUEUE, Object), | |||
ok. | |||
-spec clear(server_name()) -> | |||
[{cast(), reference()}]. | |||
clear(ServerName) -> | |||
Match = {{ServerName, '_'}, '_'}, | |||
case ets_match_take(?ETS_TABLE_QUEUE, Match) of | |||
[] -> | |||
[]; | |||
Objects -> | |||
[{Cast, TimerRef} || {_, {Cast, TimerRef}} <- Objects] | |||
end. | |||
-spec init() -> | |||
ok. | |||
init() -> | |||
ets_new(?ETS_TABLE_QUEUE), | |||
ok. | |||
-spec remove(server_name(), external_request_id()) -> | |||
{ok, cast(), reference()} | {error, not_found}. | |||
remove(ServerName, ExtRequestId) -> | |||
case ets_take(?ETS_TABLE_QUEUE, {ServerName, ExtRequestId}) of | |||
[] -> | |||
{error, not_found}; | |||
[{_, {Cast, TimerRef}}] -> | |||
{ok, Cast, TimerRef} | |||
end. | |||
%% private | |||
ets_match_take(Tid, Match) -> | |||
case ets:match_object(Tid, Match) of | |||
[] -> | |||
[]; | |||
Objects -> | |||
ets:match_delete(Tid, Match), | |||
Objects | |||
end. | |||
ets_new(Tid) -> | |||
ets:new(Tid, [ | |||
named_table, | |||
public, | |||
{read_concurrency, true}, | |||
{write_concurrency, true} | |||
]). | |||
-ifdef(ETS_TAKE). | |||
ets_take(Tid, Key) -> | |||
ets:take(Tid, Key). | |||
-else. | |||
ets_take(Tid, Key) -> | |||
case ets:lookup(Tid, Key) of | |||
[] -> | |||
[]; | |||
Objects -> | |||
ets:delete(Tid, Key), | |||
Objects | |||
end. | |||
-endif. |