mirror of
https://github.com/valitydev/limiter.git
synced 2024-11-06 00:55:22 +00:00
P2C-14: Limiter (#1)
* draft * added format * added backend stuff * added requested changes except batch in range machine * added error sort in handler, add lim_body and its validation * added start time and shard size, implemented shard id generation and time range calculation * fixed xref * fixed format * added unit tests * refactored errors * updated to new proto * added config marshaling * fixed format * added base tests * added exchange tests * fixed linter * added requested changes * updated proto ref
This commit is contained in:
parent
563fb97ea6
commit
9bf3e29636
2
Makefile
2
Makefile
@ -74,4 +74,4 @@ distclean:
|
||||
|
||||
# CALL_W_CONTAINER
|
||||
test: submodules
|
||||
$(REBAR) ct
|
||||
$(REBAR) do eunit, ct
|
||||
|
166
apps/limiter/src/lim_accounting.erl
Normal file
166
apps/limiter/src/lim_accounting.erl
Normal file
@ -0,0 +1,166 @@
|
||||
-module(lim_accounting).
|
||||
|
||||
-include_lib("damsel/include/dmsl_accounter_thrift.hrl").
|
||||
-include_lib("damsel/include/dmsl_base_thrift.hrl").
|
||||
|
||||
-export([plan/3]).
|
||||
-export([hold/3]).
|
||||
-export([commit/3]).
|
||||
-export([rollback/3]).
|
||||
-export([get_plan/2]).
|
||||
-export([get_balance/2]).
|
||||
-export([get_default_currency/0]).
|
||||
-export([create_account/2]).
|
||||
|
||||
-type currency() :: dmsl_domain_thrift:'CurrencySymbolicCode'().
|
||||
-type amount() :: dmsl_domain_thrift:'Amount'().
|
||||
-type plan_id() :: dmsl_accounter_thrift:'PlanID'().
|
||||
-type batch_id() :: dmsl_accounter_thrift:'BatchID'().
|
||||
-type posting() :: dmsl_accounter_thrift:'Posting'().
|
||||
-type batch() :: {batch_id(), [posting()]}.
|
||||
-type account_id() :: dmsl_accounter_thrift:'AccountID'().
|
||||
-type lim_context() :: lim_context:t().
|
||||
|
||||
-type balance() :: #{
|
||||
account_id := account_id(),
|
||||
own_amount := amount(),
|
||||
min_available_amount := amount(),
|
||||
max_available_amount := amount(),
|
||||
currency := currency()
|
||||
}.
|
||||
|
||||
-type invalid_request_error() :: {invalid_request, list(binary())}.
|
||||
|
||||
-export_type([account_id/0]).
|
||||
-export_type([amount/0]).
|
||||
-export_type([balance/0]).
|
||||
-export_type([plan_id/0]).
|
||||
-export_type([batch/0]).
|
||||
-export_type([posting/0]).
|
||||
-export_type([batch_id/0]).
|
||||
-export_type([invalid_request_error/0]).
|
||||
|
||||
-define(DEFAULT_CURRENCY, <<"RUB">>).
|
||||
|
||||
-spec plan(plan_id(), [batch()], lim_context()) -> ok | {error, invalid_request_error()}.
|
||||
plan(_PlanID, [], _LimitContext) ->
|
||||
error(badarg);
|
||||
plan(_PlanID, Batches, _LimitContext) when not is_list(Batches) ->
|
||||
error(badarg);
|
||||
plan(PlanID, Batches, LimitContext) ->
|
||||
lists:foldl(
|
||||
fun(Batch, _) -> hold(PlanID, Batch, LimitContext) end,
|
||||
undefined,
|
||||
Batches
|
||||
).
|
||||
|
||||
-spec hold(plan_id(), batch(), lim_context()) -> ok | {error, invalid_request_error()}.
|
||||
hold(PlanID, Batch, LimitContext) ->
|
||||
do('Hold', construct_plan_change(PlanID, Batch), LimitContext).
|
||||
|
||||
-spec commit(plan_id(), [batch()], lim_context()) -> ok | {error, invalid_request_error()}.
|
||||
commit(PlanID, Batches, LimitContext) ->
|
||||
do('CommitPlan', construct_plan(PlanID, Batches), LimitContext).
|
||||
|
||||
-spec rollback(plan_id(), [batch()], lim_context()) -> ok | {error, invalid_request_error()}.
|
||||
rollback(PlanID, Batches, LimitContext) ->
|
||||
do('RollbackPlan', construct_plan(PlanID, Batches), LimitContext).
|
||||
|
||||
-spec get_plan(plan_id(), lim_context()) -> {ok, [batch()]} | {error, notfound}.
|
||||
get_plan(PlanID, LimitContext) ->
|
||||
case call_accounter('GetPlan', {PlanID}, LimitContext) of
|
||||
{ok, #accounter_PostingPlan{batch_list = BatchList}} ->
|
||||
{ok, decode_batch_list(BatchList)};
|
||||
{exception, #accounter_PlanNotFound{}} ->
|
||||
{error, notfound}
|
||||
end.
|
||||
|
||||
-spec get_balance(account_id(), lim_context()) -> {ok, balance()} | {error, notfound}.
|
||||
get_balance(AccountID, LimitContext) ->
|
||||
case call_accounter('GetAccountByID', {AccountID}, LimitContext) of
|
||||
{ok, Result} ->
|
||||
{ok, construct_balance(AccountID, Result)};
|
||||
{exception, #accounter_AccountNotFound{}} ->
|
||||
{error, notfound}
|
||||
end.
|
||||
|
||||
do(Op, Plan, LimitContext) ->
|
||||
case call_accounter(Op, {Plan}, LimitContext) of
|
||||
{ok, _Clock} ->
|
||||
ok;
|
||||
{exception, Exception} ->
|
||||
{error, {invalid_request, convert_exception(Exception)}}
|
||||
end.
|
||||
|
||||
construct_plan_change(PlanID, {BatchID, Postings}) ->
|
||||
#accounter_PostingPlanChange{
|
||||
id = PlanID,
|
||||
batch = #accounter_PostingBatch{
|
||||
id = BatchID,
|
||||
postings = Postings
|
||||
}
|
||||
}.
|
||||
|
||||
construct_plan(PlanID, Batches) ->
|
||||
#accounter_PostingPlan{
|
||||
id = PlanID,
|
||||
batch_list = [
|
||||
#accounter_PostingBatch{
|
||||
id = BatchID,
|
||||
postings = Postings
|
||||
}
|
||||
|| {BatchID, Postings} <- Batches
|
||||
]
|
||||
}.
|
||||
|
||||
decode_batch_list(BatchList) ->
|
||||
[{BatchID, Postings} || #accounter_PostingBatch{id = BatchID, postings = Postings} <- BatchList].
|
||||
|
||||
construct_balance(
|
||||
AccountID,
|
||||
#accounter_Account{
|
||||
own_amount = OwnAmount,
|
||||
min_available_amount = MinAvailableAmount,
|
||||
max_available_amount = MaxAvailableAmount,
|
||||
currency_sym_code = Currency
|
||||
}
|
||||
) ->
|
||||
#{
|
||||
account_id => AccountID,
|
||||
own_amount => OwnAmount,
|
||||
min_available_amount => MinAvailableAmount,
|
||||
max_available_amount => MaxAvailableAmount,
|
||||
currency => Currency
|
||||
}.
|
||||
|
||||
-spec get_default_currency() -> currency().
|
||||
get_default_currency() ->
|
||||
?DEFAULT_CURRENCY.
|
||||
|
||||
-spec create_account(currency(), lim_context()) -> {ok, account_id()}.
|
||||
create_account(CurrencyCode, LimitContext) ->
|
||||
create_account(CurrencyCode, undefined, LimitContext).
|
||||
|
||||
create_account(CurrencyCode, Description, LimitContext) ->
|
||||
call_accounter(
|
||||
'CreateAccount',
|
||||
{construct_prototype(CurrencyCode, Description)},
|
||||
LimitContext
|
||||
).
|
||||
|
||||
construct_prototype(CurrencyCode, Description) ->
|
||||
#accounter_AccountPrototype{
|
||||
currency_sym_code = CurrencyCode,
|
||||
description = Description
|
||||
}.
|
||||
|
||||
%%
|
||||
|
||||
call_accounter(Function, Args, LimitContext) ->
|
||||
{ok, WoodyContext} = lim_context:woody_context(LimitContext),
|
||||
lim_client_woody:call(accounter, Function, Args, WoodyContext).
|
||||
|
||||
convert_exception(#'InvalidRequest'{errors = Errors}) ->
|
||||
Errors;
|
||||
convert_exception(#accounter_InvalidPostingParams{wrong_postings = Errors}) ->
|
||||
maps:fold(fun(_, Error, Acc) -> [Error | Acc] end, [], Errors).
|
81
apps/limiter/src/lim_body.erl
Normal file
81
apps/limiter/src/lim_body.erl
Normal file
@ -0,0 +1,81 @@
|
||||
-module(lim_body).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_base_thrift.hrl").
|
||||
-include_lib("limiter_proto/include/lim_limiter_thrift.hrl").
|
||||
|
||||
-export([get_body/3]).
|
||||
-export([create_body_from_cash/2]).
|
||||
|
||||
-type t() :: {amount, amount()} | {cash, cash()}.
|
||||
|
||||
-type amount() :: integer().
|
||||
-type cash() :: #{
|
||||
amount := amount(),
|
||||
currency := currency()
|
||||
}.
|
||||
|
||||
-type currency() :: lim_base_thrift:'CurrencySymbolicCode'().
|
||||
-type config() :: lim_config_machine:config().
|
||||
-type body_type() :: full | partial.
|
||||
-type get_body_error() :: notfound | lim_rates:convertation_error().
|
||||
|
||||
-export_type([t/0]).
|
||||
-export_type([cash/0]).
|
||||
-export_type([get_body_error/0]).
|
||||
|
||||
-spec get_body(body_type(), config(), lim_context:t()) -> {ok, t()} | {error, get_body_error()}.
|
||||
get_body(BodyType, Config = #{body_type := {cash, ConfigCurrency}}, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
{ok, Operation} = lim_context:get_operation(ContextType, LimitContext),
|
||||
case get_body_for_operation(BodyType, Operation, Config, LimitContext) of
|
||||
{ok, {cash, #{currency := ConfigCurrency}}} = Result ->
|
||||
Result;
|
||||
{ok, {cash, #{amount := Amount, currency := Currency}}} ->
|
||||
case lim_rates:get_converted_amount({Amount, Currency}, Config, LimitContext) of
|
||||
{ok, ConvertedAmount} ->
|
||||
{ok, create_body_from_cash(ConvertedAmount, ConfigCurrency)};
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec get_body_for_operation(body_type(), lim_context:context_operation(), config(), lim_context:t()) ->
|
||||
{ok, t()} | {error, notfound}.
|
||||
get_body_for_operation(full, invoice = Operation, Config, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
lim_context:get_from_context(ContextType, cost, Operation, LimitContext);
|
||||
get_body_for_operation(full, invoice_adjustment, Config, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
lim_context:get_from_context(ContextType, cost, invoice, LimitContext);
|
||||
get_body_for_operation(full, invoice_payment = Operation, Config, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
lim_context:get_from_context(ContextType, cost, Operation, LimitContext);
|
||||
get_body_for_operation(full, invoice_payment_adjustment, Config, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
lim_context:get_from_context(ContextType, cost, invoice_payment, LimitContext);
|
||||
get_body_for_operation(full, invoice_payment_refund, Config, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
lim_context:get_from_context(ContextType, cost, invoice_payment, LimitContext);
|
||||
get_body_for_operation(full, invoice_payment_chargeback = Operation, Config, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
lim_context:get_from_context(ContextType, body, Operation, LimitContext);
|
||||
get_body_for_operation(partial, invoice, _Config, _LimitContext) ->
|
||||
{error, notfound};
|
||||
get_body_for_operation(partial, invoice_adjustment, _Config, _LimitContext) ->
|
||||
{error, notfound};
|
||||
get_body_for_operation(partial, invoice_payment = Operation, Config, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
lim_context:get_from_context(ContextType, capture_cost, Operation, LimitContext);
|
||||
get_body_for_operation(partial, invoice_payment_adjustment, _Config, _LimitContext) ->
|
||||
{error, notfound};
|
||||
get_body_for_operation(partial, invoice_payment_refund = Operation, Config, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
lim_context:get_from_context(ContextType, cost, Operation, LimitContext);
|
||||
get_body_for_operation(partial, invoice_payment_chargeback, _Config, _LimitContext) ->
|
||||
{error, notfound}.
|
||||
|
||||
-spec create_body_from_cash(amount(), currency()) -> t().
|
||||
create_body_from_cash(Amount, Currency) ->
|
||||
{cash, #{amount => Amount, currency => Currency}}.
|
57
apps/limiter/src/lim_client_woody.erl
Normal file
57
apps/limiter/src/lim_client_woody.erl
Normal file
@ -0,0 +1,57 @@
|
||||
-module(lim_client_woody).
|
||||
|
||||
-export([call/4]).
|
||||
-export([call/5]).
|
||||
-export([get_service_client_url/1]).
|
||||
|
||||
-define(APP, limiter).
|
||||
-define(DEFAULT_DEADLINE, 5000).
|
||||
|
||||
%%
|
||||
-type service_name() :: atom().
|
||||
|
||||
-spec call(service_name(), woody:func(), woody:args(), woody_context:ctx()) -> woody:result().
|
||||
call(ServiceName, Function, Args, Context) ->
|
||||
EventHandler = scoper_woody_event_handler,
|
||||
call(ServiceName, Function, Args, Context, EventHandler).
|
||||
|
||||
-spec call(service_name(), woody:func(), woody:args(), woody_context:ctx(), woody:ev_handler()) -> woody:result().
|
||||
call(ServiceName, Function, Args, Context0, EventHandler) ->
|
||||
Deadline = get_service_deadline(ServiceName),
|
||||
Context1 = set_deadline(Deadline, Context0),
|
||||
Url = get_service_client_url(ServiceName),
|
||||
Service = get_service_modname(ServiceName),
|
||||
Request = {Service, Function, Args},
|
||||
woody_client:call(
|
||||
Request,
|
||||
#{url => Url, event_handler => EventHandler},
|
||||
Context1
|
||||
).
|
||||
|
||||
get_service_client_config(ServiceName) ->
|
||||
ServiceClients = genlib_app:env(?APP, service_clients, #{}),
|
||||
maps:get(ServiceName, ServiceClients, #{}).
|
||||
|
||||
-spec get_service_client_url(atom()) -> lim_maybe:maybe(woody:url()).
|
||||
get_service_client_url(ServiceName) ->
|
||||
maps:get(url, get_service_client_config(ServiceName), undefined).
|
||||
|
||||
-spec get_service_modname(service_name()) -> woody:service().
|
||||
get_service_modname(xrates) ->
|
||||
{xrates_rate_thrift, 'Rates'};
|
||||
get_service_modname(accounter) ->
|
||||
{dmsl_accounter_thrift, 'Accounter'}.
|
||||
|
||||
-spec get_service_deadline(service_name()) -> undefined | woody_deadline:deadline().
|
||||
get_service_deadline(ServiceName) ->
|
||||
ServiceClient = get_service_client_config(ServiceName),
|
||||
Timeout = maps:get(deadline, ServiceClient, ?DEFAULT_DEADLINE),
|
||||
woody_deadline:from_timeout(Timeout).
|
||||
|
||||
set_deadline(Deadline, Context) ->
|
||||
case woody_context:get_deadline(Context) of
|
||||
undefined ->
|
||||
woody_context:set_deadline(Deadline, Context);
|
||||
_AlreadySet ->
|
||||
Context
|
||||
end.
|
225
apps/limiter/src/lim_config_codec.erl
Normal file
225
apps/limiter/src/lim_config_codec.erl
Normal file
@ -0,0 +1,225 @@
|
||||
-module(lim_config_codec).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_limiter_config_thrift.hrl").
|
||||
|
||||
-export([marshal/2]).
|
||||
-export([unmarshal/2]).
|
||||
-export([marshal_config/1]).
|
||||
-export([unmarshal_body_type/1]).
|
||||
|
||||
%% Types
|
||||
|
||||
-type type_name() :: atom() | {list, atom()} | {set, atom()}.
|
||||
|
||||
-type encoded_value() :: encoded_value(any()).
|
||||
-type encoded_value(T) :: T.
|
||||
|
||||
-type decoded_value() :: decoded_value(any()).
|
||||
-type decoded_value(T) :: T.
|
||||
|
||||
maybe_apply(undefined, _) ->
|
||||
undefined;
|
||||
maybe_apply(Value, Fun) ->
|
||||
Fun(Value).
|
||||
|
||||
%% API
|
||||
|
||||
-spec marshal(type_name(), decoded_value()) -> encoded_value().
|
||||
marshal(timestamped_change, {ev, Timestamp, Change}) ->
|
||||
#limiter_config_TimestampedChange{
|
||||
change = marshal_change(Change),
|
||||
occured_at = marshal_timestamp(Timestamp)
|
||||
}.
|
||||
|
||||
marshal_timestamp({DateTime, USec}) ->
|
||||
DateTimeinSeconds = genlib_time:daytime_to_unixtime(DateTime),
|
||||
{TimeinUnit, Unit} =
|
||||
case USec of
|
||||
0 ->
|
||||
{DateTimeinSeconds, second};
|
||||
USec ->
|
||||
MicroSec = erlang:convert_time_unit(DateTimeinSeconds, second, microsecond),
|
||||
{MicroSec + USec, microsecond}
|
||||
end,
|
||||
genlib_rfc3339:format_relaxed(TimeinUnit, Unit).
|
||||
|
||||
marshal_change({created, Config}) ->
|
||||
{created, #limiter_config_CreatedChange{limit_config = marshal_config(Config)}}.
|
||||
|
||||
-spec marshal_config(decoded_value()) -> encoded_value().
|
||||
marshal_config(Config) ->
|
||||
#limiter_config_LimitConfig{
|
||||
id = lim_config_machine:id(Config),
|
||||
processor_type = lim_config_machine:processor_type(Config),
|
||||
description = lim_config_machine:description(Config),
|
||||
body_type = marshal_body_type(lim_config_machine:body_type(Config)),
|
||||
created_at = lim_config_machine:created_at(Config),
|
||||
started_at = lim_config_machine:started_at(Config),
|
||||
shard_size = lim_config_machine:shard_size(Config),
|
||||
time_range_type = marshal_time_range_type(lim_config_machine:time_range_type(Config)),
|
||||
context_type = marshal_context_type(lim_config_machine:context_type(Config)),
|
||||
type = maybe_apply(lim_config_machine:type(Config), fun marshal_type/1),
|
||||
scope = maybe_apply(lim_config_machine:scope(Config), fun marshal_scope/1)
|
||||
}.
|
||||
|
||||
marshal_body_type(amount) ->
|
||||
{amount, #limiter_config_LimitBodyTypeAmount{}};
|
||||
marshal_body_type({cash, Currency}) ->
|
||||
{cash, #limiter_config_LimitBodyTypeCash{currency = Currency}}.
|
||||
|
||||
marshal_time_range_type({calendar, CalendarType}) ->
|
||||
{calendar, marshal_calendar_time_range_type(CalendarType)};
|
||||
marshal_time_range_type({interval, Amount}) ->
|
||||
{interval, #time_range_TimeRangeTypeInterval{amount = Amount}}.
|
||||
|
||||
marshal_calendar_time_range_type(day) ->
|
||||
{day, #time_range_TimeRangeTypeCalendarDay{}};
|
||||
marshal_calendar_time_range_type(week) ->
|
||||
{week, #time_range_TimeRangeTypeCalendarWeek{}};
|
||||
marshal_calendar_time_range_type(month) ->
|
||||
{month, #time_range_TimeRangeTypeCalendarMonth{}};
|
||||
marshal_calendar_time_range_type(year) ->
|
||||
{year, #time_range_TimeRangeTypeCalendarYear{}}.
|
||||
|
||||
marshal_context_type(payment_processing) ->
|
||||
{payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}}.
|
||||
|
||||
marshal_type(turnover) ->
|
||||
{turnover, #limiter_config_LimitTypeTurnover{}}.
|
||||
|
||||
marshal_scope({scope, Type}) ->
|
||||
{scope, marshal_scope_type(Type)};
|
||||
marshal_scope(global) ->
|
||||
{scope_global, #limiter_config_LimitScopeGlobal{}}.
|
||||
|
||||
marshal_scope_type(party) ->
|
||||
{party, #limiter_config_LimitScopeTypeParty{}};
|
||||
marshal_scope_type(shop) ->
|
||||
{shop, #limiter_config_LimitScopeTypeShop{}};
|
||||
marshal_scope_type(wallet) ->
|
||||
{wallet, #limiter_config_LimitScopeTypeWallet{}};
|
||||
marshal_scope_type(identity) ->
|
||||
{identity, #limiter_config_LimitScopeTypeIdentity{}}.
|
||||
|
||||
%%
|
||||
|
||||
-spec unmarshal(type_name(), encoded_value()) -> decoded_value().
|
||||
unmarshal(timestamped_change, TimestampedChange) ->
|
||||
Timestamp = unmarshal_timestamp(TimestampedChange#limiter_config_TimestampedChange.occured_at),
|
||||
Change = unmarshal_change(TimestampedChange#limiter_config_TimestampedChange.change),
|
||||
{ev, Timestamp, Change}.
|
||||
|
||||
unmarshal_timestamp(Timestamp) when is_binary(Timestamp) ->
|
||||
try
|
||||
MicroSeconds = genlib_rfc3339:parse(Timestamp, microsecond),
|
||||
case genlib_rfc3339:is_utc(Timestamp) of
|
||||
false ->
|
||||
erlang:error({bad_timestamp, not_utc}, [Timestamp]);
|
||||
true ->
|
||||
USec = MicroSeconds rem 1000000,
|
||||
DateTime = calendar:system_time_to_universal_time(MicroSeconds, microsecond),
|
||||
{DateTime, USec}
|
||||
end
|
||||
catch
|
||||
error:Error:St ->
|
||||
erlang:raise(error, {bad_timestamp, Timestamp, Error}, St)
|
||||
end.
|
||||
|
||||
unmarshal_change({created, #limiter_config_CreatedChange{limit_config = Config}}) ->
|
||||
{created, unmarshal_config(Config)}.
|
||||
|
||||
unmarshal_config(#limiter_config_LimitConfig{
|
||||
id = ID,
|
||||
processor_type = ProcessorType,
|
||||
description = Description,
|
||||
body_type = BodyType,
|
||||
created_at = CreatedAt,
|
||||
started_at = StartedAt,
|
||||
shard_size = ShardSize,
|
||||
time_range_type = TimeRangeType,
|
||||
context_type = ContextType,
|
||||
type = Type,
|
||||
scope = Scope
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
id => ID,
|
||||
processor_type => ProcessorType,
|
||||
created_at => lim_time:from_rfc3339(CreatedAt),
|
||||
body_type => unmarshal_body_type(BodyType),
|
||||
started_at => StartedAt,
|
||||
shard_size => ShardSize,
|
||||
time_range_type => unmarshal_time_range_type(TimeRangeType),
|
||||
context_type => unmarshal_context_type(ContextType),
|
||||
type => maybe_apply(Type, fun unmarshal_type/1),
|
||||
scope => maybe_apply(Scope, fun unmarshal_scope/1),
|
||||
description => Description
|
||||
}).
|
||||
|
||||
-spec unmarshal_body_type(encoded_value()) -> decoded_value().
|
||||
unmarshal_body_type({amount, #limiter_config_LimitBodyTypeAmount{}}) ->
|
||||
amount;
|
||||
unmarshal_body_type({cash, #limiter_config_LimitBodyTypeCash{currency = Currency}}) ->
|
||||
{cash, Currency}.
|
||||
|
||||
unmarshal_time_range_type({calendar, CalendarType}) ->
|
||||
{calendar, unmarshal_calendar_time_range_type(CalendarType)};
|
||||
unmarshal_time_range_type({interval, #time_range_TimeRangeTypeInterval{amount = Amount}}) ->
|
||||
{interval, Amount}.
|
||||
|
||||
unmarshal_calendar_time_range_type({day, _}) ->
|
||||
day;
|
||||
unmarshal_calendar_time_range_type({week, _}) ->
|
||||
week;
|
||||
unmarshal_calendar_time_range_type({month, _}) ->
|
||||
month;
|
||||
unmarshal_calendar_time_range_type({year, _}) ->
|
||||
year.
|
||||
|
||||
unmarshal_context_type({payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}}) ->
|
||||
payment_processing.
|
||||
|
||||
unmarshal_type({turnover, #limiter_config_LimitTypeTurnover{}}) ->
|
||||
turnover.
|
||||
|
||||
unmarshal_scope({scope, Type}) ->
|
||||
{scope, unmarshal_scope_type(Type)};
|
||||
unmarshal_scope({scope_global, #limiter_config_LimitScopeGlobal{}}) ->
|
||||
global.
|
||||
|
||||
unmarshal_scope_type({party, _}) ->
|
||||
party;
|
||||
unmarshal_scope_type({shop, _}) ->
|
||||
shop;
|
||||
unmarshal_scope_type({wallet, _}) ->
|
||||
wallet;
|
||||
unmarshal_scope_type({identity, _}) ->
|
||||
identity.
|
||||
|
||||
%%
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
|
||||
-spec marshal_unmarshal_created_test() -> _.
|
||||
|
||||
marshal_unmarshal_created_test() ->
|
||||
Created =
|
||||
{created, #{
|
||||
id => <<"id">>,
|
||||
processor_type => <<"type">>,
|
||||
created_at => lim_time:now(),
|
||||
body_type => {cash, <<"RUB">>},
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
shard_size => 7,
|
||||
time_range_type => {calendar, day},
|
||||
context_type => payment_processing,
|
||||
type => turnover,
|
||||
scope => {scope, party},
|
||||
description => <<"description">>
|
||||
}},
|
||||
Event = {ev, lim_time:machinery_now(), Created},
|
||||
?assertEqual(Event, unmarshal(timestamped_change, marshal(timestamped_change, Event))).
|
||||
|
||||
-endif.
|
768
apps/limiter/src/lim_config_machine.erl
Normal file
768
apps/limiter/src/lim_config_machine.erl
Normal file
@ -0,0 +1,768 @@
|
||||
-module(lim_config_machine).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_limiter_thrift.hrl").
|
||||
-include_lib("limiter_proto/include/lim_base_thrift.hrl").
|
||||
|
||||
%% Accessors
|
||||
|
||||
-export([created_at/1]).
|
||||
-export([id/1]).
|
||||
-export([description/1]).
|
||||
-export([body_type/1]).
|
||||
-export([started_at/1]).
|
||||
-export([shard_size/1]).
|
||||
-export([time_range_type/1]).
|
||||
-export([processor_type/1]).
|
||||
-export([type/1]).
|
||||
-export([scope/1]).
|
||||
-export([context_type/1]).
|
||||
|
||||
%% API
|
||||
|
||||
-export([start/3]).
|
||||
-export([get/2]).
|
||||
|
||||
-export([get_limit/2]).
|
||||
-export([hold/2]).
|
||||
-export([commit/2]).
|
||||
-export([rollback/2]).
|
||||
|
||||
-export([calculate_shard_id/2]).
|
||||
-export([calculate_time_range/2]).
|
||||
-export([mk_scope_prefix/2]).
|
||||
|
||||
-type woody_context() :: woody_context:ctx().
|
||||
-type lim_context() :: lim_context:t().
|
||||
-type processor_type() :: lim_router:processor_type().
|
||||
-type processor() :: lim_router:processor().
|
||||
-type description() :: binary().
|
||||
|
||||
-type limit_type() :: turnover.
|
||||
-type limit_scope() :: global | {scope, party | shop | wallet | identity}.
|
||||
-type body_type() :: {cash, currency()} | amount.
|
||||
-type shard_size() :: pos_integer().
|
||||
-type shard_id() :: binary().
|
||||
-type prefix() :: binary().
|
||||
-type time_range_type() :: {calendar, year | month | week | day} | {interval, pos_integer()}.
|
||||
-type time_range() :: #{
|
||||
upper := timestamp(),
|
||||
lower := timestamp()
|
||||
}.
|
||||
|
||||
-type context_type() :: lim_context:context_type().
|
||||
|
||||
-type config() :: #{
|
||||
id := lim_id(),
|
||||
processor_type := processor_type(),
|
||||
created_at := lim_time:timestamp_ms(),
|
||||
body_type := body_type(),
|
||||
started_at := timestamp(),
|
||||
shard_size := shard_size(),
|
||||
time_range_type := time_range_type(),
|
||||
context_type := context_type(),
|
||||
type => limit_type(),
|
||||
scope => limit_scope(),
|
||||
description => description()
|
||||
}.
|
||||
|
||||
-type create_params() :: #{
|
||||
processor_type := processor_type(),
|
||||
body_type := body_type(),
|
||||
started_at := timestamp(),
|
||||
shard_size := shard_size(),
|
||||
time_range_type := time_range_type(),
|
||||
context_type := context_type(),
|
||||
type => limit_type(),
|
||||
scope => limit_scope(),
|
||||
description => description()
|
||||
}.
|
||||
|
||||
-type lim_id() :: lim_limiter_thrift:'LimitID'().
|
||||
-type lim_change() :: lim_limiter_thrift:'LimitChange'().
|
||||
-type limit() :: lim_limiter_thrift:'Limit'().
|
||||
-type timestamp() :: lim_base_thrift:'Timestamp'().
|
||||
-type currency() :: lim_base_thrift:'CurrencySymbolicCode'().
|
||||
|
||||
-export_type([config/0]).
|
||||
-export_type([body_type/0]).
|
||||
-export_type([limit_type/0]).
|
||||
-export_type([limit_scope/0]).
|
||||
-export_type([time_range_type/0]).
|
||||
-export_type([time_range/0]).
|
||||
-export_type([create_params/0]).
|
||||
-export_type([currency/0]).
|
||||
-export_type([lim_id/0]).
|
||||
-export_type([lim_change/0]).
|
||||
-export_type([limit/0]).
|
||||
-export_type([timestamp/0]).
|
||||
|
||||
%% Machinery callbacks
|
||||
|
||||
-behaviour(machinery).
|
||||
|
||||
-export([init/4]).
|
||||
-export([process_call/4]).
|
||||
-export([process_timeout/3]).
|
||||
-export([process_repair/4]).
|
||||
|
||||
-type timestamped_event(T) ::
|
||||
{ev, machinery:timestamp(), T}.
|
||||
|
||||
-type event() ::
|
||||
{created, config()}.
|
||||
|
||||
-type args(T) :: machinery:args(T).
|
||||
-type machine() :: machinery:machine(event(), _).
|
||||
-type handler_args() :: machinery:handler_args(_).
|
||||
-type handler_opts() :: machinery:handler_opts(_).
|
||||
-type result() :: machinery:result(timestamped_event(event()), none()).
|
||||
|
||||
-export_type([timestamped_event/1]).
|
||||
-export_type([event/0]).
|
||||
|
||||
-define(NS, 'lim/config_v1').
|
||||
|
||||
%% Handler behaviour
|
||||
|
||||
-callback get_limit(
|
||||
ID :: lim_id(),
|
||||
Config :: config(),
|
||||
LimitContext :: lim_context()
|
||||
) -> {ok, limit()} | {error, get_limit_error()}.
|
||||
|
||||
-callback hold(
|
||||
LimitChange :: lim_change(),
|
||||
Config :: config(),
|
||||
LimitContext :: lim_context()
|
||||
) -> ok | {error, hold_error()}.
|
||||
|
||||
-callback commit(
|
||||
LimitChange :: lim_change(),
|
||||
Config :: config(),
|
||||
LimitContext :: lim_context()
|
||||
) -> ok | {error, commit_error()}.
|
||||
|
||||
-callback rollback(
|
||||
LimitChange :: lim_change(),
|
||||
Config :: config(),
|
||||
LimitContext :: lim_context()
|
||||
) -> ok | {error, rollback_error()}.
|
||||
|
||||
-type get_limit_error() :: lim_turnover_processor:get_limit_error().
|
||||
-type hold_error() :: lim_turnover_processor:hold_error().
|
||||
-type commit_error() :: lim_turnover_processor:commit_error().
|
||||
-type rollback_error() :: lim_turnover_processor:rollback_error().
|
||||
|
||||
-type config_error() :: {config, notfound}.
|
||||
|
||||
-import(lim_pipeline, [do/1, unwrap/1, unwrap/2]).
|
||||
|
||||
%% Accessors
|
||||
|
||||
-spec created_at(config()) -> timestamp().
|
||||
created_at(#{created_at := CreatedAt}) ->
|
||||
lim_time:to_rfc3339(CreatedAt).
|
||||
|
||||
-spec id(config()) -> lim_id().
|
||||
id(#{id := ID}) ->
|
||||
ID.
|
||||
|
||||
-spec description(config()) -> lim_maybe:maybe(description()).
|
||||
description(#{description := ID}) ->
|
||||
ID;
|
||||
description(_) ->
|
||||
undefined.
|
||||
|
||||
-spec body_type(config()) -> body_type().
|
||||
body_type(#{body_type := BodyType}) ->
|
||||
BodyType.
|
||||
|
||||
-spec started_at(config()) -> timestamp().
|
||||
started_at(#{started_at := Value}) ->
|
||||
Value.
|
||||
|
||||
-spec shard_size(config()) -> shard_size().
|
||||
shard_size(#{shard_size := Value}) ->
|
||||
Value.
|
||||
|
||||
-spec time_range_type(config()) -> time_range_type().
|
||||
time_range_type(#{time_range_type := Value}) ->
|
||||
Value.
|
||||
|
||||
-spec processor_type(config()) -> processor_type().
|
||||
processor_type(#{processor_type := Value}) ->
|
||||
Value.
|
||||
|
||||
-spec type(config()) -> lim_maybe:maybe(limit_type()).
|
||||
type(#{type := Value}) ->
|
||||
Value;
|
||||
type(_) ->
|
||||
undefined.
|
||||
|
||||
-spec scope(config()) -> lim_maybe:maybe(limit_scope()).
|
||||
scope(#{scope := Value}) ->
|
||||
Value;
|
||||
scope(_) ->
|
||||
undefined.
|
||||
|
||||
-spec context_type(config()) -> context_type().
|
||||
context_type(#{context_type := Value}) ->
|
||||
Value.
|
||||
|
||||
%%
|
||||
|
||||
-spec start(lim_id(), create_params(), lim_context()) -> {ok, config()}.
|
||||
start(ID, Params, LimitContext) ->
|
||||
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
|
||||
Config = genlib_map:compact(Params#{id => ID, created_at => lim_time:now()}),
|
||||
case machinery:start(?NS, ID, [{created, Config}], get_backend(WoodyCtx)) of
|
||||
ok ->
|
||||
{ok, Config};
|
||||
{error, exists} ->
|
||||
{ok, Machine} = machinery:get(?NS, ID, get_backend(WoodyCtx)),
|
||||
{ok, collapse(Machine)}
|
||||
end.
|
||||
|
||||
-spec get(lim_id(), lim_context()) -> {ok, config()} | {error, notfound}.
|
||||
get(ID, LimitContext) ->
|
||||
do(fun() ->
|
||||
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
|
||||
Machine = unwrap(machinery:get(?NS, ID, get_backend(WoodyCtx))),
|
||||
collapse(Machine)
|
||||
end).
|
||||
|
||||
-spec get_limit(lim_id(), lim_context()) -> {ok, limit()} | {error, config_error() | {processor(), get_limit_error()}}.
|
||||
get_limit(ID, LimitContext) ->
|
||||
do(fun() ->
|
||||
{Handler, Config} = unwrap(get_handler(ID, LimitContext)),
|
||||
unwrap(Handler, Handler:get_limit(ID, Config, LimitContext))
|
||||
end).
|
||||
|
||||
-spec hold(lim_change(), lim_context()) -> ok | {error, config_error() | {processor(), hold_error()}}.
|
||||
hold(LimitChange = #limiter_LimitChange{id = ID}, LimitContext) ->
|
||||
do(fun() ->
|
||||
{Handler, Config} = unwrap(get_handler(ID, LimitContext)),
|
||||
unwrap(Handler, Handler:hold(LimitChange, Config, LimitContext))
|
||||
end).
|
||||
|
||||
-spec commit(lim_change(), lim_context()) -> ok | {error, config_error() | {processor(), commit_error()}}.
|
||||
commit(LimitChange = #limiter_LimitChange{id = ID}, LimitContext) ->
|
||||
do(fun() ->
|
||||
{Handler, Config} = unwrap(get_handler(ID, LimitContext)),
|
||||
unwrap(Handler, Handler:commit(LimitChange, Config, LimitContext))
|
||||
end).
|
||||
|
||||
-spec rollback(lim_change(), lim_context()) -> ok | {error, config_error() | {processor(), rollback_error()}}.
|
||||
rollback(LimitChange = #limiter_LimitChange{id = ID}, LimitContext) ->
|
||||
do(fun() ->
|
||||
{Handler, Config} = unwrap(get_handler(ID, LimitContext)),
|
||||
unwrap(Handler, Handler:rollback(LimitChange, Config, LimitContext))
|
||||
end).
|
||||
|
||||
get_handler(ID, LimitContext) ->
|
||||
do(fun() ->
|
||||
Config = #{processor_type := ProcessorType} = unwrap(config, get(ID, LimitContext)),
|
||||
{ok, Handler} = lim_router:get_handler(ProcessorType),
|
||||
{Handler, Config}
|
||||
end).
|
||||
|
||||
-spec calculate_time_range(timestamp(), config()) -> time_range().
|
||||
calculate_time_range(Timestamp, Config) ->
|
||||
StartedAt = started_at(Config),
|
||||
{StartDateTime, USec} = lim_range_codec:parse_timestamp(StartedAt),
|
||||
{CurrentDateTime, USec} = lim_range_codec:parse_timestamp(Timestamp),
|
||||
CurrentSec = calendar:datetime_to_gregorian_seconds(CurrentDateTime),
|
||||
case time_range_type(Config) of
|
||||
{calendar, Range} ->
|
||||
calculate_calendar_time_range(Range, CurrentSec, CurrentDateTime, StartDateTime);
|
||||
{interval, _Interval} ->
|
||||
erlang:error({interval_time_range_not_implemented, Config})
|
||||
end.
|
||||
|
||||
calculate_calendar_time_range(year, CurrentSec, {CurrentDate, _CurrentTime}, {StartDate, StartTime}) ->
|
||||
{_StartYear, StartMonth, StartDay} = StartDate,
|
||||
{CurrentYear, _CurrentMonth, _} = CurrentDate,
|
||||
ClampedStartDay = clamp_month_start_day(CurrentYear, StartMonth, StartDay),
|
||||
LowerSec = calendar:datetime_to_gregorian_seconds(
|
||||
{{CurrentYear, StartMonth, ClampedStartDay}, StartTime}
|
||||
),
|
||||
NextYearDay = clamp_month_start_day(CurrentYear + 1, StartMonth, StartDay),
|
||||
UpperSec = calendar:datetime_to_gregorian_seconds(
|
||||
{{CurrentYear + 1, StartMonth, NextYearDay}, StartTime}
|
||||
),
|
||||
calculate_year_time_range(CurrentSec, LowerSec, UpperSec);
|
||||
calculate_calendar_time_range(month, CurrentSec, {CurrentDate, _CurrentTime}, {StartDate, StartTime}) ->
|
||||
{_StartYear, _StartMonth, StartDay} = StartDate,
|
||||
{CurrentYear, CurrentMonth, _} = CurrentDate,
|
||||
ClampedStartDay = clamp_month_start_day(CurrentYear, CurrentMonth, StartDay),
|
||||
LowerSec = calendar:datetime_to_gregorian_seconds(
|
||||
{{CurrentYear, CurrentMonth, ClampedStartDay}, StartTime}
|
||||
),
|
||||
UpperSec =
|
||||
case CurrentMonth < 12 of
|
||||
true ->
|
||||
NextMonthDay = clamp_month_start_day(CurrentYear, CurrentMonth + 1, StartDay),
|
||||
calendar:datetime_to_gregorian_seconds(
|
||||
{{CurrentYear, CurrentMonth + 1, NextMonthDay}, StartTime}
|
||||
);
|
||||
false ->
|
||||
NextYearDay = clamp_month_start_day(CurrentYear + 1, CurrentMonth, StartDay),
|
||||
calendar:datetime_to_gregorian_seconds(
|
||||
{{CurrentYear + 1, 1, NextYearDay}, StartTime}
|
||||
)
|
||||
end,
|
||||
calculate_month_time_range(CurrentSec, LowerSec, UpperSec);
|
||||
calculate_calendar_time_range(week, CurrentSec, {CurrentDate, _CurrentTime}, {StartDate, StartTime}) ->
|
||||
StartWeekRem = calendar:date_to_gregorian_days(StartDate) rem 7,
|
||||
LowerWeek = (calendar:date_to_gregorian_days(CurrentDate) div 7) * 7 + StartWeekRem,
|
||||
UpperWeek = LowerWeek + 7,
|
||||
LowerSec = calendar:datetime_to_gregorian_seconds(
|
||||
{calendar:gregorian_days_to_date(LowerWeek), StartTime}
|
||||
),
|
||||
UpperSec = calendar:datetime_to_gregorian_seconds(
|
||||
{calendar:gregorian_days_to_date(UpperWeek), StartTime}
|
||||
),
|
||||
calculate_week_time_range(CurrentSec, LowerSec, UpperSec);
|
||||
calculate_calendar_time_range(day, CurrentSec, {CurrentDate, _CurrentTime}, {_StartDate, StartTime}) ->
|
||||
Lower = calendar:date_to_gregorian_days(CurrentDate),
|
||||
UpperDate = calendar:gregorian_days_to_date(Lower + 1),
|
||||
LowerSec = calendar:datetime_to_gregorian_seconds({CurrentDate, StartTime}),
|
||||
UpperSec = calendar:datetime_to_gregorian_seconds({UpperDate, StartTime}),
|
||||
calculate_day_time_range(CurrentSec, LowerSec, UpperSec).
|
||||
|
||||
clamp_month_start_day(Year, Month, StartDay) ->
|
||||
Last = calendar:last_day_of_the_month(Year, Month),
|
||||
case StartDay > Last of
|
||||
true ->
|
||||
Last;
|
||||
false ->
|
||||
StartDay
|
||||
end.
|
||||
|
||||
calculate_year_time_range(CurrentSec, LowerSec, UpperSec) when
|
||||
CurrentSec >= LowerSec andalso
|
||||
CurrentSec < UpperSec
|
||||
->
|
||||
mk_time_range(LowerSec, UpperSec);
|
||||
calculate_year_time_range(CurrentSec, LowerSec, _UpperSec) when CurrentSec < LowerSec ->
|
||||
{{Year, Month, Day}, Time} = calendar:gregorian_seconds_to_datetime(LowerSec),
|
||||
PrevYearDay = clamp_month_start_day(Year - 1, Month, Day),
|
||||
LowerDate = {Year - 1, Month, PrevYearDay},
|
||||
#{
|
||||
lower => marshal_timestamp({LowerDate, Time}),
|
||||
upper => marshal_timestamp(calendar:gregorian_seconds_to_datetime(LowerSec))
|
||||
}.
|
||||
|
||||
calculate_month_time_range(CurrentSec, LowerSec, UpperSec) when
|
||||
CurrentSec >= LowerSec andalso
|
||||
CurrentSec < UpperSec
|
||||
->
|
||||
mk_time_range(LowerSec, UpperSec);
|
||||
calculate_month_time_range(CurrentSec, LowerSec, _UpperSec) when CurrentSec < LowerSec ->
|
||||
{{Year, Month, Day}, Time} = calendar:gregorian_seconds_to_datetime(LowerSec),
|
||||
LowerDate =
|
||||
case Month =:= 1 of
|
||||
true ->
|
||||
PrevYearDay = clamp_month_start_day(Year - 1, 12, Day),
|
||||
{Year - 1, 12, PrevYearDay};
|
||||
false ->
|
||||
PrevMonthDay = clamp_month_start_day(Year, Month - 1, Day),
|
||||
{Year, Month - 1, PrevMonthDay}
|
||||
end,
|
||||
#{
|
||||
lower => marshal_timestamp({LowerDate, Time}),
|
||||
upper => marshal_timestamp(calendar:gregorian_seconds_to_datetime(LowerSec))
|
||||
}.
|
||||
|
||||
calculate_week_time_range(CurrentSec, LowerSec, UpperSec) when
|
||||
CurrentSec >= LowerSec andalso
|
||||
CurrentSec < UpperSec
|
||||
->
|
||||
mk_time_range(LowerSec, UpperSec);
|
||||
calculate_week_time_range(CurrentSec, LowerSec, _UpperSec) when CurrentSec < LowerSec ->
|
||||
{Date, Time} = calendar:gregorian_seconds_to_datetime(LowerSec),
|
||||
Days = calendar:date_to_gregorian_days(Date),
|
||||
LowerDate = calendar:gregorian_days_to_date(Days - 7),
|
||||
#{
|
||||
lower => marshal_timestamp({LowerDate, Time}),
|
||||
upper => marshal_timestamp(calendar:gregorian_seconds_to_datetime(LowerSec))
|
||||
}.
|
||||
|
||||
calculate_day_time_range(CurrentSec, LowerSec, UpperSec) when
|
||||
CurrentSec >= LowerSec andalso
|
||||
CurrentSec < UpperSec
|
||||
->
|
||||
mk_time_range(LowerSec, UpperSec);
|
||||
calculate_day_time_range(CurrentSec, LowerSec, _UpperSec) when CurrentSec < LowerSec ->
|
||||
{Date, Time} = calendar:gregorian_seconds_to_datetime(LowerSec),
|
||||
Days = calendar:date_to_gregorian_days(Date),
|
||||
LowerDate = calendar:gregorian_days_to_date(Days - 1),
|
||||
#{
|
||||
lower => marshal_timestamp({LowerDate, Time}),
|
||||
upper => marshal_timestamp(calendar:gregorian_seconds_to_datetime(LowerSec))
|
||||
}.
|
||||
|
||||
mk_time_range(LowerSec, UpperSec) ->
|
||||
#{
|
||||
lower => marshal_timestamp(calendar:gregorian_seconds_to_datetime(LowerSec)),
|
||||
upper => marshal_timestamp(calendar:gregorian_seconds_to_datetime(UpperSec))
|
||||
}.
|
||||
|
||||
marshal_timestamp(DateTime) ->
|
||||
lim_range_codec:marshal(timestamp, {DateTime, 0}).
|
||||
|
||||
-spec calculate_shard_id(timestamp(), config()) -> shard_id().
|
||||
calculate_shard_id(Timestamp, Config) ->
|
||||
StartedAt = started_at(Config),
|
||||
ShardSize = shard_size(Config),
|
||||
{StartDateTime, USec} = lim_range_codec:parse_timestamp(StartedAt),
|
||||
{CurrentDateTime, USec} = lim_range_codec:parse_timestamp(Timestamp),
|
||||
case time_range_type(Config) of
|
||||
{calendar, Range} ->
|
||||
Units = calculate_time_units(Range, CurrentDateTime, StartDateTime),
|
||||
SignPrefix = mk_sign_prefix(Units),
|
||||
RangePrefix = mk_prefix(Range),
|
||||
mk_shard_id(<<SignPrefix/binary, "/", RangePrefix/binary>>, Units, ShardSize);
|
||||
{interval, _Interval} ->
|
||||
erlang:error({interval_time_range_not_implemented, Config})
|
||||
end.
|
||||
|
||||
calculate_time_units(year, {CurrentDate, CurrentTime}, {StartDate, StartTime}) ->
|
||||
{StartYear, _, _} = StartDate,
|
||||
{CurrentYear, _, _} = CurrentDate,
|
||||
|
||||
StartSecBase = calendar:datetime_to_gregorian_seconds({{StartYear, 1, 1}, {0, 0, 0}}),
|
||||
StartSec = calendar:datetime_to_gregorian_seconds({StartDate, StartTime}),
|
||||
CurrentSecBase = calendar:datetime_to_gregorian_seconds({{CurrentYear, 1, 1}, {0, 0, 0}}),
|
||||
CurrentSec = calendar:datetime_to_gregorian_seconds({CurrentDate, CurrentTime}),
|
||||
|
||||
StartDelta = StartSec - StartSecBase,
|
||||
CurrentDelta = CurrentSec - (CurrentSecBase + StartDelta),
|
||||
|
||||
case CurrentDelta >= 0 of
|
||||
true ->
|
||||
CurrentYear - StartYear;
|
||||
false ->
|
||||
CurrentYear - StartYear - 1
|
||||
end;
|
||||
calculate_time_units(month, {CurrentDate, CurrentTime}, {StartDate, StartTime}) ->
|
||||
{StartYear, StartMonth, _} = StartDate,
|
||||
{CurrentYear, CurrentMonth, _} = CurrentDate,
|
||||
|
||||
StartSecBase = calendar:datetime_to_gregorian_seconds({{StartYear, StartMonth, 1}, {0, 0, 0}}),
|
||||
StartSec = calendar:datetime_to_gregorian_seconds({StartDate, StartTime}),
|
||||
CurrentSecBase = calendar:datetime_to_gregorian_seconds({{CurrentYear, CurrentMonth, 1}, {0, 0, 0}}),
|
||||
CurrentSec = calendar:datetime_to_gregorian_seconds({CurrentDate, CurrentTime}),
|
||||
|
||||
StartDelta = StartSec - StartSecBase,
|
||||
CurrentDelta = CurrentSec - (CurrentSecBase + StartDelta),
|
||||
|
||||
YearDiff = CurrentYear - StartYear,
|
||||
MonthDiff = CurrentMonth - StartMonth,
|
||||
|
||||
case CurrentDelta >= 0 of
|
||||
true ->
|
||||
YearDiff * 12 + MonthDiff;
|
||||
false ->
|
||||
YearDiff * 12 + MonthDiff - 1
|
||||
end;
|
||||
calculate_time_units(week, {CurrentDate, CurrentTime}, {StartDate, StartTime}) ->
|
||||
StartWeekRem = calendar:date_to_gregorian_days(StartDate) rem 7,
|
||||
StartWeekBase = (calendar:date_to_gregorian_days(StartDate) div 7) * 7,
|
||||
CurrentWeekBase = (calendar:date_to_gregorian_days(CurrentDate) div 7) * 7,
|
||||
|
||||
StartSecBase = calendar:datetime_to_gregorian_seconds(
|
||||
{calendar:gregorian_days_to_date(StartWeekBase), {0, 0, 0}}
|
||||
),
|
||||
StartSec = calendar:datetime_to_gregorian_seconds(
|
||||
{calendar:gregorian_days_to_date(StartWeekBase + StartWeekRem), StartTime}
|
||||
),
|
||||
CurrentSecBase = calendar:datetime_to_gregorian_seconds(
|
||||
{calendar:gregorian_days_to_date(CurrentWeekBase), {0, 0, 0}}
|
||||
),
|
||||
CurrentSec = calendar:datetime_to_gregorian_seconds(
|
||||
{calendar:gregorian_days_to_date(CurrentWeekBase + StartWeekRem), CurrentTime}
|
||||
),
|
||||
|
||||
StartDelta = StartSec - StartSecBase,
|
||||
CurrentDelta = CurrentSec - (CurrentSecBase + StartDelta),
|
||||
|
||||
StartWeeks = calendar:date_to_gregorian_days(StartDate) div 7,
|
||||
CurrentWeeks = calendar:date_to_gregorian_days(CurrentDate) div 7,
|
||||
case CurrentDelta >= 0 of
|
||||
true ->
|
||||
CurrentWeeks - StartWeeks;
|
||||
false ->
|
||||
CurrentWeeks - StartWeeks - 1
|
||||
end;
|
||||
calculate_time_units(day, {CurrentDate, CurrentTime}, {StartDate, StartTime}) ->
|
||||
StartSecBase = calendar:datetime_to_gregorian_seconds({StartDate, {0, 0, 0}}),
|
||||
StartSec = calendar:datetime_to_gregorian_seconds({StartDate, StartTime}),
|
||||
CurrentSecBase = calendar:datetime_to_gregorian_seconds({CurrentDate, {0, 0, 0}}),
|
||||
CurrentSec = calendar:datetime_to_gregorian_seconds({CurrentDate, CurrentTime}),
|
||||
StartDelta = StartSec - StartSecBase,
|
||||
CurrentDelta = CurrentSec - (CurrentSecBase + StartDelta),
|
||||
StartDays = calendar:date_to_gregorian_days(StartDate),
|
||||
CurrentDays = calendar:date_to_gregorian_days(CurrentDate),
|
||||
case CurrentDelta >= 0 of
|
||||
true ->
|
||||
CurrentDays - StartDays;
|
||||
false ->
|
||||
CurrentDays - StartDays - 1
|
||||
end.
|
||||
|
||||
mk_prefix(day) -> <<"day">>;
|
||||
mk_prefix(week) -> <<"week">>;
|
||||
mk_prefix(month) -> <<"month">>;
|
||||
mk_prefix(year) -> <<"year">>.
|
||||
|
||||
mk_sign_prefix(Units) when Units >= 0 -> <<"future">>;
|
||||
mk_sign_prefix(_) -> <<"past">>.
|
||||
|
||||
mk_shard_id(Prefix, Units0, ShardSize) ->
|
||||
Units1 = abs(Units0),
|
||||
ID = list_to_binary(integer_to_list(Units1 div ShardSize)),
|
||||
<<Prefix/binary, "/", ID/binary>>.
|
||||
|
||||
-spec mk_scope_prefix(config(), lim_context()) -> {ok, prefix()}.
|
||||
mk_scope_prefix(#{scope := global}, _LimitContext) ->
|
||||
{ok, <<>>};
|
||||
mk_scope_prefix(#{scope := {scope, party}}, LimitContext) ->
|
||||
{ok, PartyID} = lim_context:get_from_context(payment_processing, owner_id, invoice, LimitContext),
|
||||
{ok, <<"/", PartyID/binary>>};
|
||||
mk_scope_prefix(#{scope := {scope, shop}}, LimitContext) ->
|
||||
{ok, PartyID} = lim_context:get_from_context(payment_processing, owner_id, invoice, LimitContext),
|
||||
{ok, ShopID} = lim_context:get_from_context(payment_processing, shop_id, invoice, LimitContext),
|
||||
{ok, <<"/", PartyID/binary, "/", ShopID/binary>>}.
|
||||
|
||||
%%% Machinery callbacks
|
||||
|
||||
-spec init(args([event()]), machine(), handler_args(), handler_opts()) -> result().
|
||||
init(Events, _Machine, _HandlerArgs, _HandlerOpts) ->
|
||||
#{
|
||||
events => emit_events(Events)
|
||||
}.
|
||||
|
||||
-spec process_call(args(_), machine(), handler_args(), handler_opts()) -> no_return().
|
||||
process_call(_Args, _Machine, _HandlerArgs, _HandlerOpts) ->
|
||||
not_implemented(call).
|
||||
|
||||
-spec process_timeout(machine(), handler_args(), handler_opts()) -> no_return().
|
||||
process_timeout(_Machine, _HandlerArgs, _HandlerOpts) ->
|
||||
not_implemented(timeout).
|
||||
|
||||
-spec process_repair(args(_), machine(), handler_args(), handler_opts()) -> no_return().
|
||||
process_repair(_Args, _Machine, _HandlerArgs, _HandlerOpts) ->
|
||||
not_implemented(repair).
|
||||
|
||||
%%% Internal functions
|
||||
|
||||
emit_events(Events) ->
|
||||
emit_timestamped_events(Events, lim_time:machinery_now()).
|
||||
|
||||
emit_timestamped_events(Events, Ts) ->
|
||||
[{ev, Ts, Body} || Body <- Events].
|
||||
|
||||
collapse(#{history := History}) ->
|
||||
lists:foldl(fun(Ev, St) -> apply_event(Ev, St) end, undefined, History).
|
||||
|
||||
-spec get_backend(woody_context()) -> machinery_mg_backend:backend().
|
||||
get_backend(WoodyCtx) ->
|
||||
lim_utils:get_backend(?NS, WoodyCtx).
|
||||
|
||||
-spec not_implemented(any()) -> no_return().
|
||||
not_implemented(What) ->
|
||||
erlang:error({not_implemented, What}).
|
||||
|
||||
%%
|
||||
|
||||
%%
|
||||
|
||||
-spec apply_event(machinery:event(timestamped_event(event())), lim_maybe:maybe(config())) -> config().
|
||||
apply_event({_ID, _Ts, {ev, _EvTs, Event}}, Config) ->
|
||||
apply_event_(Event, Config).
|
||||
|
||||
-spec apply_event_(event(), lim_maybe:maybe(config())) -> config().
|
||||
apply_event_({created, Config}, undefined) ->
|
||||
Config.
|
||||
|
||||
%%
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
|
||||
-spec check_sign_prefix_test() -> _.
|
||||
|
||||
check_sign_prefix_test() ->
|
||||
?assertEqual(<<"past">>, mk_sign_prefix(-10)),
|
||||
?assertEqual(<<"future">>, mk_sign_prefix(0)),
|
||||
?assertEqual(<<"future">>, mk_sign_prefix(10)).
|
||||
|
||||
-spec check_calculate_day_time_range_test() -> _.
|
||||
check_calculate_day_time_range_test() ->
|
||||
Config0 = #{
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
time_range_type => {calendar, day}
|
||||
},
|
||||
?assertEqual(
|
||||
#{lower => <<"2000-01-01T00:00:00Z">>, upper => <<"2000-01-02T00:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-01T02:00:00Z">>, Config0)
|
||||
),
|
||||
?assertEqual(
|
||||
#{lower => <<"1999-12-31T00:00:00Z">>, upper => <<"2000-01-01T00:00:00Z">>},
|
||||
calculate_time_range(<<"1999-12-31T02:00:00Z">>, Config0)
|
||||
),
|
||||
?assertEqual(
|
||||
#{lower => <<"2000-01-10T00:00:00Z">>, upper => <<"2000-01-11T00:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-10T02:00:00Z">>, Config0)
|
||||
),
|
||||
Config1 = Config0#{started_at => <<"2000-01-01T03:00:00Z">>},
|
||||
?assertEqual(
|
||||
#{lower => <<"1999-12-31T03:00:00Z">>, upper => <<"2000-01-01T03:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-01T02:00:00Z">>, Config1)
|
||||
).
|
||||
|
||||
-spec check_calculate_week_time_range_test() -> _.
|
||||
check_calculate_week_time_range_test() ->
|
||||
Config0 = #{
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
time_range_type => {calendar, week}
|
||||
},
|
||||
?assertEqual(
|
||||
#{lower => <<"2000-01-01T00:00:00Z">>, upper => <<"2000-01-08T00:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-01T02:00:00Z">>, Config0)
|
||||
),
|
||||
?assertEqual(
|
||||
#{lower => <<"1999-12-25T00:00:00Z">>, upper => <<"2000-01-01T00:00:00Z">>},
|
||||
calculate_time_range(<<"1999-12-31T02:00:00Z">>, Config0)
|
||||
),
|
||||
?assertEqual(
|
||||
#{lower => <<"2000-09-30T00:00:00Z">>, upper => <<"2000-10-07T00:00:00Z">>},
|
||||
calculate_time_range(<<"2000-10-03T02:00:00Z">>, Config0)
|
||||
),
|
||||
Config1 = Config0#{started_at => <<"2000-01-01T03:00:00Z">>},
|
||||
?assertEqual(
|
||||
#{lower => <<"1999-12-25T03:00:00Z">>, upper => <<"2000-01-01T03:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-01T02:00:00Z">>, Config1)
|
||||
).
|
||||
|
||||
-spec check_calculate_month_time_range_test() -> _.
|
||||
check_calculate_month_time_range_test() ->
|
||||
Config0 = #{
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
time_range_type => {calendar, month}
|
||||
},
|
||||
?assertEqual(
|
||||
#{lower => <<"2000-01-01T00:00:00Z">>, upper => <<"2000-02-01T00:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-01T02:00:00Z">>, Config0)
|
||||
),
|
||||
?assertEqual(
|
||||
#{lower => <<"1999-12-01T00:00:00Z">>, upper => <<"2000-01-01T00:00:00Z">>},
|
||||
calculate_time_range(<<"1999-12-31T02:00:00Z">>, Config0)
|
||||
),
|
||||
?assertEqual(
|
||||
#{lower => <<"2000-10-01T00:00:00Z">>, upper => <<"2000-11-01T00:00:00Z">>},
|
||||
calculate_time_range(<<"2000-10-03T02:00:00Z">>, Config0)
|
||||
),
|
||||
Config1 = Config0#{started_at => <<"2000-01-01T03:00:00Z">>},
|
||||
?assertEqual(
|
||||
#{lower => <<"1999-12-01T03:00:00Z">>, upper => <<"2000-01-01T03:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-01T02:00:00Z">>, Config1)
|
||||
).
|
||||
|
||||
-spec check_calculate_year_time_range_test() -> _.
|
||||
check_calculate_year_time_range_test() ->
|
||||
Config0 = #{
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
time_range_type => {calendar, year}
|
||||
},
|
||||
?assertEqual(
|
||||
#{lower => <<"2000-01-01T00:00:00Z">>, upper => <<"2001-01-01T00:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-01T02:00:00Z">>, Config0)
|
||||
),
|
||||
?assertEqual(
|
||||
#{lower => <<"1999-01-01T00:00:00Z">>, upper => <<"2000-01-01T00:00:00Z">>},
|
||||
calculate_time_range(<<"1999-12-31T02:00:00Z">>, Config0)
|
||||
),
|
||||
?assertEqual(
|
||||
#{lower => <<"2010-01-01T00:00:00Z">>, upper => <<"2011-01-01T00:00:00Z">>},
|
||||
calculate_time_range(<<"2010-10-03T02:00:00Z">>, Config0)
|
||||
),
|
||||
Config1 = Config0#{started_at => <<"2000-01-01T03:00:00Z">>},
|
||||
?assertEqual(
|
||||
#{lower => <<"1999-01-01T03:00:00Z">>, upper => <<"2000-01-01T03:00:00Z">>},
|
||||
calculate_time_range(<<"2000-01-01T02:00:00Z">>, Config1)
|
||||
).
|
||||
|
||||
-spec check_calculate_day_shard_id_test() -> _.
|
||||
check_calculate_day_shard_id_test() ->
|
||||
Config0 = #{
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
shard_size => 1,
|
||||
time_range_type => {calendar, day}
|
||||
},
|
||||
?assertEqual(<<"future/day/0">>, calculate_shard_id(<<"2000-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/day/2">>, calculate_shard_id(<<"2000-01-03T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"past/day/1">>, calculate_shard_id(<<"1999-12-31T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/day/1">>, calculate_shard_id(<<"2000-01-02T23:59:59Z">>, Config0)),
|
||||
?assertEqual(<<"future/day/1">>, calculate_shard_id(<<"2000-01-04T00:00:00Z">>, Config0#{shard_size => 2})),
|
||||
?assertEqual(<<"future/day/366">>, calculate_shard_id(<<"2001-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/day/12">>, calculate_shard_id(<<"2001-01-01T00:00:00Z">>, Config0#{shard_size => 30})),
|
||||
Config1 = Config0#{started_at => <<"2000-01-01T03:00:00Z">>},
|
||||
?assertEqual(<<"past/day/1">>, calculate_shard_id(<<"2000-01-01T00:00:00Z">>, Config1)),
|
||||
?assertEqual(<<"future/day/1">>, calculate_shard_id(<<"2000-01-03T00:00:00Z">>, Config1)).
|
||||
|
||||
-spec check_calculate_week_shard_id_test() -> _.
|
||||
check_calculate_week_shard_id_test() ->
|
||||
Config0 = #{
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
shard_size => 1,
|
||||
time_range_type => {calendar, week}
|
||||
},
|
||||
?assertEqual(<<"future/week/0">>, calculate_shard_id(<<"2000-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"past/week/1">>, calculate_shard_id(<<"1999-12-31T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/week/1">>, calculate_shard_id(<<"2000-01-08T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/week/1">>, calculate_shard_id(<<"2000-01-15T00:00:00Z">>, Config0#{shard_size => 2})),
|
||||
?assertEqual(<<"future/week/52">>, calculate_shard_id(<<"2001-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/week/13">>, calculate_shard_id(<<"2001-01-01T00:00:00Z">>, Config0#{shard_size => 4})),
|
||||
Config1 = Config0#{started_at => <<"2000-01-02T03:00:00Z">>},
|
||||
?assertEqual(<<"past/week/1">>, calculate_shard_id(<<"2000-01-02T00:00:00Z">>, Config1)),
|
||||
?assertEqual(<<"future/week/0">>, calculate_shard_id(<<"2000-01-09T00:00:00Z">>, Config1)).
|
||||
|
||||
-spec check_calculate_month_shard_id_test() -> _.
|
||||
check_calculate_month_shard_id_test() ->
|
||||
Config0 = #{
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
shard_size => 1,
|
||||
time_range_type => {calendar, month}
|
||||
},
|
||||
?assertEqual(<<"future/month/0">>, calculate_shard_id(<<"2000-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"past/month/1">>, calculate_shard_id(<<"1999-12-31T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/month/1">>, calculate_shard_id(<<"2000-02-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/month/1">>, calculate_shard_id(<<"2000-03-01T00:00:00Z">>, Config0#{shard_size => 2})),
|
||||
?assertEqual(<<"future/month/12">>, calculate_shard_id(<<"2001-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/month/1">>, calculate_shard_id(<<"2001-01-01T00:00:00Z">>, Config0#{shard_size => 12})),
|
||||
Config1 = Config0#{started_at => <<"2000-01-02T03:00:00Z">>},
|
||||
?assertEqual(<<"past/month/1">>, calculate_shard_id(<<"2000-01-02T00:00:00Z">>, Config1)),
|
||||
?assertEqual(<<"future/month/0">>, calculate_shard_id(<<"2000-02-02T00:00:00Z">>, Config1)).
|
||||
|
||||
-spec check_calculate_year_shard_id_test() -> _.
|
||||
check_calculate_year_shard_id_test() ->
|
||||
Config0 = #{
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
shard_size => 1,
|
||||
time_range_type => {calendar, year}
|
||||
},
|
||||
?assertEqual(<<"future/year/0">>, calculate_shard_id(<<"2000-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"past/year/1">>, calculate_shard_id(<<"1999-12-31T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/year/1">>, calculate_shard_id(<<"2001-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/year/1">>, calculate_shard_id(<<"2003-01-01T00:00:00Z">>, Config0#{shard_size => 2})),
|
||||
?assertEqual(<<"future/year/10">>, calculate_shard_id(<<"2010-01-01T00:00:00Z">>, Config0)),
|
||||
?assertEqual(<<"future/year/2">>, calculate_shard_id(<<"2020-01-01T00:00:00Z">>, Config0#{shard_size => 10})),
|
||||
Config1 = Config0#{started_at => <<"2000-01-02T03:00:00Z">>},
|
||||
?assertEqual(<<"past/year/1">>, calculate_shard_id(<<"2000-01-01T00:00:00Z">>, Config1)),
|
||||
?assertEqual(<<"future/year/0">>, calculate_shard_id(<<"2001-01-01T00:00:00Z">>, Config1)).
|
||||
|
||||
-endif.
|
112
apps/limiter/src/lim_config_machinery_schema.erl
Normal file
112
apps/limiter/src/lim_config_machinery_schema.erl
Normal file
@ -0,0 +1,112 @@
|
||||
-module(lim_config_machinery_schema).
|
||||
|
||||
%% Storage schema behaviour
|
||||
-behaviour(machinery_mg_schema).
|
||||
|
||||
-export([get_version/1]).
|
||||
-export([marshal/3]).
|
||||
-export([unmarshal/3]).
|
||||
|
||||
%% Constants
|
||||
|
||||
-define(CURRENT_EVENT_FORMAT_VERSION, 1).
|
||||
|
||||
%% Internal types
|
||||
|
||||
-type type() :: machinery_mg_schema:t().
|
||||
-type value(T) :: machinery_mg_schema:v(T).
|
||||
-type value_type() :: machinery_mg_schema:vt().
|
||||
-type context() :: machinery_mg_schema:context().
|
||||
|
||||
-type event() :: lim_config_machine:timestamped_event(lim_config_machine:event()).
|
||||
-type aux_state() :: term().
|
||||
-type call_args() :: term().
|
||||
-type call_response() :: term().
|
||||
|
||||
-type data() ::
|
||||
aux_state()
|
||||
| event()
|
||||
| call_args()
|
||||
| call_response().
|
||||
|
||||
%% machinery_mg_schema callbacks
|
||||
|
||||
-spec get_version(value_type()) -> machinery_mg_schema:version().
|
||||
get_version(event) ->
|
||||
?CURRENT_EVENT_FORMAT_VERSION;
|
||||
get_version(aux_state) ->
|
||||
undefined.
|
||||
|
||||
-spec marshal(type(), value(data()), context()) -> {machinery_msgpack:t(), context()}.
|
||||
marshal({event, FormatVersion}, TimestampedChange, Context) ->
|
||||
marshal_event(FormatVersion, TimestampedChange, Context);
|
||||
marshal(T, V, C) when
|
||||
T =:= {args, init} orelse
|
||||
T =:= {args, call} orelse
|
||||
T =:= {args, repair} orelse
|
||||
T =:= {aux_state, undefined} orelse
|
||||
T =:= {response, call} orelse
|
||||
T =:= {response, {repair, success}} orelse
|
||||
T =:= {response, {repair, failure}}
|
||||
->
|
||||
machinery_mg_schema_generic:marshal(T, V, C).
|
||||
|
||||
-spec unmarshal(type(), machinery_msgpack:t(), context()) -> {data(), context()}.
|
||||
unmarshal({event, FormatVersion}, EncodedChange, Context) ->
|
||||
unmarshal_event(FormatVersion, EncodedChange, Context);
|
||||
unmarshal(T, V, C) when
|
||||
T =:= {args, init} orelse
|
||||
T =:= {args, call} orelse
|
||||
T =:= {args, repair} orelse
|
||||
T =:= {aux_state, undefined} orelse
|
||||
T =:= {response, call} orelse
|
||||
T =:= {response, {repair, success}} orelse
|
||||
T =:= {response, {repair, failure}}
|
||||
->
|
||||
machinery_mg_schema_generic:unmarshal(T, V, C).
|
||||
|
||||
%% Internals
|
||||
|
||||
-spec marshal_event(machinery_mg_schema:version(), event(), context()) -> {machinery_msgpack:t(), context()}.
|
||||
marshal_event(1, TimestampedChange, Context) ->
|
||||
ThriftChange = lim_config_codec:marshal(timestamped_change, TimestampedChange),
|
||||
Type = {struct, struct, {lim_limiter_config_thrift, 'TimestampedChange'}},
|
||||
{{bin, lim_proto_utils:serialize(Type, ThriftChange)}, Context}.
|
||||
|
||||
-spec unmarshal_event(machinery_mg_schema:version(), machinery_msgpack:t(), context()) -> {event(), context()}.
|
||||
unmarshal_event(1, EncodedChange, Context) ->
|
||||
{bin, EncodedThriftChange} = EncodedChange,
|
||||
Type = {struct, struct, {lim_limiter_config_thrift, 'TimestampedChange'}},
|
||||
ThriftChange = lim_proto_utils:deserialize(Type, EncodedThriftChange),
|
||||
{lim_config_codec:unmarshal(timestamped_change, ThriftChange), Context}.
|
||||
|
||||
%%
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
|
||||
-spec marshal_unmarshal_created_test() -> _.
|
||||
|
||||
marshal_unmarshal_created_test() ->
|
||||
Created =
|
||||
{created, #{
|
||||
id => <<"id">>,
|
||||
processor_type => <<"type">>,
|
||||
created_at => lim_time:now(),
|
||||
body_type => {cash, <<"RUB">>},
|
||||
started_at => <<"2000-01-01T00:00:00Z">>,
|
||||
shard_size => 7,
|
||||
time_range_type => {calendar, day},
|
||||
context_type => payment_processing,
|
||||
type => turnover,
|
||||
scope => {scope, party},
|
||||
description => <<"description">>
|
||||
}},
|
||||
Event = {ev, lim_time:machinery_now(), Created},
|
||||
{Marshaled, _} = marshal_event(1, Event, {}),
|
||||
{Unmarshaled, _} = unmarshal_event(1, Marshaled, {}),
|
||||
?assertEqual(Event, Unmarshaled).
|
||||
|
||||
-endif.
|
93
apps/limiter/src/lim_configurator.erl
Normal file
93
apps/limiter/src/lim_configurator.erl
Normal file
@ -0,0 +1,93 @@
|
||||
-module(lim_configurator).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_configurator_thrift.hrl").
|
||||
|
||||
%% Woody handler
|
||||
|
||||
-behaviour(woody_server_thrift_handler).
|
||||
|
||||
-export([handle_function/4]).
|
||||
|
||||
%%
|
||||
|
||||
-type lim_context() :: lim_context:t().
|
||||
|
||||
%%
|
||||
|
||||
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), woody:options()) -> {ok, woody:result()}.
|
||||
handle_function(Fn, Args, WoodyCtx, Opts) ->
|
||||
{ok, LimitContext} = lim_context:create(WoodyCtx),
|
||||
scoper:scope(
|
||||
configurator,
|
||||
fun() -> handle_function_(Fn, Args, LimitContext, Opts) end
|
||||
).
|
||||
|
||||
-spec handle_function_(woody:func(), woody:args(), lim_context(), woody:options()) -> {ok, woody:result()}.
|
||||
handle_function_(
|
||||
'Create',
|
||||
{#limiter_cfg_LimitCreateParams{
|
||||
id = ID,
|
||||
name = Name,
|
||||
description = Description,
|
||||
started_at = StartedAt,
|
||||
body_type = BodyType
|
||||
}},
|
||||
LimitContext,
|
||||
_Opts
|
||||
) ->
|
||||
case mk_limit_config(Name) of
|
||||
{ok, Config} ->
|
||||
{ok, LimitConfig} = lim_config_machine:start(
|
||||
ID,
|
||||
Config#{
|
||||
description => Description,
|
||||
started_at => StartedAt,
|
||||
body_type => lim_config_codec:unmarshal_body_type(BodyType)
|
||||
},
|
||||
LimitContext
|
||||
),
|
||||
{ok, lim_config_codec:marshal_config(LimitConfig)};
|
||||
{error, {name, notfound}} ->
|
||||
woody_error:raise(
|
||||
business,
|
||||
#limiter_cfg_LimitConfigNameNotFound{}
|
||||
)
|
||||
end;
|
||||
handle_function_('Get', {LimitID}, LimitContext, _Opts) ->
|
||||
scoper:add_meta(#{limit_config_id => LimitID}),
|
||||
case lim_config_machine:get(LimitID, LimitContext) of
|
||||
{ok, LimitConfig} ->
|
||||
{ok, lim_config_codec:marshal_config(LimitConfig)};
|
||||
{error, notfound} ->
|
||||
woody_error:raise(business, #limiter_cfg_LimitConfigNotFound{})
|
||||
end.
|
||||
|
||||
mk_limit_config(<<"ShopMonthTurnover">>) ->
|
||||
{ok, #{
|
||||
processor_type => <<"TurnoverProcessor">>,
|
||||
type => turnover,
|
||||
scope => {scope, shop},
|
||||
shard_size => 12,
|
||||
context_type => payment_processing,
|
||||
time_range_type => {calendar, month}
|
||||
}};
|
||||
mk_limit_config(<<"PartyMonthTurnover">>) ->
|
||||
{ok, #{
|
||||
processor_type => <<"TurnoverProcessor">>,
|
||||
type => turnover,
|
||||
scope => {scope, party},
|
||||
shard_size => 12,
|
||||
context_type => payment_processing,
|
||||
time_range_type => {calendar, month}
|
||||
}};
|
||||
mk_limit_config(<<"GlobalMonthTurnover">>) ->
|
||||
{ok, #{
|
||||
processor_type => <<"TurnoverProcessor">>,
|
||||
type => turnover,
|
||||
scope => global,
|
||||
shard_size => 12,
|
||||
context_type => payment_processing,
|
||||
time_range_type => {calendar, month}
|
||||
}};
|
||||
mk_limit_config(_) ->
|
||||
{error, {name, notfound}}.
|
305
apps/limiter/src/lim_context.erl
Normal file
305
apps/limiter/src/lim_context.erl
Normal file
@ -0,0 +1,305 @@
|
||||
-module(lim_context).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_limiter_context_thrift.hrl").
|
||||
|
||||
-export([create/1]).
|
||||
-export([woody_context/1]).
|
||||
-export([get_operation/2]).
|
||||
-export([get_from_context/3]).
|
||||
-export([get_from_context/4]).
|
||||
|
||||
-export([set_context/2]).
|
||||
-export([set_clock/2]).
|
||||
|
||||
-export([clock/1]).
|
||||
|
||||
-type woody_context() :: woody_context:ctx().
|
||||
-type timestamp() :: binary().
|
||||
-type thrift_context() :: lim_limiter_thrift:'LimitContext'().
|
||||
-type clock() :: lim_limiter_thrift:'Clock'().
|
||||
-type id() :: binary().
|
||||
-type cash() :: lim_body:cash().
|
||||
|
||||
-type t() :: #{
|
||||
woody_context := woody_context(),
|
||||
context => context(),
|
||||
clock => clock()
|
||||
}.
|
||||
|
||||
-type context_type() :: payment_processing.
|
||||
-type context_operation() :: payment_processing_operation().
|
||||
|
||||
-type context() :: #{
|
||||
payment_processing => payment_processing_context()
|
||||
}.
|
||||
|
||||
-type payment_processing_context() :: #{
|
||||
op := payment_processing_operation(),
|
||||
invoice => payment_processing_invoice()
|
||||
}.
|
||||
|
||||
-type payment_processing_operation() ::
|
||||
invoice
|
||||
| invoice_adjustment
|
||||
| invoice_payment
|
||||
| invoice_payment_adjustment
|
||||
| invoice_payment_refund
|
||||
| invoice_payment_chargeback.
|
||||
|
||||
-type payment_processing_invoice() :: #{
|
||||
id => id(),
|
||||
owner_id => id(),
|
||||
shop_id => id(),
|
||||
cost => cash(),
|
||||
created_at => timestamp(),
|
||||
effective_adjustment => payment_processing_adjustment(),
|
||||
effective_payment => payment_processing_payment()
|
||||
}.
|
||||
|
||||
-type payment_processing_adjustment() :: #{
|
||||
id => id()
|
||||
}.
|
||||
|
||||
-type payment_processing_payment() :: #{
|
||||
id => id(),
|
||||
owner_id => id(),
|
||||
shop_id => id(),
|
||||
cost => cash(),
|
||||
capture_cost => cash(),
|
||||
created_at => timestamp(),
|
||||
flow => instant | hold,
|
||||
payer => payment_resource | customer | recurrent,
|
||||
effective_adjustment => payment_processing_payment_adjustment(),
|
||||
effective_refund => payment_processing_payment_refund(),
|
||||
effective_chargeback => payment_processing_payment_chargeback()
|
||||
}.
|
||||
|
||||
-type payment_processing_payment_adjustment() :: #{
|
||||
id => id(),
|
||||
created_at => timestamp()
|
||||
}.
|
||||
|
||||
-type payment_processing_payment_refund() :: #{
|
||||
id => id(),
|
||||
cost => cash(),
|
||||
created_at => timestamp()
|
||||
}.
|
||||
|
||||
-type payment_processing_payment_chargeback() :: #{
|
||||
id => id(),
|
||||
levy => cash(),
|
||||
body => cash(),
|
||||
created_at => timestamp()
|
||||
}.
|
||||
|
||||
-export_type([t/0]).
|
||||
-export_type([context_type/0]).
|
||||
-export_type([context_operation/0]).
|
||||
|
||||
-spec create(woody_context()) -> {ok, t()}.
|
||||
create(WoodyContext) ->
|
||||
{ok, #{woody_context => WoodyContext}}.
|
||||
|
||||
-spec woody_context(t()) -> {ok, woody_context()}.
|
||||
woody_context(Context) ->
|
||||
{ok, maps:get(woody_context, Context)}.
|
||||
|
||||
-spec clock(t()) -> {ok, clock()} | {error, notfound}.
|
||||
clock(#{clock := Clock}) ->
|
||||
{ok, Clock};
|
||||
clock(_) ->
|
||||
{error, notfound}.
|
||||
|
||||
-spec set_context(thrift_context(), t()) -> t().
|
||||
set_context(Context, LimContext) ->
|
||||
LimContext#{context => unmarshal_context(Context)}.
|
||||
|
||||
-spec set_clock(clock(), t()) -> t().
|
||||
set_clock(Clock, LimContext) ->
|
||||
LimContext#{clock => Clock}.
|
||||
|
||||
-spec get_operation(context_type(), t()) -> {ok, atom()} | {error, notfound}.
|
||||
get_operation(Type, #{context := Context}) ->
|
||||
case maps:get(Type, Context, undefined) of
|
||||
undefined ->
|
||||
{error, notfound};
|
||||
#{op := Operation} ->
|
||||
{ok, Operation}
|
||||
end.
|
||||
|
||||
-spec get_from_context(context_type(), atom(), t()) -> {ok, term()} | {error, notfound}.
|
||||
get_from_context(payment_processing, ValueName, LimContext = #{context := Context}) ->
|
||||
case maps:get(payment_processing, Context, undefined) of
|
||||
undefined ->
|
||||
{error, notfound};
|
||||
#{op := Operation} ->
|
||||
get_from_context(payment_processing, ValueName, Operation, LimContext)
|
||||
end;
|
||||
get_from_context(_, _ValueName, _LimContext) ->
|
||||
{error, notfound}.
|
||||
|
||||
-spec get_from_context(context_type(), atom(), context_operation(), t()) -> {ok, term()} | {error, notfound}.
|
||||
get_from_context(payment_processing, ValueName, Op, #{context := #{payment_processing := Context}}) ->
|
||||
case get_payment_processing_operation_context(Op, Context) of
|
||||
{ok, OperationContext} ->
|
||||
case maps:get(ValueName, OperationContext, undefined) of
|
||||
undefined ->
|
||||
{error, notfound};
|
||||
Value ->
|
||||
{ok, Value}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
get_from_context(_, _ValueName, _Op, _LimContext) ->
|
||||
{error, notfound}.
|
||||
|
||||
get_payment_processing_operation_context(invoice, #{invoice := Invoice}) ->
|
||||
{ok, Invoice};
|
||||
get_payment_processing_operation_context(invoice_adjustment, #{invoice := #{effective_adjustment := Adjustment}}) ->
|
||||
{ok, Adjustment};
|
||||
get_payment_processing_operation_context(invoice_payment, #{invoice := #{effective_payment := Payment}}) ->
|
||||
{ok, Payment};
|
||||
get_payment_processing_operation_context(
|
||||
invoice_payment_adjustment,
|
||||
#{invoice := #{effective_payment := #{effective_adjustment := Adjustment}}}
|
||||
) ->
|
||||
{ok, Adjustment};
|
||||
get_payment_processing_operation_context(
|
||||
invoice_payment_refund,
|
||||
#{invoice := #{effective_payment := #{effective_refund := Refund}}}
|
||||
) ->
|
||||
{ok, Refund};
|
||||
get_payment_processing_operation_context(
|
||||
invoice_payment_chargeback,
|
||||
#{invoice := #{effective_payment := #{effective_chargeback := Chargeback}}}
|
||||
) ->
|
||||
{ok, Chargeback};
|
||||
get_payment_processing_operation_context(_, _) ->
|
||||
{error, notfound}.
|
||||
|
||||
%%
|
||||
|
||||
unmarshal_context(#limiter_context_LimitContext{payment_processing = PaymentProcessing}) ->
|
||||
#{payment_processing => unmarshal_payment_processing_context(PaymentProcessing)};
|
||||
unmarshal_context(_) ->
|
||||
#{}.
|
||||
|
||||
unmarshal_payment_processing_context(#limiter_context_ContextPaymentProcessing{
|
||||
op = {Operation, _},
|
||||
invoice = Invoice
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
op => Operation,
|
||||
invoice => maybe_unmarshal(Invoice, fun unmarshal_payment_processing_invoice/1)
|
||||
}).
|
||||
|
||||
unmarshal_payment_processing_invoice(#limiter_context_Invoice{
|
||||
id = ID,
|
||||
owner_id = OwnerID,
|
||||
shop_id = ShopID,
|
||||
cost = Cost,
|
||||
created_at = CreatedAt,
|
||||
effective_payment = EffectivePayment,
|
||||
effective_adjustment = EffectiveAdjustment
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
id => maybe_unmarshal(ID, fun unmarshal_string/1),
|
||||
owner_id => maybe_unmarshal(OwnerID, fun unmarshal_string/1),
|
||||
shop_id => maybe_unmarshal(ShopID, fun unmarshal_string/1),
|
||||
cost => maybe_unmarshal(Cost, fun unmarshal_cash/1),
|
||||
created_at => maybe_unmarshal(CreatedAt, fun unmarshal_string/1),
|
||||
effective_adjustment => maybe_unmarshal(
|
||||
EffectiveAdjustment,
|
||||
fun unmarshal_payment_processing_invoice_adjustment/1
|
||||
),
|
||||
effective_payment => maybe_unmarshal(EffectivePayment, fun unmarshal_payment_processing_invoice_payment/1)
|
||||
}).
|
||||
|
||||
unmarshal_payment_processing_invoice_adjustment(#limiter_context_InvoiceAdjustment{id = ID}) ->
|
||||
genlib_map:compact(#{
|
||||
id => maybe_unmarshal(ID, fun unmarshal_string/1)
|
||||
}).
|
||||
|
||||
unmarshal_payment_processing_invoice_payment(#limiter_context_InvoicePayment{
|
||||
id = ID,
|
||||
owner_id = OwnerID,
|
||||
shop_id = ShopID,
|
||||
cost = Cost,
|
||||
capture_cost = CaptureCost,
|
||||
created_at = CreatedAt,
|
||||
flow = Flow,
|
||||
payer = Payer,
|
||||
effective_adjustment = EffectiveAdjustment,
|
||||
effective_refund = EffectiveRefund,
|
||||
effective_chargeback = EffectiveChargeback
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
id => maybe_unmarshal(ID, fun unmarshal_string/1),
|
||||
owner_id => maybe_unmarshal(OwnerID, fun unmarshal_string/1),
|
||||
shop_id => maybe_unmarshal(ShopID, fun unmarshal_string/1),
|
||||
cost => maybe_unmarshal(Cost, fun unmarshal_cash/1),
|
||||
capture_cost => maybe_unmarshal(CaptureCost, fun unmarshal_cash/1),
|
||||
created_at => maybe_unmarshal(CreatedAt, fun unmarshal_string/1),
|
||||
flow => maybe_unmarshal(Flow, fun unmarshal_payment_processing_invoice_payment_flow/1),
|
||||
payer => maybe_unmarshal(Payer, fun unmarshal_payment_processing_invoice_payment_payer/1),
|
||||
effective_adjustment => maybe_unmarshal(
|
||||
EffectiveAdjustment,
|
||||
fun unmarshal_payment_processing_invoice_payment_adjustment/1
|
||||
),
|
||||
effective_refund => maybe_unmarshal(EffectiveRefund, fun unmarshal_payment_processing_invoice_payment_refund/1),
|
||||
effective_chargeback => maybe_unmarshal(
|
||||
EffectiveChargeback,
|
||||
fun unmarshal_payment_processing_invoice_payment_chargeback/1
|
||||
)
|
||||
}).
|
||||
|
||||
unmarshal_payment_processing_invoice_payment_flow({Flow, _}) ->
|
||||
Flow.
|
||||
|
||||
unmarshal_payment_processing_invoice_payment_payer({Payer, _}) ->
|
||||
Payer.
|
||||
|
||||
unmarshal_payment_processing_invoice_payment_adjustment(#limiter_context_InvoicePaymentAdjustment{
|
||||
id = ID,
|
||||
created_at = CreatedAt
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
id => maybe_unmarshal(ID, fun unmarshal_string/1),
|
||||
created_at => maybe_unmarshal(CreatedAt, fun unmarshal_string/1)
|
||||
}).
|
||||
|
||||
unmarshal_payment_processing_invoice_payment_refund(#limiter_context_InvoicePaymentRefund{
|
||||
id = ID,
|
||||
cost = Cost,
|
||||
created_at = CreatedAt
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
id => maybe_unmarshal(ID, fun unmarshal_string/1),
|
||||
cost => maybe_unmarshal(Cost, fun unmarshal_cash/1),
|
||||
created_at => maybe_unmarshal(CreatedAt, fun unmarshal_string/1)
|
||||
}).
|
||||
|
||||
unmarshal_payment_processing_invoice_payment_chargeback(#limiter_context_InvoicePaymentChargeback{
|
||||
id = ID,
|
||||
levy = Levy,
|
||||
body = Body,
|
||||
created_at = CreatedAt
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
id => maybe_unmarshal(ID, fun unmarshal_string/1),
|
||||
levy => maybe_unmarshal(Levy, fun unmarshal_cash/1),
|
||||
body => maybe_unmarshal(Body, fun unmarshal_cash/1),
|
||||
created_at => maybe_unmarshal(CreatedAt, fun unmarshal_string/1)
|
||||
}).
|
||||
|
||||
unmarshal_cash(#limiter_base_Cash{amount = Amount, currency = #limiter_base_CurrencyRef{symbolic_code = Currency}}) ->
|
||||
lim_body:create_body_from_cash(Amount, Currency).
|
||||
|
||||
unmarshal_string(Value) ->
|
||||
Value.
|
||||
|
||||
maybe_unmarshal(undefined, _) ->
|
||||
undefined;
|
||||
maybe_unmarshal(Value, UnmarshalFun) ->
|
||||
UnmarshalFun(Value).
|
153
apps/limiter/src/lim_handler.erl
Normal file
153
apps/limiter/src/lim_handler.erl
Normal file
@ -0,0 +1,153 @@
|
||||
-module(lim_handler).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_limiter_thrift.hrl").
|
||||
|
||||
%% Woody handler
|
||||
|
||||
-behaviour(woody_server_thrift_handler).
|
||||
|
||||
-export([handle_function/4]).
|
||||
|
||||
%%
|
||||
|
||||
-type lim_context() :: lim_context:t().
|
||||
|
||||
-define(LIMIT_CHANGE(ID), #limiter_LimitChange{id = ID}).
|
||||
|
||||
-define(CASH(
|
||||
Amount,
|
||||
Currency
|
||||
),
|
||||
#limiter_base_Cash{amount = Amount, currency = #limiter_base_CurrencyRef{symbolic_code = Currency}}
|
||||
).
|
||||
|
||||
%%
|
||||
|
||||
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), woody:options()) -> {ok, woody:result()}.
|
||||
handle_function(Fn, Args, WoodyCtx, Opts) ->
|
||||
{ok, LimitContext} = lim_context:create(WoodyCtx),
|
||||
scoper:scope(
|
||||
limiter,
|
||||
fun() -> handle_function_(Fn, Args, LimitContext, Opts) end
|
||||
).
|
||||
|
||||
-spec handle_function_(woody:func(), woody:args(), lim_context(), woody:options()) -> {ok, woody:result()}.
|
||||
handle_function_('Get', {LimitID, Clock, Context}, LimitContext, _Opts) ->
|
||||
scoper:add_meta(#{limit_id => LimitID}),
|
||||
case
|
||||
lim_config_machine:get_limit(
|
||||
LimitID,
|
||||
lim_context:set_context(Context, lim_context:set_clock(Clock, LimitContext))
|
||||
)
|
||||
of
|
||||
{ok, Limit} ->
|
||||
{ok, Limit};
|
||||
{error, Error} ->
|
||||
handle_get_error(Error)
|
||||
end;
|
||||
handle_function_('Hold', {LimitChange = ?LIMIT_CHANGE(LimitID), Clock, Context}, LimitContext, _Opts) ->
|
||||
scoper:add_meta(#{limit_id => LimitID}),
|
||||
case
|
||||
lim_config_machine:hold(
|
||||
LimitChange,
|
||||
lim_context:set_context(Context, lim_context:set_clock(Clock, LimitContext))
|
||||
)
|
||||
of
|
||||
ok ->
|
||||
{ok, {vector, #limiter_VectorClock{state = <<>>}}};
|
||||
{error, Error} ->
|
||||
handle_hold_error(Error)
|
||||
end;
|
||||
handle_function_('Commit', {LimitChange = ?LIMIT_CHANGE(LimitID), Clock, Context}, LimitContext, _Opts) ->
|
||||
scoper:add_meta(#{limit_id => LimitID}),
|
||||
case
|
||||
lim_config_machine:commit(
|
||||
LimitChange,
|
||||
lim_context:set_context(Context, lim_context:set_clock(Clock, LimitContext))
|
||||
)
|
||||
of
|
||||
ok ->
|
||||
{ok, {vector, #limiter_VectorClock{state = <<>>}}};
|
||||
{error, Error} ->
|
||||
handle_commit_error(Error)
|
||||
end;
|
||||
handle_function_('Rollback', {LimitChange = ?LIMIT_CHANGE(LimitID), Clock, Context}, LimitContext, _Opts) ->
|
||||
scoper:add_meta(#{limit_id => LimitID}),
|
||||
case
|
||||
lim_config_machine:rollback(
|
||||
LimitChange,
|
||||
lim_context:set_context(Context, lim_context:set_clock(Clock, LimitContext))
|
||||
)
|
||||
of
|
||||
ok ->
|
||||
{ok, {vector, #limiter_VectorClock{state = <<>>}}};
|
||||
{error, Error} ->
|
||||
handle_rollback_error(Error)
|
||||
end.
|
||||
|
||||
-spec handle_get_error(_) -> no_return().
|
||||
handle_get_error({_, {limit, notfound}}) ->
|
||||
woody_error:raise(business, #limiter_LimitNotFound{});
|
||||
handle_get_error({_, {range, notfound}}) ->
|
||||
woody_error:raise(business, #limiter_LimitNotFound{});
|
||||
handle_get_error(Error) ->
|
||||
handle_default_error(Error).
|
||||
|
||||
-spec handle_hold_error(_) -> no_return().
|
||||
handle_hold_error({_, {invalid_request, Errors}}) ->
|
||||
woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors});
|
||||
handle_hold_error(Error) ->
|
||||
handle_default_error(Error).
|
||||
|
||||
-spec handle_commit_error(_) -> no_return().
|
||||
handle_commit_error({_, {forbidden_operation_amount, Error}}) ->
|
||||
handle_forbidden_operation_amount_error(Error);
|
||||
handle_commit_error({_, {plan, notfound}}) ->
|
||||
woody_error:raise(business, #limiter_LimitChangeNotFound{});
|
||||
handle_commit_error({_, {invalid_request, Errors}}) ->
|
||||
woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors});
|
||||
handle_commit_error(Error) ->
|
||||
handle_default_error(Error).
|
||||
|
||||
-spec handle_rollback_error(_) -> no_return().
|
||||
handle_rollback_error({_, {plan, notfound}}) ->
|
||||
woody_error:raise(business, #limiter_LimitChangeNotFound{});
|
||||
handle_rollback_error({_, {invalid_request, Errors}}) ->
|
||||
woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors});
|
||||
handle_rollback_error(Error) ->
|
||||
handle_default_error(Error).
|
||||
|
||||
-spec handle_default_error(_) -> no_return().
|
||||
handle_default_error({config, notfound}) ->
|
||||
woody_error:raise(business, #limiter_LimitNotFound{});
|
||||
handle_default_error(Error) ->
|
||||
handle_unknown_error(Error).
|
||||
|
||||
-spec handle_unknown_error(_) -> no_return().
|
||||
handle_unknown_error(Error) ->
|
||||
erlang:error({unknown_error, Error}).
|
||||
|
||||
-spec handle_forbidden_operation_amount_error(_) -> no_return().
|
||||
handle_forbidden_operation_amount_error(#{
|
||||
type := Type,
|
||||
partial := Partial,
|
||||
full := Full
|
||||
}) ->
|
||||
case Type of
|
||||
positive ->
|
||||
woody_error:raise(business, #limiter_ForbiddenOperationAmount{
|
||||
amount = Partial,
|
||||
allowed_range = #limiter_base_AmountRange{
|
||||
upper = {inclusive, Full},
|
||||
lower = {inclusive, 0}
|
||||
}
|
||||
});
|
||||
negative ->
|
||||
woody_error:raise(business, #limiter_ForbiddenOperationAmount{
|
||||
amount = Partial,
|
||||
allowed_range = #limiter_base_AmountRange{
|
||||
upper = {inclusive, 0},
|
||||
lower = {inclusive, Full}
|
||||
}
|
||||
})
|
||||
end.
|
53
apps/limiter/src/lim_maybe.erl
Normal file
53
apps/limiter/src/lim_maybe.erl
Normal file
@ -0,0 +1,53 @@
|
||||
%%%
|
||||
%%% Call me maybe
|
||||
%%%
|
||||
|
||||
-module(lim_maybe).
|
||||
|
||||
-type maybe(T) ::
|
||||
undefined | T.
|
||||
|
||||
-export_type([maybe/1]).
|
||||
|
||||
-export([from_result/1]).
|
||||
-export([to_list/1]).
|
||||
-export([apply/2]).
|
||||
-export([apply/3]).
|
||||
-export([get_defined/1]).
|
||||
-export([get_defined/2]).
|
||||
|
||||
%%
|
||||
|
||||
-spec from_result({ok, T} | {error, _}) -> maybe(T).
|
||||
from_result({ok, T}) ->
|
||||
T;
|
||||
from_result({error, _}) ->
|
||||
undefined.
|
||||
|
||||
-spec to_list(maybe(T)) -> [T].
|
||||
to_list(undefined) ->
|
||||
[];
|
||||
to_list(T) ->
|
||||
[T].
|
||||
|
||||
-spec apply(fun(), Arg :: undefined | term()) -> term().
|
||||
apply(Fun, Arg) ->
|
||||
lim_maybe:apply(Fun, Arg, undefined).
|
||||
|
||||
-spec apply(fun(), Arg :: undefined | term(), Default :: term()) -> term().
|
||||
apply(Fun, Arg, _Default) when Arg =/= undefined ->
|
||||
Fun(Arg);
|
||||
apply(_Fun, undefined, Default) ->
|
||||
Default.
|
||||
|
||||
-spec get_defined([maybe(T)]) -> T.
|
||||
get_defined([]) ->
|
||||
erlang:error(badarg);
|
||||
get_defined([Value | _Tail]) when Value =/= undefined ->
|
||||
Value;
|
||||
get_defined([undefined | Tail]) ->
|
||||
get_defined(Tail).
|
||||
|
||||
-spec get_defined(maybe(T), maybe(T)) -> T.
|
||||
get_defined(V1, V2) ->
|
||||
get_defined([V1, V2]).
|
94
apps/limiter/src/lim_p_transfer.erl
Normal file
94
apps/limiter/src/lim_p_transfer.erl
Normal file
@ -0,0 +1,94 @@
|
||||
-module(lim_p_transfer).
|
||||
|
||||
-include_lib("damsel/include/dmsl_accounter_thrift.hrl").
|
||||
-include_lib("damsel/include/dmsl_base_thrift.hrl").
|
||||
|
||||
-export([construct_postings/3]).
|
||||
-export([reverse_postings/1]).
|
||||
-export([assert_partial_posting_amount/2]).
|
||||
|
||||
-type amount() :: integer().
|
||||
-type currency() :: binary().
|
||||
-type account_id() :: lim_accounting:account_id().
|
||||
-type posting() :: lim_accounting:posting().
|
||||
-type body() :: lim_body:t().
|
||||
|
||||
-type forbidden_operation_amount_error() :: #{
|
||||
type := positive | negative,
|
||||
partial := amount(),
|
||||
full := amount(),
|
||||
currency := currency()
|
||||
}.
|
||||
|
||||
-export_type([forbidden_operation_amount_error/0]).
|
||||
|
||||
-spec construct_postings(account_id(), account_id(), body()) -> [posting()].
|
||||
construct_postings(AccountFrom, AccountTo, {cash, #{amount := Amount, currency := Currency}}) ->
|
||||
[
|
||||
#accounter_Posting{
|
||||
from_id = AccountFrom,
|
||||
to_id = AccountTo,
|
||||
amount = Amount,
|
||||
currency_sym_code = Currency,
|
||||
description = <<>>
|
||||
}
|
||||
];
|
||||
construct_postings(AccountFrom, AccountTo, {amount, Amount}) ->
|
||||
[
|
||||
#accounter_Posting{
|
||||
from_id = AccountFrom,
|
||||
to_id = AccountTo,
|
||||
amount = Amount,
|
||||
currency_sym_code = lim_accounting:get_default_currency(),
|
||||
description = <<>>
|
||||
}
|
||||
].
|
||||
|
||||
-spec reverse_postings([posting()]) -> [posting()].
|
||||
reverse_postings(Postings) ->
|
||||
[
|
||||
Posting#accounter_Posting{
|
||||
from_id = AccountTo,
|
||||
to_id = AccountFrom
|
||||
}
|
||||
|| Posting = #accounter_Posting{from_id = AccountFrom, to_id = AccountTo} <- Postings
|
||||
].
|
||||
|
||||
-spec assert_partial_posting_amount([posting()], [posting()]) -> ok | {error, forbidden_operation_amount_error()}.
|
||||
assert_partial_posting_amount(
|
||||
[#accounter_Posting{amount = Partial, currency_sym_code = Currency} | _Rest],
|
||||
[#accounter_Posting{amount = Full, currency_sym_code = Currency} | _Rest]
|
||||
) ->
|
||||
compare_amount(Partial, Full, Currency);
|
||||
assert_partial_posting_amount(
|
||||
[#accounter_Posting{amount = Partial, currency_sym_code = PartialCurrency} | _Rest],
|
||||
[#accounter_Posting{amount = Full, currency_sym_code = FullCurrency} | _Rest]
|
||||
) ->
|
||||
erlang:error({invalid_partial_cash, {Partial, PartialCurrency}, {Full, FullCurrency}}).
|
||||
|
||||
compare_amount(Partial, Full, Currency) when Full > 0 ->
|
||||
case Partial =< Full of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error,
|
||||
{forbidden_operation_amount, #{
|
||||
type => positive,
|
||||
partial => Partial,
|
||||
full => Full,
|
||||
currency => Currency
|
||||
}}}
|
||||
end;
|
||||
compare_amount(Partial, Full, Currency) when Full < 0 ->
|
||||
case Partial >= Full of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error,
|
||||
{forbidden_operation_amount, #{
|
||||
type => negative,
|
||||
partial => Partial,
|
||||
full => Full,
|
||||
currency => Currency
|
||||
}}}
|
||||
end.
|
74
apps/limiter/src/lim_pipeline.erl
Normal file
74
apps/limiter/src/lim_pipeline.erl
Normal file
@ -0,0 +1,74 @@
|
||||
%%%
|
||||
%%% Pipeline
|
||||
%%%
|
||||
|
||||
-module(lim_pipeline).
|
||||
|
||||
-export([do/1]).
|
||||
-export([do/2]).
|
||||
-export([unwrap/1]).
|
||||
-export([unwrap/2]).
|
||||
-export([expect/2]).
|
||||
-export([valid/2]).
|
||||
|
||||
%%
|
||||
|
||||
-type thrown(_E) ::
|
||||
no_return().
|
||||
|
||||
-type result(T, E) ::
|
||||
{ok, T} | {error, E}.
|
||||
|
||||
-spec do(fun(() -> ok | T | thrown(E))) -> ok | result(T, E).
|
||||
do(Fun) ->
|
||||
try Fun() of
|
||||
ok ->
|
||||
ok;
|
||||
R ->
|
||||
{ok, R}
|
||||
catch
|
||||
Thrown -> {error, Thrown}
|
||||
end.
|
||||
|
||||
-spec do(Tag, fun(() -> ok | T | thrown(E))) -> ok | result(T, {Tag, E}).
|
||||
do(Tag, Fun) ->
|
||||
do(fun() -> unwrap(Tag, do(Fun)) end).
|
||||
|
||||
-spec unwrap
|
||||
(ok) -> ok;
|
||||
({ok, V}) -> V;
|
||||
({error, E}) -> thrown(E).
|
||||
unwrap(ok) ->
|
||||
ok;
|
||||
unwrap({ok, V}) ->
|
||||
V;
|
||||
unwrap({error, E}) ->
|
||||
throw(E).
|
||||
|
||||
-spec expect
|
||||
(_E, ok) -> ok;
|
||||
(_E, {ok, V}) -> V;
|
||||
(E, {error, _}) -> thrown(E).
|
||||
expect(_, ok) ->
|
||||
ok;
|
||||
expect(_, {ok, V}) ->
|
||||
V;
|
||||
expect(E, {error, _}) ->
|
||||
throw(E).
|
||||
|
||||
-spec unwrap
|
||||
(_Tag, ok) -> ok;
|
||||
(_Tag, {ok, V}) -> V;
|
||||
(Tag, {error, E}) -> thrown({Tag, E}).
|
||||
unwrap(_, ok) ->
|
||||
ok;
|
||||
unwrap(_, {ok, V}) ->
|
||||
V;
|
||||
unwrap(Tag, {error, E}) ->
|
||||
throw({Tag, E}).
|
||||
|
||||
-spec valid(T, T) -> ok | {error, T}.
|
||||
valid(V, V) ->
|
||||
ok;
|
||||
valid(_, V) ->
|
||||
{error, V}.
|
67
apps/limiter/src/lim_proto_utils.erl
Normal file
67
apps/limiter/src/lim_proto_utils.erl
Normal file
@ -0,0 +1,67 @@
|
||||
-module(lim_proto_utils).
|
||||
|
||||
-export([serialize/2]).
|
||||
-export([deserialize/2]).
|
||||
|
||||
-type thrift_type() ::
|
||||
thrift_base_type()
|
||||
| thrift_collection_type()
|
||||
| thrift_enum_type()
|
||||
| thrift_struct_type().
|
||||
|
||||
-type thrift_base_type() ::
|
||||
bool
|
||||
| double
|
||||
| i8
|
||||
| i16
|
||||
| i32
|
||||
| i64
|
||||
| string.
|
||||
|
||||
-type thrift_collection_type() ::
|
||||
{list, thrift_type()}
|
||||
| {set, thrift_type()}
|
||||
| {map, thrift_type(), thrift_type()}.
|
||||
|
||||
-type thrift_enum_type() ::
|
||||
{enum, thrift_type_ref()}.
|
||||
|
||||
-type thrift_struct_type() ::
|
||||
{struct, thrift_struct_flavor(), thrift_type_ref() | thrift_struct_def()}.
|
||||
|
||||
-type thrift_struct_flavor() :: struct | union | exception.
|
||||
|
||||
-type thrift_type_ref() :: {module(), Name :: atom()}.
|
||||
|
||||
-type thrift_struct_def() :: list({
|
||||
Tag :: pos_integer(),
|
||||
Requireness :: required | optional | undefined,
|
||||
Type :: thrift_struct_type(),
|
||||
Name :: atom(),
|
||||
Default :: any()
|
||||
}).
|
||||
|
||||
-spec serialize(thrift_type(), term()) -> binary().
|
||||
serialize(Type, Data) ->
|
||||
Codec0 = thrift_strict_binary_codec:new(),
|
||||
case thrift_strict_binary_codec:write(Codec0, Type, Data) of
|
||||
{ok, Codec1} ->
|
||||
thrift_strict_binary_codec:close(Codec1);
|
||||
{error, Reason} ->
|
||||
erlang:error({thrift, {protocol, Reason}})
|
||||
end.
|
||||
|
||||
-spec deserialize(thrift_type(), binary()) -> term().
|
||||
deserialize(Type, Data) ->
|
||||
Codec0 = thrift_strict_binary_codec:new(Data),
|
||||
case thrift_strict_binary_codec:read(Codec0, Type) of
|
||||
{ok, Result, Codec1} ->
|
||||
case thrift_strict_binary_codec:close(Codec1) of
|
||||
<<>> ->
|
||||
Result;
|
||||
Leftovers ->
|
||||
erlang:error({thrift, {protocol, {excess_binary_data, Leftovers}}})
|
||||
end;
|
||||
{error, Reason} ->
|
||||
erlang:error({thrift, {protocol, Reason}})
|
||||
end.
|
178
apps/limiter/src/lim_range_codec.erl
Normal file
178
apps/limiter/src/lim_range_codec.erl
Normal file
@ -0,0 +1,178 @@
|
||||
-module(lim_range_codec).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_limiter_range_thrift.hrl").
|
||||
|
||||
-export([marshal/2]).
|
||||
-export([unmarshal/2]).
|
||||
-export([parse_timestamp/1]).
|
||||
|
||||
%% Types
|
||||
|
||||
-type type_name() :: atom() | {list, atom()} | {set, atom()}.
|
||||
|
||||
-type encoded_value() :: encoded_value(any()).
|
||||
-type encoded_value(T) :: T.
|
||||
|
||||
-type decoded_value() :: decoded_value(any()).
|
||||
-type decoded_value(T) :: T.
|
||||
|
||||
%% API
|
||||
|
||||
-spec marshal(type_name(), decoded_value()) -> encoded_value().
|
||||
marshal(timestamped_change, {ev, Timestamp, Change}) ->
|
||||
#limiter_range_TimestampedChange{
|
||||
change = marshal(change, Change),
|
||||
occured_at = marshal(timestamp, Timestamp)
|
||||
};
|
||||
marshal(change, {created, Range}) ->
|
||||
{created, #limiter_range_CreatedChange{limit_range = marshal(range, Range)}};
|
||||
marshal(change, {time_range_created, TimeRange}) ->
|
||||
{time_range_created, #limiter_range_TimeRangeCreatedChange{time_range = marshal(time_range, TimeRange)}};
|
||||
marshal(
|
||||
range,
|
||||
Range = #{
|
||||
id := ID,
|
||||
type := Type,
|
||||
created_at := CreatedAt
|
||||
}
|
||||
) ->
|
||||
#limiter_range_LimitRange{
|
||||
id = ID,
|
||||
type = marshal(time_range_type, Type),
|
||||
created_at = CreatedAt,
|
||||
currency = lim_range_machine:currency(Range)
|
||||
};
|
||||
marshal(time_range, #{
|
||||
upper := Upper,
|
||||
lower := Lower,
|
||||
account_id_from := AccountIDFrom,
|
||||
account_id_to := AccountIDTo
|
||||
}) ->
|
||||
#time_range_TimeRange{
|
||||
upper = Upper,
|
||||
lower = Lower,
|
||||
account_id_from = AccountIDFrom,
|
||||
account_id_to = AccountIDTo
|
||||
};
|
||||
marshal(time_range_type, {calendar, SubType}) ->
|
||||
{calendar, marshal(time_range_sub_type, SubType)};
|
||||
marshal(time_range_type, {interval, Interval}) ->
|
||||
{interval, #time_range_TimeRangeTypeInterval{amount = Interval}};
|
||||
marshal(time_range_sub_type, year) ->
|
||||
{year, #time_range_TimeRangeTypeCalendarYear{}};
|
||||
marshal(time_range_sub_type, month) ->
|
||||
{month, #time_range_TimeRangeTypeCalendarMonth{}};
|
||||
marshal(time_range_sub_type, week) ->
|
||||
{week, #time_range_TimeRangeTypeCalendarWeek{}};
|
||||
marshal(time_range_sub_type, day) ->
|
||||
{day, #time_range_TimeRangeTypeCalendarDay{}};
|
||||
marshal(timestamp, {DateTime, USec}) ->
|
||||
DateTimeinSeconds = genlib_time:daytime_to_unixtime(DateTime),
|
||||
{TimeinUnit, Unit} =
|
||||
case USec of
|
||||
0 ->
|
||||
{DateTimeinSeconds, second};
|
||||
USec ->
|
||||
MicroSec = erlang:convert_time_unit(DateTimeinSeconds, second, microsecond),
|
||||
{MicroSec + USec, microsecond}
|
||||
end,
|
||||
genlib_rfc3339:format_relaxed(TimeinUnit, Unit).
|
||||
|
||||
-spec unmarshal(type_name(), encoded_value()) -> decoded_value().
|
||||
unmarshal(timestamped_change, TimestampedChange) ->
|
||||
Timestamp = unmarshal(timestamp, TimestampedChange#limiter_range_TimestampedChange.occured_at),
|
||||
Change = unmarshal(change, TimestampedChange#limiter_range_TimestampedChange.change),
|
||||
{ev, Timestamp, Change};
|
||||
unmarshal(change, {created, #limiter_range_CreatedChange{limit_range = Range}}) ->
|
||||
{created, unmarshal(range, Range)};
|
||||
unmarshal(change, {time_range_created, #limiter_range_TimeRangeCreatedChange{time_range = Range}}) ->
|
||||
{time_range_created, unmarshal(time_range, Range)};
|
||||
unmarshal(range, #limiter_range_LimitRange{
|
||||
id = ID,
|
||||
type = Type,
|
||||
created_at = CreatedAt,
|
||||
currency = Currency
|
||||
}) ->
|
||||
genlib_map:compact(#{
|
||||
id => ID,
|
||||
type => unmarshal(time_range_type, Type),
|
||||
created_at => CreatedAt,
|
||||
currency => Currency
|
||||
});
|
||||
unmarshal(time_range, #time_range_TimeRange{
|
||||
upper = Upper,
|
||||
lower = Lower,
|
||||
account_id_from = AccountIDFrom,
|
||||
account_id_to = AccountIDTo
|
||||
}) ->
|
||||
#{
|
||||
upper => Upper,
|
||||
lower => Lower,
|
||||
account_id_from => AccountIDFrom,
|
||||
account_id_to => AccountIDTo
|
||||
};
|
||||
unmarshal(time_range_type, {calendar, SubType}) ->
|
||||
{calendar, unmarshal(time_range_sub_type, SubType)};
|
||||
unmarshal(time_range_type, {interval, #time_range_TimeRangeTypeInterval{amount = Interval}}) ->
|
||||
{interval, Interval};
|
||||
unmarshal(time_range_sub_type, {year, _}) ->
|
||||
year;
|
||||
unmarshal(time_range_sub_type, {month, _}) ->
|
||||
month;
|
||||
unmarshal(time_range_sub_type, {week, _}) ->
|
||||
week;
|
||||
unmarshal(time_range_sub_type, {day, _}) ->
|
||||
day;
|
||||
unmarshal(timestamp, Timestamp) when is_binary(Timestamp) ->
|
||||
parse_timestamp(Timestamp).
|
||||
|
||||
-spec parse_timestamp(binary()) -> machinery:timestamp().
|
||||
parse_timestamp(Bin) ->
|
||||
try
|
||||
MicroSeconds = genlib_rfc3339:parse(Bin, microsecond),
|
||||
case genlib_rfc3339:is_utc(Bin) of
|
||||
false ->
|
||||
erlang:error({bad_timestamp, not_utc}, [Bin]);
|
||||
true ->
|
||||
USec = MicroSeconds rem 1000000,
|
||||
DateTime = calendar:system_time_to_universal_time(MicroSeconds, microsecond),
|
||||
{DateTime, USec}
|
||||
end
|
||||
catch
|
||||
error:Error:St ->
|
||||
erlang:raise(error, {bad_timestamp, Bin, Error}, St)
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
|
||||
-spec marshal_unmarshal_created_test() -> _.
|
||||
|
||||
marshal_unmarshal_created_test() ->
|
||||
Created =
|
||||
{created, #{
|
||||
id => <<"id">>,
|
||||
type => {calendar, day},
|
||||
created_at => <<"2000-01-01T00:00:00Z">>,
|
||||
currency => <<"USD">>
|
||||
}},
|
||||
Event = {ev, lim_time:machinery_now(), Created},
|
||||
?assertEqual(Event, unmarshal(timestamped_change, marshal(timestamped_change, Event))).
|
||||
|
||||
-spec marshal_unmarshal_time_range_created_test() -> _.
|
||||
marshal_unmarshal_time_range_created_test() ->
|
||||
TimeRangeCreated =
|
||||
{time_range_created, #{
|
||||
account_id_from => 25,
|
||||
account_id_to => 175,
|
||||
upper => <<"2000-01-01T00:00:00Z">>,
|
||||
lower => <<"2000-01-01T00:00:00Z">>
|
||||
}},
|
||||
Event = {ev, lim_time:machinery_now(), TimeRangeCreated},
|
||||
?assertEqual(Event, unmarshal(timestamped_change, marshal(timestamped_change, Event))).
|
||||
|
||||
-endif.
|
268
apps/limiter/src/lim_range_machine.erl
Normal file
268
apps/limiter/src/lim_range_machine.erl
Normal file
@ -0,0 +1,268 @@
|
||||
-module(lim_range_machine).
|
||||
|
||||
%% Accessors
|
||||
|
||||
-export([id/1]).
|
||||
-export([created_at/1]).
|
||||
-export([type/1]).
|
||||
-export([ranges/1]).
|
||||
-export([currency/1]).
|
||||
|
||||
%% API
|
||||
|
||||
-export([get/2]).
|
||||
-export([ensure_exist/2]).
|
||||
-export([get_range/2]).
|
||||
-export([get_range_balance/3]).
|
||||
-export([ensure_range_exist/3]).
|
||||
-export([ensure_range_exist_in_state/3]).
|
||||
|
||||
%% Machinery callbacks
|
||||
|
||||
-behaviour(machinery).
|
||||
|
||||
-export([init/4]).
|
||||
-export([process_call/4]).
|
||||
-export([process_timeout/3]).
|
||||
-export([process_repair/4]).
|
||||
|
||||
-type args(T) :: machinery:args(T).
|
||||
-type machine() :: machinery:machine(event(), _).
|
||||
-type handler_args() :: machinery:handler_args(_).
|
||||
-type handler_opts() :: machinery:handler_opts(_).
|
||||
-type result() :: machinery:result(timestamped_event(event()), none()).
|
||||
-type response(T) :: machinery:response(T).
|
||||
|
||||
%%
|
||||
|
||||
-type woody_context() :: woody_context:ctx().
|
||||
-type lim_context() :: lim_context:t().
|
||||
-type timestamp() :: lim_config_machine:timestamp().
|
||||
-type lim_id() :: lim_config_machine:lim_id().
|
||||
-type time_range_type() :: lim_config_machine:time_range_type().
|
||||
-type time_range() :: lim_config_machine:time_range().
|
||||
-type currency() :: lim_config_machine:currency().
|
||||
|
||||
-type limit_range_state() :: #{
|
||||
id := lim_id(),
|
||||
type := time_range_type(),
|
||||
created_at := timestamp(),
|
||||
currency => currency(),
|
||||
ranges => [time_range_ext()]
|
||||
}.
|
||||
|
||||
-type timestamped_event(T) ::
|
||||
{ev, machinery:timestamp(), T}.
|
||||
|
||||
-type event() ::
|
||||
{created, limit_range()}
|
||||
| {time_range_created, time_range_ext()}.
|
||||
|
||||
-type limit_range() :: #{
|
||||
id := lim_id(),
|
||||
type := time_range_type(),
|
||||
created_at := timestamp(),
|
||||
currency => currency()
|
||||
}.
|
||||
|
||||
-type time_range_ext() :: #{
|
||||
account_id_from := lim_accounting:account_id(),
|
||||
account_id_to := lim_accounting:account_id(),
|
||||
upper := timestamp(),
|
||||
lower := timestamp()
|
||||
}.
|
||||
|
||||
-type create_params() :: #{
|
||||
id := lim_id(),
|
||||
type := time_range_type(),
|
||||
created_at := timestamp(),
|
||||
currency => currency()
|
||||
}.
|
||||
|
||||
-type range_call() ::
|
||||
{add_range, time_range()}.
|
||||
|
||||
-export_type([timestamped_event/1]).
|
||||
-export_type([event/0]).
|
||||
|
||||
-define(NS, 'lim/range_v1').
|
||||
|
||||
-import(lim_pipeline, [do/1, unwrap/1, unwrap/2]).
|
||||
|
||||
%% Accessors
|
||||
|
||||
-spec id(limit_range_state()) -> lim_id().
|
||||
id(State) ->
|
||||
maps:get(id, State).
|
||||
|
||||
-spec created_at(limit_range_state()) -> timestamp().
|
||||
created_at(State) ->
|
||||
maps:get(created_at, State).
|
||||
|
||||
-spec type(limit_range_state()) -> time_range_type().
|
||||
type(State) ->
|
||||
maps:get(type, State).
|
||||
|
||||
-spec ranges(limit_range_state()) -> [time_range_ext()].
|
||||
ranges(#{ranges := Ranges}) ->
|
||||
Ranges;
|
||||
ranges(_State) ->
|
||||
[].
|
||||
|
||||
-spec currency(limit_range_state()) -> currency().
|
||||
currency(#{currency := Currency}) ->
|
||||
Currency;
|
||||
currency(_State) ->
|
||||
lim_accounting:get_default_currency().
|
||||
|
||||
%%% API
|
||||
|
||||
-spec get(lim_id(), lim_context()) -> {ok, limit_range_state()} | {error, notfound}.
|
||||
get(ID, LimitContext) ->
|
||||
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
|
||||
get_state(ID, WoodyCtx).
|
||||
|
||||
-spec ensure_exist(create_params(), lim_context()) -> {ok, limit_range_state()}.
|
||||
ensure_exist(Params = #{id := ID}, LimitContext) ->
|
||||
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
|
||||
case get_state(ID, WoodyCtx) of
|
||||
{ok, State} ->
|
||||
{ok, State};
|
||||
{error, notfound} ->
|
||||
_ = start(ID, Params, WoodyCtx),
|
||||
case get_state(ID, WoodyCtx) of
|
||||
{ok, State} ->
|
||||
{ok, State};
|
||||
{error, notfound} ->
|
||||
erlang:error({cant_get_after_start, ID})
|
||||
end
|
||||
end.
|
||||
|
||||
-spec get_range(time_range(), limit_range_state()) -> {ok, time_range_ext()} | {error, notfound}.
|
||||
get_range(TimeRange, State) ->
|
||||
find_time_range(TimeRange, ranges(State)).
|
||||
|
||||
-spec get_range_balance(time_range(), limit_range_state(), lim_context()) ->
|
||||
{ok, lim_accounting:balance()}
|
||||
| {error, {range, notfound}}.
|
||||
get_range_balance(TimeRange, State, LimitContext) ->
|
||||
do(fun() ->
|
||||
#{account_id_to := AccountID} = unwrap(range, find_time_range(TimeRange, ranges(State))),
|
||||
{ok, Balance} = lim_accounting:get_balance(AccountID, LimitContext),
|
||||
Balance
|
||||
end).
|
||||
|
||||
-spec ensure_range_exist(lim_id(), time_range(), lim_context()) ->
|
||||
{ok, time_range_ext()}
|
||||
| {error, {limit_range, notfound}}.
|
||||
ensure_range_exist(ID, TimeRange, LimitContext) ->
|
||||
do(fun() ->
|
||||
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
|
||||
State = unwrap(limit_range, get_state(ID, WoodyCtx)),
|
||||
unwrap(ensure_range_exist_in_state(TimeRange, State, LimitContext))
|
||||
end).
|
||||
|
||||
-spec ensure_range_exist_in_state(time_range(), limit_range_state(), lim_context()) -> {ok, time_range_ext()}.
|
||||
ensure_range_exist_in_state(TimeRange, State, LimitContext) ->
|
||||
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
|
||||
case find_time_range(TimeRange, ranges(State)) of
|
||||
{error, notfound} ->
|
||||
call(id(State), {add_range, TimeRange}, WoodyCtx);
|
||||
{ok, Range} ->
|
||||
{ok, Range}
|
||||
end.
|
||||
|
||||
%%% Machinery callbacks
|
||||
|
||||
-spec init(args([event()]), machine(), handler_args(), handler_opts()) -> result().
|
||||
init(Events, _Machine, _HandlerArgs, _HandlerOpts) ->
|
||||
#{
|
||||
events => emit_events(Events)
|
||||
}.
|
||||
|
||||
-spec process_call(args(range_call()), machine(), handler_args(), handler_opts()) ->
|
||||
{response(time_range_ext()), result()} | no_return().
|
||||
process_call({add_range, TimeRange0}, Machine, _HandlerArgs, #{woody_ctx := WoodyCtx}) ->
|
||||
State = collapse(Machine),
|
||||
case find_time_range(TimeRange0, ranges(State)) of
|
||||
{error, notfound} ->
|
||||
Currency = currency(State),
|
||||
{ok, LimitContext} = lim_context:create(WoodyCtx),
|
||||
{ok, AccountIDFrom} = lim_accounting:create_account(Currency, LimitContext),
|
||||
{ok, AccountIDTo} = lim_accounting:create_account(Currency, LimitContext),
|
||||
TimeRange1 = TimeRange0#{
|
||||
account_id_from => AccountIDFrom,
|
||||
account_id_to => AccountIDTo
|
||||
},
|
||||
{TimeRange1, #{events => emit_events([{time_range_created, TimeRange1}])}};
|
||||
{ok, Range} ->
|
||||
{Range, #{}}
|
||||
end.
|
||||
|
||||
-spec process_timeout(machine(), handler_args(), handler_opts()) -> no_return().
|
||||
process_timeout(_Machine, _HandlerArgs, _HandlerOpts) ->
|
||||
not_implemented(timeout).
|
||||
|
||||
-spec process_repair(args(_), machine(), handler_args(), handler_opts()) -> no_return().
|
||||
process_repair(_Args, _Machine, _HandlerArgs, _HandlerOpts) ->
|
||||
not_implemented(repair).
|
||||
|
||||
%%% Internal functions
|
||||
|
||||
find_time_range(_TimeRange, []) ->
|
||||
{error, notfound};
|
||||
find_time_range(#{lower := Lower}, [Head = #{lower := Lower} | _Rest]) ->
|
||||
{ok, Head};
|
||||
find_time_range(TimeRange, [_Head | Rest]) ->
|
||||
find_time_range(TimeRange, Rest).
|
||||
|
||||
%%
|
||||
|
||||
-spec start(lim_id(), create_params(), woody_context()) -> ok | {error, exists}.
|
||||
start(ID, Params, WoodyCtx) ->
|
||||
machinery:start(?NS, ID, [{created, Params}], get_backend(WoodyCtx)).
|
||||
|
||||
-spec call(lim_id(), range_call(), woody_context()) -> {ok, response(_)} | {error, notfound}.
|
||||
call(ID, Msg, WoodyCtx) ->
|
||||
machinery:call(?NS, ID, Msg, get_backend(WoodyCtx)).
|
||||
|
||||
-spec get_state(lim_id(), woody_context()) -> {ok, limit_range_state()} | {error, notfound}.
|
||||
get_state(ID, WoodyCtx) ->
|
||||
case machinery:get(?NS, ID, get_backend(WoodyCtx)) of
|
||||
{ok, Machine} ->
|
||||
{ok, collapse(Machine)};
|
||||
{error, notfound} = Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
emit_events(Events) ->
|
||||
emit_timestamped_events(Events, lim_time:machinery_now()).
|
||||
|
||||
emit_timestamped_events(Events, Ts) ->
|
||||
[{ev, Ts, Body} || Body <- Events].
|
||||
|
||||
collapse(#{history := History}) ->
|
||||
lists:foldl(fun(Ev, St) -> apply_event(Ev, St) end, undefined, History).
|
||||
|
||||
-spec get_backend(woody_context()) -> machinery_mg_backend:backend().
|
||||
get_backend(WoodyCtx) ->
|
||||
lim_utils:get_backend(?NS, WoodyCtx).
|
||||
|
||||
-spec not_implemented(any()) -> no_return().
|
||||
not_implemented(What) ->
|
||||
erlang:error({not_implemented, What}).
|
||||
|
||||
%%
|
||||
|
||||
-spec apply_event(machinery:event(timestamped_event(event())), lim_maybe:maybe(limit_range_state())) ->
|
||||
limit_range_state().
|
||||
apply_event({_ID, _Ts, {ev, _EvTs, Event}}, Config) ->
|
||||
apply_event_(Event, Config).
|
||||
|
||||
-spec apply_event_(event(), lim_maybe:maybe(limit_range_state())) -> limit_range_state().
|
||||
apply_event_({created, LimitRange}, undefined) ->
|
||||
LimitRange;
|
||||
apply_event_({time_range_created, TimeRange}, LimitRange = #{ranges := Ranges}) ->
|
||||
LimitRange#{ranges => [TimeRange | Ranges]};
|
||||
apply_event_({time_range_created, TimeRange}, LimitRange) ->
|
||||
LimitRange#{ranges => [TimeRange]}.
|
119
apps/limiter/src/lim_range_machinery_schema.erl
Normal file
119
apps/limiter/src/lim_range_machinery_schema.erl
Normal file
@ -0,0 +1,119 @@
|
||||
-module(lim_range_machinery_schema).
|
||||
|
||||
%% Storage schema behaviour
|
||||
-behaviour(machinery_mg_schema).
|
||||
|
||||
-export([get_version/1]).
|
||||
-export([marshal/3]).
|
||||
-export([unmarshal/3]).
|
||||
|
||||
%% Constants
|
||||
|
||||
-define(CURRENT_EVENT_FORMAT_VERSION, 1).
|
||||
|
||||
%% Internal types
|
||||
|
||||
-type type() :: machinery_mg_schema:t().
|
||||
-type value(T) :: machinery_mg_schema:v(T).
|
||||
-type value_type() :: machinery_mg_schema:vt().
|
||||
-type context() :: machinery_mg_schema:context().
|
||||
|
||||
-type event() :: lim_range_machine:timestamped_event(lim_range_machine:event()).
|
||||
-type aux_state() :: term().
|
||||
-type call_args() :: term().
|
||||
-type call_response() :: term().
|
||||
|
||||
-type data() ::
|
||||
aux_state()
|
||||
| event()
|
||||
| call_args()
|
||||
| call_response().
|
||||
|
||||
%% machinery_mg_schema callbacks
|
||||
|
||||
-spec get_version(value_type()) -> machinery_mg_schema:version().
|
||||
get_version(event) ->
|
||||
?CURRENT_EVENT_FORMAT_VERSION;
|
||||
get_version(aux_state) ->
|
||||
undefined.
|
||||
|
||||
-spec marshal(type(), value(data()), context()) -> {machinery_msgpack:t(), context()}.
|
||||
marshal({event, FormatVersion}, TimestampedChange, Context) ->
|
||||
marshal_event(FormatVersion, TimestampedChange, Context);
|
||||
marshal(T, V, C) when
|
||||
T =:= {args, init} orelse
|
||||
T =:= {args, call} orelse
|
||||
T =:= {args, repair} orelse
|
||||
T =:= {aux_state, undefined} orelse
|
||||
T =:= {response, call} orelse
|
||||
T =:= {response, {repair, success}} orelse
|
||||
T =:= {response, {repair, failure}}
|
||||
->
|
||||
machinery_mg_schema_generic:marshal(T, V, C).
|
||||
|
||||
-spec unmarshal(type(), machinery_msgpack:t(), context()) -> {data(), context()}.
|
||||
unmarshal({event, FormatVersion}, EncodedChange, Context) ->
|
||||
unmarshal_event(FormatVersion, EncodedChange, Context);
|
||||
unmarshal(T, V, C) when
|
||||
T =:= {args, init} orelse
|
||||
T =:= {args, call} orelse
|
||||
T =:= {args, repair} orelse
|
||||
T =:= {aux_state, undefined} orelse
|
||||
T =:= {response, call} orelse
|
||||
T =:= {response, {repair, success}} orelse
|
||||
T =:= {response, {repair, failure}}
|
||||
->
|
||||
machinery_mg_schema_generic:unmarshal(T, V, C).
|
||||
|
||||
%% Internals
|
||||
|
||||
-spec marshal_event(machinery_mg_schema:version(), event(), context()) -> {machinery_msgpack:t(), context()}.
|
||||
marshal_event(1, TimestampedChange, Context) ->
|
||||
ThriftChange = lim_range_codec:marshal(timestamped_change, TimestampedChange),
|
||||
Type = {struct, struct, {lim_limiter_range_thrift, 'TimestampedChange'}},
|
||||
{{bin, lim_proto_utils:serialize(Type, ThriftChange)}, Context}.
|
||||
|
||||
-spec unmarshal_event(machinery_mg_schema:version(), machinery_msgpack:t(), context()) -> {event(), context()}.
|
||||
unmarshal_event(1, EncodedChange, Context) ->
|
||||
{bin, EncodedThriftChange} = EncodedChange,
|
||||
Type = {struct, struct, {lim_limiter_range_thrift, 'TimestampedChange'}},
|
||||
ThriftChange = lim_proto_utils:deserialize(Type, EncodedThriftChange),
|
||||
{lim_range_codec:unmarshal(timestamped_change, ThriftChange), Context}.
|
||||
|
||||
%%
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
|
||||
-spec marshal_unmarshal_created_test() -> _.
|
||||
|
||||
marshal_unmarshal_created_test() ->
|
||||
Created =
|
||||
{created, #{
|
||||
id => <<"id">>,
|
||||
type => {calendar, day},
|
||||
created_at => <<"2000-01-01T00:00:00Z">>,
|
||||
currency => <<"USD">>
|
||||
}},
|
||||
Event = {ev, lim_time:machinery_now(), Created},
|
||||
{Marshaled, _} = marshal_event(1, Event, {}),
|
||||
{Unmarshaled, _} = unmarshal_event(1, Marshaled, {}),
|
||||
?assertEqual(Event, Unmarshaled).
|
||||
|
||||
-spec marshal_unmarshal_time_range_created_test() -> _.
|
||||
marshal_unmarshal_time_range_created_test() ->
|
||||
TimeRangeCreated =
|
||||
{time_range_created, #{
|
||||
account_id_from => 25,
|
||||
account_id_to => 175,
|
||||
upper => <<"2000-01-01T00:00:00Z">>,
|
||||
lower => <<"2000-01-01T00:00:00Z">>
|
||||
}},
|
||||
Event = {ev, lim_time:machinery_now(), TimeRangeCreated},
|
||||
{Marshaled, _} = marshal_event(1, Event, {}),
|
||||
{Unmarshaled, _} = unmarshal_event(1, Marshaled, {}),
|
||||
?assertEqual(Event, Unmarshaled).
|
||||
|
||||
-endif.
|
71
apps/limiter/src/lim_rates.erl
Normal file
71
apps/limiter/src/lim_rates.erl
Normal file
@ -0,0 +1,71 @@
|
||||
-module(lim_rates).
|
||||
|
||||
-include_lib("xrates_proto/include/xrates_rate_thrift.hrl").
|
||||
-include_lib("limiter_proto/include/lim_base_thrift.hrl").
|
||||
-include_lib("limiter_proto/include/lim_limiter_thrift.hrl").
|
||||
|
||||
-export([get_converted_amount/3]).
|
||||
|
||||
-type amount() :: dmsl_domain_thrift:'Amount'().
|
||||
-type currency() :: dmsl_domain_thrift:'CurrencySymbolicCode'().
|
||||
-type limit_context() :: lim_context:t().
|
||||
-type config() :: lim_config_machine:config().
|
||||
|
||||
-type convertation_error() :: quote_not_found | currency_not_found.
|
||||
|
||||
-export_type([convertation_error/0]).
|
||||
|
||||
-define(APP, limiter).
|
||||
-define(DEFAULT_FACTOR, 1.1).
|
||||
-define(DEFAULT_FACTOR_NAME, <<"DEFAULT">>).
|
||||
|
||||
-spec get_converted_amount({amount(), currency()}, config(), limit_context()) ->
|
||||
{ok, amount()}
|
||||
| {error, convertation_error()}.
|
||||
get_converted_amount(Cash = {_Amount, Currency}, Config, LimitContext) ->
|
||||
Factor = get_exchange_factor(Currency),
|
||||
case
|
||||
call_rates(
|
||||
'GetConvertedAmount',
|
||||
{<<"CBR">>, construct_conversion_request(Cash, Config, LimitContext)},
|
||||
LimitContext
|
||||
)
|
||||
of
|
||||
{ok, #base_Rational{p = P, q = Q}} ->
|
||||
Rational = genlib_rational:new(P, Q),
|
||||
{ok, genlib_rational:round(genlib_rational:mul(Rational, Factor))};
|
||||
{exception, #rate_QuoteNotFound{}} ->
|
||||
{error, quote_not_found};
|
||||
{exception, #rate_CurrencyNotFound{}} ->
|
||||
{error, currency_not_found}
|
||||
end.
|
||||
|
||||
construct_conversion_request({Amount, Currency}, Config = #{body_type := {cash, DestinationCurrency}}, LimitContext) ->
|
||||
ContextType = lim_config_machine:context_type(Config),
|
||||
{ok, Timestamp} = lim_context:get_from_context(ContextType, created_at, LimitContext),
|
||||
#rate_ConversionRequest{
|
||||
source = Currency,
|
||||
destination = DestinationCurrency,
|
||||
amount = Amount,
|
||||
datetime = Timestamp
|
||||
}.
|
||||
|
||||
get_exchange_factor(Currency) ->
|
||||
Factors = genlib_app:env(?APP, exchange_factors, #{}),
|
||||
case maps:get(Currency, Factors, undefined) of
|
||||
undefined ->
|
||||
case maps:get(?DEFAULT_FACTOR_NAME, Factors, undefined) of
|
||||
undefined ->
|
||||
?DEFAULT_FACTOR;
|
||||
DefaultFactor ->
|
||||
DefaultFactor
|
||||
end;
|
||||
Factor ->
|
||||
Factor
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
call_rates(Function, Args, LimitContext) ->
|
||||
{ok, WoodyContext} = lim_context:woody_context(LimitContext),
|
||||
lim_client_woody:call(xrates, Function, Args, WoodyContext).
|
17
apps/limiter/src/lim_router.erl
Normal file
17
apps/limiter/src/lim_router.erl
Normal file
@ -0,0 +1,17 @@
|
||||
-module(lim_router).
|
||||
|
||||
-export([get_handler/1]).
|
||||
|
||||
-type processor_type() :: binary().
|
||||
-type processor() :: module().
|
||||
|
||||
-export_type([processor_type/0]).
|
||||
-export_type([processor/0]).
|
||||
|
||||
-spec get_handler(processor_type()) ->
|
||||
{ok, processor()}
|
||||
| {error, notfound}.
|
||||
get_handler(<<"TurnoverProcessor">>) ->
|
||||
{ok, lim_turnover_processor};
|
||||
get_handler(_) ->
|
||||
{error, notfound}.
|
26
apps/limiter/src/lim_string.erl
Normal file
26
apps/limiter/src/lim_string.erl
Normal file
@ -0,0 +1,26 @@
|
||||
%%%
|
||||
%%% String manipultion facilities.
|
||||
|
||||
-module(lim_string).
|
||||
|
||||
-export([join/1]).
|
||||
-export([join/2]).
|
||||
|
||||
%%
|
||||
|
||||
-type fragment() ::
|
||||
iodata()
|
||||
| char()
|
||||
| atom()
|
||||
| number().
|
||||
|
||||
-spec join([fragment()]) -> binary().
|
||||
join(Fragments) ->
|
||||
join(<<>>, Fragments).
|
||||
|
||||
-spec join(Delim, [fragment()]) -> binary() when
|
||||
Delim ::
|
||||
char()
|
||||
| iodata().
|
||||
join(Delim, Fragments) ->
|
||||
genlib_string:join(Delim, lists:map(fun genlib:to_binary/1, Fragments)).
|
27
apps/limiter/src/lim_time.erl
Normal file
27
apps/limiter/src/lim_time.erl
Normal file
@ -0,0 +1,27 @@
|
||||
-module(lim_time).
|
||||
|
||||
-export([now/0]).
|
||||
-export([to_rfc3339/1]).
|
||||
-export([from_rfc3339/1]).
|
||||
-export([machinery_now/0]).
|
||||
|
||||
-type timestamp_ms() :: integer().
|
||||
|
||||
-export_type([timestamp_ms/0]).
|
||||
|
||||
-spec now() -> timestamp_ms().
|
||||
now() ->
|
||||
erlang:system_time(millisecond).
|
||||
|
||||
-spec to_rfc3339(timestamp_ms()) -> binary().
|
||||
to_rfc3339(Timestamp) ->
|
||||
genlib_rfc3339:format_relaxed(Timestamp, millisecond).
|
||||
|
||||
-spec from_rfc3339(binary()) -> timestamp_ms().
|
||||
from_rfc3339(BTimestamp) ->
|
||||
genlib_rfc3339:parse(BTimestamp, millisecond).
|
||||
|
||||
-spec machinery_now() -> machinery:timestamp().
|
||||
machinery_now() ->
|
||||
Now = {_, _, USec} = os:timestamp(),
|
||||
{calendar:now_to_universal_time(Now), USec}.
|
181
apps/limiter/src/lim_turnover_processor.erl
Normal file
181
apps/limiter/src/lim_turnover_processor.erl
Normal file
@ -0,0 +1,181 @@
|
||||
-module(lim_turnover_processor).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_base_thrift.hrl").
|
||||
-include_lib("limiter_proto/include/lim_limiter_thrift.hrl").
|
||||
-include_lib("damsel/include/dmsl_accounter_thrift.hrl").
|
||||
|
||||
-behaviour(lim_config_machine).
|
||||
|
||||
-export([get_limit/3]).
|
||||
-export([hold/3]).
|
||||
-export([commit/3]).
|
||||
-export([rollback/3]).
|
||||
|
||||
-type lim_context() :: lim_context:t().
|
||||
-type lim_id() :: lim_config_machine:lim_id().
|
||||
-type lim_change() :: lim_config_machine:lim_change().
|
||||
-type limit() :: lim_config_machine:limit().
|
||||
-type config() :: lim_config_machine:config().
|
||||
-type amount() :: integer().
|
||||
|
||||
-type forbidden_operation_amount_error() :: #{
|
||||
type := positive | negative,
|
||||
partial := amount(),
|
||||
full := amount()
|
||||
}.
|
||||
|
||||
-type get_limit_error() :: {limit | range, notfound}.
|
||||
|
||||
-type hold_error() ::
|
||||
lim_body:get_body_error()
|
||||
| lim_accounting:invalid_request_error().
|
||||
|
||||
-type commit_error() ::
|
||||
{forbidden_operation_amount, forbidden_operation_amount_error()}
|
||||
| {plan, notfound}
|
||||
| {full | partial, lim_body:get_body_error()}
|
||||
| lim_accounting:invalid_request_error().
|
||||
|
||||
-type rollback_error() :: {plan, notfound} | lim_accounting:invalid_request_error().
|
||||
|
||||
-export_type([get_limit_error/0]).
|
||||
-export_type([hold_error/0]).
|
||||
-export_type([commit_error/0]).
|
||||
-export_type([rollback_error/0]).
|
||||
|
||||
-import(lim_pipeline, [do/1, unwrap/1, unwrap/2]).
|
||||
|
||||
-spec get_limit(lim_id(), config(), lim_context()) -> {ok, limit()} | {error, get_limit_error()}.
|
||||
get_limit(LimitID, Config, LimitContext) ->
|
||||
do(fun() ->
|
||||
{ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext),
|
||||
LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext),
|
||||
LimitRange = unwrap(limit, lim_range_machine:get(LimitRangeID, LimitContext)),
|
||||
TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config),
|
||||
#{own_amount := Amount} =
|
||||
unwrap(lim_range_machine:get_range_balance(TimeRange, LimitRange, LimitContext)),
|
||||
#limiter_Limit{
|
||||
id = LimitRangeID,
|
||||
amount = Amount,
|
||||
creation_time = lim_config_machine:created_at(Config),
|
||||
description = lim_config_machine:description(Config)
|
||||
}
|
||||
end).
|
||||
|
||||
-spec hold(lim_change(), config(), lim_context()) -> ok | {error, hold_error()}.
|
||||
hold(LimitChange = #limiter_LimitChange{id = LimitID}, Config = #{body_type := BodyType}, LimitContext) ->
|
||||
do(fun() ->
|
||||
{ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext),
|
||||
{ok, Body} = lim_body:get_body(full, Config, LimitContext),
|
||||
LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext),
|
||||
Currency =
|
||||
case BodyType of
|
||||
{cash, CashCurrency} -> CashCurrency;
|
||||
amount -> undefined
|
||||
end,
|
||||
CreateParams = genlib_map:compact(#{
|
||||
id => LimitRangeID,
|
||||
type => lim_config_machine:time_range_type(Config),
|
||||
created_at => Timestamp,
|
||||
currency => Currency
|
||||
}),
|
||||
{ok, LimitRangeState} = lim_range_machine:ensure_exist(CreateParams, LimitContext),
|
||||
TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config),
|
||||
{ok, #{account_id_from := AccountIDFrom, account_id_to := AccountIDTo}} =
|
||||
lim_range_machine:ensure_range_exist_in_state(TimeRange, LimitRangeState, LimitContext),
|
||||
Postings = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, Body),
|
||||
lim_accounting:hold(construct_plan_id(LimitChange), {1, Postings}, LimitContext)
|
||||
end).
|
||||
|
||||
-spec commit(lim_change(), config(), lim_context()) -> ok | {error, commit_error()}.
|
||||
commit(LimitChange, Config, LimitContext) ->
|
||||
do(fun() ->
|
||||
case lim_body:get_body(partial, Config, LimitContext) of
|
||||
{ok, Body} ->
|
||||
unwrap(partial_commit(Body, LimitChange, Config, LimitContext));
|
||||
{error, notfound} ->
|
||||
PlanID = construct_plan_id(LimitChange),
|
||||
[Batch] = unwrap(plan, lim_accounting:get_plan(PlanID, LimitContext)),
|
||||
unwrap(lim_accounting:commit(PlanID, [Batch], LimitContext))
|
||||
end
|
||||
end).
|
||||
|
||||
-spec rollback(lim_change(), config(), lim_context()) -> ok | {error, rollback_error()}.
|
||||
rollback(LimitChange, _Config, LimitContext) ->
|
||||
do(fun() ->
|
||||
PlanID = construct_plan_id(LimitChange),
|
||||
BatchList = unwrap(plan, lim_accounting:get_plan(PlanID, LimitContext)),
|
||||
unwrap(lim_accounting:rollback(PlanID, BatchList, LimitContext))
|
||||
end).
|
||||
|
||||
construct_plan_id(#limiter_LimitChange{change_id = ChangeID}) ->
|
||||
ChangeID.
|
||||
|
||||
construct_range_id(LimitID, Timestamp, Config, LimitContext) ->
|
||||
{ok, Prefix} = lim_config_machine:mk_scope_prefix(Config, LimitContext),
|
||||
ShardID = lim_config_machine:calculate_shard_id(Timestamp, Config),
|
||||
<<LimitID/binary, Prefix/binary, "/", ShardID/binary>>.
|
||||
|
||||
partial_commit(PartialBody, LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) ->
|
||||
do(fun() ->
|
||||
{ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext),
|
||||
{ok, FullBody} = lim_body:get_body(full, Config, LimitContext),
|
||||
ok = unwrap(assert_partial_body(PartialBody, FullBody)),
|
||||
|
||||
LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext),
|
||||
{ok, LimitRangeState} = lim_range_machine:get(
|
||||
LimitRangeID,
|
||||
LimitContext
|
||||
),
|
||||
TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config),
|
||||
{ok, #{account_id_from := AccountIDFrom, account_id_to := AccountIDTo}} =
|
||||
lim_range_machine:get_range(TimeRange, LimitRangeState),
|
||||
|
||||
PartialPostings = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, PartialBody),
|
||||
FullPostings = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, FullBody),
|
||||
NewBatchList = [{2, lim_p_transfer:reverse_postings(FullPostings)} | [{3, PartialPostings}]],
|
||||
|
||||
PlanID = construct_plan_id(LimitChange),
|
||||
unwrap(lim_accounting:plan(PlanID, NewBatchList, LimitContext)),
|
||||
unwrap(lim_accounting:commit(PlanID, [{1, FullPostings} | NewBatchList], LimitContext))
|
||||
end).
|
||||
|
||||
assert_partial_body(
|
||||
{cash, #{amount := Partial, currency := Currency}},
|
||||
{cash, #{amount := Full, currency := Currency}}
|
||||
) ->
|
||||
compare_amount(Partial, Full, Currency);
|
||||
assert_partial_body(
|
||||
{cash, #{amount := Partial, currency := PartialCurrency}},
|
||||
{cash, #{amount := Full, currency := FullCurrency}}
|
||||
) ->
|
||||
erlang:error({invalid_partial_cash, {Partial, PartialCurrency}, {Full, FullCurrency}}).
|
||||
|
||||
compare_amount(Partial, Full, Currency) when Full > 0 ->
|
||||
case Partial =< Full of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error,
|
||||
{forbidden_operation_amount,
|
||||
genlib_map:compact(#{
|
||||
type => positive,
|
||||
partial => Partial,
|
||||
full => Full,
|
||||
currency => Currency
|
||||
})}}
|
||||
end;
|
||||
compare_amount(Partial, Full, Currency) when Full < 0 ->
|
||||
case Partial >= Full of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error,
|
||||
{forbidden_operation_amount,
|
||||
genlib_map:compact(#{
|
||||
type => negative,
|
||||
partial => Partial,
|
||||
full => Full,
|
||||
currency => Currency
|
||||
})}}
|
||||
end.
|
30
apps/limiter/src/lim_utils.erl
Normal file
30
apps/limiter/src/lim_utils.erl
Normal file
@ -0,0 +1,30 @@
|
||||
-module(lim_utils).
|
||||
|
||||
-export([get_backend/2]).
|
||||
-export([get_woody_client/1]).
|
||||
|
||||
-type woody_context() :: woody_context:ctx().
|
||||
|
||||
-spec get_backend(atom(), woody_context()) -> machinery_mg_backend:backend().
|
||||
get_backend(NS, WoodyCtx) ->
|
||||
Backend = maps:get(NS, genlib_app:env(limiter, backends, #{})),
|
||||
{Mod, Opts} = machinery_utils:get_backend(Backend),
|
||||
{Mod, Opts#{
|
||||
woody_ctx => WoodyCtx
|
||||
}}.
|
||||
|
||||
%%% Internal functions
|
||||
|
||||
-spec get_woody_client(woody:url()) -> machinery_mg_client:woody_client().
|
||||
get_woody_client(Url) ->
|
||||
genlib_map:compact(#{
|
||||
url => Url,
|
||||
event_handler => get_woody_event_handlers()
|
||||
}).
|
||||
|
||||
-spec get_woody_event_handlers() -> woody:ev_handlers().
|
||||
get_woody_event_handlers() ->
|
||||
genlib_app:env(limiter, woody_event_handlers, [
|
||||
scoper_woody_event_handler,
|
||||
hay_woody_event_handler
|
||||
]).
|
@ -6,6 +6,9 @@
|
||||
kernel,
|
||||
stdlib,
|
||||
damsel,
|
||||
limiter_proto,
|
||||
xrates_proto,
|
||||
machinery,
|
||||
woody,
|
||||
how_are_you, % must be after ranch and before any woody usage
|
||||
scoper, % should be before any scoper event handler usage
|
||||
|
154
apps/limiter/src/limiter.erl
Normal file
154
apps/limiter/src/limiter.erl
Normal file
@ -0,0 +1,154 @@
|
||||
-module(limiter).
|
||||
|
||||
%% Application callbacks
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-export([start/2]).
|
||||
-export([stop/1]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
%%
|
||||
|
||||
-spec start(normal, any()) -> {ok, pid()} | {error, any()}.
|
||||
start(_StartType, _StartArgs) ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
-spec stop(any()) -> ok.
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
init([]) ->
|
||||
ServiceOpts = genlib_app:env(?MODULE, services, #{}),
|
||||
Healthcheck = enable_health_logging(genlib_app:env(?MODULE, health_check, #{})),
|
||||
|
||||
{Backends, MachineHandlers, ModernizerHandlers} = lists:unzip3([
|
||||
contruct_backend_childspec('lim/range_v1', lim_range_machine),
|
||||
contruct_backend_childspec('lim/config_v1', lim_config_machine)
|
||||
]),
|
||||
ok = application:set_env(limiter, backends, maps:from_list(Backends)),
|
||||
|
||||
RouteOptsEnv = genlib_app:env(?MODULE, route_opts, #{}),
|
||||
EventHandlers = genlib_app:env(?MODULE, woody_event_handlers, [woody_event_handler_default]),
|
||||
EventHandlerOpts = genlib_app:env(?MODULE, scoper_event_handler_options, #{}),
|
||||
RouteOpts = RouteOptsEnv#{event_handler => {scoper_woody_event_handler, EventHandlerOpts}},
|
||||
|
||||
ChildSpec = woody_server:child_spec(
|
||||
?MODULE,
|
||||
#{
|
||||
ip => get_ip_address(),
|
||||
port => get_port(),
|
||||
protocol_opts => get_protocol_opts(),
|
||||
transport_opts => get_transport_opts(),
|
||||
shutdown_timeout => get_shutdown_timeout(),
|
||||
event_handler => EventHandlers,
|
||||
handlers => get_handler_specs(ServiceOpts),
|
||||
additional_routes =>
|
||||
machinery_mg_backend:get_routes(MachineHandlers, RouteOpts) ++
|
||||
machinery_modernizer_mg_backend:get_routes(ModernizerHandlers, RouteOpts) ++
|
||||
[erl_health_handle:get_route(Healthcheck)] ++ get_prometheus_route()
|
||||
}
|
||||
),
|
||||
{ok,
|
||||
{
|
||||
#{strategy => one_for_all, intensity => 6, period => 30},
|
||||
[ChildSpec]
|
||||
}}.
|
||||
|
||||
-spec get_ip_address() -> inet:ip_address().
|
||||
get_ip_address() ->
|
||||
{ok, Address} = inet:parse_address(genlib_app:env(?MODULE, ip, "::")),
|
||||
Address.
|
||||
|
||||
-spec get_port() -> inet:port_number().
|
||||
get_port() ->
|
||||
genlib_app:env(?MODULE, port, 8022).
|
||||
|
||||
-spec get_protocol_opts() -> woody_server_thrift_http_handler:protocol_opts().
|
||||
get_protocol_opts() ->
|
||||
genlib_app:env(?MODULE, protocol_opts, #{}).
|
||||
|
||||
-spec get_transport_opts() -> woody_server_thrift_http_handler:transport_opts().
|
||||
get_transport_opts() ->
|
||||
genlib_app:env(?MODULE, transport_opts, #{}).
|
||||
|
||||
-spec get_shutdown_timeout() -> timeout().
|
||||
get_shutdown_timeout() ->
|
||||
genlib_app:env(?MODULE, shutdown_timeout, 0).
|
||||
|
||||
-spec get_handler_specs(map()) -> [woody:http_handler(woody:th_handler())].
|
||||
get_handler_specs(ServiceOpts) ->
|
||||
LimiterService = maps:get(limiter, ServiceOpts, #{}),
|
||||
ConfiguratorService = maps:get(configurator, ServiceOpts, #{}),
|
||||
[
|
||||
{
|
||||
maps:get(path, LimiterService, <<"/v1/limiter">>),
|
||||
{{lim_limiter_thrift, 'Limiter'}, lim_handler}
|
||||
},
|
||||
{
|
||||
maps:get(path, ConfiguratorService, <<"/v1/configurator">>),
|
||||
{{lim_configurator_thrift, 'Configurator'}, lim_configurator}
|
||||
}
|
||||
].
|
||||
|
||||
%%
|
||||
|
||||
-spec enable_health_logging(erl_health:check()) -> erl_health:check().
|
||||
enable_health_logging(Check) ->
|
||||
EvHandler = {erl_health_event_handler, []},
|
||||
maps:map(
|
||||
fun(_, Runner) -> #{runner => Runner, event_handler => EvHandler} end,
|
||||
Check
|
||||
).
|
||||
|
||||
-spec get_prometheus_route() -> [{iodata(), module(), _Opts :: any()}].
|
||||
get_prometheus_route() ->
|
||||
[{"/metrics/[:registry]", prometheus_cowboy2_handler, []}].
|
||||
|
||||
contruct_backend_childspec(NS, Handler) ->
|
||||
Schema = get_namespace_schema(NS),
|
||||
{
|
||||
construct_machinery_backend_spec(NS, Schema),
|
||||
construct_machinery_handler_spec(NS, Handler, Schema),
|
||||
construct_machinery_modernizer_spec(NS, Schema)
|
||||
}.
|
||||
|
||||
construct_machinery_backend_spec(NS, Schema) ->
|
||||
{NS,
|
||||
{machinery_mg_backend, #{
|
||||
schema => Schema,
|
||||
client => get_service_client(automaton)
|
||||
}}}.
|
||||
|
||||
construct_machinery_handler_spec(NS, Handler, Schema) ->
|
||||
{Handler, #{
|
||||
path => lim_string:join(["/v1/stateproc/", NS]),
|
||||
backend_config => #{schema => Schema}
|
||||
}}.
|
||||
|
||||
construct_machinery_modernizer_spec(NS, Schema) ->
|
||||
#{
|
||||
path => lim_string:join(["/v1/modernizer/", NS]),
|
||||
backend_config => #{schema => Schema}
|
||||
}.
|
||||
|
||||
get_namespace_schema('lim/range_v1') ->
|
||||
lim_range_machinery_schema;
|
||||
get_namespace_schema('lim/config_v1') ->
|
||||
lim_config_machinery_schema.
|
||||
|
||||
get_service_client(ServiceID) ->
|
||||
case lim_client_woody:get_service_client_url(ServiceID) of
|
||||
undefined ->
|
||||
error({unknown_service, ServiceID});
|
||||
Url ->
|
||||
lim_utils:get_woody_client(Url)
|
||||
end.
|
78
apps/limiter/test/lim_client.erl
Normal file
78
apps/limiter/test/lim_client.erl
Normal file
@ -0,0 +1,78 @@
|
||||
-module(lim_client).
|
||||
|
||||
-include_lib("limiter_proto/include/lim_limiter_thrift.hrl").
|
||||
-include_lib("limiter_proto/include/lim_configurator_thrift.hrl").
|
||||
|
||||
-export([new/0]).
|
||||
-export([get/3]).
|
||||
-export([hold/3]).
|
||||
-export([commit/3]).
|
||||
|
||||
-export([create_config/2]).
|
||||
-export([get_config/2]).
|
||||
|
||||
-type client() :: woody_context:ctx().
|
||||
|
||||
-type limit_id() :: lim_limiter_thrift:'LimitID'().
|
||||
-type limit_change() :: lim_limiter_thrift:'LimitChange'().
|
||||
-type limit_context() :: lim_limiter_thrift:'LimitContext'().
|
||||
-type clock() :: lim_limiter_thrift:'Clock'().
|
||||
-type limit_config_params() :: lim_limiter_config_thrift:'LimitCreateParams'().
|
||||
|
||||
%%% API
|
||||
|
||||
-spec new() -> client().
|
||||
new() ->
|
||||
woody_context:new().
|
||||
|
||||
-spec get(limit_id(), limit_context(), client()) -> woody:result() | no_return().
|
||||
get(LimitID, Context, Client) ->
|
||||
call('Get', {LimitID, clock(), Context}, Client).
|
||||
|
||||
-spec hold(limit_change(), limit_context(), client()) -> woody:result() | no_return().
|
||||
hold(LimitChange, Context, Client) ->
|
||||
call('Hold', {LimitChange, clock(), Context}, Client).
|
||||
|
||||
-spec commit(limit_change(), limit_context(), client()) -> woody:result() | no_return().
|
||||
commit(LimitChange, Context, Client) ->
|
||||
call('Commit', {LimitChange, clock(), Context}, Client).
|
||||
|
||||
%%
|
||||
|
||||
-spec create_config(limit_config_params(), client()) -> woody:result() | no_return().
|
||||
create_config(LimitCreateParams, Client) ->
|
||||
call_configurator('Create', {LimitCreateParams}, Client).
|
||||
|
||||
-spec get_config(limit_id(), client()) -> woody:result() | no_return().
|
||||
get_config(LimitConfigID, Client) ->
|
||||
call_configurator('Get', {LimitConfigID}, Client).
|
||||
|
||||
%%% Internal functions
|
||||
|
||||
-spec call(atom(), tuple(), client()) -> woody:result() | no_return().
|
||||
call(Function, Args, Client) ->
|
||||
Call = {{lim_limiter_thrift, 'Limiter'}, Function, Args},
|
||||
Opts = #{
|
||||
url => <<"http://limiter:8022/v1/limiter">>,
|
||||
event_handler => scoper_woody_event_handler,
|
||||
transport_opts => #{
|
||||
max_connections => 10000
|
||||
}
|
||||
},
|
||||
woody_client:call(Call, Opts, Client).
|
||||
|
||||
-spec call_configurator(atom(), tuple(), client()) -> woody:result() | no_return().
|
||||
call_configurator(Function, Args, Client) ->
|
||||
Call = {{lim_configurator_thrift, 'Configurator'}, Function, Args},
|
||||
Opts = #{
|
||||
url => <<"http://limiter:8022/v1/configurator">>,
|
||||
event_handler => scoper_woody_event_handler,
|
||||
transport_opts => #{
|
||||
max_connections => 10000
|
||||
}
|
||||
},
|
||||
woody_client:call(Call, Opts, Client).
|
||||
|
||||
-spec clock() -> clock().
|
||||
clock() ->
|
||||
{vector, #limiter_VectorClock{state = <<>>}}.
|
104
apps/limiter/test/lim_configurator_SUITE.erl
Normal file
104
apps/limiter/test/lim_configurator_SUITE.erl
Normal file
@ -0,0 +1,104 @@
|
||||
-module(lim_configurator_SUITE).
|
||||
|
||||
-include_lib("stdlib/include/assert.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-include_lib("limiter_proto/include/lim_configurator_thrift.hrl").
|
||||
|
||||
-export([all/0]).
|
||||
|
||||
-export([groups/0]).
|
||||
-export([init_per_suite/1]).
|
||||
-export([end_per_suite/1]).
|
||||
-export([init_per_testcase/2]).
|
||||
-export([end_per_testcase/2]).
|
||||
|
||||
-export([create_config/1]).
|
||||
-export([get_config/1]).
|
||||
|
||||
-type test_case_name() :: atom().
|
||||
|
||||
-define(RATE_SOURCE_ID, <<"dummy_source_id">>).
|
||||
|
||||
%% tests descriptions
|
||||
|
||||
-spec all() -> [test_case_name()].
|
||||
all() ->
|
||||
[
|
||||
{group, default}
|
||||
].
|
||||
|
||||
-spec groups() -> [{atom(), list(), [test_case_name()]}].
|
||||
groups() ->
|
||||
[
|
||||
{default, [], [
|
||||
create_config,
|
||||
get_config
|
||||
]}
|
||||
].
|
||||
|
||||
-type config() :: [{atom(), any()}].
|
||||
|
||||
-spec init_per_suite(config()) -> config().
|
||||
init_per_suite(Config) ->
|
||||
% dbg:tracer(), dbg:p(all, c),
|
||||
% dbg:tpl({machinery, '_', '_'}, x),
|
||||
Apps =
|
||||
genlib_app:start_application_with(limiter, [
|
||||
{service_clients, #{
|
||||
accounter => #{
|
||||
url => <<"http://shumway:8022/accounter">>
|
||||
},
|
||||
automaton => #{
|
||||
url => <<"http://machinegun:8022/v1/automaton">>
|
||||
}
|
||||
}}
|
||||
]),
|
||||
[{apps, Apps}] ++ Config.
|
||||
|
||||
-spec end_per_suite(config()) -> _.
|
||||
end_per_suite(Config) ->
|
||||
[application:stop(App) || App <- proplists:get_value(apps, Config)],
|
||||
Config.
|
||||
|
||||
-spec init_per_testcase(test_case_name(), config()) -> config().
|
||||
init_per_testcase(_Name, C) ->
|
||||
C.
|
||||
|
||||
-spec end_per_testcase(test_case_name(), config()) -> config().
|
||||
end_per_testcase(_Name, _C) ->
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-spec create_config(config()) -> _.
|
||||
create_config(_C) ->
|
||||
Client = lim_client:new(),
|
||||
Params = #limiter_cfg_LimitCreateParams{
|
||||
id = <<"ID">>,
|
||||
name = <<"GlobalMonthTurnover">>,
|
||||
description = <<"description">>,
|
||||
started_at = <<"2000-01-01T00:00:00Z">>,
|
||||
body_type = {cash, #limiter_config_LimitBodyTypeCash{currency = <<"RUB">>}}
|
||||
},
|
||||
{ok, #limiter_config_LimitConfig{}} = lim_client:create_config(Params, Client).
|
||||
|
||||
-spec get_config(config()) -> _.
|
||||
get_config(C) ->
|
||||
ID = <<"ID">>,
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
{ok, #limiter_config_LimitConfig{id = ID}} = lim_client:get_config(ID, Client).
|
||||
|
||||
%%
|
||||
|
||||
prepare_environment(ID, LimitName, _C) ->
|
||||
Client = lim_client:new(),
|
||||
Params = #limiter_cfg_LimitCreateParams{
|
||||
id = ID,
|
||||
name = LimitName,
|
||||
description = <<"description">>,
|
||||
started_at = <<"2000-01-01T00:00:00Z">>,
|
||||
body_type = {cash, #limiter_config_LimitBodyTypeCash{currency = <<"RUB">>}}
|
||||
},
|
||||
{ok, LimitConfig} = lim_client:create_config(Params, Client),
|
||||
#{config => LimitConfig, client => Client}.
|
85
apps/limiter/test/lim_mock.erl
Normal file
85
apps/limiter/test/lim_mock.erl
Normal file
@ -0,0 +1,85 @@
|
||||
-module(lim_mock).
|
||||
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-export([start_mocked_service_sup/0]).
|
||||
-export([stop_mocked_service_sup/1]).
|
||||
-export([mock_services/2]).
|
||||
|
||||
-define(APP, limiter).
|
||||
|
||||
-spec start_mocked_service_sup() -> _.
|
||||
start_mocked_service_sup() ->
|
||||
{ok, SupPid} = genlib_adhoc_supervisor:start_link(#{}, []),
|
||||
_ = unlink(SupPid),
|
||||
SupPid.
|
||||
|
||||
-spec stop_mocked_service_sup(pid()) -> _.
|
||||
stop_mocked_service_sup(SupPid) ->
|
||||
exit(SupPid, shutdown).
|
||||
|
||||
-define(HOST_IP, "::").
|
||||
-define(HOST_PORT, 8080).
|
||||
-define(HOST_NAME, "localhost").
|
||||
-define(HOST_URL, ?HOST_NAME ++ ":" ++ integer_to_list(?HOST_PORT)).
|
||||
|
||||
-spec mock_services(_, _) -> _.
|
||||
mock_services(Services, SupOrConfig) ->
|
||||
maps:map(fun set_cfg/2, mock_services_(Services, SupOrConfig)).
|
||||
|
||||
set_cfg(Service, Url) ->
|
||||
{ok, Clients} = application:get_env(?APP, service_clients),
|
||||
#{Service := Cfg} = Clients,
|
||||
ok = application:set_env(
|
||||
?APP,
|
||||
service_clients,
|
||||
Clients#{Service => Cfg#{url => Url}}
|
||||
).
|
||||
|
||||
mock_services_(Services, Config) when is_list(Config) ->
|
||||
mock_services_(Services, ?config(test_sup, Config));
|
||||
mock_services_(Services, SupPid) when is_pid(SupPid) ->
|
||||
Name = lists:map(fun get_service_name/1, Services),
|
||||
|
||||
{ok, IP} = inet:parse_address(?HOST_IP),
|
||||
ServerID = {dummy, Name},
|
||||
Options = #{
|
||||
ip => IP,
|
||||
port => 0,
|
||||
event_handler => scoper_woody_event_handler,
|
||||
handlers => lists:map(fun mock_service_handler/1, Services),
|
||||
transport_opts => #{num_acceptors => 1}
|
||||
},
|
||||
ChildSpec = woody_server:child_spec(ServerID, Options),
|
||||
{ok, _} = supervisor:start_child(SupPid, ChildSpec),
|
||||
{IP, Port} = woody_server:get_addr(ServerID, Options),
|
||||
lists:foldl(
|
||||
fun(Service, Acc) ->
|
||||
ServiceName = get_service_name(Service),
|
||||
Acc#{ServiceName => make_url(ServiceName, Port)}
|
||||
end,
|
||||
#{},
|
||||
Services
|
||||
).
|
||||
|
||||
get_service_name({ServiceName, _Fun}) ->
|
||||
ServiceName;
|
||||
get_service_name({ServiceName, _WoodyService, _Fun}) ->
|
||||
ServiceName.
|
||||
|
||||
mock_service_handler({ServiceName, Fun}) ->
|
||||
mock_service_handler(ServiceName, get_service_modname(ServiceName), Fun);
|
||||
mock_service_handler({ServiceName, WoodyService, Fun}) ->
|
||||
mock_service_handler(ServiceName, WoodyService, Fun).
|
||||
|
||||
mock_service_handler(ServiceName, WoodyService, Fun) ->
|
||||
{make_path(ServiceName), {WoodyService, {lim_mock_service, #{function => Fun}}}}.
|
||||
|
||||
get_service_modname(xrates) ->
|
||||
{xrates_rate_thrift, 'Rates'}.
|
||||
|
||||
make_url(ServiceName, Port) ->
|
||||
iolist_to_binary(["http://", ?HOST_NAME, ":", integer_to_list(Port), make_path(ServiceName)]).
|
||||
|
||||
make_path(ServiceName) ->
|
||||
"/" ++ atom_to_list(ServiceName).
|
9
apps/limiter/test/lim_mock_service.erl
Normal file
9
apps/limiter/test/lim_mock_service.erl
Normal file
@ -0,0 +1,9 @@
|
||||
-module(lim_mock_service).
|
||||
|
||||
-behaviour(woody_server_thrift_handler).
|
||||
|
||||
-export([handle_function/4]).
|
||||
|
||||
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), #{}) -> {ok, term()}.
|
||||
handle_function(FunName, Args, _, #{function := Fun}) ->
|
||||
Fun(FunName, Args).
|
321
apps/limiter/test/lim_turnover_SUITE.erl
Normal file
321
apps/limiter/test/lim_turnover_SUITE.erl
Normal file
@ -0,0 +1,321 @@
|
||||
-module(lim_turnover_SUITE).
|
||||
|
||||
-include_lib("stdlib/include/assert.hrl").
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
|
||||
-include_lib("limiter_proto/include/lim_configurator_thrift.hrl").
|
||||
-include_lib("xrates_proto/include/xrates_rate_thrift.hrl").
|
||||
|
||||
-export([all/0]).
|
||||
|
||||
-export([groups/0]).
|
||||
-export([init_per_suite/1]).
|
||||
-export([end_per_suite/1]).
|
||||
-export([init_per_testcase/2]).
|
||||
-export([end_per_testcase/2]).
|
||||
|
||||
-export([commit_with_default_exchange/1]).
|
||||
-export([partial_commit_with_exchange/1]).
|
||||
-export([commit_with_exchange/1]).
|
||||
-export([get_rate/1]).
|
||||
-export([get_limit_notfound/1]).
|
||||
-export([hold_ok/1]).
|
||||
-export([commit_ok/1]).
|
||||
-export([rollback_ok/1]).
|
||||
-export([get_config_ok/1]).
|
||||
|
||||
-type test_case_name() :: atom().
|
||||
|
||||
-define(RATE_SOURCE_ID, <<"dummy_source_id">>).
|
||||
|
||||
%% tests descriptions
|
||||
|
||||
-spec all() -> [test_case_name()].
|
||||
all() ->
|
||||
[
|
||||
{group, default}
|
||||
].
|
||||
|
||||
-spec groups() -> [{atom(), list(), [test_case_name()]}].
|
||||
groups() ->
|
||||
[
|
||||
{default, [], [
|
||||
commit_with_default_exchange,
|
||||
partial_commit_with_exchange,
|
||||
commit_with_exchange,
|
||||
get_rate,
|
||||
get_limit_notfound,
|
||||
hold_ok,
|
||||
commit_ok,
|
||||
rollback_ok,
|
||||
get_config_ok
|
||||
]}
|
||||
].
|
||||
|
||||
-type config() :: [{atom(), any()}].
|
||||
|
||||
-spec init_per_suite(config()) -> config().
|
||||
init_per_suite(Config) ->
|
||||
% dbg:tracer(), dbg:p(all, c),
|
||||
% dbg:tpl({lim_handler, '_', '_'}, x),
|
||||
Apps =
|
||||
genlib_app:start_application_with(limiter, [
|
||||
{service_clients, #{
|
||||
accounter => #{
|
||||
url => <<"http://shumway:8022/accounter">>
|
||||
},
|
||||
automaton => #{
|
||||
url => <<"http://machinegun:8022/v1/automaton">>
|
||||
},
|
||||
xrates => #{
|
||||
url => <<"http://xrates:8022/xrates">>
|
||||
}
|
||||
}},
|
||||
{exchange_factors, #{
|
||||
<<"DEFAULT">> => {1, 1},
|
||||
<<"USD">> => {105, 100},
|
||||
<<"EUR">> => {12, 10}
|
||||
}}
|
||||
]),
|
||||
[{apps, Apps}] ++ Config.
|
||||
|
||||
-spec end_per_suite(config()) -> _.
|
||||
end_per_suite(Config) ->
|
||||
[application:stop(App) || App <- proplists:get_value(apps, Config)],
|
||||
Config.
|
||||
|
||||
-spec init_per_testcase(test_case_name(), config()) -> config().
|
||||
init_per_testcase(_Name, C) ->
|
||||
[{test_sup, lim_mock:start_mocked_service_sup()} | C].
|
||||
|
||||
-spec end_per_testcase(test_case_name(), config()) -> config().
|
||||
end_per_testcase(_Name, C) ->
|
||||
lim_mock:stop_mocked_service_sup(?config(test_sup, C)),
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-spec commit_with_default_exchange(config()) -> _.
|
||||
commit_with_default_exchange(C) ->
|
||||
Rational = #base_Rational{p = 1000000, q = 100},
|
||||
mock_exchange(Rational, C),
|
||||
ID = lim_time:to_rfc3339(lim_time:now()),
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
Context = #limiter_context_LimitContext{
|
||||
payment_processing = #limiter_context_ContextPaymentProcessing{
|
||||
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
|
||||
invoice = #limiter_context_Invoice{
|
||||
created_at = <<"2000-01-01T00:00:00Z">>,
|
||||
cost = #limiter_base_Cash{
|
||||
amount = 10000,
|
||||
currency = #limiter_base_CurrencyRef{symbolic_code = <<"SOME_CURRENCY">>}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timestamp = lim_time:to_rfc3339(lim_time:now()),
|
||||
LimitChangeID = <<Timestamp/binary, "Commit">>,
|
||||
Change = #limiter_LimitChange{
|
||||
id = ID,
|
||||
change_id = LimitChangeID
|
||||
},
|
||||
{ok, {vector, _}} = hold_and_commit(Change, Context, Client),
|
||||
{ok, #limiter_Limit{amount = 10000}} = lim_client:get(ID, Context, Client).
|
||||
|
||||
-spec partial_commit_with_exchange(config()) -> _.
|
||||
partial_commit_with_exchange(C) ->
|
||||
Rational = #base_Rational{p = 800000, q = 100},
|
||||
mock_exchange(Rational, C),
|
||||
ID = lim_time:to_rfc3339(lim_time:now()),
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
Context = #limiter_context_LimitContext{
|
||||
payment_processing = #limiter_context_ContextPaymentProcessing{
|
||||
op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}},
|
||||
invoice = #limiter_context_Invoice{
|
||||
effective_payment = #limiter_context_InvoicePayment{
|
||||
created_at = <<"2000-01-01T00:00:00Z">>,
|
||||
cost = #limiter_base_Cash{
|
||||
amount = 10000,
|
||||
currency = #limiter_base_CurrencyRef{symbolic_code = <<"USD">>}
|
||||
},
|
||||
capture_cost = #limiter_base_Cash{
|
||||
amount = 8000,
|
||||
currency = #limiter_base_CurrencyRef{symbolic_code = <<"USD">>}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timestamp = lim_time:to_rfc3339(lim_time:now()),
|
||||
LimitChangeID = <<Timestamp/binary, "PartialCommit">>,
|
||||
Change = #limiter_LimitChange{
|
||||
id = ID,
|
||||
change_id = LimitChangeID
|
||||
},
|
||||
{ok, {vector, _}} = hold_and_commit(Change, Context, Client),
|
||||
{ok, #limiter_Limit{amount = 8400}} = lim_client:get(ID, Context, Client).
|
||||
|
||||
-spec commit_with_exchange(config()) -> _.
|
||||
commit_with_exchange(C) ->
|
||||
Rational = #base_Rational{p = 1000000, q = 100},
|
||||
mock_exchange(Rational, C),
|
||||
ID = lim_time:to_rfc3339(lim_time:now()),
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
Context = #limiter_context_LimitContext{
|
||||
payment_processing = #limiter_context_ContextPaymentProcessing{
|
||||
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
|
||||
invoice = #limiter_context_Invoice{
|
||||
created_at = <<"2000-01-01T00:00:00Z">>,
|
||||
cost = #limiter_base_Cash{
|
||||
amount = 10000,
|
||||
currency = #limiter_base_CurrencyRef{symbolic_code = <<"USD">>}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timestamp = lim_time:to_rfc3339(lim_time:now()),
|
||||
LimitChangeID = <<Timestamp/binary, "Commit">>,
|
||||
Change = #limiter_LimitChange{
|
||||
id = ID,
|
||||
change_id = LimitChangeID
|
||||
},
|
||||
{ok, {vector, _}} = hold_and_commit(Change, Context, Client),
|
||||
{ok, #limiter_Limit{amount = 10500}} = lim_client:get(ID, Context, Client).
|
||||
|
||||
-spec get_rate(config()) -> _.
|
||||
get_rate(C) ->
|
||||
Rational = #base_Rational{p = 10, q = 10},
|
||||
mock_exchange(Rational, C),
|
||||
Request = #rate_ConversionRequest{
|
||||
source = <<"RUB">>,
|
||||
destination = <<"USD">>,
|
||||
amount = 100,
|
||||
datetime = <<"Timestamp">>
|
||||
},
|
||||
WoodyContext = woody_context:new(),
|
||||
{ok, Rational} = lim_client_woody:call(
|
||||
xrates,
|
||||
'GetConvertedAmount',
|
||||
{?RATE_SOURCE_ID, Request},
|
||||
WoodyContext
|
||||
).
|
||||
|
||||
-spec get_limit_notfound(config()) -> _.
|
||||
get_limit_notfound(C) ->
|
||||
ID = lim_time:to_rfc3339(lim_time:now()),
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
Context = #limiter_context_LimitContext{
|
||||
payment_processing = #limiter_context_ContextPaymentProcessing{
|
||||
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
|
||||
invoice = #limiter_context_Invoice{created_at = <<"2000-01-01T00:00:00Z">>}
|
||||
}
|
||||
},
|
||||
{exception, #limiter_LimitNotFound{}} = lim_client:get(ID, Context, Client).
|
||||
|
||||
-spec hold_ok(config()) -> _.
|
||||
hold_ok(C) ->
|
||||
ID = <<"ID">>,
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
Context = #limiter_context_LimitContext{
|
||||
payment_processing = #limiter_context_ContextPaymentProcessing{
|
||||
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
|
||||
invoice = #limiter_context_Invoice{
|
||||
created_at = <<"2000-01-01T00:00:00Z">>,
|
||||
cost = #limiter_base_Cash{
|
||||
amount = 10,
|
||||
currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timestamp = lim_time:to_rfc3339(lim_time:now()),
|
||||
LimitChangeID = <<Timestamp/binary, "Hold">>,
|
||||
Change = #limiter_LimitChange{
|
||||
id = ID,
|
||||
change_id = LimitChangeID
|
||||
},
|
||||
{ok, {vector, #limiter_VectorClock{}}} = lim_client:hold(Change, Context, Client),
|
||||
{ok, #limiter_Limit{}} = lim_client:get(ID, Context, Client).
|
||||
|
||||
-spec commit_ok(config()) -> _.
|
||||
commit_ok(C) ->
|
||||
ID = <<"ID">>,
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
Context = #limiter_context_LimitContext{
|
||||
payment_processing = #limiter_context_ContextPaymentProcessing{
|
||||
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
|
||||
invoice = #limiter_context_Invoice{
|
||||
created_at = <<"2000-01-01T00:00:00Z">>,
|
||||
cost = #limiter_base_Cash{
|
||||
amount = 10,
|
||||
currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timestamp = lim_time:to_rfc3339(lim_time:now()),
|
||||
LimitChangeID = <<Timestamp/binary, "Commit">>,
|
||||
Change = #limiter_LimitChange{
|
||||
id = ID,
|
||||
change_id = LimitChangeID
|
||||
},
|
||||
{ok, {vector, _}} = hold_and_commit(Change, Context, Client),
|
||||
{ok, #limiter_Limit{}} = lim_client:get(ID, Context, Client).
|
||||
|
||||
-spec rollback_ok(config()) -> _.
|
||||
rollback_ok(C) ->
|
||||
ID = <<"ID">>,
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
Context = #limiter_context_LimitContext{
|
||||
payment_processing = #limiter_context_ContextPaymentProcessing{
|
||||
op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}},
|
||||
invoice = #limiter_context_Invoice{
|
||||
effective_payment = #limiter_context_InvoicePayment{
|
||||
created_at = <<"2000-01-01T00:00:00Z">>,
|
||||
cost = #limiter_base_Cash{
|
||||
amount = 10,
|
||||
currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>}
|
||||
},
|
||||
capture_cost = #limiter_base_Cash{
|
||||
amount = 0,
|
||||
currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Timestamp = lim_time:to_rfc3339(lim_time:now()),
|
||||
LimitChangeID = <<Timestamp/binary, "Rollback">>,
|
||||
Change = #limiter_LimitChange{
|
||||
id = ID,
|
||||
change_id = LimitChangeID
|
||||
},
|
||||
{ok, {vector, _}} = hold_and_commit(Change, Context, Client),
|
||||
{ok, #limiter_Limit{}} = lim_client:get(ID, Context, Client).
|
||||
|
||||
-spec get_config_ok(config()) -> _.
|
||||
get_config_ok(C) ->
|
||||
ID = <<"ID">>,
|
||||
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
|
||||
{ok, #limiter_config_LimitConfig{}} = lim_client:get_config(ID, Client).
|
||||
|
||||
%%
|
||||
|
||||
hold_and_commit(Change, Context, Client) ->
|
||||
{ok, {vector, _}} = lim_client:hold(Change, Context, Client),
|
||||
{ok, {vector, _}} = lim_client:commit(Change, Context, Client).
|
||||
|
||||
mock_exchange(Rational, C) ->
|
||||
lim_mock:mock_services([{xrates, fun('GetConvertedAmount', _) -> {ok, Rational} end}], C).
|
||||
|
||||
prepare_environment(ID, LimitName, _C) ->
|
||||
Client = lim_client:new(),
|
||||
Params = #limiter_cfg_LimitCreateParams{
|
||||
id = ID,
|
||||
name = LimitName,
|
||||
description = <<"description">>,
|
||||
started_at = <<"2000-01-01T00:00:00Z">>,
|
||||
body_type = {cash, #limiter_config_LimitBodyTypeCash{currency = <<"RUB">>}}
|
||||
},
|
||||
{ok, LimitConfig} = lim_client:create_config(Params, Client),
|
||||
#{config => LimitConfig, client => Client}.
|
@ -13,8 +13,21 @@
|
||||
{service_clients, #{
|
||||
accounter => #{
|
||||
url => <<"http://shumway:8022/accounter">>
|
||||
},
|
||||
automaton => #{
|
||||
url => <<"http://machinegun:8022/v1/automaton">>
|
||||
},
|
||||
xrates => #{
|
||||
url => <<"http://xrates:8022/xrates">>
|
||||
}
|
||||
}},
|
||||
|
||||
{exchange_factors, #{
|
||||
<<"DEFAULT">> => {1, 1},
|
||||
<<"USD">> => {105, 100},
|
||||
<<"EUR">> => {12, 10}
|
||||
}},
|
||||
|
||||
{protocol_opts, #{
|
||||
% How much to wait for another request before closing a keepalive connection? (ms)
|
||||
request_timeout => 5000,
|
||||
@ -42,6 +55,15 @@
|
||||
}}
|
||||
]},
|
||||
|
||||
{scoper_event_handler_options, #{
|
||||
event_handler_opts => #{
|
||||
formatter_opts => #{
|
||||
max_length => 1000,
|
||||
max_printable_string_length => 80
|
||||
}
|
||||
}
|
||||
}},
|
||||
|
||||
{health_check, #{
|
||||
% disk => {erl_health, disk , ["/", 99]},
|
||||
% memory => {erl_health, cg_memory, [99]},
|
||||
|
@ -11,9 +11,24 @@ services:
|
||||
working_dir: $PWD
|
||||
command: /sbin/init
|
||||
depends_on:
|
||||
machinegun:
|
||||
condition: service_healthy
|
||||
shumway:
|
||||
condition: service_healthy
|
||||
|
||||
machinegun:
|
||||
image: dr2.rbkmoney.com/rbkmoney/machinegun:0da2ffc23221e1e3f8557b03d48d11d2dd18fac0
|
||||
command: /opt/machinegun/bin/machinegun foreground
|
||||
volumes:
|
||||
- ./test/machinegun/config.yaml:/opt/machinegun/etc/config.yaml
|
||||
- ./test/log/machinegun:/var/log/machinegun
|
||||
- ./test/machinegun/cookie:/opt/machinegun/etc/cookie
|
||||
healthcheck:
|
||||
test: "curl http://localhost:8022/"
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 10
|
||||
|
||||
shumway:
|
||||
image: dr2.rbkmoney.com/rbkmoney/shumway:e946e83703e02f4359cd536b15fb94457f9bfb20
|
||||
restart: unless-stopped
|
||||
|
@ -20,7 +20,7 @@
|
||||
{elvis_style, function_naming_convention, #{regex => "^([a-z][a-z0-9]*_?)*$"}},
|
||||
{elvis_style, state_record_and_type},
|
||||
{elvis_style, no_spec_with_records},
|
||||
{elvis_style, dont_repeat_yourself, #{min_complexity => 10}},
|
||||
{elvis_style, dont_repeat_yourself, #{min_complexity => 10, ignore => [lim_config_machine]}},
|
||||
{elvis_style, no_debug_call, #{ignore => [elvis, elvis_utils]}}
|
||||
]
|
||||
},
|
||||
@ -43,7 +43,7 @@
|
||||
{elvis_style, function_naming_convention, #{regex => "^([a-z][a-z0-9]*_?)*$"}},
|
||||
{elvis_style, state_record_and_type},
|
||||
{elvis_style, no_spec_with_records},
|
||||
{elvis_style, dont_repeat_yourself, #{min_complexity => 10}},
|
||||
{elvis_style, dont_repeat_yourself, #{min_complexity => 10, ignore => [lim_turnover_SUITE]}},
|
||||
{elvis_style, no_debug_call, #{ignore => [elvis, elvis_utils]}}
|
||||
]
|
||||
},
|
||||
|
14
rebar.config
14
rebar.config
@ -31,6 +31,20 @@
|
||||
{branch, "release/erlang/master"}
|
||||
}
|
||||
},
|
||||
{limiter_proto,
|
||||
{git, "git@github.com:rbkmoney/limiter-proto.git",
|
||||
{branch, "master"}
|
||||
}
|
||||
},
|
||||
{xrates_proto,
|
||||
{git, "git@github.com:rbkmoney/xrates-proto.git",
|
||||
{branch, "master"}
|
||||
}
|
||||
},
|
||||
{machinery,
|
||||
{git, "https://github.com/rbkmoney/machinery.git",
|
||||
{branch, "master"}}
|
||||
},
|
||||
{erl_health,
|
||||
{git, "https://github.com/rbkmoney/erlang-health.git",
|
||||
{branch, "master"}}
|
||||
|
16
rebar.lock
16
rebar.lock
@ -33,7 +33,19 @@
|
||||
0},
|
||||
{<<"idna">>,{pkg,<<"idna">>,<<"6.0.0">>},2},
|
||||
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.0.0">>},1},
|
||||
{<<"limiter_proto">>,
|
||||
{git,"git@github.com:rbkmoney/limiter-proto.git",
|
||||
{ref,"84cc6b7355aa838c2a91dfab64d000f57ff63bf7"}},
|
||||
0},
|
||||
{<<"machinery">>,
|
||||
{git,"https://github.com/rbkmoney/machinery.git",
|
||||
{ref,"db7c94b9913451e9558afa19f2fe77bf48d391da"}},
|
||||
0},
|
||||
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
|
||||
{<<"mg_proto">>,
|
||||
{git,"https://github.com/rbkmoney/machinegun_proto.git",
|
||||
{ref,"d814d6948d4ff13f6f41d12c6613f59c805750b2"}},
|
||||
1},
|
||||
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},2},
|
||||
{<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.0">>},3},
|
||||
{<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.6.0">>},0},
|
||||
@ -57,6 +69,10 @@
|
||||
{<<"woody">>,
|
||||
{git,"https://github.com/rbkmoney/woody_erlang.git",
|
||||
{ref,"58f56b462429ab1fee65e1bdb34b73512406ba00"}},
|
||||
0},
|
||||
{<<"xrates_proto">>,
|
||||
{git,"git@github.com:rbkmoney/xrates-proto.git",
|
||||
{ref,"66906cd0a8ee9a00fb447f3c3e5b09d3c6fab942"}},
|
||||
0}]}.
|
||||
[
|
||||
{pkg_hash,[
|
||||
|
17
test/machinegun/config.yaml
Normal file
17
test/machinegun/config.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
service_name: machinegun
|
||||
|
||||
erlang:
|
||||
secret_cookie_file: "/opt/machinegun/etc/cookie"
|
||||
|
||||
namespaces:
|
||||
lim/config_v1:
|
||||
processor:
|
||||
url: http://limiter:8022/v1/stateproc/lim/config_v1
|
||||
pool_size: 500
|
||||
lim/range_v1:
|
||||
processor:
|
||||
url: http://limiter:8022/v1/stateproc/lim/range_v1
|
||||
pool_size: 500
|
||||
|
||||
storage:
|
||||
type: memory
|
1
test/machinegun/cookie
Normal file
1
test/machinegun/cookie
Normal file
@ -0,0 +1 @@
|
||||
BENDER-COOKIE-MONSTER
|
Loading…
Reference in New Issue
Block a user