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:
Артем 2021-05-17 13:34:16 +03:00 committed by GitHub
parent 563fb97ea6
commit 9bf3e29636
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 4007 additions and 3 deletions

View File

@ -74,4 +74,4 @@ distclean:
# CALL_W_CONTAINER
test: submodules
$(REBAR) ct
$(REBAR) do eunit, ct

View 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).

View 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}}.

View 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.

View 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.

View 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.

View 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.

View 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}}.

View 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).

View 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.

View 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]).

View 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.

View 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}.

View 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.

View 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.

View 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]}.

View 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.

View 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).

View 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}.

View 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)).

View 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}.

View 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.

View 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
]).

View File

@ -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

View 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.

View 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 = <<>>}}.

View 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}.

View 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).

View 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).

View 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}.

View File

@ -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]},

View File

@ -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

View File

@ -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]}}
]
},

View File

@ -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"}}

View File

@ -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,[

View 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
View File

@ -0,0 +1 @@
BENDER-COOKIE-MONSTER