Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0fca897
add shumaich proto
aenglisc Nov 25, 2020
b65b2f1
add shumaich proto to app.src
aenglisc Nov 25, 2020
49da102
update compose file
aenglisc Nov 25, 2020
b18d264
add bender client
aenglisc Nov 25, 2020
bd7f022
add bender client to app.src
aenglisc Nov 25, 2020
bca20aa
add gen_sequence to utils
aenglisc Nov 25, 2020
e46bb17
add new accounting module
aenglisc Nov 25, 2020
7b6fd9c
fix thrift compiler version
aenglisc Nov 25, 2020
d88626a
add shumaich service handling
aenglisc Nov 26, 2020
c92dc7f
update new accounter
aenglisc Nov 29, 2020
21539f0
update hg_proto
aenglisc Nov 29, 2020
784168d
update account balances test
aenglisc Nov 30, 2020
aaba872
fix format
aenglisc Nov 30, 2020
8562e08
fix xref
aenglisc Nov 30, 2020
fef9a7b
some fixes
aenglisc Nov 30, 2020
b46924d
type fix
aenglisc Nov 30, 2020
27b264f
type fix
aenglisc Nov 30, 2020
1e9f9e6
Merge remote-tracking branch 'origin/master' into HG-544/ft/shumaich
kehitt Jun 23, 2021
186fb65
fix format
kehitt Jun 23, 2021
a6b0dc3
accounter retries via hg_retry
kehitt Jul 12, 2021
6f9b7ae
fix test
kehitt Jul 12, 2021
cb86a3c
fix clock decoding, relax retry strategy
kehitt Jul 12, 2021
0c9806a
fix balance consistency
kehitt Jul 12, 2021
0c66e4c
relax default retry strategy
kehitt Jul 14, 2021
42831ae
tighter default retry strategy
kehitt Jul 15, 2021
4e65175
fail machine (somewhat) properly when out of retries to accounter
kehitt Sep 6, 2021
e1daeb5
fix plan function, remove duplicate code
kehitt Sep 10, 2021
cb7c878
fix types
kehitt Sep 10, 2021
8b2b9fa
only retry sometimes
kehitt Sep 14, 2021
a821836
change hg_accounting_new api
kehitt Sep 20, 2021
230cae8
work around a shumway bug
kehitt Sep 21, 2021
2ffa288
add some logging
kehitt Sep 21, 2021
8a1e73c
fix types
kehitt Sep 22, 2021
42efffa
another one
kehitt Sep 22, 2021
8d70d14
update shumaich to fix the Hold bug
kehitt Sep 24, 2021
55eca2f
bump proto
kehitt Sep 28, 2021
91f62ac
bump shumway in tests
kehitt Sep 29, 2021
b9298a4
some review fixes
kehitt Oct 5, 2021
bc9deda
fixes for review fixes
kehitt Oct 5, 2021
a8afcf9
fix docker-compose
kehitt Oct 5, 2021
d74d2d1
remove constant container names
kehitt Oct 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/hellgate/src/hellgate.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
fault_detector_proto,
hg_proto,
shumpune_proto,
shumaich_proto,
cowboy,
how_are_you, % must be after ranch and before any woody usage
woody,
scoper, % should be before any scoper event handler usage
gproc,
dmt_client,
party_client,
bender_client,
woody_user_identity,
payproc_errors,
erl_health,
Expand Down
297 changes: 297 additions & 0 deletions apps/hellgate/src/hg_accounting_new.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
%%% Accounting for shumaich
%%%
%%% TODO
%%% - Brittle posting id assignment, it should be a level upper, maybe even in
%%% `hg_cashflow`.
%%% - Stuff cash flow details in the posting description fields.

-module(hg_accounting_new).

-export([get_account/1]).
-export([get_account/2]).

-export([get_balance/1]).
-export([get_balance/2]).

-export([collect_account_map/6]).
-export([collect_merchant_account_map/2]).
-export([collect_provider_account_map/3]).
-export([collect_system_account_map/4]).
-export([collect_external_account_map/4]).

-export([hold/4]).

-export([plan/3]).
-export([plan/4]).

-export([commit/4]).

-export([rollback/4]).

