Finally implement the "sticky" chain node #9

Merged
zxq9 merged 3 commits from designated-hitter into master 2025-10-29 15:50:10 +09:00
4 changed files with 1468 additions and 53 deletions

1332
src/: Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
%%% %%%
%%% The get/set admin functions are for setting or checking things like the Gajumaru %%% The get/set admin functions are for setting or checking things like the Gajumaru
%%% "network ID" and list of addresses of nodes you want to use for answering %%% "network ID" and list of addresses of nodes you want to use for answering
%%% queries to the blockchain. %%% queries to the blockchain. Get functions are arity 0, and set functions are arity 1.
%%% %%%
%%% The JSON query interface functions are the blockchain query functions themselves %%% The JSON query interface functions are the blockchain query functions themselves
%%% which are translated to network queries and return Erlang messages as responses. %%% which are translated to network queries and return Erlang messages as responses.
@ -18,8 +18,8 @@
%%% a desired call to a smart contract on the chain to call data serialized in a form %%% a desired call to a smart contract on the chain to call data serialized in a form
%%% that a Gajumaru compatible wallet or library can sign and submit to a Gajumaru node. %%% that a Gajumaru compatible wallet or library can sign and submit to a Gajumaru node.
%%% %%%
%%% This module does not implement the OTP application behavior. %%% NOTE:
%%% helper functions. %%% This module does not implement the OTP application behavior. Refer to hakuzaru.erl.
%%% @end %%% @end
-module(hz). -module(hz).
@ -225,7 +225,7 @@
NetworkID :: string(), NetworkID :: string(),
Reason :: term(). Reason :: term().
%% @doc %% @doc
%% Returns the network ID or the atom `none' if it is unset. %% Returns the network ID or the atom `none' if unavailable.
%% Checking this is not normally necessary, but if network ID assignment is dynamic %% Checking this is not normally necessary, but if network ID assignment is dynamic
%% in your system it may be necessary to call this before attempting to form %% in your system it may be necessary to call this before attempting to form
%% call data or perform other actions on chain that require a signature. %% call data or perform other actions on chain that require a signature.
@ -241,7 +241,9 @@ network_id() ->
%% @doc %% @doc
%% Returns the list of currently assigned nodes. %% Returns the list of currently assigned nodes.
%% The normal reason to call this is in preparation for altering the nodes list or %% The normal reason to call this is in preparation for altering the nodes list or
%% checking the current list in debugging. %% checking the current list in debugging. Note that the first node in the list is
%% the "sticky" node: the one that will be used for submitting transactions and
%% querying `next_nonce'.
chain_nodes() -> chain_nodes() ->
hz_man:chain_nodes(). hz_man:chain_nodes().
@ -251,19 +253,26 @@ chain_nodes() ->
when List :: [chain_node()], when List :: [chain_node()],
Reason :: {invalid, [term()]}. Reason :: {invalid, [term()]}.
%% @doc %% @doc
%% Sets the nodes that are intended to be used as your interface to the peer %% Sets the chain nodes that will be queried whenever you communicate with the chain.
%% network. The common situation is that your project runs a non-mining node as
%% part of your backend infrastructure. Typically one or two nodes is plenty, but
%% this may need to expand depending on how much query load your application generates.
%% The Hakuzaru manager will load balance by round-robin distribution.
%% %%
%% NOTE: When load balancing in this way be aware that there can be race conditions %% The common situation is that a project runs a non-mining node as part of the backend
%% among the backend nodes with regard to a single account's current nonce when performing %% infrastructure. Typically one or two nodes is plenty, but this may need to expand
%% contract calls in quick succession. Round robin distribution is extremely useful when %% depending on how much query load your application generates.
%% performing rapid lookups to the chain, but does not work well when submitting many %%
%% transactions to the chain from a single user in a short period of time. A future version %% There are two situations: one node, or multiple nodes.
%% of this library will allow the caller to designate a single node as "sticky" to be used %%
%% exclusively in the case of nonce reads and TX submissions. %% Single node:
%% In the case of a single node, everything passes through that one node. Duh.
%%
%% Multiple nodes:
%% In the case of multiple nodes a distinction is made between the node to which
%% transactions that update the chain state are made and to which `next_nonce' queries
%% are made, and nodes that are used for read-only queries. The node to which stateful
%% transactions are submitted is called the "sticky node". This is the first node
%% (head position) in the list of nodes submitted to the chain when `chain_nodes/1'
%% is called. If using multiple nodes but the sticky node should also be used for
%% read-only queries, submit the sticky node at the head of the list and again in
%% the tail.
chain_nodes(List) when is_list(List) -> chain_nodes(List) when is_list(List) ->
hz_man:chain_nodes(List). hz_man:chain_nodes(List).
@ -271,7 +280,16 @@ chain_nodes(List) when is_list(List) ->
-spec tls() -> boolean(). -spec tls() -> boolean().
%% @doc %% @doc
%% Check whether TLS is in use. %% Check whether TLS is in use. The typical situation is to not use TLS as nodes that
%% serve as part of the backend of an application are typically run in the same
%% backend network as the application service. When accessing chain nodes over the WAN
%% however, TLS is strongly recommended to avoid a MITM attack.
%%
%% In this version of Hakuzaru TLS is either on or off for all nodes, making a mixed
%% infrastructure complicated to support without two Hakuzaru instances. This will
%% likely become a per-node setting in the future.
%%
%% TLS defaults to `false'.
tls() -> tls() ->
hz_man:tls(). hz_man:tls().
@ -281,6 +299,8 @@ tls() ->
%% @doc %% @doc
%% Set TLS true or false. That's what a boolean is, by the way, `true' or `false'. %% Set TLS true or false. That's what a boolean is, by the way, `true' or `false'.
%% This is a condescending comment. That means I am talking down to you. %% This is a condescending comment. That means I am talking down to you.
%%
%% TLS defaults to `false'.
tls(Boolean) -> tls(Boolean) ->
hz_man:tls(Boolean). hz_man:tls(Boolean).
@ -291,6 +311,8 @@ tls(Boolean) ->
when Timeout :: pos_integer() | infinity. when Timeout :: pos_integer() | infinity.
%% @doc %% @doc
%% Returns the current request timeout setting in milliseconds. %% Returns the current request timeout setting in milliseconds.
%% The default timeout is 5,000ms.
%% The max timeout is 120,000ms.
timeout() -> timeout() ->
hz_man:timeout(). hz_man:timeout().
@ -300,6 +322,8 @@ timeout() ->
when MS :: pos_integer() | infinity. when MS :: pos_integer() | infinity.
%% @doc %% @doc
%% Sets the request timeout in milliseconds. %% Sets the request timeout in milliseconds.
%% The default timeout is 5,000ms.
%% The max timeout is 120,000ms.
timeout(MS) -> timeout(MS) ->
hz_man:timeout(MS). hz_man:timeout(MS).
@ -576,18 +600,18 @@ acc_pending_txs(AccountID) ->
%% Retrieve the next nonce for the given account %% Retrieve the next nonce for the given account
next_nonce(AccountID) -> next_nonce(AccountID) ->
% case request(["/v3/accounts/", AccountID, "/next-nonce"]) of case request_sticky(["/v3/accounts/", AccountID, "/next-nonce"]) of
% {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce}; {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce};
% {ok, #{"reason" := "Account not found"}} -> {ok, 1};
% {ok, #{"reason" := Reason}} -> {error, Reason};
% Error -> Error
% end.
case request(["/v3/accounts/", AccountID]) of
{ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1};
{ok, #{"reason" := "Account not found"}} -> {ok, 1}; {ok, #{"reason" := "Account not found"}} -> {ok, 1};
{ok, #{"reason" := Reason}} -> {error, Reason}; {ok, #{"reason" := Reason}} -> {error, Reason};
Error -> Error Error -> Error
end. end.
% case request_sticky(["/v3/accounts/", AccountID]) of
% {ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1};
% {ok, #{"reason" := "Account not found"}} -> {ok, 1};
% {ok, #{"reason" := Reason}} -> {error, Reason};
% Error -> Error
% end.
-spec dry_run(TX) -> {ok, Result} | {error, Reason} -spec dry_run(TX) -> {ok, Result} | {error, Reason}
@ -729,7 +753,7 @@ tx_info(ID) ->
post_tx(Data) when is_binary(Data) -> post_tx(Data) when is_binary(Data) ->
JSON = zj:binary_encode(#{tx => Data}), JSON = zj:binary_encode(#{tx => Data}),
request("/v3/transactions", JSON); request_sticky("/v3/transactions", JSON);
post_tx(Data) when is_list(Data) -> post_tx(Data) when is_list(Data) ->
post_tx(list_to_binary(Data)). post_tx(list_to_binary(Data)).
@ -841,6 +865,14 @@ status_chainends() ->
request("/v3/status/chain-ends"). request("/v3/status/chain-ends").
request_sticky(Path) ->
hz_man:request_sticky(unicode:characters_to_list(Path)).
request_sticky(Path, Payload) ->
hz_man:request_sticky(unicode:characters_to_list(Path), Payload).
request(Path) -> request(Path) ->
hz_man:request(unicode:characters_to_list(Path)). hz_man:request(unicode:characters_to_list(Path)).

View File

@ -4,7 +4,7 @@
-copyright("Craig Everett <ceverett@tsuriai.jp>"). -copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("MIT"). -license("MIT").
-export([connect/4, slowly_connect/4]). -export([connect/4, connect_slowly/4]).
connect(Node = {Host, Port}, Request, From, Timeout) -> connect(Node = {Host, Port}, Request, From, Timeout) ->
@ -206,7 +206,7 @@ read_hval(_, Received, _, _, _) ->
{error, headers}. {error, headers}.
slowly_connect(Node, {get, Path}, From, Timeout) -> connect_slowly(Node, {get, Path}, From, Timeout) ->
HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}], HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}],
URL = lists:flatten(url(Node, Path)), URL = lists:flatten(url(Node, Path)),
Request = {URL, []}, Request = {URL, []},
@ -217,7 +217,7 @@ slowly_connect(Node, {get, Path}, From, Timeout) ->
BAD -> {error, BAD} BAD -> {error, BAD}
end, end,
gen_server:reply(From, Result); gen_server:reply(From, Result);
slowly_connect(Node, {post, Path, Payload}, From, Timeout) -> connect_slowly(Node, {post, Path, Payload}, From, Timeout) ->
HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}], HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}],
URL = lists:flatten(url(Node, Path)), URL = lists:flatten(url(Node, Path)),
Request = {URL, [], "application/json", Payload}, Request = {URL, [], "application/json", Payload},

View File

@ -21,7 +21,7 @@
timeout/0, timeout/1]). timeout/0, timeout/1]).
%% The whole point of this module: %% The whole point of this module:
-export([request/1, request/2]). -export([request_sticky/1, request_sticky/2, request/1, request/2]).
%% gen_server goo %% gen_server goo
-export([start_link/0]). -export([start_link/0]).
@ -40,11 +40,11 @@
req = none :: none | binary()}). req = none :: none | binary()}).
-record(s, -record(s,
{tls = false :: boolean(), {tls = false :: boolean(),
chain_nodes = {[], []} :: {[hz:chain_node()], [hz:chain_node()]}, chain_nodes = {[], []} :: {[hz:chain_node()], [hz:chain_node()]},
sticky = none :: none | hz:chain_node(), sticky = none :: none | hz:chain_node(),
fetchers = [] :: [#fetcher{}], fetchers = [] :: [#fetcher{}],
timeout = 5000 :: pos_integer()}). timeout = 5000 :: pos_integer()}).
-type state() :: #s{}. -type state() :: #s{}.
@ -94,6 +94,25 @@ timeout(Value) when 0 < Value, Value =< 120000 ->
gen_server:cast(?MODULE, {timeout, Value}). gen_server:cast(?MODULE, {timeout, Value}).
-spec request_sticky(Path) -> {ok, Value} | {error, Reason}
when Path :: unicode:charlist(),
Value :: map(),
Reason :: hz:chain_error().
request_sticky(Path) ->
gen_server:call(?MODULE, {request_sticky, {get, Path}}, infinity).
-spec request_sticky(Path, Data) -> {ok, Value} | {error, Reason}
when Path :: unicode:charlist(),
Data :: unicode:charlist(),
Value :: map(),
Reason :: hz:chain_error().
request_sticky(Path, Data) ->
gen_server:call(?MODULE, {request_sticky, {post, Path, Data}}, infinity).
-spec request(Path) -> {ok, Value} | {error, Reason} -spec request(Path) -> {ok, Value} | {error, Reason}
when Path :: unicode:charlist(), when Path :: unicode:charlist(),
Value :: map(), Value :: map(),
@ -145,10 +164,13 @@ init(none) ->
handle_call({request, Request}, From, State) -> handle_call({request, Request}, From, State) ->
NewState = do_request(Request, From, State), NewState = do_request(Request, From, State),
{noreply, NewState}; {noreply, NewState};
handle_call({request_sticky, Request}, From, State) ->
NewState = do_request_sticky(Request, From, State),
{noreply, NewState};
handle_call(tls, _, State = #s{tls = TLS}) -> handle_call(tls, _, State = #s{tls = TLS}) ->
{reply, TLS, State}; {reply, TLS, State};
handle_call(chain_nodes, _, State = #s{chain_nodes = {Wait, Used}}) -> handle_call(chain_nodes, _, State) ->
Nodes = lists:append(Wait, Used), Nodes = do_chain_nodes(State),
{reply, Nodes, State}; {reply, Nodes, State};
handle_call(timeout, _, State = #s{timeout = Value}) -> handle_call(timeout, _, State = #s{timeout = Value}) ->
{reply, Value, State}; {reply, Value, State};
@ -160,10 +182,9 @@ handle_call(Unexpected, From, State) ->
handle_cast({tls, Boolean}, State) -> handle_cast({tls, Boolean}, State) ->
NewState = do_tls(Boolean, State), NewState = do_tls(Boolean, State),
{noreply, NewState}; {noreply, NewState};
handle_cast({chain_nodes, []}, State) -> handle_cast({chain_nodes, List}, State) ->
{noreply, State#s{chain_nodes = {[], []}}}; NewState = do_chain_nodes(List, State),
handle_cast({chain_nodes, ToUse}, State) -> {noreply, NewState};
{noreply, State#s{chain_nodes = {ToUse, []}}};
handle_cast({timeout, Value}, State) -> handle_cast({timeout, Value}, State) ->
{noreply, State#s{timeout = Value}}; {noreply, State#s{timeout = Value}};
handle_cast(Unexpected, State) -> handle_cast(Unexpected, State) ->
@ -218,6 +239,23 @@ terminate(_, _) ->
%%% Doer Functions %%% Doer Functions
do_chain_nodes(#s{sticky = none, chain_nodes = {Wait, Used}}) ->
lists:append(Wait, Used);
do_chain_nodes(#s{sticky = Sticky, chain_nodes = {Wait, Used}}) ->
case lists:append(Wait, Used) of
[Sticky] -> [Sticky];
Nodes -> [Sticky | Nodes]
end.
do_chain_nodes([], State) ->
State#s{sticky = none, chain_nodes = {[], []}};
do_chain_nodes(List = [Sticky], State) ->
State#s{sticky = Sticky, chain_nodes = {List, []}};
do_chain_nodes([Sticky | List], State) ->
State#s{sticky = Sticky, chain_nodes = {List, []}}.
do_tls(true, State) -> do_tls(true, State) ->
ok = ssl:start(), ok = ssl:start(),
State#s{tls = true}; State#s{tls = true};
@ -227,17 +265,21 @@ do_tls(_, State) ->
State. State.
do_request(_, From, State = #s{chain_nodes = {[], []}}) -> do_request_sticky(_, From, State = #s{sticky = none}) ->
ok = gen_server:reply(From, {error, no_nodes}), ok = gen_server:reply(From, {error, no_nodes}),
State; State;
do_request(Request, do_request_sticky(Request,
From, From,
State = #s{tls = false, State = #s{tls = TLS,
fetchers = Fetchers, fetchers = Fetchers,
chain_nodes = {[Node | Rest], Used}, sticky = Node,
timeout = Timeout}) -> timeout = Timeout}) ->
Now = erlang:system_time(nanosecond), Now = erlang:system_time(nanosecond),
Fetcher = fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end, Fetcher =
case TLS of
true -> fun() -> hz_fetcher:connect_slowly(Node, Request, From, Timeout) end;
false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end
end,
{PID, Mon} = spawn_monitor(Fetcher), {PID, Mon} = spawn_monitor(Fetcher),
New = #fetcher{pid = PID, New = #fetcher{pid = PID,
mon = Mon, mon = Mon,
@ -245,15 +287,24 @@ do_request(Request,
node = Node, node = Node,
from = From, from = From,
req = Request}, req = Request},
State#s{fetchers = [New | Fetchers], chain_nodes = {Rest, [Node | Used]}}; State#s{fetchers = [New | Fetchers]}.
do_request(_, From, State = #s{chain_nodes = {[], []}}) ->
ok = gen_server:reply(From, {error, no_nodes}),
State;
do_request(Request, do_request(Request,
From, From,
State = #s{tls = true, State = #s{tls = TLS,
fetchers = Fetchers, fetchers = Fetchers,
chain_nodes = {[Node | Rest], Used}, chain_nodes = {[Node | Rest], Used},
timeout = Timeout}) -> timeout = Timeout}) ->
Now = erlang:system_time(nanosecond), Now = erlang:system_time(nanosecond),
Fetcher = fun() -> hz_fetcher:slowly_connect(Node, Request, From, Timeout) end, Fetcher =
case TLS of
true -> fun() -> hz_fetcher:connect_slowly(Node, Request, From, Timeout) end;
false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end
end,
{PID, Mon} = spawn_monitor(Fetcher), {PID, Mon} = spawn_monitor(Fetcher),
New = #fetcher{pid = PID, New = #fetcher{pid = PID,
mon = Mon, mon = Mon,