-include_lib("damsel/include/dmsl_payment_processing_thrift.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-include_lib("shumaich_proto/include/shumaich_shumaich_thrift.hrl").

-type amount() :: dmsl_domain_thrift:'Amount'().
-type currency_code() :: dmsl_domain_thrift:'CurrencySymbolicCode'().
-type account_id() :: dmsl_accounter_thrift:'AccountID'().
-type plan_id() :: dmsl_accounter_thrift:'PlanID'().
-type batch_id() :: dmsl_accounter_thrift:'BatchID'().
-type final_cash_flow() :: dmsl_domain_thrift:'FinalCashFlow'().
-type batch() :: {batch_id(), final_cash_flow()}.
-type clock() :: dmsl_domain_thrift:'AccounterClock'().

-type payment() :: dmsl_domain_thrift:'InvoicePayment'().
-type shop() :: dmsl_domain_thrift:'Shop'().
-type payment_institution() :: dmsl_domain_thrift:'PaymentInstitution'().
-type provider() :: dmsl_domain_thrift:'Provider'().
-type varset() :: pm_selector:varset().
-type revision() :: hg_domain:revision().

-export_type([batch/0]).
-export_type([clock/0]).

-type account() :: #{
account_id => account_id(),
currency_code => currency_code()
}.

-type balance() :: #{
account_id => account_id(),
own_amount => amount(),
min_available_amount => amount(),
max_available_amount => amount(),
clock => clock()
}.

-define(DEFAULT_RETRY_STRATEGY, {exponential, 10, 1.1, 100}).

-spec get_account(account_id()) -> {ok, account()} | {error, not_ready | {account_not_found, _}}.
get_account(AccountID) ->
get_account(AccountID, undefined).

-spec get_account(account_id(), undefined | clock()) -> {ok, account()} | {error, not_ready | {account_not_found, _}}.
get_account(AccountID, Clock) ->
case call_accounter('GetAccountByID', {AccountID, to_accounter_clock(Clock)}) of
{ok, Result} ->
{ok, construct_account(AccountID, Result)};
{error, _} = Error ->
Error
end.

-spec get_balance(account_id()) -> {ok, balance()} | {error, not_ready | {account_not_found, _}}.
get_balance(AccountID) ->
get_balance(AccountID, undefined).

-spec get_balance(account_id(), undefined | clock()) -> {ok, balance()} | {error, not_ready | {account_not_found, _}}.
get_balance(AccountID, Clock) ->
case call_accounter('GetBalanceByID', {AccountID, to_accounter_clock(Clock)}) of
{ok, Result} ->
{ok, construct_balance(AccountID, Result)};
{error, _} = Error ->
Error
end.

-spec collect_account_map(payment(), shop(), payment_institution(), provider(), varset(), revision()) -> map().
collect_account_map(Payment, Shop, PaymentInstitution, Provider, VS, Revision) ->
hg_accounting:collect_account_map(Payment, Shop, PaymentInstitution, Provider, VS, Revision).

-spec collect_merchant_account_map(shop(), map()) -> map().
collect_merchant_account_map(Shop, Acc) ->
hg_accounting:collect_merchant_account_map(Shop, Acc).

-spec collect_provider_account_map(payment(), provider(), map()) -> map().
collect_provider_account_map(Payment, Provider, Acc) ->
hg_accounting:collect_provider_account_map(Payment, Provider, Acc).

-spec collect_system_account_map(payment(), payment_institution(), revision(), map()) -> map().
collect_system_account_map(Payment, PaymentInstitution, Revision, Acc) ->
hg_accounting:collect_system_account_map(Payment, PaymentInstitution, Revision, Acc).

-spec collect_external_account_map(payment(), varset(), revision(), map()) -> map().
collect_external_account_map(Payment, VS, Revision, Acc) ->
hg_accounting:collect_external_account_map(Payment, VS, Revision, Acc).

%%
-spec plan(plan_id(), [batch()], hg_datetime:timestamp()) ->
{ok, clock()} | {error, not_ready | {invalid_posting_params, _}}.
plan(_PlanID, [], _Timestamp) ->
error(badarg);
plan(_PlanID, Batches, _Timestamp) when not is_list(Batches) ->
error(badarg);
plan(PlanID, Batches, Timestamp) ->
execute_plan(PlanID, Batches, Timestamp, undefined).

-spec plan(plan_id(), [batch()], hg_datetime:timestamp(), clock()) ->
{ok, clock()} | {error, not_ready | {invalid_posting_params, _}}.
plan(_PlanID, [], _Timestamp, _Clock) ->
error(badarg);
plan(_PlanID, Batches, _Timestamp, _Clock) when not is_list(Batches) ->
error(badarg);
plan(PlanID, Batches, Timestamp, Clock) ->
execute_plan(PlanID, Batches, Timestamp, Clock).

-spec hold(plan_id(), batch(), hg_datetime:timestamp(), clock() | undefined) ->
{ok, clock()} | {error, not_ready | {invalid_posting_params, _}}.
hold(PlanID, Batch, Timestamp, Clock) ->
AccounterClock = to_accounter_clock(Clock),
do('Hold', construct_plan_change(PlanID, Batch, Timestamp), AccounterClock).

-spec commit(plan_id(), [batch()], hg_datetime:timestamp(), clock() | undefined) ->
{ok, clock()} | {error, not_ready | {invalid_posting_params, _}}.
commit(PlanID, Batches, Timestamp, Clock) ->
AccounterClock = to_accounter_clock(Clock),
do('CommitPlan', construct_plan(PlanID, Batches, Timestamp), AccounterClock).

-spec rollback(plan_id(), [batch()], hg_datetime:timestamp(), clock() | undefined) ->
{ok, clock()} | {error, not_ready | {invalid_posting_params, _}}.
rollback(PlanID, Batches, Timestamp, Clock) ->
AccounterClock = to_accounter_clock(Clock),
do('RollbackPlan', construct_plan(PlanID, Batches, Timestamp), AccounterClock).

do(Op, Plan, PreviousClock) ->
case call_accounter(Op, {Plan, PreviousClock}) of
{ok, Clock} ->
{ok, to_domain_clock(Clock)};
{error, _} = Error ->
Error
end.

execute_plan(_PlanID, [], _Timestamp, Clock) ->
{ok, Clock};
execute_plan(PlanID, [Batch | Rest], Timestamp, Clock) ->
case hold(PlanID, Batch, Timestamp, Clock) of
{ok, NewClock} ->
execute_plan(PlanID, Rest, Timestamp, NewClock);
{error, _} = Error ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я кстати только сейчас осознал, что с ш̤̖͙͍̄͂а̦̣̫̪̱̬̃͛̊ͯͧм̟͍͈̞̝̩̓̄ͬ̉̂ͯӓ̱͔͕̻͔̯̦́̒̓̑̑̑и̩̖ͥч͕̊̈́ͫͮ͌̉̎ем мы можем оказаться в ситуации, когда половина плана захолдирована, и машина упала. Надо бы наверное понять, что с этим делать, если что. По идее эта ошибка может только на этапе интеграции проявиться, поэтому повода какую-то обвязку по обработке в виде кода вроде как нет, но тем не менее.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Мне казалось, что шамаич умеет в идемпотентность, и поэтому ничего страшного что мы после поднятия машины попробуем захолдировать захолженные холды еще раз. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут я скорее про ошибку InvalidPostingParams, с которой в общем случае не пойми что делать. Да и это вообще не докоп, а так, наблюдение.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не совсем понятен кейс когда ты такую ошибку можешь получить

Error
end.

construct_plan_change(PlanID, {BatchID, Cashflow}, Timestamp) ->
#shumaich_PostingPlanChange{
id = PlanID,
creation_time = Timestamp,
batch = #shumaich_PostingBatch{
id = BatchID,
postings = collect_postings(Cashflow)
}
}.

construct_plan(PlanID, Batches, Timestamp) ->
#shumaich_PostingPlan{
id = PlanID,
creation_time = Timestamp,
batch_list = [
#shumaich_PostingBatch{
id = BatchID,
postings = collect_postings(Cashflow)
}
|| {BatchID, Cashflow} <- Batches
]
}.

collect_postings(Cashflow) ->
[
#shumaich_Posting{
from_account = #shumaich_Account{id = Source, currency_symbolic_code = CurrencyCode},
to_account = #shumaich_Account{id = Destination, currency_symbolic_code = CurrencyCode},
amount = Amount,
currency_symbolic_code = CurrencyCode,
description = construct_posting_description(Details)
}
|| #domain_FinalCashFlowPosting{
source = #domain_FinalCashFlowAccount{account_id = Source},
destination = #domain_FinalCashFlowAccount{account_id = Destination},
details = Details,
volume = #domain_Cash{
amount = Amount,
currency = #domain_CurrencyRef{symbolic_code = CurrencyCode}
}
} <- Cashflow
].

construct_posting_description(Details) when is_binary(Details) ->
Details;
construct_posting_description(undefined) ->
<<>>.

%%

construct_account(
AccountID,
#shumaich_Account{
currency_symbolic_code = CurrencyCode
}
) ->
#{
account_id => AccountID,
currency_code => CurrencyCode
}.

construct_balance(
AccountID,
#shumaich_Balance{
own_amount = OwnAmount,
min_available_amount = MinAvailableAmount,
max_available_amount = MaxAvailableAmount,
clock = Clock
}
) ->
genlib_map:compact(#{
account_id => AccountID,
own_amount => OwnAmount,
min_available_amount => MinAvailableAmount,
max_available_amount => MaxAvailableAmount,
clock => to_domain_clock(Clock)
}).

%%

call_accounter(Function, Args) ->
hg_retry:apply(
fun() ->
case call_service(Function, Args) of
{ok, _} = Ok ->
{return, Ok};
{error, ErrorType} = Error ->
{map_error_action(ErrorType), Error}
end
end,
get_retry_strategy(Function)
).

call_service(Function, Args) ->
case hg_woody_wrapper:call(accounter_new, Function, Args) of
{ok, _} = Ok ->
Ok;
{exception, Exception} ->
{error, map_exception(Exception)}
end.

map_error_action(not_ready) ->
retry;
map_error_action(_) ->
return.

map_exception(#shumaich_NotReady{}) ->
not_ready;
map_exception(#shumaich_AccountNotFound{account_id = AccountID}) ->
{account_not_found, AccountID};
map_exception(#shumaich_PlanNotFound{plan_id = PlanID}) ->
{plan_not_found, PlanID};
map_exception(#shumaich_InvalidPostingParams{wrong_postings = WrongPostings}) ->
{invalid_posting_params, WrongPostings}.

to_domain_clock({latest, #shumaich_LatestClock{}}) ->
undefined;
to_domain_clock({vector, #shumaich_VectorClock{state = State}}) ->
{vector, #domain_VectorClock{state = State}}.

to_accounter_clock(undefined) ->
{latest, #shumaich_LatestClock{}};
to_accounter_clock({vector, #domain_VectorClock{state = State}}) ->
{vector, #shumaich_VectorClock{state = State}}.

get_retry_strategy(Function) ->
PolicyConfig = genlib_app:env(hellgate, accounter_retry_policy, #{}),
hg_retry:new_strategy(maps:get(Function, PolicyConfig, ?DEFAULT_RETRY_STRATEGY)).
24 changes: 23 additions & 1 deletion apps/hellgate/src/hg_retry.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
-export([
next_step/1,
skip_steps/2,
new_strategy/1
new_strategy/1,
apply/2
]).

-type retries_num() :: pos_integer() | infinity.
Expand Down Expand Up @@ -58,4 +59,25 @@ skip_steps(Strategy, N) when N > 0 ->
end,
skip_steps(NewStrategy, N - 1).

-type retry_fun_result() :: any().
-type retry_fun_action() :: retry | return.
-type retry_fun() :: fun(() -> {retry_fun_action(), retry_fun_result()}).

-compile({no_auto_import, [apply/2]}).

-spec apply(retry_fun(), strategy()) -> retry_fun_result().
apply(Fun, Strategy) ->
case Fun() of
{return, Result} ->
Result;
{retry, Error} ->
case next_step(Strategy) of
{wait, Timeout, NextStrategy} ->
_ = timer:sleep(Timeout),
apply(Fun, NextStrategy);
finish ->
Error
end
end.

%%% Internal functions
16 changes: 16 additions & 0 deletions apps/hellgate/src/hg_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@

-export([format_reason/1]).

-export([gen_sequence/2]).
-export([gen_sequence/3]).

-type sequence_params() :: #{minimum => integer()}.
-type woody_context() :: woody_context:ctx().

%%

-spec unique_id() -> dmsl_base_thrift:'ID'().
Expand Down Expand Up @@ -80,3 +86,13 @@ unwrap_result({error, E}) ->
%% TODO: fix this dirty hack
format_reason(V) ->
genlib:to_binary(V).

-spec gen_sequence(binary(), woody_context()) -> integer().
gen_sequence(SequenceID, WoodyContext) ->
gen_sequence(SequenceID, WoodyContext, #{}).

-spec gen_sequence(binary(), woody_context(), sequence_params()) -> integer().
gen_sequence(SequenceID, WoodyContext, Params) ->
case bender_generator_client:gen_sequence(SequenceID, WoodyContext, Params) of
{ok, {_, ID}} -> ID
end.
1 change: 1 addition & 0 deletions apps/hellgate/test/hg_ct_helper.erl
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ start_app(hellgate = AppName) ->
}},
{services, #{
accounter => <<"http://shumway:8022/shumpune">>,
accounter_new => <<"http://shumway:8022/shumaich">>,
automaton => <<"http://machinegun:8022/v1/automaton">>,
customer_management => #{
url => <<"http://hellgate:8022/v1/processing/customer_management">>,
Expand Down
Loading