diff --git a/.github/workflows/erlang-checks.yml b/.github/workflows/erlang-checks.yml index 638bb2e2..18e7d100 100644 --- a/.github/workflows/erlang-checks.yml +++ b/.github/workflows/erlang-checks.yml @@ -38,5 +38,5 @@ jobs: thrift-version: ${{ needs.setup.outputs.thrift-version }} run-ct-with-compose: true use-coveralls: true - cache-version: v103 + cache-version: v107 upload-coverage: false diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 0d3ffccf..00000000 --- a/TODO.md +++ /dev/null @@ -1,13 +0,0 @@ -# Invoicing - -* Handle error properly while calling `Automaton`, perfect to pass them untouched with the help of latest `woody` release. -* Better and easier to compehend flow control in machines. -* More familiar flow control handling of machines, e.g. catching and wrapping thrown exceptions. -* Explicit stage denotion in the invoice machine? -* __Submachine abstraction and payment submachine implementation__. -* __Invoice access control__. -* __Proper behaviours around machines w/ internal datastructures marshalling, event sources and dispatching.__ - -# Tests - -* __Add generic albeit more complex test suite which covers as many state transitions with expected effects as possible__. diff --git a/apps/ff_cth/src/ct_helper.erl b/apps/ff_cth/src/ct_helper.erl index 2b1ca745..98f8cebf 100644 --- a/apps/ff_cth/src/ct_helper.erl +++ b/apps/ff_cth/src/ct_helper.erl @@ -197,8 +197,9 @@ stop_app(AppName) -> -spec set_context(config()) -> ok. set_context(C) -> - ok = ff_context:save( - ff_context:create(#{ + ok = op_context:save( + op_context:key(fistful), + op_context:create(#{ party_client => party_client:create_client(), woody_context => cfg('$woody_ctx', C) }) @@ -206,7 +207,7 @@ set_context(C) -> -spec unset_context() -> ok. unset_context() -> - ok = ff_context:cleanup(). + ok = op_context:cleanup(fistful). %% diff --git a/apps/ff_cth/src/ct_limiter.erl b/apps/ff_cth/src/ct_limiter.erl index 897605a4..52b90424 100644 --- a/apps/ff_cth/src/ct_limiter.erl +++ b/apps/ff_cth/src/ct_limiter.erl @@ -13,7 +13,7 @@ -type withdrawal() :: ff_withdrawal:withdrawal_state() | dmsl_wthd_domain_thrift:'Withdrawal'(). -type limit() :: limproto_limiter_thrift:'Limit'(). --type config() :: ct_suite:ct_config(). +-type config() :: ct_helper:config(). -type id() :: binary(). -spec init_per_suite(config()) -> _. diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index c9edc197..4191a6d4 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -83,7 +83,6 @@ start_processing_apps(Options) -> dmt_client, party_client, {fistful, [ - {machinery_backend, progressor}, {services, services(Options)} ]}, ff_server, @@ -150,6 +149,7 @@ start_optional_apps(_) -> setup_dominant(Config0, Options) -> Config1 = setup_dominant_internal(Config0, Options), + _ = ct_domain_config:cleanup(), Config2 = ct_limiter:init_per_suite(Config1), DomainConfig = domain_config(Config2, Options), _ = ct_domain_config:upsert(DomainConfig), @@ -250,56 +250,41 @@ progressor_namespaces() -> #{ 'ff/source_v1' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/source_v1', - %% TODO Party client create - handler => {fistful, #{handler => ff_source_machine, party_client => #{}}}, - schema => ff_source_machinery_schema + ns => 'ff/source_v1' } } }, 'ff/destination_v2' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/destination_v2', - %% TODO Party client create - handler => {fistful, #{handler => ff_destination_machine, party_client => #{}}}, - schema => ff_destination_machinery_schema + ns => 'ff/destination_v2' } } }, 'ff/deposit_v1' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/deposit_v1', - %% TODO Party client create - handler => {fistful, #{handler => ff_deposit_machine, party_client => #{}}}, - schema => ff_deposit_machinery_schema + ns => 'ff/deposit_v1' } } }, 'ff/withdrawal_v2' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/withdrawal_v2', - %% TODO Party client create - handler => {fistful, #{handler => ff_withdrawal_machine, party_client => #{}}}, - schema => ff_withdrawal_machinery_schema + ns => 'ff/withdrawal_v2' } } }, 'ff/withdrawal/session_v2' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/withdrawal/session_v2', - %% TODO Party client create - handler => {fistful, #{handler => ff_withdrawal_session_machine, party_client => #{}}}, - schema => ff_withdrawal_session_machinery_schema + ns => 'ff/withdrawal/session_v2' } } } diff --git a/apps/ff_server/src/ff_codec.erl b/apps/ff_server/src/ff_codec.erl index 42449f1a..2fc078c4 100644 --- a/apps/ff_server/src/ff_codec.erl +++ b/apps/ff_server/src/ff_codec.erl @@ -23,12 +23,15 @@ -type decoded_value() :: decoded_value(any()). -type decoded_value(T) :: T. +-type timestamp() :: {calendar:datetime(), non_neg_integer()}. + -export_type([codec/0]). -export_type([type_name/0]). -export_type([encoded_value/0]). -export_type([encoded_value/1]). -export_type([decoded_value/0]). -export_type([decoded_value/1]). +-export_type([timestamp/0]). %% Callbacks @@ -298,7 +301,7 @@ marshal(_, Other) -> -spec unmarshal(type_name(), encoded_value()) -> decoded_value(). unmarshal({list, T}, V) -> - [marshal(T, E) || E <- V]; + [unmarshal(T, E) || E <- V]; unmarshal({set, T}, V) -> ordsets:from_list([unmarshal(T, E) || E <- ordsets:to_list(V)]); unmarshal(id, V) -> @@ -360,25 +363,13 @@ unmarshal(three_ds_verification, Value) when Value =:= authentication_could_not_be_performed -> Value; +unmarshal(complex_action, undefined) -> + undefined; unmarshal(complex_action, #repairer_ComplexAction{ timer = TimerAction, remove = RemoveAction }) -> - unmarshal(timer_action, TimerAction) ++ unmarshal(remove_action, RemoveAction); -unmarshal(timer_action, undefined) -> - []; -unmarshal(timer_action, {set_timer, SetTimerAction}) -> - [{set_timer, unmarshal(set_timer_action, SetTimerAction)}]; -unmarshal(timer_action, {unset_timer, #repairer_UnsetTimerAction{}}) -> - [unset_timer]; -unmarshal(remove_action, undefined) -> - []; -unmarshal(remove_action, #repairer_RemoveAction{}) -> - [remove]; -unmarshal(set_timer_action, #repairer_SetTimerAction{ - timer = Timer -}) -> - unmarshal(timer, Timer); + unmarshal_repairer_complex_action(TimerAction, RemoveAction); unmarshal(timer, {timeout, Timeout}) -> {timeout, unmarshal(integer, Timeout)}; unmarshal(timer, {deadline, Deadline}) -> @@ -576,6 +567,15 @@ unmarshal(range, #'fistful_base_EventRange'{ unmarshal(bool, V) when is_boolean(V) -> V. +unmarshal_repairer_complex_action(_, #repairer_RemoveAction{}) -> + remove; +unmarshal_repairer_complex_action(undefined, undefined) -> + idle; +unmarshal_repairer_complex_action({set_timer, #repairer_SetTimerAction{timer = Timer}}, undefined) -> + prg_action:schedule_timer(unmarshal(timer, Timer)); +unmarshal_repairer_complex_action({unset_timer, _}, undefined) -> + suspend. + maybe_unmarshal(_Type, undefined) -> undefined; maybe_unmarshal(Type, Value) -> @@ -586,7 +586,7 @@ maybe_marshal(_Type, undefined) -> maybe_marshal(Type, Value) -> marshal(Type, Value). --spec parse_timestamp(binary()) -> machinery:timestamp(). +-spec parse_timestamp(binary()) -> timestamp(). parse_timestamp(Bin) -> try MicroSeconds = genlib_rfc3339:parse(Bin, microsecond), diff --git a/apps/ff_server/src/ff_deposit_codec.erl b/apps/ff_server/src/ff_deposit_codec.erl index 2a9fc92c..716793a9 100644 --- a/apps/ff_server/src/ff_deposit_codec.erl +++ b/apps/ff_server/src/ff_deposit_codec.erl @@ -211,7 +211,7 @@ deposit_timestamped_change_codec_test() -> external_id => genlib:unique() }, Change = {created, Deposit}, - TimestampedChange = {ev, machinery_time:now(), Change}, + TimestampedChange = {ev, {calendar:universal_time(), 0}, Change}, Type = {struct, struct, {fistful_deposit_thrift, 'TimestampedChange'}}, Binary = ff_proto_utils:serialize(Type, marshal(timestamped_change, TimestampedChange)), Decoded = ff_proto_utils:deserialize(Type, Binary), diff --git a/apps/ff_server/src/ff_deposit_machinery_schema.erl b/apps/ff_server/src/ff_deposit_machinery_schema.erl deleted file mode 100644 index 57f436b4..00000000 --- a/apps/ff_server/src/ff_deposit_machinery_schema.erl +++ /dev/null @@ -1,88 +0,0 @@ --module(ff_deposit_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() :: ff_machine:timestamped_event(ff_deposit:event()). --type aux_state() :: ff_machine:auxst(). --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, Format}, TimestampedChange, Context) -> - marshal_event(Format, 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()) -> {value(data()), context()}. -unmarshal({event, FormatVersion}, EncodedChange, Context) -> - unmarshal_event(FormatVersion, EncodedChange, Context); -unmarshal({aux_state, undefined} = T, V, C0) -> - {AuxState, C1} = machinery_mg_schema_generic:unmarshal(T, V, C0), - {AuxState, C1#{ctx => get_aux_state_ctx(AuxState)}}; -unmarshal(T, V, C) when - T =:= {args, init} orelse - T =:= {args, call} orelse - T =:= {args, repair} 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 = ff_deposit_codec:marshal(timestamped_change, TimestampedChange), - Type = {struct, struct, {fistful_deposit_thrift, 'TimestampedChange'}}, - {{bin, ff_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, {fistful_deposit_thrift, 'TimestampedChange'}}, - ThriftChange = ff_proto_utils:deserialize(Type, EncodedThriftChange), - {ff_deposit_codec:unmarshal(timestamped_change, ThriftChange), Context}. - -get_aux_state_ctx(AuxState) when is_map(AuxState) -> - maps:get(ctx, AuxState, undefined); -get_aux_state_ctx(_) -> - undefined. diff --git a/apps/ff_server/src/ff_deposit_repair.erl b/apps/ff_server/src/ff_deposit_repair.erl index ab3a4346..90f700f3 100644 --- a/apps/ff_server/src/ff_deposit_repair.erl +++ b/apps/ff_server/src/ff_deposit_repair.erl @@ -21,5 +21,11 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, notfound} -> woody_error:raise(business, #fistful_DepositNotFound{}); {error, working} -> - woody_error:raise(business, #fistful_MachineAlreadyWorking{}) + woody_error:raise(business, #fistful_MachineAlreadyWorking{}); + {error, {failed, Reason}} -> + erlang:error(Reason); + {error, failed} -> + erlang:error(failed); + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. diff --git a/apps/ff_server/src/ff_destination_machinery_schema.erl b/apps/ff_server/src/ff_destination_machinery_schema.erl deleted file mode 100644 index 10a5a33c..00000000 --- a/apps/ff_server/src/ff_destination_machinery_schema.erl +++ /dev/null @@ -1,84 +0,0 @@ --module(ff_destination_machinery_schema). - -%% Storage schema behaviour --behaviour(machinery_mg_schema). - --include_lib("fistful_proto/include/fistful_destination_thrift.hrl"). --include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). - -%% Constants - --define(CURRENT_EVENT_FORMAT_VERSION, 1). - --export([get_version/1]). --export([marshal/3]). --export([unmarshal/3]). - -%% 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() :: ff_machine:timestamped_event(ff_destination:event()). --type aux_state() :: ff_machine:auxst(). --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, Format}, TimestampedChange, Context) -> - marshal_event(Format, 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 = ff_destination_codec:marshal(timestamped_change, TimestampedChange), - Type = {struct, struct, {fistful_destination_thrift, 'TimestampedChange'}}, - {{bin, ff_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, {fistful_destination_thrift, 'TimestampedChange'}}, - ThriftChange = ff_proto_utils:deserialize(Type, EncodedThriftChange), - {ff_destination_codec:unmarshal(timestamped_change, ThriftChange), Context}. diff --git a/apps/ff_server/src/ff_machine_handler.erl b/apps/ff_server/src/ff_machine_handler.erl index 07a2568b..f39925b3 100644 --- a/apps/ff_server/src/ff_machine_handler.erl +++ b/apps/ff_server/src/ff_machine_handler.erl @@ -22,7 +22,7 @@ init(Request, Opts) -> maybe {method_is_valid, true} ?= {method_is_valid, Method =:= <<"GET">>}, {process_id_is_valid, true} ?= {process_id_is_valid, is_binary(ProcessID)}, - {ok, Trace} ?= ff_machine:trace(NS, ProcessID), + {ok, Trace} ?= ff_machine_trace:trace(NS, ProcessID), Body = unicode:characters_to_binary(json:encode(Trace)), Req = cowboy_req:reply(200, #{}, Body, Request), {ok, Req, undefined} diff --git a/apps/ff_server/src/ff_server.app.src b/apps/ff_server/src/ff_server.app.src index bc8ab0aa..6821218b 100644 --- a/apps/ff_server/src/ff_server.app.src +++ b/apps/ff_server/src/ff_server.app.src @@ -15,6 +15,7 @@ fistful_proto, ff_validator, fistful, + prg_machine, ff_transfer, thrift ]}, diff --git a/apps/ff_server/src/ff_server.erl b/apps/ff_server/src/ff_server.erl index 6e901bed..a92fe25d 100644 --- a/apps/ff_server/src/ff_server.erl +++ b/apps/ff_server/src/ff_server.erl @@ -65,15 +65,6 @@ init([]) -> {ok, Ip} = inet:parse_address(IpEnv), WoodyOpts = maps:with([net_opts, handler_limits], WoodyOptsEnv), - %% NOTE See 'sys.config' - %% TODO Refactor after namespaces params moved from progressor' - %% application env. - Backends = [ - contruct_backend_childspec(N, H, S, PartyClient) - || {N, H, S} <- get_namespaces_params() - ], - ok = application:set_env(fistful, backends, maps:from_list(Backends)), - Services = [ {ff_withdrawal_adapter_host, ff_withdrawal_adapter_host}, @@ -107,9 +98,16 @@ init([]) -> ) ), PartyClientSpec = party_client:child_spec(party_client, PartyClient), + PrgMachineSpec = prg_machine_registry:get_child_spec([ + ff_deposit_machine, + ff_source_machine, + ff_destination_machine, + ff_withdrawal_machine, + ff_withdrawal_session_machine + ]), % TODO % - Zero thoughts given while defining this strategy. - {ok, {#{strategy => one_for_one}, [PartyClientSpec, ServicesChildSpec]}}. + {ok, {#{strategy => one_for_one}, [PartyClientSpec, PrgMachineSpec, ServicesChildSpec]}}. -spec enable_health_logging(erl_health:check()) -> erl_health:check(). enable_health_logging(Check) -> @@ -125,36 +123,6 @@ get_handler(Service, Handler, WrapperOpts) -> {Path, ServiceSpec} = ff_services:get_service_spec(Service), {Path, {ServiceSpec, wrap_handler(Handler, WrapperOpts)}}. --define(PROCESSOR_OPT_PATTERN(NS, Handler, Schema), #{ - processor := #{ - client := machinery_prg_backend, - options := #{ - namespace := NS, - handler := {fistful, #{handler := Handler, party_client := _}}, - schema := Schema - } - } -}). - --spec get_namespaces_params() -> - [{machinery:namespace(), MachineryImpl :: module(), Schema :: module()}]. -get_namespaces_params() -> - {ok, Namespaces} = application:get_env(progressor, namespaces), - lists:map( - fun({_, ?PROCESSOR_OPT_PATTERN(NS, Handler, Schema)}) -> - {NS, Handler, Schema} - end, - maps:to_list(Namespaces) - ). - -contruct_backend_childspec(NS, Handler, Schema, PartyClient) -> - {NS, - {machinery_prg_backend, #{ - namespace => NS, - handler => {fistful, #{handler => Handler, party_client => PartyClient}}, - schema => Schema - }}}. - wrap_handler(Handler, WrapperOpts) -> FullOpts = maps:merge(#{handler => Handler}, WrapperOpts), {ff_woody_wrapper, FullOpts}. diff --git a/apps/ff_server/src/ff_source_machinery_schema.erl b/apps/ff_server/src/ff_source_machinery_schema.erl deleted file mode 100644 index 59ddecd5..00000000 --- a/apps/ff_server/src/ff_source_machinery_schema.erl +++ /dev/null @@ -1,84 +0,0 @@ --module(ff_source_machinery_schema). - -%% Storage schema behaviour --behaviour(machinery_mg_schema). - --include_lib("fistful_proto/include/fistful_source_thrift.hrl"). --include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). - --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() :: ff_machine:timestamped_event(ff_source:event()). --type aux_state() :: ff_machine:auxst(). --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, Format}, TimestampedChange, Context) -> - marshal_event(Format, 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 = ff_source_codec:marshal(timestamped_change, TimestampedChange), - Type = {struct, struct, {fistful_source_thrift, 'TimestampedChange'}}, - {{bin, ff_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, {fistful_source_thrift, 'TimestampedChange'}}, - ThriftChange = ff_proto_utils:deserialize(Type, EncodedThriftChange), - {ff_source_codec:unmarshal(timestamped_change, ThriftChange), Context}. diff --git a/apps/ff_server/src/ff_withdrawal_codec.erl b/apps/ff_server/src/ff_withdrawal_codec.erl index 84f4120c..f0eba474 100644 --- a/apps/ff_server/src/ff_withdrawal_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_codec.erl @@ -501,11 +501,36 @@ unmarshal_repair_scenario_test() -> events => [ {status_changed, pending} ], - action => [ - {set_timer, {timeout, 0}} - ] + action => timeout }}, unmarshal(repair_scenario, Scenario) ). +-spec unmarshal_repair_scenario_deadline_timer_test() -> _. +unmarshal_repair_scenario_deadline_timer_test() -> + Deadline = <<"2099-06-13T12:34:56.789Z">>, + Scenario = { + add_events, + #wthd_AddEventsRepair{ + events = [], + action = #repairer_ComplexAction{ + timer = + {set_timer, #repairer_SetTimerAction{ + timer = {deadline, Deadline} + }} + } + } + }, + ParsedDeadline = ff_codec:unmarshal(timestamp, Deadline), + ExpectedAt = prg_action:marshal_timer({deadline, ParsedDeadline}), + ?assertEqual( + {add_events, #{ + events => [], + action => {schedule, #{at => ExpectedAt, action => timeout}} + }}, + unmarshal(repair_scenario, Scenario) + ), + %% Machinery {datetime, micro} from ff_codec must map to the same wire timestamp. + ?assertEqual(ExpectedAt, prg_action:marshal_timer({deadline, ParsedDeadline})). + -endif. diff --git a/apps/ff_server/src/ff_withdrawal_machinery_schema.erl b/apps/ff_server/src/ff_withdrawal_machinery_schema.erl deleted file mode 100644 index a34969ab..00000000 --- a/apps/ff_server/src/ff_withdrawal_machinery_schema.erl +++ /dev/null @@ -1,90 +0,0 @@ --module(ff_withdrawal_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 event() :: ff_machine:timestamped_event(ff_withdrawal:event()). --type aux_state() :: ff_machine:auxst(). --type call_args() :: term(). --type call_response() :: term(). --type context() :: machinery_mg_schema:context(). - --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, Format}, TimestampedChange, Context) -> - marshal_event(Format, TimestampedChange, Context); -marshal(T, V, C) when - T =:= {args, init} orelse - T =:= {args, call} orelse - T =:= {args, repair} orelse - T =:= {args, notification} 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({aux_state, undefined} = T, V, C0) -> - {AuxState, C1} = machinery_mg_schema_generic:unmarshal(T, V, C0), - {AuxState, C1#{ctx => get_aux_state_ctx(AuxState)}}; -unmarshal(T, V, C) when - T =:= {args, init} orelse - T =:= {args, call} orelse - T =:= {args, repair} orelse - T =:= {args, notification} 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 = ff_withdrawal_codec:marshal(timestamped_change, TimestampedChange), - Type = {struct, struct, {fistful_wthd_thrift, 'TimestampedChange'}}, - {{bin, ff_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, {fistful_wthd_thrift, 'TimestampedChange'}}, - ThriftChange = ff_proto_utils:deserialize(Type, EncodedThriftChange), - {ff_withdrawal_codec:unmarshal(timestamped_change, ThriftChange), Context}. - -get_aux_state_ctx(AuxState) when is_map(AuxState) -> - maps:get(ctx, AuxState, undefined); -get_aux_state_ctx(_) -> - undefined. diff --git a/apps/ff_server/src/ff_withdrawal_repair.erl b/apps/ff_server/src/ff_withdrawal_repair.erl index 897a54d5..dcf3c101 100644 --- a/apps/ff_server/src/ff_withdrawal_repair.erl +++ b/apps/ff_server/src/ff_withdrawal_repair.erl @@ -21,5 +21,11 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, notfound} -> woody_error:raise(business, #fistful_WithdrawalNotFound{}); {error, working} -> - woody_error:raise(business, #fistful_MachineAlreadyWorking{}) + woody_error:raise(business, #fistful_MachineAlreadyWorking{}); + {error, {failed, Reason}} -> + erlang:error(Reason); + {error, failed} -> + erlang:error(failed); + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. diff --git a/apps/ff_server/src/ff_withdrawal_session_codec.erl b/apps/ff_server/src/ff_withdrawal_session_codec.erl index 61bce6f8..a7729bcf 100644 --- a/apps/ff_server/src/ff_withdrawal_session_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_session_codec.erl @@ -24,7 +24,7 @@ marshal_state(State, ID, Context) -> context = marshal(ctx, Context) }. --spec marshal_event(ff_withdrawal_machine:event()) -> fistful_wthd_session_thrift:'Event'(). +-spec marshal_event(ff_withdrawal_session_machine:event()) -> fistful_wthd_session_thrift:'Event'(). marshal_event({EventID, {ev, Timestamp, Change}}) -> #wthd_session_Event{ sequence = ff_codec:marshal(event_id, EventID), diff --git a/apps/ff_server/src/ff_withdrawal_session_machinery_schema.erl b/apps/ff_server/src/ff_withdrawal_session_machinery_schema.erl deleted file mode 100644 index 004dcde9..00000000 --- a/apps/ff_server/src/ff_withdrawal_session_machinery_schema.erl +++ /dev/null @@ -1,87 +0,0 @@ --module(ff_withdrawal_session_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() :: ff_machine:timestamped_event(ff_withdrawal_session:event()). --type aux_state() :: ff_machine:auxst(). --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, Format}, TimestampedChange, Context) -> - marshal_event(Format, 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({aux_state, FormatVersion}, EncodedChange, Context) -> - unmarshal_aux_state(FormatVersion, EncodedChange, Context); -unmarshal(T, V, C) when - T =:= {args, init} orelse - T =:= {args, call} orelse - T =:= {args, repair} 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 = ff_withdrawal_session_codec:marshal(timestamped_change, TimestampedChange), - Type = {struct, struct, {fistful_wthd_session_thrift, 'TimestampedChange'}}, - {{bin, ff_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, {fistful_wthd_session_thrift, 'TimestampedChange'}}, - ThriftChange = ff_proto_utils:deserialize(Type, EncodedThriftChange), - {ff_withdrawal_session_codec:unmarshal(timestamped_change, ThriftChange), Context}. - --spec unmarshal_aux_state(machinery_mg_schema:version(), machinery_msgpack:t(), context()) -> {aux_state(), context()}. -unmarshal_aux_state(undefined = Version, EncodedAuxState, Context0) -> - machinery_mg_schema_generic:unmarshal({aux_state, Version}, EncodedAuxState, Context0). diff --git a/apps/ff_server/src/ff_withdrawal_session_repair.erl b/apps/ff_server/src/ff_withdrawal_session_repair.erl index 6eb716bf..b68e1502 100644 --- a/apps/ff_server/src/ff_withdrawal_session_repair.erl +++ b/apps/ff_server/src/ff_withdrawal_session_repair.erl @@ -21,5 +21,11 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, notfound} -> woody_error:raise(business, #fistful_WithdrawalSessionNotFound{}); {error, working} -> - woody_error:raise(business, #fistful_MachineAlreadyWorking{}) + woody_error:raise(business, #fistful_MachineAlreadyWorking{}); + {error, {failed, Reason}} -> + erlang:error(Reason); + {error, failed} -> + erlang:error(failed); + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. diff --git a/apps/ff_server/src/ff_woody_wrapper.erl b/apps/ff_server/src/ff_woody_wrapper.erl index bf7830aa..a6c47de0 100644 --- a/apps/ff_server/src/ff_woody_wrapper.erl +++ b/apps/ff_server/src/ff_woody_wrapper.erl @@ -36,7 +36,7 @@ handle_function(Func, Args, WoodyContext0, #{handler := Handler} = Opts) -> WoodyContext = ensure_woody_deadline_set(WoodyContext0, Opts), {HandlerMod, HandlerOptions} = get_handler_opts(Handler), - ok = ff_context:save(create_context(WoodyContext, Opts)), + ok = op_context:save(op_context:key(fistful), create_context(WoodyContext, Opts)), try HandlerMod:handle_function( Func, @@ -44,7 +44,7 @@ handle_function(Func, Args, WoodyContext0, #{handler := Handler} = Opts) -> HandlerOptions ) after - ff_context:cleanup() + op_context:cleanup(fistful) end. %% Internal functions @@ -54,7 +54,7 @@ create_context(WoodyContext, Opts) -> woody_context => WoodyContext, party_client => maps:get(party_client, Opts) }, - ff_context:create(ContextOptions). + op_context:create(ContextOptions). -spec ensure_woody_deadline_set(woody_context:ctx(), options()) -> woody_context:ctx(). ensure_woody_deadline_set(WoodyContext, Opts) -> diff --git a/apps/ff_server/test/ff_destination_handler_SUITE.erl b/apps/ff_server/test/ff_destination_handler_SUITE.erl index c5a740ab..58639d4c 100644 --- a/apps/ff_server/test/ff_destination_handler_SUITE.erl +++ b/apps/ff_server/test/ff_destination_handler_SUITE.erl @@ -167,10 +167,11 @@ trace_destination_test(C) -> ], #{<<"NS">> := #{}} ], - <<"events">> := [ - #{<<"event_id">> := 1, <<"event_payload">> := #{<<"created">> := _}, <<"event_timestamp">> := _}, - #{<<"event_id">> := 2, <<"event_payload">> := #{<<"account">> := _}, <<"event_timestamp">> := _} - ], + <<"events">> := + [ + #{<<"event_id">> := 1, <<"event_payload">> := #{<<"created">> := _}, <<"event_timestamp">> := _}, + #{<<"event_id">> := 2, <<"event_payload">> := #{<<"account">> := _}, <<"event_timestamp">> := _} + ], <<"task_status">> := <<"finished">>, <<"task_type">> := <<"init">> } diff --git a/apps/ff_server/test/ff_source_handler_SUITE.erl b/apps/ff_server/test/ff_source_handler_SUITE.erl index c87b9905..de890ccd 100644 --- a/apps/ff_server/test/ff_source_handler_SUITE.erl +++ b/apps/ff_server/test/ff_source_handler_SUITE.erl @@ -132,8 +132,7 @@ trace_source_ok_test(C) -> ], <<"task_status">> := <<"finished">>, <<"task_type">> := <<"init">> - }, - #{<<"task_status">> := <<"finished">>, <<"task_type">> := <<"timeout">>} + } ] = json:decode(Body), ok. diff --git a/apps/ff_server/test/ff_withdrawal_session_repair_SUITE.erl b/apps/ff_server/test/ff_withdrawal_session_repair_SUITE.erl index b5873257..fbc58ddc 100644 --- a/apps/ff_server/test/ff_withdrawal_session_repair_SUITE.erl +++ b/apps/ff_server/test/ff_withdrawal_session_repair_SUITE.erl @@ -173,9 +173,8 @@ repair_failed_session_with_failure(C) -> <<"task_type">> := <<"repair">> }, #{ - <<"error">> := - <<"{exception,error,{unable_to_finish_session,{error,notfound}}}">>, - <<"task_status">> := <<"error">>, + <<"error">> := null, + <<"task_status">> := <<"finished">>, <<"task_type">> := <<"timeout">> } ] = json:decode(TraceBody), @@ -216,7 +215,7 @@ create_failed_session(PartyID, DestinationID, _C) -> ok = ff_withdrawal_session_machine:create(ID, TransferData, SessionParams), ID. --spec get_session_status(machinery:id()) -> ff_withdrawal_session:status(). +-spec get_session_status(prg_machine:id()) -> ff_withdrawal_session:status(). get_session_status(ID) -> {ok, SessionMachine} = ff_withdrawal_session_machine:get(ID), Session = ff_withdrawal_session_machine:session(SessionMachine), diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal.erl b/apps/ff_transfer/src/ff_adapter_withdrawal.erl index 39f04321..702c2489 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal.erl @@ -74,7 +74,7 @@ }. -type finish_status() :: success | {success, transaction_info()} | {failure, failure()}. --type timer() :: machinery:timer(). +-type timer() :: prg_action:timer(). -type transaction_info() :: ff_adapter:transaction_info(). -type failure() :: ff_adapter:failure(). diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl index 44ec83d0..ae9c6628 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl @@ -340,7 +340,7 @@ unmarshal(intent, {finish, #wthd_provider_FinishIntent{status = {failure, Failur unmarshal(intent, {sleep, #wthd_provider_SleepIntent{timer = Timer, callback_tag = Tag}}) -> {sleep, genlib_map:compact(#{ - timer => ff_codec:unmarshal(timer, Timer), + timer => unmarshal_provider_timer(ff_codec:unmarshal(timer, Timer)), tag => Tag })}; unmarshal(process_callback_result, _NotImplemented) -> @@ -421,3 +421,32 @@ unmarshal_msgpack({arr, V}) when is_list(V) -> [unmarshal_msgpack(ListItem) || ListItem <- V]; unmarshal_msgpack({obj, V}) when is_map(V) -> maps:fold(fun(Key, Value, Map) -> Map#{unmarshal_msgpack(Key) => unmarshal_msgpack(Value)} end, #{}, V). + +%% base.Timer deadline on the wire is base.Timestamp (RFC3339). +%% prg_action:timer() accepts {deadline, calendar:datetime() | {datetime(), USec} | binary()}. +unmarshal_provider_timer({deadline, Deadline}) when is_binary(Deadline) -> + {deadline, Deadline}; +unmarshal_provider_timer({deadline, {DateTime, USec}}) when is_integer(USec) -> + {deadline, {DateTime, USec}}; +unmarshal_provider_timer(Timer) -> + Timer. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec unmarshal_provider_timer_preserves_microseconds_test() -> _. +unmarshal_provider_timer_preserves_microseconds_test() -> + Dt = {{2026, 6, 13}, {12, 34, 56}}, + USec = 789000, + ?assertEqual( + {deadline, {Dt, USec}}, + unmarshal_provider_timer({deadline, {Dt, USec}}) + ), + ?assertEqual( + prg_action:marshal_timer({deadline, {Dt, USec}}), + prg_action:marshal_timer(unmarshal_provider_timer({deadline, {Dt, USec}})) + ). + +-endif. diff --git a/apps/ff_transfer/src/ff_adjustment.erl b/apps/ff_transfer/src/ff_adjustment.erl index 0ea5e808..82439cac 100644 --- a/apps/ff_transfer/src/ff_adjustment.erl +++ b/apps/ff_transfer/src/ff_adjustment.erl @@ -51,6 +51,7 @@ -type create_error() :: none(). -export_type([id/0]). +-export_type([action/0]). -export_type([event/0]). -export_type([changes/0]). -export_type([cash_flow_change/0]). @@ -91,7 +92,7 @@ -type target_status() :: term(). -type final_cash_flow() :: ff_cash_flow:final_cash_flow(). -type p_transfer() :: ff_postings_transfer:transfer(). --type action() :: machinery:action() | undefined. +-type action() :: prg_action:t(). -type process_result() :: {action(), [event()]}. -type legacy_event() :: any(). -type external_id() :: id(). @@ -157,7 +158,7 @@ create(Params) -> operation_timestamp => Timestamp, external_id => maps:get(external_id, Params, undefined) }), - {ok, {continue, [{created, Adjustment}]}}. + {ok, {timeout, [{created, Adjustment}]}}. %% Transfer logic callbacks @@ -233,10 +234,10 @@ do_process_transfer(p_transfer_start, Adjustment) -> create_p_transfer(Adjustment); do_process_transfer(p_transfer_prepare, Adjustment) -> {ok, Events} = ff_pipeline:with(p_transfer, Adjustment, fun ff_postings_transfer:prepare/1), - {continue, Events}; + {timeout, Events}; do_process_transfer(p_transfer_commit, Adjustment) -> {ok, Events} = ff_pipeline:with(p_transfer, Adjustment, fun ff_postings_transfer:commit/1), - {continue, Events}; + {timeout, Events}; do_process_transfer(finish, Adjustment) -> process_transfer_finish(Adjustment). @@ -250,11 +251,11 @@ create_p_transfer(Adjustment) -> {ok, FinalCashFlow} = ff_cash_flow:combine(Old, New), PTransferID = construct_p_transfer_id(id(Adjustment)), {ok, PostingsTransferEvents} = ff_postings_transfer:create(PTransferID, FinalCashFlow), - {continue, [{p_transfer, Ev} || Ev <- PostingsTransferEvents]}. + {timeout, [{p_transfer, Ev} || Ev <- PostingsTransferEvents]}. -spec process_transfer_finish(adjustment()) -> process_result(). process_transfer_finish(_Adjustment) -> - {undefined, [{status_changed, succeeded}]}. + {idle, [{status_changed, succeeded}]}. -spec construct_p_transfer_id(id()) -> id(). construct_p_transfer_id(ID) -> diff --git a/apps/ff_transfer/src/ff_adjustment_utils.erl b/apps/ff_transfer/src/ff_adjustment_utils.erl index 783a8ccf..f4c2ceeb 100644 --- a/apps/ff_transfer/src/ff_adjustment_utils.erl +++ b/apps/ff_transfer/src/ff_adjustment_utils.erl @@ -60,7 +60,7 @@ -type adjustment() :: ff_adjustment:adjustment(). -type event() :: ff_adjustment:event(). -type final_cash_flow() :: ff_cash_flow:final_cash_flow(). --type action() :: machinery:action() | undefined. +-type action() :: ff_adjustment:action(). -type changes() :: ff_adjustment:changes(). -type domain_revision() :: ff_domain_config:revision(). diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index 59e1c576..e248ccec 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -134,7 +134,7 @@ -type is_negative() :: boolean(). -type cash() :: ff_cash:cash(). -type cash_range() :: ff_range:range(cash()). --type action() :: machinery:action() | undefined. +-type action() :: prg_action:t(). -type p_transfer() :: ff_postings_transfer:transfer(). -type currency_id() :: ff_currency:id(). -type external_id() :: id(). @@ -356,7 +356,7 @@ do_process_transfer(p_transfer_start, Deposit) -> create_p_transfer(Deposit); do_process_transfer(p_transfer_prepare, Deposit) -> {ok, Events} = ff_pipeline:with(p_transfer, Deposit, fun ff_postings_transfer:prepare/1), - {continue, Events}; + {timeout, Events}; do_process_transfer(p_transfer_commit, Deposit) -> {ok, Events} = ff_pipeline:with(p_transfer, Deposit, fun ff_postings_transfer:commit/1), {ok, Wallet} = ff_party:get_wallet( @@ -365,10 +365,10 @@ do_process_transfer(p_transfer_commit, Deposit) -> domain_revision(Deposit) ), ok = ff_party:wallet_log_balance(wallet_id(Deposit), Wallet), - {continue, Events}; + {timeout, Events}; do_process_transfer(p_transfer_cancel, Deposit) -> {ok, Events} = ff_pipeline:with(p_transfer, Deposit, fun ff_postings_transfer:cancel/1), - {continue, Events}; + {timeout, Events}; do_process_transfer(limit_check, Deposit) -> process_limit_check(Deposit); do_process_transfer({fail, Reason}, Deposit) -> @@ -381,7 +381,7 @@ create_p_transfer(Deposit) -> FinalCashFlow = make_final_cash_flow(Deposit), PTransferID = construct_p_transfer_id(id(Deposit)), {ok, PostingsTransferEvents} = ff_postings_transfer:create(PTransferID, FinalCashFlow), - {continue, [{p_transfer, Ev} || Ev <- PostingsTransferEvents]}. + {timeout, [{p_transfer, Ev} || Ev <- PostingsTransferEvents]}. -spec process_limit_check(deposit_state()) -> process_result(). process_limit_check(Deposit) -> @@ -411,18 +411,20 @@ process_limit_check(Deposit) -> expected_range => Range, balance => Cash }, - [{limit_check, {wallet_receiver, {failed, Details}}}] + [{limit_check, {wallet_receiver, {failed, Details}}}]; + {error, {terms_violation, {wallet_limit, {invalid_terms, Details}}}} -> + [{limit_check, {wallet_receiver, {failed, #{reason => invalid_terms, details => Details}}}}] end, - {continue, Events}. + {timeout, Events}. -spec process_transfer_finish(deposit_state()) -> process_result(). process_transfer_finish(_Deposit) -> - {undefined, [{status_changed, succeeded}]}. + {idle, [{status_changed, succeeded}]}. -spec process_transfer_fail(fail_type(), deposit_state()) -> process_result(). process_transfer_fail(limit_check, Deposit) -> Failure = build_failure(limit_check, Deposit), - {undefined, [{status_changed, {failed, Failure}}]}. + {idle, [{status_changed, {failed, Failure}}]}. -spec make_final_cash_flow(deposit_state()) -> final_cash_flow(). make_final_cash_flow(Deposit) -> @@ -555,15 +557,16 @@ is_limit_check_ok({wallet_receiver, {failed, _Details}}) -> -spec validate_wallet_limits(terms(), wallet()) -> {ok, valid} - | {error, {terms_violation, {wallet_limit, {cash_range, {cash(), cash_range()}}}}}. + | {error, {terms_violation, {wallet_limit, {cash_range, {cash(), cash_range()}}}}} + | {error, {terms_violation, {wallet_limit, {invalid_terms, term()}}}}. validate_wallet_limits(Terms, Wallet) -> case ff_party:validate_wallet_limits(Terms, Wallet) of {ok, valid} = Result -> Result; {error, {terms_violation, {cash_range, {Cash, CashRange}}}} -> {error, {terms_violation, {wallet_limit, {cash_range, {Cash, CashRange}}}}}; - {error, {invalid_terms, _Details} = Reason} -> - erlang:error(Reason) + {error, {invalid_terms, _} = Reason} -> + {error, {terms_violation, {wallet_limit, Reason}}} end. %% Helpers diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index b55985da..6fadf27f 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -4,14 +4,21 @@ -module(ff_deposit_machine). --behaviour(machinery). +-behaviour(prg_machine). + +-define(EVENT_FORMAT_VERSION, 1). %% API --type id() :: machinery:id(). +-type id() :: prg_machine:id(). -type change() :: ff_deposit:event(). --type event() :: {integer(), ff_machine:timestamped_event(change())}. --type st() :: ff_machine:st(deposit()). +-type timestamp() :: prg_machine:timestamp(). +-type timestamped_event(T) :: {ev, timestamp(), T}. +-type event() :: {integer(), timestamped_event(change())}. +-type st() :: #{ + model := deposit(), + ctx := ctx() +}. -type deposit() :: ff_deposit:deposit_state(). -type external_id() :: id(). -type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}. @@ -23,6 +30,7 @@ -type repair_error() :: ff_repair:repair_error(). -type repair_response() :: ff_repair:repair_response(). +-type repair_call_error() :: ff_machine_lib:repair_call_error(). -type unknown_deposit_error() :: {unknown_deposit, id()}. @@ -37,6 +45,7 @@ -export_type([external_id/0]). -export_type([create_error/0]). -export_type([repair_error/0]). +-export_type([repair_call_error/0]). %% API @@ -51,21 +60,25 @@ -export([deposit/1]). -export([ctx/1]). -%% Machinery - --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). - -%% Pipeline +%% prg_machine --import(ff_pipeline, [do/1, unwrap/1]). +-export([namespace/0]). +-export([init/2]). +-export([process_signal/2]). +-export([process_call/2]). +-export([process_repair/2]). +-export([process_notification/2]). +-export([marshal_event_body/2]). +-export([unmarshal_event_body/1]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% Internal types -type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). -define(NS, 'ff/deposit_v1'). @@ -75,11 +88,7 @@ ok | {error, ff_deposit:create_error() | exists}. create(Params, Ctx) -> - do(fun() -> - #{id := ID} = Params, - Events = unwrap(ff_deposit:create(Params)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) - end). + ff_machine_lib:create(?NS, fun ff_deposit:create/1, Params, Ctx). -spec get(id()) -> {ok, st()} @@ -91,85 +100,81 @@ get(ID) -> {ok, st()} | {error, unknown_deposit_error()}. get(ID, {After, Limit}) -> - case ff_machine:get(ff_deposit, ?NS, ID, {After, Limit, forward}) of - {ok, _Machine} = Result -> - Result; - {error, notfound} -> - {error, {unknown_deposit, ID}} - end. + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, {unknown_deposit, ID}). -spec events(id(), event_range()) -> {ok, [event()]} | {error, unknown_deposit_error()}. events(ID, {After, Limit}) -> - case ff_machine:history(ff_deposit, ?NS, ID, {After, Limit, forward}) of - {ok, History} -> - {ok, [{EventID, TsEv} || {EventID, _, TsEv} <- History]}; - {error, notfound} -> - {error, {unknown_deposit, ID}} - end. + ff_machine_lib:events(?NS, ID, {After, Limit}, {unknown_deposit, ID}). -spec repair(id(), ff_repair:scenario()) -> - {ok, repair_response()} | {error, notfound | working | {failed, repair_error()}}. + {ok, repair_response()} | {error, repair_call_error()}. repair(ID, Scenario) -> - machinery:repair(?NS, ID, Scenario, backend()). + ff_machine_lib:repair(?NS, ID, Scenario). %% Accessors -spec deposit(st()) -> deposit(). -deposit(St) -> - ff_machine:model(St). +deposit(#{model := Model}) -> + Model. -spec ctx(st()) -> ctx(). -ctx(St) -> - ff_machine:ctx(St). +ctx(#{ctx := Ctx}) -> + Ctx. -%% Machinery +%% prg_machine --type machine() :: ff_machine:machine(event()). --type result() :: ff_machine:result(event()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. --spec init({[event()], ctx()}, machine(), handler_args(), handler_opts()) -> result(). -init({Events, Ctx}, #{}, _, _Opts) -> +-spec init({[change()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> #{ - events => ff_machine:emit_events(Events), - action => continue, - aux_state => #{ctx => Ctx} + events => Events, + action => timeout, + auxst => #{ctx => Ctx} }. --spec process_timeout(machine(), handler_args(), handler_opts()) -> result(). -process_timeout(Machine, _, _Opts) -> - St = ff_machine:collapse(ff_deposit, Machine), - Deposit = deposit(St), - process_result(ff_deposit:process_transfer(Deposit)). +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Deposit = prg_machine:collapse(?MODULE, Machine), + ff_machine_lib:to_prg_result(ff_deposit:process_transfer(Deposit)). --spec process_call(_CallArgs, machine(), handler_args(), handler_opts()) -> no_return(). -process_call(CallArgs, _Machine, _, _Opts) -> +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> erlang:error({unexpected_call, CallArgs}). --spec process_repair(ff_repair:scenario(), machine(), handler_args(), handler_opts()) -> - {ok, {repair_response(), result()}} | {error, repair_error()}. -process_repair(Scenario, Machine, _Args, _Opts) -> - ff_repair:apply_scenario(ff_deposit, Machine, Scenario). +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + ff_machine_lib:process_repair(?MODULE, Machine, Scenario). --spec process_notification(_, machine(), handler_args(), handler_opts()) -> result() | no_return(). -process_notification(_Args, _Machine, _HandlerArgs, _Opts) -> +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> #{}. -%% Internals - -backend() -> - fistful:backend(?NS). - -process_result({Action, Events}) -> - genlib_map:compact(#{ - events => set_events(Events), - action => Action - }). - -set_events([]) -> - undefined; -set_events(Events) -> - ff_machine:emit_events(Events). +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + prg_machine:event_body(), + term() +) -> term(). +apply_event(_EventID, _Ts, Body, Model) -> + ff_deposit:apply_event(Body, Model). + +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Timestamp, Body) -> + ff_machine_lib:marshal_event_body(deposit, ?EVENT_FORMAT_VERSION, Body, Timestamp). + +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + ff_machine_lib:unmarshal_event_body(deposit, Payload). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + ff_machine_lib:marshal_aux_state(AuxSt). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(Payload) when is_binary(Payload) -> + ff_machine_lib:unmarshal_aux_state(Payload). diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index eb4b210d..1f486b34 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -214,11 +214,14 @@ is_accessible(Destination) -> ff_account:is_accessible(account(Destination)). -spec apply_event(event(), ff_maybe:'maybe'(destination_state())) -> destination_state(). -apply_event({created, Destination}, undefined) -> +apply_event(Ev, State) -> + apply_event_(Ev, State). + +apply_event_({created, Destination}, undefined) -> Destination; -apply_event({status_changed, S}, Destination) -> +apply_event_({status_changed, S}, Destination) -> Destination#{status => S}; -apply_event({account, Ev}, #{account := Account} = Destination) -> +apply_event_({account, Ev}, #{account := Account} = Destination) -> Destination#{account => ff_account:apply_event(Ev, Account)}; -apply_event({account, Ev}, Destination) -> - apply_event({account, Ev}, Destination#{account => undefined}). +apply_event_({account, Ev}, Destination) -> + apply_event_({account, Ev}, Destination#{account => undefined}). diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index 5ae942c3..f824f22a 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -4,18 +4,27 @@ -module(ff_destination_machine). +-behaviour(prg_machine). + +-define(EVENT_FORMAT_VERSION, 1). + %% API --type id() :: machinery:id(). +-type id() :: prg_machine:id(). -type ctx() :: ff_entity_context:context(). -type destination() :: ff_destination:destination_state(). -type change() :: ff_destination:event(). --type event() :: {integer(), ff_machine:timestamped_event(change())}. +-type timestamp() :: prg_machine:timestamp(). +-type timestamped_event(T) :: {ev, timestamp(), T}. +-type event() :: {integer(), timestamped_event(change())}. -type events() :: [event()]. -type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}. -type params() :: ff_destination:params(). --type st() :: ff_machine:st(destination()). +-type st() :: #{ + model := destination(), + ctx := ctx() +}. -type repair_error() :: ff_repair:repair_error(). -type repair_response() :: ff_repair:repair_response(). @@ -40,99 +49,108 @@ -export([destination/1]). -export([ctx/1]). -%% Machinery - --behaviour(machinery). +%% prg_machine --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). +-export([namespace/0]). +-export([init/2]). +-export([process_signal/2]). +-export([process_call/2]). +-export([process_repair/2]). +-export([process_notification/2]). +-export([marshal_event_body/2]). +-export([unmarshal_event_body/1]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([apply_event/4]). -%% Pipeline +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). --import(ff_pipeline, [do/1, unwrap/1]). - -%% -define(NS, 'ff/destination_v2'). +%% API + -spec create(params(), ctx()) -> ok | {error, ff_destination:create_error() | exists}. -create(#{id := ID} = Params, Ctx) -> - do(fun() -> - Events = unwrap(ff_destination:create(Params)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) - end). +create(Params, Ctx) -> + ff_machine_lib:create(?NS, fun ff_destination:create/1, Params, Ctx). -spec get(id()) -> {ok, st()} | {error, notfound}. get(ID) -> - ff_machine:get(ff_destination, ?NS, ID). + get(ID, {undefined, undefined}). -spec get(id(), event_range()) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - ff_machine:get(ff_destination, ?NS, ID, {After, Limit, forward}). + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, notfound). -spec events(id(), event_range()) -> {ok, events()} | {error, notfound}. events(ID, {After, Limit}) -> - do(fun() -> - History = unwrap(ff_machine:history(ff_destination, ?NS, ID, {After, Limit, forward})), - [{EventID, TsEv} || {EventID, _, TsEv} <- History] - end). + ff_machine_lib:events(?NS, ID, {After, Limit}, notfound). %% Accessors -spec destination(st()) -> destination(). -destination(St) -> - ff_machine:model(St). +destination(#{model := Model}) -> + Model. -spec ctx(st()) -> ctx(). -ctx(St) -> - ff_machine:ctx(St). - -%% Machinery +ctx(#{ctx := Ctx}) -> + Ctx. --type machine() :: ff_machine:machine(change()). --type result() :: ff_machine:result(change()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). +%% prg_machine --spec init({[change()], ctx()}, machine(), _, handler_opts()) -> result(). -init({Events, Ctx}, #{}, _, _Opts) -> - #{ - events => ff_machine:emit_events(Events), - aux_state => #{ctx => Ctx} - }. +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. -%% +-spec init({[change()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + ff_machine_lib:init_result(Events, Ctx). --spec process_timeout(machine(), handler_args(), handler_opts()) -> result(). -process_timeout(_Machine, _, _Opts) -> +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, _Machine) -> #{}. -%% +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). --spec process_call(_CallArgs, machine(), handler_args(), handler_opts()) -> {ok, result()}. -process_call(_CallArgs, #{}, _, _Opts) -> - {ok, #{}}. +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + ff_machine_lib:process_repair(?MODULE, Machine, Scenario). --spec process_repair(ff_repair:scenario(), machine(), handler_args(), handler_opts()) -> - {ok, {repair_response(), result()}} | {error, repair_error()}. -process_repair(Scenario, Machine, _Args, _Opts) -> - ff_repair:apply_scenario(ff_destination, Machine, Scenario). - --spec process_notification(_, machine(), handler_args(), handler_opts()) -> result() | no_return(). -process_notification(_Args, _Machine, _HandlerArgs, _Opts) -> +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> #{}. -%% Internals - -backend() -> - fistful:backend(?NS). +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + prg_machine:event_body(), + term() +) -> term(). +apply_event(_EventID, _Ts, Body, Model) -> + ff_destination:apply_event(Body, Model). + +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Timestamp, Body) -> + ff_machine_lib:marshal_event_body(destination, ?EVENT_FORMAT_VERSION, Body, Timestamp). + +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + ff_machine_lib:unmarshal_event_body(destination, Payload). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + ff_machine_lib:marshal_aux_state(AuxSt). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(Payload) when is_binary(Payload) -> + ff_machine_lib:unmarshal_aux_state(Payload). diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl new file mode 100644 index 00000000..4d4476d8 --- /dev/null +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -0,0 +1,169 @@ +-module(ff_machine_codec). + +-export([marshal_event/3]). +-export([unmarshal_event/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([payload_to_binary/1]). + +-export_type([domain/0]). + +-type domain() :: deposit | source | destination | withdrawal | withdrawal_session. +-type format_version() :: pos_integer(). +-type timestamped_event() :: {ev, term(), term()}. +-type event_payload() :: {bin, binary()}. + +-spec marshal_event(domain(), format_version(), timestamped_event()) -> event_payload(). +marshal_event(deposit, 1, Timestamped) -> + marshal_thrift_event( + Timestamped, + fun(T) -> ff_deposit_codec:marshal(timestamped_change, T) end, + fistful_deposit_thrift, + 'TimestampedChange' + ); +marshal_event(source, 1, Timestamped) -> + marshal_thrift_event( + Timestamped, + fun(T) -> ff_source_codec:marshal(timestamped_change, T) end, + fistful_source_thrift, + 'TimestampedChange' + ); +marshal_event(destination, 1, Timestamped) -> + marshal_thrift_event( + Timestamped, + fun(T) -> ff_destination_codec:marshal(timestamped_change, T) end, + fistful_destination_thrift, + 'TimestampedChange' + ); +marshal_event(withdrawal, 1, Timestamped) -> + marshal_thrift_event( + Timestamped, + fun(T) -> ff_withdrawal_codec:marshal(timestamped_change, T) end, + fistful_wthd_thrift, + 'TimestampedChange' + ); +marshal_event(withdrawal_session, 1, Timestamped) -> + marshal_thrift_event( + Timestamped, + fun(T) -> ff_withdrawal_session_codec:marshal(timestamped_change, T) end, + fistful_wthd_session_thrift, + 'TimestampedChange' + ); +marshal_event(Domain, Format, _Timestamped) -> + erlang:error({unknown_event_format, Domain, Format}). + +-spec unmarshal_event(domain(), binary()) -> timestamped_event(). +unmarshal_event(deposit, Payload) -> + unmarshal_thrift_event( + Payload, + fun(T) -> ff_deposit_codec:unmarshal(timestamped_change, T) end, + fistful_deposit_thrift, + 'TimestampedChange' + ); +unmarshal_event(source, Payload) -> + unmarshal_thrift_event( + Payload, + fun(T) -> ff_source_codec:unmarshal(timestamped_change, T) end, + fistful_source_thrift, + 'TimestampedChange' + ); +unmarshal_event(destination, Payload) -> + unmarshal_thrift_event( + Payload, + fun(T) -> ff_destination_codec:unmarshal(timestamped_change, T) end, + fistful_destination_thrift, + 'TimestampedChange' + ); +unmarshal_event(withdrawal, Payload) -> + unmarshal_thrift_event( + Payload, + fun(T) -> ff_withdrawal_codec:unmarshal(timestamped_change, T) end, + fistful_wthd_thrift, + 'TimestampedChange' + ); +unmarshal_event(withdrawal_session, Payload) -> + unmarshal_thrift_event( + Payload, + fun(T) -> ff_withdrawal_session_codec:unmarshal(timestamped_change, T) end, + fistful_wthd_session_thrift, + 'TimestampedChange' + ). + +%% aux_state: legacy machinery_prg_backend wrote plain term_to_binary(AuxSt). +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + term_to_binary(AuxSt). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(<<>>) -> + #{}; +unmarshal_aux_state(Payload) when is_binary(Payload) -> + binary_to_term(Payload). + +%% Event payload: write the legacy envelope term_to_binary({bin, ThriftBin}) +%% (machinery_prg_backend used machinery_utils:encode(term, ...)). +-spec payload_to_binary(event_payload()) -> binary(). +payload_to_binary(Payload) -> + term_to_binary(Payload). + +-spec marshal_thrift_event( + timestamped_event(), + fun((timestamped_event()) -> term()), + atom(), + atom() +) -> event_payload(). +marshal_thrift_event(Timestamped, MarshalFun, ThriftModule, ThriftStruct) -> + ThriftChange = MarshalFun(Timestamped), + Type = {struct, struct, {ThriftModule, ThriftStruct}}, + {bin, ff_proto_utils:serialize(Type, ThriftChange)}. + +%% Legacy machinery_prg_backend stored events as term_to_binary({bin, ThriftBin}). +legacy_thrift_payload(Payload) when is_binary(Payload) -> + case binary_to_term(Payload) of + {bin, Bin} when is_binary(Bin) -> + Bin; + Other -> + erlang:error({legacy_msgpack_event, Other}) + end. + +-spec unmarshal_thrift_event( + binary(), + fun((term()) -> timestamped_event()), + atom(), + atom() +) -> timestamped_event(). +unmarshal_thrift_event(Payload, UnmarshalFun, ThriftModule, ThriftStruct) -> + ThriftBin = legacy_thrift_payload(Payload), + Type = {struct, struct, {ThriftModule, ThriftStruct}}, + ThriftChange = ff_proto_utils:deserialize(Type, ThriftBin), + UnmarshalFun(ThriftChange). + +%% --- Golden tests: legacy FF format compatibility (stage 1) ---------------- + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec aux_state_roundtrip_test() -> _. +aux_state_roundtrip_test() -> + AuxSt = #{ctx => #{<<"k">> => <<"v">>}, model => some_model}, + ?assertEqual(AuxSt, unmarshal_aux_state(marshal_aux_state(AuxSt))). + +-spec aux_state_empty_test() -> _. +aux_state_empty_test() -> + ?assertEqual(#{}, unmarshal_aux_state(<<>>)). + +-spec aux_state_reads_legacy_term_to_binary_test() -> _. +aux_state_reads_legacy_term_to_binary_test() -> + %% Legacy machinery wrote aux_state as plain term_to_binary(AuxSt). + AuxSt = #{ctx => #{}, model => legacy}, + ?assertEqual(AuxSt, unmarshal_aux_state(term_to_binary(AuxSt))). + +-spec legacy_thrift_payload_reads_legacy_envelope_test() -> _. +legacy_thrift_payload_reads_legacy_envelope_test() -> + ThriftBin = <<0, 1, 2, 3, 4>>, + %% Legacy machinery_prg_backend wrote events as term_to_binary({bin, ThriftBin}). + ?assertEqual(ThriftBin, legacy_thrift_payload(term_to_binary({bin, ThriftBin}))). + +-endif. diff --git a/apps/ff_transfer/src/ff_machine_lib.erl b/apps/ff_transfer/src/ff_machine_lib.erl new file mode 100644 index 00000000..78417046 --- /dev/null +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -0,0 +1,213 @@ +-module(ff_machine_lib). + +%%% Shared helpers for the ff_* prg_machine handlers and their thin machine +%%% clients. Extracted to remove the per-namespace copy-paste. + +-export([create/4]). +-export([get/5]). +-export([events/4]). +-export([repair/3]). +-export([init_result/2]). +-export([init_result/3]). +-export([machine_to_st/2]). +-export([to_prg_result/1]). +-export([process_repair/3]). +-export([process_repair/4]). +-export([to_repair_machine/1]). +-export([from_repair_result/2]). +-export([repair_events_to_domain/1]). +-export([event_body_from_timestamped/1]). +-export([history_to_events/1]). +-export([codec_timestamp/1]). +-export([marshal_event_body/3]). +-export([marshal_event_body/4]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + +-export_type([repair_call_error/0]). + +-import(ff_pipeline, [do/1, unwrap/1]). + +-type timestamp() :: prg_machine:timestamp(). +-type timestamped_event(T) :: {ev, timestamp(), T}. +-type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}. +-type create_fun() :: fun((map()) -> {ok, [term()]} | {error, term()}). + +-type processor_error() :: prg_machine:processor_error(). + +-type repair_call_error() :: + notfound + | working + | failed + | {failed, ff_repair:repair_error()} + | processor_error(). + +-spec create(prg_machine:namespace(), create_fun(), map(), ff_entity_context:context()) -> + ok | {error, term()}. +create(NS, CreateFun, Params, Ctx) -> + do(fun() -> + #{id := ID} = Params, + Events = unwrap(CreateFun(Params)), + unwrap(prg_machine:start(NS, ID, {Events, Ctx})) + end). + +-spec get(prg_machine:namespace(), prg_machine:id(), event_range(), module(), term()) -> + {ok, map()} | {error, term()}. +get(NS, ID, {After, Limit}, Handler, NotFoundError) -> + case prg_machine:get(NS, ID, prg_machine:history_range(After, Limit, forward)) of + {ok, Machine} -> + {ok, machine_to_st(Handler, Machine)}; + {error, notfound} -> + {error, NotFoundError}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) + end. + +-spec events(prg_machine:namespace(), prg_machine:id(), event_range(), term()) -> + {ok, [{prg_machine:event_id(), timestamped_event(term())}]} | {error, term()}. +events(NS, ID, {After, Limit}, NotFoundError) -> + case prg_machine:get_history(NS, ID, After, Limit, forward) of + {ok, History} -> + {ok, history_to_events(History)}; + {error, notfound} -> + {error, NotFoundError}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) + end. + +-spec repair(prg_machine:namespace(), prg_machine:id(), ff_repair:scenario()) -> + {ok, ff_repair:repair_response()} | {error, repair_call_error()}. +repair(NS, ID, Scenario) -> + case prg_machine:repair(NS, ID, Scenario) of + {ok, Response} -> + {ok, Response}; + {error, notfound} -> + {error, notfound}; + {error, working} -> + {error, working}; + {error, {repair, {failed, Reason}}} -> + {error, {failed, Reason}}; + {error, failed} = Error -> + Error; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception} + end. + +-spec init_result([term()], ff_entity_context:context()) -> prg_machine:result(). +init_result(Events, Ctx) -> + #{ + events => Events, + auxst => #{ctx => Ctx} + }. + +-spec init_result([term()], ff_entity_context:context(), prg_action:t()) -> prg_machine:result(). +init_result(Events, Ctx, Action) -> + (init_result(Events, Ctx))#{action => Action}. + +-spec machine_to_st(module(), prg_machine:machine()) -> map(). +machine_to_st(Handler, #{aux_state := AuxState} = Machine) -> + #{ + model => prg_machine:collapse(Handler, Machine), + ctx => ctx(AuxState) + }. + +-spec to_prg_result({prg_action:t(), [term()]}) -> prg_machine:result(). +to_prg_result({Action, Events}) -> + #{ + events => Events, + action => Action + }. + +-spec process_repair(module(), prg_machine:machine(), ff_repair:scenario()) -> + prg_machine:result() | {error, term()}. +process_repair(Handler, Machine, Scenario) -> + case ff_repair:apply_scenario(Handler, to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_repair(module(), prg_machine:machine(), ff_repair:scenario(), ff_repair:processors()) -> + prg_machine:result() | {error, term()}. +process_repair(Handler, Machine, Scenario, ScenarioProcessors) -> + case ff_repair:apply_scenario(Handler, to_repair_machine(Machine), Scenario, ScenarioProcessors) of + {ok, {_Response, Result}} -> + from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec to_repair_machine(prg_machine:machine()) -> ff_repair:machine(). +to_repair_machine(#{namespace := NS, id := ID, history := History, aux_state := AuxState}) -> + #{ + namespace => NS, + id => ID, + history => [{EventID, {ev, Timestamp, Body}} || {EventID, Timestamp, Body} <- History], + aux_state => AuxState + }. + +-spec from_repair_result(ff_repair:scenario_result(), prg_machine:machine()) -> prg_machine:result(). +from_repair_result(#{events := Events} = Result, _Machine) -> + PrgResult = to_prg_result({maps:get(action, Result, idle), repair_events_to_domain(Events)}), + case maps:is_key(aux_state, Result) of + true -> + PrgResult#{auxst => maps:get(aux_state, Result)}; + false -> + PrgResult + end. + +-spec repair_events_to_domain([timestamped_event(T)]) -> [T]. +repair_events_to_domain(Events) -> + [event_body_from_timestamped(E) || E <- Events]. + +-spec event_body_from_timestamped(timestamped_event(T) | T) -> T. +event_body_from_timestamped({ev, _Timestamp, Change}) -> + Change; +event_body_from_timestamped(Change) -> + Change. + +-spec history_to_events(prg_machine:history()) -> + [{prg_machine:event_id(), timestamped_event(term())}]. +history_to_events(History) -> + [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. + +-spec codec_timestamp(timestamp() | calendar:datetime()) -> timestamp(). +codec_timestamp({DateTime, USec}) when is_integer(USec) -> + {DateTime, USec}; +codec_timestamp(DateTime) -> + {DateTime, 0}. + +-spec marshal_event_body(ff_machine_codec:domain(), pos_integer(), prg_machine:event_body()) -> + {pos_integer(), binary()}. +marshal_event_body(Domain, Format, Body) -> + marshal_event_body(Domain, Format, Body, prg_machine:timestamp()). + +-spec marshal_event_body( + ff_machine_codec:domain(), pos_integer(), prg_machine:event_body(), timestamp() +) -> + {pos_integer(), binary()}. +marshal_event_body(Domain, Format, Body, Timestamp) -> + Timestamped = {ev, Timestamp, Body}, + Encoded = ff_machine_codec:marshal_event(Domain, Format, Timestamped), + {Format, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(ff_machine_codec:domain(), binary()) -> + prg_machine:event_body(). +unmarshal_event_body(Domain, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(Domain, Payload), + event_body_from_timestamped(Timestamped). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + ff_machine_codec:marshal_aux_state(AuxSt). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(Payload) when is_binary(Payload) -> + ff_machine_codec:unmarshal_aux_state(Payload). + +ctx(#{ctx := Ctx}) -> + Ctx; +ctx(_AuxState) -> + ff_entity_context:new(). diff --git a/apps/ff_transfer/src/ff_machine_trace.erl b/apps/ff_transfer/src/ff_machine_trace.erl new file mode 100644 index 00000000..9291c191 --- /dev/null +++ b/apps/ff_transfer/src/ff_machine_trace.erl @@ -0,0 +1,168 @@ +%%% Progressor trace → JSON-compatible maps (replaces ff_machine:trace/2). + +-module(ff_machine_trace). + +-export([trace/2]). + +-type namespace() :: prg_machine:namespace(). +-type id() :: prg_machine:id(). +-type trace() :: [trace_unit()]. +-type trace_unit() :: map(). + +-spec trace(namespace(), id()) -> {ok, trace()} | {error, term()}. +trace(NS, ID) -> + case progressor:trace(#{ns => NS, id => ID}) of + {ok, RawTrace} -> + case prg_machine_registry:lookup(NS) of + {ok, Handler} -> + {ok, lists:map(fun(Unit) -> unmarshal_trace_unit(Unit, Handler) end, RawTrace)}; + {error, _} = Error -> + Error + end; + {error, _} = Error -> + Error + end. + +unmarshal_trace_unit(TraceUnit, Handler) -> + Args = decode_trace_value(maps:get(args, TraceUnit, undefined)), + Events = maps:get(events, TraceUnit, []), + Context = decode_context(maps:get(context, TraceUnit, undefined)), + OtelTraceID = extract_trace_id(Context), + Error = extract_error(TraceUnit), + maps:merge( + maps:without([response, context], TraceUnit), + #{ + args => json_compatible_value(Args), + events => unmarshal_trace_events(Events, Handler), + otel_trace_id => OtelTraceID, + error => Error + } + ). + +unmarshal_trace_events(Events, Handler) -> + lists:map(fun(Event) -> unmarshal_trace_event(Event, Handler) end, Events). + +unmarshal_trace_event(Event, Handler) -> + Payload = maps:get(event_payload, Event), + EventID = maps:get(event_id, Event), + Ts = maps:get(event_timestamp, Event), + Body = prg_machine:unmarshal_event_body(Handler, Payload), + #{ + event_id => EventID, + event_payload => json_compatible_value(Body), + event_timestamp => Ts + }. + +decode_context(undefined) -> + #{}; +decode_context(<<>>) -> + #{}; +decode_context(Ctx) when is_map(Ctx) -> + Ctx; +decode_context(Value) -> + woody_rpc_helper:decode_rpc_context(decode_term(Value)). + +extract_trace_id(#{<<"otel">> := [OtelTraceID | _]}) -> + OtelTraceID; +extract_trace_id(#{otel := [OtelTraceID | _]}) -> + OtelTraceID; +extract_trace_id(_) -> + null. + +extract_error(#{response := Response}) -> + extract_error_response(decode_trace_value(Response)); +extract_error(_) -> + null. + +extract_error_response({error, Reason}) -> + unicode:characters_to_binary(io_lib:format("~p", [Reason])); +extract_error_response(_) -> + null. + +decode_trace_value(Value) -> + decode_term(Value). + +%% progressor trace may return storage blobs (binary) or already-decoded terms. +%% Same contract as prg_machine:decode_term/1. +-spec decode_term(term()) -> term(). +decode_term(Bin) when is_binary(Bin) -> + binary_to_term(Bin); +decode_term(Term) -> + Term. + +json_compatible_value([]) -> + []; +json_compatible_value(V) when is_list(V) -> + case io_lib:printable_unicode_list(V) of + true -> + unicode:characters_to_binary(V); + false -> + [json_compatible_value(E) || E <- V] + end; +json_compatible_value(V) when is_map(V) -> + maps:fold( + fun(K, Val, Acc) -> + Acc#{json_compatible_key(K) => json_compatible_value(Val)} + end, + #{}, + V + ); +json_compatible_value({K, V}) when is_atom(K) -> + #{K => json_compatible_value(V)}; +json_compatible_value(V) when is_tuple(V) -> + [json_compatible_value(E) || E <- tuple_to_list(V)]; +json_compatible_value(true) -> + true; +json_compatible_value(false) -> + false; +json_compatible_value(null) -> + null; +json_compatible_value(undefined) -> + null; +json_compatible_value(V) when is_atom(V) -> + erlang:atom_to_binary(V); +json_compatible_value(V) when is_integer(V) -> + V; +json_compatible_value(V) when is_float(V) -> + V; +json_compatible_value(V) when is_binary(V) -> + try unicode:characters_to_binary(V) of + Binary when is_binary(Binary) -> + Binary; + _ -> + content(<<"base64">>, base64:encode(V)) + catch + _:_ -> + content(<<"base64">>, base64:encode(V)) + end; +json_compatible_value(V) -> + CompatVal = unicode:characters_to_binary(io_lib:format("~p", [V])), + content(<<"unknown">>, CompatVal). + +json_compatible_key(K) when is_atom(K); is_integer(K); is_float(K) -> + K; +json_compatible_key(K) when is_list(K) -> + case io_lib:printable_unicode_list(K) of + true -> + unicode:characters_to_binary(K); + false -> + unicode:characters_to_binary(io_lib:format("~p", [K])) + end; +json_compatible_key(K) when is_binary(K) -> + try unicode:characters_to_binary(K) of + Binary when is_binary(Binary) -> + Binary; + _ -> + base64:encode(K) + catch + _:_ -> + base64:encode(K) + end; +json_compatible_key(K) -> + unicode:characters_to_binary(io_lib:format("~p", [K])). + +content(Type, Payload) -> + #{ + <<"content_type">> => Type, + <<"content">> => Payload + }. diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index e5b2c3bb..96c186a8 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -192,9 +192,12 @@ is_accessible(Source) -> ff_account:is_accessible(account(Source)). -spec apply_event(event(), ff_maybe:'maybe'(source_state())) -> source_state(). -apply_event({created, Source}, undefined) -> +apply_event(Ev, State) -> + apply_event_(Ev, State). + +apply_event_({created, Source}, undefined) -> Source; -apply_event({account, Ev}, #{account := Account} = Source) -> +apply_event_({account, Ev}, #{account := Account} = Source) -> Source#{account => ff_account:apply_event(Ev, Account)}; -apply_event({account, Ev}, Source) -> - apply_event({account, Ev}, Source#{account => undefined}). +apply_event_({account, Ev}, Source) -> + apply_event_({account, Ev}, Source#{account => undefined}). diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index 04449e56..6b8107d1 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -4,18 +4,27 @@ -module(ff_source_machine). +-behaviour(prg_machine). + +-define(EVENT_FORMAT_VERSION, 1). + %% API --type id() :: machinery:id(). +-type id() :: prg_machine:id(). -type ctx() :: ff_entity_context:context(). -type source() :: ff_source:source_state(). -type change() :: ff_source:event(). --type event() :: {integer(), ff_machine:timestamped_event(change())}. +-type timestamp() :: prg_machine:timestamp(). +-type timestamped_event(T) :: {ev, timestamp(), T}. +-type event() :: {integer(), timestamped_event(change())}. -type events() :: [event()]. -type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}. -type params() :: ff_source:params(). --type st() :: ff_machine:st(source()). +-type st() :: #{ + model := source(), + ctx := ctx() +}. -type repair_error() :: ff_repair:repair_error(). -type repair_response() :: ff_repair:repair_response(). @@ -40,100 +49,108 @@ -export([source/1]). -export([ctx/1]). -%% Machinery - --behaviour(machinery). +%% prg_machine --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). +-export([namespace/0]). +-export([init/2]). +-export([process_signal/2]). +-export([process_call/2]). +-export([process_repair/2]). +-export([process_notification/2]). +-export([marshal_event_body/2]). +-export([unmarshal_event_body/1]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([apply_event/4]). -%% Pipeline +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). --import(ff_pipeline, [do/1, unwrap/1]). - -%% -define(NS, 'ff/source_v1'). +%% API + -spec create(params(), ctx()) -> ok | {error, ff_source:create_error() | exists}. -create(#{id := ID} = Params, Ctx) -> - do(fun() -> - Events = unwrap(ff_source:create(Params)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) - end). +create(Params, Ctx) -> + ff_machine_lib:create(?NS, fun ff_source:create/1, Params, Ctx). -spec get(id()) -> {ok, st()} | {error, notfound}. get(ID) -> - ff_machine:get(ff_source, ?NS, ID). + get(ID, {undefined, undefined}). -spec get(id(), event_range()) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - ff_machine:get(ff_source, ?NS, ID, {After, Limit, forward}). + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, notfound). -spec events(id(), event_range()) -> {ok, events()} | {error, notfound}. events(ID, {After, Limit}) -> - do(fun() -> - History = unwrap(ff_machine:history(ff_source, ?NS, ID, {After, Limit, forward})), - [{EventID, TsEv} || {EventID, _, TsEv} <- History] - end). + ff_machine_lib:events(?NS, ID, {After, Limit}, notfound). %% Accessors -spec source(st()) -> source(). -source(St) -> - ff_machine:model(St). +source(#{model := Model}) -> + Model. -spec ctx(st()) -> ctx(). -ctx(St) -> - ff_machine:ctx(St). - -%% Machinery +ctx(#{ctx := Ctx}) -> + Ctx. --type machine() :: ff_machine:machine(change()). --type result() :: ff_machine:result(change()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). +%% prg_machine --spec init({[change()], ctx()}, machine(), _, handler_opts()) -> result(). -init({Events, Ctx}, #{}, _, _Opts) -> - #{ - events => ff_machine:emit_events(Events), - action => continue, - aux_state => #{ctx => Ctx} - }. +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. -%% +-spec init({[change()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + ff_machine_lib:init_result(Events, Ctx). --spec process_timeout(machine(), handler_args(), handler_opts()) -> result(). -process_timeout(_Machine, _, _Opts) -> +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, _Machine) -> #{}. -%% +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). --spec process_call(_CallArgs, machine(), handler_args(), handler_opts()) -> {ok, result()}. -process_call(_CallArgs, #{}, _, _Opts) -> - {ok, #{}}. +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + ff_machine_lib:process_repair(?MODULE, Machine, Scenario). --spec process_repair(ff_repair:scenario(), machine(), handler_args(), handler_opts()) -> - {ok, {repair_response(), result()}} | {error, repair_error()}. -process_repair(Scenario, Machine, _Args, _Opts) -> - ff_repair:apply_scenario(ff_source, Machine, Scenario). - --spec process_notification(_, machine(), handler_args(), handler_opts()) -> result() | no_return(). -process_notification(_Args, _Machine, _HandlerArgs, _Opts) -> +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> #{}. -%% Internals - -backend() -> - fistful:backend(?NS). +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + prg_machine:event_body(), + term() +) -> term(). +apply_event(_EventID, _Ts, Body, Model) -> + ff_source:apply_event(Body, Model). + +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Timestamp, Body) -> + ff_machine_lib:marshal_event_body(source, ?EVENT_FORMAT_VERSION, Body, Timestamp). + +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + ff_machine_lib:unmarshal_event_body(source, Payload). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + ff_machine_lib:marshal_aux_state(AuxSt). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(Payload) when is_binary(Payload) -> + ff_machine_lib:unmarshal_aux_state(Payload). diff --git a/apps/ff_transfer/src/ff_transfer.app.src b/apps/ff_transfer/src/ff_transfer.app.src index 2fc2fff2..90749508 100644 --- a/apps/ff_transfer/src/ff_transfer.app.src +++ b/apps/ff_transfer/src/ff_transfer.app.src @@ -8,8 +8,8 @@ genlib, ff_core, progressor, - machinery, - machinery_extra, + op_context, + prg_machine, damsel, fistful, limiter_proto diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 9d650624..2bcfcb0b 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -182,7 +182,7 @@ -type invalid_withdrawal_status_error() :: {invalid_withdrawal_status, status()}. --type action() :: sleep | continue | undefined. +-type action() :: prg_action:t(). -export_type([withdrawal/0]). -export_type([withdrawal_state/0]). @@ -500,7 +500,7 @@ start_adjustment(Params, Withdrawal) -> {error, {unknown_adjustment, _}} -> do_start_adjustment(Params, Withdrawal); {ok, _Adjustment} -> - {ok, {undefined, []}} + {ok, {idle, []}} end. -spec find_adjustment(adjustment_id(), withdrawal_state()) -> {ok, adjustment()} | {error, unknown_adjustment_error()}. @@ -575,7 +575,7 @@ format_activity(Activity) -> finalize_session(SessionID, SessionResult, Withdrawal) -> case get_session_by_id(SessionID, Withdrawal) of #{id := SessionID, result := SessionResult} -> - {ok, {undefined, []}}; + {ok, {idle, []}}; #{id := SessionID, result := OtherSessionResult} -> logger:warning("Session result mismatch - current: ~p, new: ~p", [OtherSessionResult, SessionResult]), {error, result_mismatch}; @@ -598,7 +598,7 @@ get_session_by_id(SessionID, Withdrawal) -> try_finish_session(SessionID, SessionResult, Withdrawal) -> case is_current_session(SessionID, Withdrawal) of true -> - {ok, {continue, [{session_finished, {SessionID, SessionResult}}]}}; + {ok, {timeout, [{session_finished, {SessionID, SessionResult}}]}}; false -> {error, old_session} end. @@ -741,7 +741,7 @@ do_process_transfer(p_transfer_prepare, Withdrawal) -> ok = do_rollback_routing([route(Withdrawal)], Withdrawal), Tr = ff_withdrawal_route_attempt_utils:get_current_p_transfer(attempts(Withdrawal)), {ok, Events} = ff_postings_transfer:prepare(Tr), - {continue, [{p_transfer, Ev} || Ev <- Events]}; + {timeout, [{p_transfer, Ev} || Ev <- Events]}; do_process_transfer(p_transfer_commit, Withdrawal) -> ok = commit_routes_limits([route(Withdrawal)], Withdrawal), Tr = ff_withdrawal_route_attempt_utils:get_current_p_transfer(attempts(Withdrawal)), @@ -749,12 +749,12 @@ do_process_transfer(p_transfer_commit, Withdrawal) -> DomainRevision = final_domain_revision(Withdrawal), {ok, Wallet} = fetch_wallet(wallet_id(Withdrawal), party_id(Withdrawal), DomainRevision), ok = ff_party:wallet_log_balance(wallet_id(Withdrawal), Wallet), - {continue, [{p_transfer, Ev} || Ev <- Events]}; + {timeout, [{p_transfer, Ev} || Ev <- Events]}; do_process_transfer(p_transfer_cancel, Withdrawal) -> ok = rollback_routes_limits([route(Withdrawal)], Withdrawal), Tr = ff_withdrawal_route_attempt_utils:get_current_p_transfer(attempts(Withdrawal)), {ok, Events} = ff_postings_transfer:cancel(Tr), - {continue, [{p_transfer, Ev} || Ev <- Events]}; + {timeout, [{p_transfer, Ev} || Ev <- Events]}; do_process_transfer(limit_check, Withdrawal) -> process_limit_check(Withdrawal); do_process_transfer(session_starting, Withdrawal) -> @@ -774,21 +774,21 @@ do_process_transfer(rollback_routing, Withdrawal) -> process_routing(Withdrawal) -> case do_process_routing(Withdrawal) of {ok, [Route | _Rest]} -> - {continue, [ + {timeout, [ {route_changed, Route} ]}; {error, {route_not_found, _Rejected} = Reason} -> Events = process_transfer_fail(Reason, Withdrawal), - {continue, Events}; + {timeout, Events}; {error, {inconsistent_quote_route, _Data} = Reason} -> Events = process_transfer_fail(Reason, Withdrawal), - {continue, Events} + {timeout, Events} end. -spec process_rollback_routing(withdrawal_state()) -> process_result(). process_rollback_routing(Withdrawal) -> ok = do_rollback_routing([], Withdrawal), - {undefined, []}. + {idle, []}. -spec do_process_routing(withdrawal_state()) -> {ok, [route()]} | {error, Reason} when Reason :: ff_withdrawal_routing:route_not_found() | attempt_limit_exceeded | InconsistentQuote, @@ -905,16 +905,18 @@ process_limit_check(Withdrawal) -> expected_range => Range, balance => Cash }, - [{limit_check, {wallet_sender, {failed, Details}}}] + [{limit_check, {wallet_sender, {failed, Details}}}]; + {error, {terms_violation, {wallet_limit, {invalid_terms, Details}}}} -> + [{limit_check, {wallet_sender, {failed, #{reason => invalid_terms, details => Details}}}}] end, - {continue, Events}. + {timeout, Events}. -spec process_p_transfer_creation(withdrawal_state()) -> process_result(). process_p_transfer_creation(Withdrawal) -> FinalCashFlow = make_final_cash_flow(Withdrawal), PTransferID = construct_p_transfer_id(Withdrawal), {ok, PostingsTransferEvents} = ff_postings_transfer:create(PTransferID, FinalCashFlow), - {continue, [{p_transfer, Ev} || Ev <- PostingsTransferEvents]}. + {timeout, [{p_transfer, Ev} || Ev <- PostingsTransferEvents]}. -spec process_session_creation(withdrawal_state()) -> process_result(). process_session_creation(Withdrawal) -> @@ -950,7 +952,7 @@ process_session_creation(Withdrawal) -> dest_auth_data => AuthData }, ok = create_session(ID, TransferData, SessionParams), - {continue, [{session_started, ID}]}. + {timeout, [{session_started, ID}]}. -spec construct_session_id(withdrawal_state()) -> id(). construct_session_id(Withdrawal) -> @@ -976,11 +978,11 @@ create_session(ID, TransferData, SessionParams) -> -spec process_session_sleep(withdrawal_state()) -> process_result(). process_session_sleep(_Withdrawal) -> - {sleep, []}. + {suspend, []}. -spec process_transfer_finish(withdrawal_state()) -> process_result(). process_transfer_finish(_Withdrawal) -> - {undefined, [{status_changed, succeeded}]}. + {idle, [{status_changed, succeeded}]}. -spec process_transfer_fail(fail_type(), withdrawal_state()) -> [event()]. process_transfer_fail(FailType, Withdrawal) -> @@ -988,11 +990,11 @@ process_transfer_fail(FailType, Withdrawal) -> [{status_changed, {failed, Failure}}]. -spec handle_child_result(process_result(), withdrawal_state()) -> process_result(). -handle_child_result({undefined, Events} = Result, Withdrawal) -> +handle_child_result({idle, Events} = Result, Withdrawal) -> NextWithdrawal = lists:foldl(fun(E, Acc) -> apply_event(E, Acc) end, Withdrawal, Events), case is_active(NextWithdrawal) of true -> - {continue, Events}; + {timeout, Events}; false -> DomainRevision = final_domain_revision(Withdrawal), {ok, Wallet} = fetch_wallet(wallet_id(Withdrawal), party_id(Withdrawal), DomainRevision), @@ -1452,15 +1454,16 @@ is_limit_check_ok({wallet_sender, {failed, _Details}}) -> -spec validate_wallet_limits(terms(), wallet()) -> {ok, valid} - | {error, {terms_violation, {wallet_limit, {cash_range, {cash(), cash_range()}}}}}. + | {error, {terms_violation, {wallet_limit, {cash_range, {cash(), cash_range()}}}}} + | {error, {terms_violation, {wallet_limit, {invalid_terms, term()}}}}. validate_wallet_limits(Terms, Wallet) -> case ff_party:validate_wallet_limits(Terms, Wallet) of {ok, valid} = Result -> Result; {error, {terms_violation, {cash_range, {Cash, CashRange}}}} -> {error, {terms_violation, {wallet_limit, {cash_range, {Cash, CashRange}}}}}; - {error, {invalid_terms, _Details} = Reason} -> - erlang:error(Reason) + {error, {invalid_terms, _} = Reason} -> + {error, {terms_violation, {wallet_limit, Reason}}} end. %% Adjustment validators @@ -1661,11 +1664,11 @@ process_route_change(Withdrawal, Reason) -> {error, {route_not_found, _Rejected}} -> %% No more routes, return last error Events = process_transfer_fail(Reason, Withdrawal), - {continue, Events} + {timeout, Events} end; false -> Events = process_transfer_fail(Reason, Withdrawal), - {undefined, Events} + {idle, Events} end. -spec is_failure_transient(fail_type(), withdrawal_state()) -> boolean(). @@ -1734,17 +1737,17 @@ do_process_route_change(Routes, Withdrawal, Reason) -> AttemptLimit = get_attempt_limit(Withdrawal), case ff_withdrawal_route_attempt_utils:next_route(Routes, Attempts, AttemptLimit) of {ok, Route} -> - {continue, [ + {timeout, [ {route_changed, Route} ]}; {error, route_not_found} -> %% No more routes, return last error Events = process_transfer_fail(Reason, Withdrawal), - {continue, Events}; + {timeout, Events}; {error, attempt_limit_exceeded} -> %% Attempt limit exceeded, return last error Events = process_transfer_fail(Reason, Withdrawal), - {continue, Events} + {timeout, Events} end. -spec handle_adjustment_changes(ff_adjustment:changes()) -> [event()]. @@ -1813,8 +1816,6 @@ get_quote_field(provider_id, #{route := Route}) -> get_quote_field(terminal_id, #{route := Route}) -> ff_withdrawal_routing:get_terminal(Route). -%% - -spec apply_event(event() | legacy_event(), ff_maybe:'maybe'(withdrawal_state())) -> withdrawal_state(). apply_event(Ev, T0) -> T1 = apply_event_(Ev, T0), diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 9b492914..ef069a72 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -4,17 +4,23 @@ -module(ff_withdrawal_machine). --behaviour(machinery). +-behaviour(prg_machine). + +-define(EVENT_FORMAT_VERSION, 1). %% API --type id() :: machinery:id(). +-type id() :: prg_machine:id(). -type change() :: ff_withdrawal:event(). --type event() :: {integer(), ff_machine:timestamped_event(change())}. --type st() :: ff_machine:st(withdrawal()). +-type timestamp() :: prg_machine:timestamp(). +-type timestamped_event(T) :: {ev, timestamp(), T}. +-type event() :: {integer(), timestamped_event(change())}. +-type st() :: #{ + model := withdrawal(), + ctx := ctx() +}. -type withdrawal() :: ff_withdrawal:withdrawal_state(). -type external_id() :: id(). --type action() :: ff_withdrawal:action(). -type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}. -type params() :: ff_withdrawal:params(). @@ -22,17 +28,32 @@ ff_withdrawal:create_error() | exists. --type start_adjustment_error() :: - ff_withdrawal:start_adjustment_error() - | unknown_withdrawal_error(). +-type repair_error() :: ff_repair:repair_error(). +-type repair_response() :: ff_repair:repair_response(). +-type repair_call_error() :: ff_machine_lib:repair_call_error(). -type unknown_withdrawal_error() :: {unknown_withdrawal, id()}. --type repair_error() :: ff_repair:repair_error(). --type repair_response() :: ff_repair:repair_response(). +-type action() :: ff_withdrawal:action(). + +-type adjustment_params() :: ff_withdrawal:adjustment_params(). + +-type start_adjustment_error() :: + ff_withdrawal:start_adjustment_error() + | unknown_withdrawal_error(). -type notify_args() :: {session_finished, session_id(), session_result()}. +-type notify_error() :: + notfound + | failed + | timeout + | {unknown_namespace, prg_machine:namespace()} + | prg_machine:processor_error() + | term(). + +-type session_id() :: ff_withdrawal_session:id(). +-type session_result() :: ff_withdrawal_session:session_result(). -export_type([id/0]). -export_type([st/0]). @@ -46,6 +67,7 @@ -export_type([create_error/0]). -export_type([repair_error/0]). -export_type([repair_response/0]). +-export_type([repair_call_error/0]). -export_type([start_adjustment_error/0]). %% API @@ -56,7 +78,6 @@ -export([events/2]). -export([repair/2]). -export([notify/2]). - -export([start_adjustment/2]). %% Accessors @@ -64,29 +85,25 @@ -export([withdrawal/1]). -export([ctx/1]). -%% Machinery - --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). +%% prg_machine -%% Pipeline - --import(ff_pipeline, [do/1, unwrap/1]). +-export([namespace/0]). +-export([init/2]). +-export([process_signal/2]). +-export([process_call/2]). +-export([process_repair/2]). +-export([process_notification/2]). +-export([marshal_event_body/2]). +-export([unmarshal_event_body/1]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% Internal types -type ctx() :: ff_entity_context:context(). - --type adjustment_params() :: ff_withdrawal:adjustment_params(). - --type session_id() :: ff_withdrawal_session:id(). --type session_result() :: ff_withdrawal_session:session_result(). - --type call() :: - {start_adjustment, adjustment_params()}. +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). -define(NS, 'ff/withdrawal_v2'). @@ -96,11 +113,7 @@ ok | {error, ff_withdrawal:create_error() | exists}. create(Params, Ctx) -> - do(fun() -> - #{id := ID} = Params, - Events = unwrap(ff_withdrawal:create(Params)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) - end). + ff_machine_lib:create(?NS, fun ff_withdrawal:create/1, Params, Ctx). -spec get(id()) -> {ok, st()} @@ -112,28 +125,18 @@ get(ID) -> {ok, st()} | {error, unknown_withdrawal_error()}. get(ID, {After, Limit}) -> - case ff_machine:get(ff_withdrawal, ?NS, ID, {After, Limit, forward}) of - {ok, _Machine} = Result -> - Result; - {error, notfound} -> - {error, {unknown_withdrawal, ID}} - end. + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, {unknown_withdrawal, ID}). -spec events(id(), event_range()) -> {ok, [event()]} | {error, unknown_withdrawal_error()}. events(ID, {After, Limit}) -> - case ff_machine:history(ff_withdrawal, ?NS, ID, {After, Limit, forward}) of - {ok, History} -> - {ok, [{EventID, TsEv} || {EventID, _, TsEv} <- History]}; - {error, notfound} -> - {error, {unknown_withdrawal, ID}} - end. + ff_machine_lib:events(?NS, ID, {After, Limit}, {unknown_withdrawal, ID}). -spec repair(id(), ff_repair:scenario()) -> - {ok, repair_response()} | {error, notfound | working | {failed, repair_error()}}. + {ok, repair_response()} | {error, repair_call_error()}. repair(ID, Scenario) -> - machinery:repair(?NS, ID, Scenario, backend()). + ff_machine_lib:repair(?NS, ID, Scenario). -spec start_adjustment(id(), adjustment_params()) -> ok @@ -141,99 +144,99 @@ repair(ID, Scenario) -> start_adjustment(WithdrawalID, Params) -> call(WithdrawalID, {start_adjustment, Params}). --spec notify(id(), notify_args()) -> - ok | {error, notfound} | no_return(). +-spec notify(id(), notify_args()) -> ok | {error, notify_error()} | no_return(). notify(ID, Args) -> - machinery:notify(?NS, ID, Args, backend()). + prg_machine:notify(?NS, ID, Args). %% Accessors -spec withdrawal(st()) -> withdrawal(). -withdrawal(St) -> - ff_machine:model(St). +withdrawal(#{model := Model}) -> + Model. -spec ctx(st()) -> ctx(). -ctx(St) -> - ff_machine:ctx(St). - -%% Machinery +ctx(#{ctx := Ctx}) -> + Ctx. --type machine() :: ff_machine:machine(event()). --type result() :: ff_machine:result(event()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). +%% prg_machine -backend() -> - fistful:backend(?NS). +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. --spec init({[event()], ctx()}, machine(), handler_args(), handler_opts()) -> result(). -init({Events, Ctx}, #{}, _, _Opts) -> +-spec init({[change()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> #{ - events => ff_machine:emit_events(Events), - action => continue, - aux_state => #{ctx => Ctx} + events => Events, + action => timeout, + auxst => #{ctx => Ctx} }. --spec process_timeout(machine(), handler_args(), handler_opts()) -> result(). -process_timeout(Machine, _, _Opts) -> - St = ff_machine:collapse(ff_withdrawal, Machine), - Withdrawal = withdrawal(St), - process_result(ff_withdrawal:process_transfer(Withdrawal), St). +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Withdrawal = prg_machine:collapse(?MODULE, Machine), + ff_machine_lib:to_prg_result(ff_withdrawal:process_transfer(Withdrawal)). --spec process_call(call(), machine(), handler_args(), handler_opts()) -> no_return(). -process_call({start_adjustment, Params}, Machine, _, _Opts) -> - do_start_adjustment(Params, Machine); -process_call(CallArgs, _Machine, _, _Opts) -> +-spec process_call({start_adjustment, adjustment_params()}, machine()) -> + {ok | {error, start_adjustment_error()}, prg_result()}. +process_call({start_adjustment, Params}, Machine) -> + Withdrawal = prg_machine:collapse(?MODULE, Machine), + case ff_withdrawal:start_adjustment(Params, Withdrawal) of + {ok, Result} -> + {ok, ff_machine_lib:to_prg_result(Result)}; + {error, _Reason} = Error -> + {Error, #{}} + end; +process_call(CallArgs, _Machine) -> erlang:error({unexpected_call, CallArgs}). --spec process_repair(ff_repair:scenario(), machine(), handler_args(), handler_opts()) -> - {ok, {repair_response(), result()}} | {error, repair_error()}. -process_repair(Scenario, Machine, _Args, _Opts) -> - ff_repair:apply_scenario(ff_withdrawal, Machine, Scenario). +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + ff_machine_lib:process_repair(?MODULE, Machine, Scenario). --spec process_notification(notify_args(), machine(), handler_args(), handler_opts()) -> result() | no_return(). -process_notification({session_finished, SessionID, SessionResult}, Machine, _HandlerArgs, _Opts) -> - St = ff_machine:collapse(ff_withdrawal, Machine), - case ff_withdrawal:finalize_session(SessionID, SessionResult, withdrawal(St)) of +-spec process_notification(notify_args(), machine()) -> prg_result(). +process_notification({session_finished, SessionID, SessionResult}, Machine) -> + Withdrawal = prg_machine:collapse(?MODULE, Machine), + case ff_withdrawal:finalize_session(SessionID, SessionResult, Withdrawal) of {ok, Result} -> - process_result(Result, St); + ff_machine_lib:to_prg_result(Result); {error, Reason} -> erlang:error({unable_to_finalize_session, Reason}) end. --spec do_start_adjustment(adjustment_params(), machine()) -> {Response, result()} when - Response :: ok | {error, ff_withdrawal:start_adjustment_error()}. -do_start_adjustment(Params, Machine) -> - St = ff_machine:collapse(ff_withdrawal, Machine), - case ff_withdrawal:start_adjustment(Params, withdrawal(St)) of - {ok, Result} -> - {ok, process_result(Result, St)}; - {error, _Reason} = Error -> - {Error, #{}} - end. +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + prg_machine:event_body(), + term() +) -> term(). +apply_event(_EventID, _Ts, Body, Model) -> + ff_withdrawal:apply_event(Body, Model). + +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Timestamp, Body) -> + ff_machine_lib:marshal_event_body(withdrawal, ?EVENT_FORMAT_VERSION, Body, Timestamp). -process_result({Action, Events}, St) -> - genlib_map:compact(#{ - events => set_events(Events), - action => set_action(Action, St) - }). +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + ff_machine_lib:unmarshal_event_body(withdrawal, Payload). -set_events([]) -> - undefined; -set_events(Events) -> - ff_machine:emit_events(Events). +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + ff_machine_lib:marshal_aux_state(AuxSt). -set_action(continue, _St) -> - continue; -set_action(undefined, _St) -> - undefined; -set_action(sleep, _St) -> - unset_timer. +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(Payload) when is_binary(Payload) -> + ff_machine_lib:unmarshal_aux_state(Payload). call(ID, Call) -> - case machinery:call(?NS, ID, Call, backend()) of + case prg_machine:call(?NS, ID, Call) of {ok, Reply} -> Reply; {error, notfound} -> - {error, {unknown_withdrawal, ID}} + {error, {unknown_withdrawal, ID}}; + {error, failed} -> + {error, failed}; + {error, _} = Error -> + Error end. diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index a830d426..035e4380 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -96,15 +96,9 @@ opts := ff_adapter:opts() }. --type id() :: machinery:id(). +-type id() :: binary(). --type action() :: - undefined - | continue - | {setup_callback, ff_withdrawal_callback:tag(), machinery:timer()} - | {setup_timer, machinery:timer()} - | retry - | finish. +-type action() :: prg_action:t(). -type process_result() :: {action(), [event()]}. @@ -183,21 +177,24 @@ create(ID, Data, Params) -> {ok, [{created, Session}]}. -spec apply_event(event(), undefined | session_state()) -> session_state(). -apply_event({created, Session}, undefined) -> +apply_event(Ev, State) -> + apply_event_(Ev, State). + +apply_event_({created, Session}, undefined) -> Session; -apply_event({next_state, AdapterState}, Session) -> +apply_event_({next_state, AdapterState}, Session) -> Session#{adapter_state => AdapterState}; -apply_event({transaction_bound, TransactionInfo}, Session) -> +apply_event_({transaction_bound, TransactionInfo}, Session) -> Session#{transaction_info => TransactionInfo}; -apply_event({finished, success = Result}, Session) -> +apply_event_({finished, success = Result}, Session) -> Session#{status => {finished, success}, result => Result}; -apply_event({finished, {success, TransactionInfo} = Result}, Session) -> +apply_event_({finished, {success, TransactionInfo} = Result}, Session) -> %% for backward compatibility with events stored in DB - take TransactionInfo here. %% @see ff_adapter_withdrawal:rebind_transaction_info/1 Session#{status => {finished, success}, result => Result, transaction_info => TransactionInfo}; -apply_event({finished, {failed, _} = Result} = Status, Session) -> +apply_event_({finished, {failed, _} = Result} = Status, Session) -> Session#{status => Status, result => Result}; -apply_event({callback, _Ev} = WrappedEvent, Session) -> +apply_event_({callback, _Ev} = WrappedEvent, Session) -> Callbacks0 = callbacks_index(Session), Callbacks1 = ff_withdrawal_callback_utils:apply_event(WrappedEvent, Callbacks0), set_callbacks_index(Callbacks1, Session). @@ -208,7 +205,22 @@ process_session(#{status := {finished, _}, id := ID, result := Result, withdrawa WithdrawalID = ff_adapter_withdrawal:id(Withdrawal), case ff_withdrawal_machine:notify(WithdrawalID, {session_finished, ID, Result}) of ok -> - {finish, []}; + {suspend, []}; + {error, failed} -> + %% The target withdrawal machine is already in error. The legacy + %% machinery notify was fire-and-forget, so a broken target never + %% poisoned the session — keep that behaviour, just log a warning. + _ = logger:warning( + "Withdrawal ~p is in error, dropping session_finished notification for session ~p", + [WithdrawalID, ID] + ), + {suspend, []}; + {error, notfound} -> + _ = logger:warning( + "Withdrawal ~p not found, dropping session_finished notification for session ~p", + [WithdrawalID, ID] + ), + {suspend, []}; {error, _} = Error -> erlang:error({unable_to_finish_session, Error}) end; @@ -247,13 +259,13 @@ process_callback(#{tag := CallbackTag} = Params, Session) -> {ok, Callback} = find_callback(CallbackTag, Session), case ff_withdrawal_callback:status(Callback) of succeeded -> - {ok, {ff_withdrawal_callback:response(Callback), {undefined, []}}}; + {ok, {ff_withdrawal_callback:response(Callback), {idle, []}}}; pending -> case status(Session) of active -> do_process_callback(Params, Callback, Session); {finished, _} -> - {error, {{session_already_finished, make_session_finish_params(Session)}, {undefined, []}}} + {error, {{session_already_finished, make_session_finish_params(Session)}, {idle, []}}} end end. @@ -301,14 +313,15 @@ process_adapter_intent(Intent, Session, Events0) -> process_adapter_intent({finish, {success, _TransactionInfo}}, _Session) -> %% we ignore TransactionInfo here %% @see ff_adapter_withdrawal:rebind_transaction_info/1 - {continue, [{finished, success}]}; + {timeout, [{finished, success}]}; process_adapter_intent({finish, Result}, _Session) -> - {continue, [{finished, Result}]}; + {timeout, [{finished, Result}]}; process_adapter_intent({sleep, #{timer := Timer, tag := Tag}}, Session) -> + ok = ff_machine_tag:create_binding(ff_withdrawal_session_machine:namespace(), Tag, id(Session)), Events = create_callback(Tag, Session), - {{setup_callback, Tag, Timer}, Events}; + {prg_action:schedule_timer(Timer), Events}; process_adapter_intent({sleep, #{timer := Timer}}, _Session) -> - {{setup_timer, Timer}, []}. + {prg_action:schedule_timer(Timer), []}. %% diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 25abaf0d..d81ad28b 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -1,18 +1,13 @@ %%% %%% Withdrawal session machine %%% -%%% TODOs -%%% -%%% - The way we ask `fistful` for a machinery backend smells like a circular -%%% dependency injection. -%%% - Dehydrate events upon saving. -%%% -module(ff_withdrawal_session_machine). --behaviour(machinery). +-behaviour(prg_machine). -define(NS, 'ff/withdrawal/session_v2'). +-define(EVENT_FORMAT_VERSION, 1). %% API @@ -26,13 +21,19 @@ -export([repair/2]). -export([process_callback/1]). -%% machinery +%% prg_machine --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). +-export([namespace/0]). +-export([init/2]). +-export([process_signal/2]). +-export([process_call/2]). +-export([process_repair/2]). +-export([process_notification/2]). +-export([marshal_event_body/2]). +-export([unmarshal_event_body/1]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% %% Types @@ -40,28 +41,31 @@ -type repair_error() :: ff_repair:repair_error(). -type repair_response() :: ff_repair:repair_response(). +-type repair_call_error() :: ff_machine_lib:repair_call_error(). +-export_type([id/0]). +-export_type([st/0]). +-export_type([event/0]). +-export_type([params/0]). +-export_type([event_range/0]). -export_type([repair_error/0]). -export_type([repair_response/0]). +-export_type([repair_call_error/0]). -%% -%% Internal types -%% - --type id() :: machinery:id(). +-type id() :: prg_machine:id(). -type data() :: ff_withdrawal_session:data(). -type params() :: ff_withdrawal_session:params(). +-type change() :: ff_withdrawal_session:event(). --type machine() :: ff_machine:machine(event()). --type result() :: ff_machine:result(event()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). - --type st() :: ff_machine:st(session()). +-type st() :: #{ + model := session(), + ctx := ctx() +}. -type session() :: ff_withdrawal_session:session_state(). --type event() :: ff_withdrawal_session:event(). --type action() :: ff_withdrawal_session:action(). +-type event() :: {integer(), timestamped_event(change())}. -type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}. +-type timestamp() :: prg_machine:timestamp(). +-type timestamped_event(T) :: {ev, timestamp(), T}. -type callback_params() :: ff_withdrawal_session:callback_params(). -type process_callback_response() :: ff_withdrawal_session:process_callback_response(). @@ -69,9 +73,9 @@ {unknown_session, {tag, id()}} | ff_withdrawal_session:process_callback_error(). --type process_result() :: ff_withdrawal_session:process_result(). - -type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). %% Pipeline @@ -81,51 +85,43 @@ %% API %% --define(SESSION_RETRY_TIME_LIMIT, 24 * 60 * 60). --define(MAX_SESSION_RETRY_TIMEOUT, 4 * 60 * 60). - -spec session(st()) -> session(). -session(St) -> - ff_machine:model(St). +session(#{model := Model}) -> + Model. -spec ctx(st()) -> ctx(). -ctx(St) -> - ff_machine:ctx(St). - -%% +ctx(#{ctx := Ctx}) -> + Ctx. -spec create(id(), data(), params()) -> ok | {error, exists}. create(ID, Data, Params) -> do(fun() -> Events = unwrap(ff_withdrawal_session:create(ID, Data, Params)), - unwrap(machinery:start(?NS, ID, Events, backend())) + unwrap(prg_machine:start(?NS, ID, Events)) end). -spec get(id()) -> {ok, st()} | {error, notfound}. get(ID) -> - ff_machine:get(ff_withdrawal_session, ?NS, ID). + get(ID, {undefined, undefined}). -spec get(id(), event_range()) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - ff_machine:get(ff_withdrawal_session, ?NS, ID, {After, Limit, forward}). + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, notfound). -spec events(id(), event_range()) -> - {ok, [{integer(), ff_machine:timestamped_event(event())}]} + {ok, [event()]} | {error, notfound}. events(ID, {After, Limit}) -> - do(fun() -> - History = unwrap(ff_machine:history(ff_withdrawal_session, ?NS, ID, {After, Limit, forward})), - [{EventID, TsEv} || {EventID, _, TsEv} <- History] - end). + ff_machine_lib:events(?NS, ID, {After, Limit}, notfound). -spec repair(id(), ff_repair:scenario()) -> - {ok, repair_response()} | {error, notfound | working | {failed, repair_error()}}. + {ok, repair_response()} | {error, repair_call_error()}. repair(ID, Scenario) -> - machinery:repair(?NS, ID, Scenario, backend()). + ff_machine_lib:repair(?NS, ID, Scenario). -spec process_callback(callback_params()) -> {ok, process_callback_response()} @@ -138,104 +134,84 @@ process_callback(#{tag := Tag} = Params) -> {error, {unknown_session, {tag, Tag}}} end. -%% machinery callbacks - --spec init([event()], machine(), handler_args(), handler_opts()) -> result(). -init(Events, #{}, _, _Opts) -> - #{ - events => ff_machine:emit_events(Events), - action => continue, - aux_state => #{ctx => ff_entity_context:new()} - }. - --spec process_timeout(machine(), handler_args(), handler_opts()) -> result(). -process_timeout(Machine, _, _Opts) -> - State = ff_machine:collapse(ff_withdrawal_session, Machine), - Session = session(State), - process_result(ff_withdrawal_session:process_session(Session), State). - --spec process_call(any(), machine(), handler_args(), handler_opts()) -> {Response, result()} | no_return() when - Response :: - {ok, process_callback_response()} - | {error, ff_withdrawal_session:process_callback_error()}. - -process_call({process_callback, Params}, Machine, _, _Opts) -> - do_process_callback(Params, Machine); -process_call(CallArgs, #{}, _, _Opts) -> +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init([change()], machine()) -> prg_result(). +init(Events, _Machine) -> + ff_machine_lib:init_result(Events, ff_entity_context:new(), timeout). + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Session = prg_machine:collapse(?MODULE, Machine), + ff_machine_lib:to_prg_result(ff_withdrawal_session:process_session(Session)). + +-spec process_call({process_callback, callback_params()}, machine()) -> + {{ok, process_callback_response()} | {error, process_callback_error()}, prg_result()}. +process_call({process_callback, Params}, Machine) -> + Session = prg_machine:collapse(?MODULE, Machine), + case ff_withdrawal_session:process_callback(Params, Session) of + {ok, {Response, Result}} -> + {{ok, Response}, ff_machine_lib:to_prg_result(Result)}; + {error, {Reason, _Result}} -> + {{error, Reason}, #{}} + end; +process_call(CallArgs, _Machine) -> erlang:error({unexpected_call, CallArgs}). --spec process_repair(ff_repair:scenario(), machine(), handler_args(), handler_opts()) -> - {ok, {repair_response(), result()}} | {error, repair_error()}. -process_repair(Scenario, Machine, _Args, _Opts) -> +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> ScenarioProcessors = #{ set_session_result => fun(Args, RMachine) -> - State = ff_machine:collapse(ff_withdrawal_session, RMachine), - {Action, Events} = ff_withdrawal_session:set_session_result(Args, session(State)), - {ok, {ok, #{action => set_action(Action, State), events => Events}}} + Session = prg_machine:collapse(?MODULE, ff_repair:to_prg_machine(RMachine)), + {Action, Events} = ff_withdrawal_session:set_session_result(Args, Session), + {ok, {ok, #{action => Action, events => Events}}} end }, - ff_repair:apply_scenario(ff_withdrawal_session, Machine, Scenario, ScenarioProcessors). + ff_machine_lib:process_repair(?MODULE, Machine, Scenario, ScenarioProcessors). --spec process_notification(_, machine(), handler_args(), handler_opts()) -> result() | no_return(). -process_notification(_Args, _Machine, _HandlerArgs, _Opts) -> +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> #{}. -%% -%% Internals -%% +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + prg_machine:event_body(), + term() +) -> term(). +apply_event(_EventID, _Ts, Body, Model) -> + ff_withdrawal_session:apply_event(Body, Model). --spec process_result(process_result(), st()) -> result(). -process_result({Action, Events}, St) -> - genlib_map:compact(#{ - events => set_events(Events), - action => set_action(Action, St) - }). - --spec set_events([event()]) -> undefined | ff_machine:timestamped_event(event()). -set_events([]) -> - undefined; -set_events(Events) -> - ff_machine:emit_events(Events). - --spec set_action(action(), st()) -> undefined | machinery:action() | [machinery:action()]. -set_action(continue, _St) -> - continue; -set_action(undefined, _St) -> - undefined; -set_action({setup_callback, Tag, Timer}, St) -> - ok = ff_machine_tag:create_binding(?NS, Tag, ff_withdrawal_session:id(session(St))), - timer_action(Timer); -set_action({setup_timer, Timer}, _St) -> - timer_action(Timer); -set_action(finish, _St) -> - unset_timer. +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Timestamp, Body) -> + ff_machine_lib:marshal_event_body(withdrawal_session, ?EVENT_FORMAT_VERSION, Body, Timestamp). -%% +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + ff_machine_lib:unmarshal_event_body(withdrawal_session, Payload). --spec timer_action(machinery:timer()) -> machinery:action(). -timer_action(Timer) -> - {set_timer, Timer}. +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + ff_machine_lib:marshal_aux_state(AuxSt). -backend() -> - fistful:backend(?NS). +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(Payload) when is_binary(Payload) -> + ff_machine_lib:unmarshal_aux_state(Payload). call(Ref, Call) -> - case machinery:call(?NS, Ref, Call, backend()) of + case prg_machine:call(?NS, Ref, Call) of {ok, Reply} -> Reply; {error, notfound} -> - {error, {unknown_session, Ref}} - end. - --spec do_process_callback(callback_params(), machine()) -> {Response, result()} when - Response :: - {ok, process_callback_response()} - | {error, ff_withdrawal_session:process_callback_error()}. -do_process_callback(Params, Machine) -> - St = ff_machine:collapse(ff_withdrawal_session, Machine), - case ff_withdrawal_session:process_callback(Params, session(St)) of - {ok, {Response, Result}} -> - {{ok, Response}, process_result(Result, St)}; - {error, {Reason, _Result}} -> - {{error, Reason}, #{}} + {error, {unknown_session, Ref}}; + {error, failed} -> + erlang:error({failed, ?NS, Ref}); + {error, {exception, _, _}} -> + erlang:error({failed, ?NS, Ref}); + {error, _} = Error -> + Error end. diff --git a/apps/ff_transfer/test/ff_ct_machine.erl b/apps/ff_transfer/test/ff_ct_machine.erl index f40d5c63..51e03841 100644 --- a/apps/ff_transfer/test/ff_ct_machine.erl +++ b/apps/ff_transfer/test/ff_ct_machine.erl @@ -1,28 +1,35 @@ %%% -%%% Test machine +%%% Test machine — hooks prg_machine timeout processing for CT %%% -module(ff_ct_machine). --dialyzer({nowarn_function, dispatch_signal/4}). - -export([load_per_suite/0]). -export([unload_per_suite/0]). -export([set_hook/2]). -export([clear_hook/1]). +-define(DISPATCH_TABLE, prg_machine_dispatch). + -spec load_per_suite() -> ok. load_per_suite() -> - meck:new(machinery, [no_link, passthrough]), - meck:expect(machinery, dispatch_signal, fun dispatch_signal/4), - meck:expect(machinery, dispatch_call, fun dispatch_call/4). + case lists:member(prg_machine, meck:mocked()) of + true -> + ok; + false -> + ok = meck:new(prg_machine, [no_link, passthrough]), + meck:expect(prg_machine, process, fun process/3) + end. -spec unload_per_suite() -> ok. unload_per_suite() -> - meck:unload(machinery). + case lists:member(prg_machine, meck:mocked()) of + true -> meck:unload(prg_machine); + false -> ok + end. --type hook() :: fun((machinery:machine(_, _), module(), _Args) -> _). +-type hook() :: fun((prg_machine:machine(), module(), _) -> _). -spec set_hook(timeout, hook()) -> ok. set_hook(timeout = On, Fun) when is_function(Fun, 3) -> @@ -33,21 +40,28 @@ clear_hook(timeout = On) -> _ = persistent_term:erase({?MODULE, hook, On}), ok. -dispatch_signal({init, Args}, Machine, {Handler, HandlerArgs}, Opts) -> - Handler:init(Args, Machine, HandlerArgs, Opts); -dispatch_signal(timeout, Machine, {Handler, HandlerArgs}, Opts) when Handler =/= fistful -> - _ = - case persistent_term:get({?MODULE, hook, timeout}, undefined) of - Fun when is_function(Fun) -> - Fun(Machine, Handler, HandlerArgs); - undefined -> - ok - end, - Handler:process_timeout(Machine, HandlerArgs, Opts); -dispatch_signal(timeout, Machine, {Handler, HandlerArgs}, Opts) -> - Handler:process_timeout(Machine, HandlerArgs, Opts); -dispatch_signal({notification, Args}, Machine, {Handler, HandlerArgs}, Opts) -> - Handler:process_notification(Args, Machine, HandlerArgs, Opts). - -dispatch_call(Args, Machine, {Handler, HandlerArgs}, Opts) -> - Handler:process_call(Args, Machine, HandlerArgs, Opts). +process({timeout, _BinArgs, #{process_id := ID} = _Process} = Call, #{ns := NS} = Opts, BinCtx) -> + case persistent_term:get({?MODULE, hook, timeout}, undefined) of + Fun when is_function(Fun, 3) -> + Handler = handler_module(NS), + {ok, Machine} = prg_machine:get(NS, ID), + _ = Fun(Machine, Handler, undefined), + call_original_process(Call, Opts, BinCtx); + undefined -> + call_original_process(Call, Opts, BinCtx) + end; +process(Call, Opts, BinCtx) -> + call_original_process(Call, Opts, BinCtx). + +call_original_process(Call, Opts, BinCtx) -> + 'prg_machine_meck_original':process(Call, Opts, BinCtx). + +-dialyzer({nowarn_function, call_original_process/3}). + +handler_module(NS) -> + case ets:lookup(?DISPATCH_TABLE, NS) of + [{NS, Handler}] -> + Handler; + [] -> + erlang:error({unknown_namespace, NS}) + end. diff --git a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl index b03b5b69..4ec2c5e9 100644 --- a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl @@ -72,7 +72,6 @@ groups() -> -spec init_per_suite(config()) -> config(). init_per_suite(C) -> - ff_ct_machine:load_per_suite(), ct_helper:makeup_cfg( [ ct_helper:test_case_name(init), @@ -83,7 +82,7 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> _. end_per_suite(C) -> - ff_ct_machine:unload_per_suite(), + maybe_unload_ff_ct_machine(), ok = ct_payment_system:shutdown(C). %% @@ -543,30 +542,34 @@ await_provider_retry(FirstAmount, SecondAmount, TotalAmount, C) -> }, Activity = {fail, session}, {ok, Barrier} = ff_ct_barrier:start_link(), - ok = ff_ct_machine:set_hook( - timeout, - fun - (Machine, ff_withdrawal_machine, _Args) -> - Withdrawal = ff_machine:model(ff_machine:collapse(ff_withdrawal, Machine)), - case {ff_withdrawal:id(Withdrawal), ff_withdrawal:activity(Withdrawal)} of - {WithdrawalID1, Activity} -> - ff_ct_barrier:enter(Barrier, _Timeout = 10000); - _ -> - ok - end; - (_Machine, _Handler, _Args) -> - false - end - ), - ok = ff_withdrawal_machine:create(WithdrawalParams1, ff_entity_context:new()), - _ = await_withdrawal_activity(Activity, WithdrawalID1), - ok = ff_withdrawal_machine:create(WithdrawalParams2, ff_entity_context:new()), - ?assertEqual(succeeded, await_final_withdrawal_status(WithdrawalID2)), - ok = ff_ct_barrier:release(Barrier), - Status = await_final_withdrawal_status(WithdrawalID1), - ok = ff_ct_machine:clear_hook(timeout), - ok = ff_ct_barrier:stop(Barrier), - Status. + try + ok = ff_ct_machine:load_per_suite(), + ok = ff_ct_machine:set_hook( + timeout, + fun + (Machine, ff_withdrawal_machine, _Args) -> + Withdrawal = prg_machine:collapse(ff_withdrawal_machine, Machine), + case {ff_withdrawal:id(Withdrawal), ff_withdrawal:activity(Withdrawal)} of + {WithdrawalID1, Activity} -> + ff_ct_barrier:enter(Barrier, _Timeout = 10000); + _ -> + ok + end; + (_Machine, _Handler, _Args) -> + ok + end + ), + ok = ff_withdrawal_machine:create(WithdrawalParams1, ff_entity_context:new()), + _ = await_withdrawal_activity(Activity, WithdrawalID1), + ok = ff_withdrawal_machine:create(WithdrawalParams2, ff_entity_context:new()), + ?assertEqual(succeeded, await_final_withdrawal_status(WithdrawalID2)), + ok = ff_ct_barrier:release(Barrier), + await_final_withdrawal_status(WithdrawalID1) + after + ok = ff_ct_machine:clear_hook(timeout), + maybe_unload_ff_ct_machine(), + ok = ff_ct_barrier:stop(Barrier) + end. set_retryable_errors(PartyID, ErrorList) -> application:set_env(ff_transfer, withdrawal, #{ @@ -613,7 +616,12 @@ prepare_standard_environment({_Amount, Currency} = WithdrawalCash, C) -> PartyID, Currency, #domain_TermSetHierarchyRef{id = 1}, #domain_PaymentInstitutionRef{id = 1} ), ok = await_wallet_balance({0, Currency}, WalletID), - DestinationID = ct_objects:create_destination(PartyID, undefined), + DestinationID = create_destination( + PartyID, + Currency, + #{sender => <<"SenderToken">>, receiver => <<"ReceiverToken">>}, + C + ), SourceID = ct_objects:create_source(PartyID, Currency), {_DepositID, _} = ct_objects:create_deposit(PartyID, WalletID, SourceID, WithdrawalCash), ok = await_wallet_balance(WithdrawalCash, WalletID), @@ -655,7 +663,7 @@ await_withdrawal_activity(Activity, WithdrawalID) -> {ok, Machine} = ff_withdrawal_machine:get(WithdrawalID), ff_withdrawal:activity(ff_withdrawal_machine:withdrawal(Machine)) end, - genlib_retry:linear(20, 1000) + genlib_retry:linear(50, 200) ). create_party(_C) -> @@ -677,20 +685,7 @@ get_wallet_balance(ID) -> create_destination(IID, Currency, AuthData, _C) -> ID = genlib:bsuuid(), - StoreSource = ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}), - Resource = - {bank_card, #'fistful_base_ResourceBankCard'{ - bank_card = #'fistful_base_BankCard'{ - token = maps:get(token, StoreSource), - bin = maps:get(bin, StoreSource, undefined), - masked_pan = maps:get(masked_pan, StoreSource, undefined), - exp_date = #'fistful_base_BankCardExpDate'{ - month = 12, - year = 2025 - }, - cardholder_name = maps:get(cardholder_name, StoreSource, undefined) - } - }}, + Resource = {bank_card, #{bank_card => ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025})}}, Params = genlib_map:compact(#{ id => ID, party_id => IID, @@ -702,3 +697,9 @@ create_destination(IID, Currency, AuthData, _C) -> }), ok = ff_destination_machine:create(Params, ff_entity_context:new()), ID. + +maybe_unload_ff_ct_machine() -> + case lists:member(prg_machine, meck:mocked()) of + true -> ff_ct_machine:unload_per_suite(); + false -> ok + end. diff --git a/apps/fistful/src/ff_context.erl b/apps/fistful/src/ff_context.erl deleted file mode 100644 index 4af3aec8..00000000 --- a/apps/fistful/src/ff_context.erl +++ /dev/null @@ -1,93 +0,0 @@ --module(ff_context). - --export([create/0]). --export([create/1]). --export([save/1]). --export([load/0]). --export([cleanup/0]). - --export([get_woody_context/1]). --export([get_party_client_context/1]). --export([get_party_client/1]). - --opaque context() :: #{ - woody_context := woody_context(), - party_client_context := party_client_context(), - party_client => party_client() -}. - --type options() :: #{ - woody_context => woody_context(), - party_client_context => party_client_context(), - party_client => party_client() -}. - --export_type([context/0]). --export_type([options/0]). - -%% Internal types - --type woody_context() :: woody_context:ctx(). --type party_client() :: party_client:client(). --type party_client_context() :: party_client:context(). - --define(REGISTRY_KEY, {p, l, {?MODULE, stored_context}}). - -%% API - --spec create() -> context(). -create() -> - create(#{}). - --spec create(options()) -> context(). -create(Options0) -> - Options1 = ensure_woody_context_exists(Options0), - ensure_party_context_exists(Options1). - --spec save(context()) -> ok. -save(Context) -> - true = - try - gproc:reg(?REGISTRY_KEY, Context) - catch - error:badarg -> - gproc:set_value(?REGISTRY_KEY, Context) - end, - ok. - --spec load() -> context() | no_return(). -load() -> - gproc:get_value(?REGISTRY_KEY). - --spec cleanup() -> ok. -cleanup() -> - true = gproc:unreg(?REGISTRY_KEY), - ok. - --spec get_woody_context(context()) -> woody_context(). -get_woody_context(#{woody_context := WoodyContext}) -> - WoodyContext. - --spec get_party_client(context()) -> party_client(). -get_party_client(#{party_client := PartyClient}) -> - PartyClient; -get_party_client(Context) -> - error(no_party_client, [Context]). - --spec get_party_client_context(context()) -> party_client_context(). -get_party_client_context(#{party_client_context := PartyContext}) -> - PartyContext. - -%% Internal functions - --spec ensure_woody_context_exists(options()) -> options(). -ensure_woody_context_exists(#{woody_context := _WoodyContext} = Options) -> - Options; -ensure_woody_context_exists(Options) -> - Options#{woody_context => woody_context:new()}. - --spec ensure_party_context_exists(options()) -> options(). -ensure_party_context_exists(#{party_client_context := _PartyContext} = Options) -> - Options; -ensure_party_context_exists(#{woody_context := WoodyContext} = Options) -> - Options#{party_client_context => party_client:create_context(#{woody_context => WoodyContext})}. diff --git a/apps/fistful/src/ff_limit.erl b/apps/fistful/src/ff_limit.erl deleted file mode 100644 index 171aa07a..00000000 --- a/apps/fistful/src/ff_limit.erl +++ /dev/null @@ -1,303 +0,0 @@ -%%% -%%% Limit tracker -%%% -%%% Behaviour: -%%% -%%% - If _limit_ is exceeded then there's no need to reject the transaction. -%%% -%%% - _Account_ operation is idempotent as long as the transaction is nor -%%% confirmed neither rejected. -%%% -%%% After that any transaction w/ the same ID will be handled regularly as a -%%% distinct transaction. -%%% -%%% - Limit itself is _not_ part of the state, just the _timespan_, implicitly. -%%% -%%% In a nutshell, we derive underlying 'account ID' from the timespan for -%%% the sake of simplicity. There are side effect though: -%%% * limits are independent in a sense that, for example, _daily_ limit -%%% changes do not count towards _monthly_ limit, and -%%% * there is no way to know that two transactions are one and the same if -%%% their IDs are equal but their timestamps are too far apart. -%%% -%%% - Accounting does not respect timezone-related quirks. -%%% -%%% If you want to, you should do it yourself. For example, you could convert -%%% UTC timestamps to timezone-specific timestamps and feed them here. -%%% -%%% For some reason which I can not wrap my head around `localtime` can -%%% resolve one UTC timestamp to _two_ timezone-specific timestamps in the -%%% middle of DST transition and let us resolve ambiguity. I believe taking -%%% earliest one would do the trick. -%%% - --module(ff_limit). - -%% API - --export([account/4]). --export([confirm/4]). --export([reject/4]). - --export([get/4]). - -%% Machinery - --behaviour(machinery). - --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). - -%% Types - --type limit(T) :: {id(), range(T), timespan()}. --type range(T) :: ff_range:range(T). --type timespan() :: day | week | month | year. --type trxid() :: binary(). --type delta(T) :: ord(T). --type trx(T) :: {trxid(), timestamp(), delta(T)}. --type record(T) :: ff_indef:indef(T). - --type timestamp() :: machinery:timestamp(). -% totally ordered --type ord(T) :: T. - -%% API - --type namespace() :: machinery:namespace(). --type id() :: machinery:id(). --type backend() :: machinery:backend(_). - --spec account(namespace(), limit(T), trx(T), backend()) -> - {ok, record(T)} - | {error, {exceeded, record(T)}} - | {error, {conflict, trx(T)}}. - --spec confirm(namespace(), limit(T), trx(T), backend()) -> - {ok, record(T)} - | {error, {conflict, trx(T)}}. - --spec reject(namespace(), limit(T), trx(T), backend()) -> - {ok, record(T)} - | {error, {conflict, trx(T)}}. - --spec get(namespace(), limit(T), timestamp(), backend()) -> {ok, record(T)} | {error, notfound}. - -account(NS, Limit, Trx, Backend) -> - ID = construct_limit_machine_id(Limit, Trx), - Range = get_limit_range(Limit), - lazycall(NS, ID, {account, Trx, Range}, Backend). - -confirm(NS, Limit, Trx, Backend) -> - ID = construct_limit_machine_id(Limit, Trx), - lazycall(NS, ID, {confirm, Trx}, Backend). - -reject(NS, Limit, Trx, Backend) -> - ID = construct_limit_machine_id(Limit, Trx), - lazycall(NS, ID, {reject, Trx}, Backend). - -get(NS, Limit, Ts, Backend) -> - ID = construct_limit_machine_id_(Limit, Ts), - case machinery:get(NS, ID, {undefined, 0, forward}, Backend) of - {ok, #{aux_state := St}} -> - {ok, head(St)}; - {error, notfound} -> - {error, notfound} - end. - -lazycall(NS, ID, Call, Backend) -> - case machinery:call(NS, ID, {undefined, 0, forward}, Call, Backend) of - {ok, Response} -> - Response; - {error, notfound} -> - _ = machinery:start(NS, ID, 0, Backend), - lazycall(NS, ID, Call, Backend) - end. - -construct_limit_machine_id(Limit, Trx) -> - construct_limit_machine_id_(Limit, get_trx_ts(Trx)). - -construct_limit_machine_id_(Limit, Ts) -> - ID = get_limit_id(Limit), - Span = get_limit_span(Limit), - Bucket = find_bucket(Ts, Span), - ff_string:join($/, [ - limit, - ID, - Span, - Bucket - ]). - -find_bucket({{Date, _Time}, _USec}, Span) -> - find_bucket(Date, Span); -find_bucket(Date, day) -> - calendar:date_to_gregorian_days(Date); -find_bucket(Date, week) -> - {Y, W} = calendar:iso_week_number(Date), - Y * 100 + W; -find_bucket({Y, M, _}, month) -> - Y * 100 + M; -find_bucket({Y, _, _}, year) -> - Y. - -%% Machinery - --type ev(T) :: - {seed, ord(T)} - | {account, trx(T)} - | {confirm, trx(T)} - | {reject, trx(T)}. - --type auxst(T) :: #{ - head := ff_indef:indef(T), - trxs := #{trxid() => trx(T)} -}. - --type machine(T) :: machinery:machine(ev(T), auxst(T)). --type result(T) :: machinery:result(ev(T), auxst(T)). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). - --spec init(ord(T), machine(T), _, handler_opts()) -> result(T). - --spec process_timeout(machine(T), _, handler_opts()) -> result(T). - --type call(T) :: - {account, trx(T), limit(T)} - | {confirm, trx(T)} - | {reject, trx(T)}. - --spec process_call(call(T), machine(T), _, handler_opts()) -> - { - {ok, record(T)} - | {error, {conflict, ord(T)}}, - result(T) - }. - --spec process_repair(ff_repair:scenario(), machine(_), handler_args(), handler_opts()) -> no_return(). - -init(Seed, #{}, _, _Opts) -> - #{ - events => [{seed, Seed}], - aux_state => new_st(Seed) - }. - -process_timeout(#{}, _, _Opts) -> - #{}. - -process_call({account, Trx, Limit}, #{aux_state := St}, _, _Opts) -> - process_account(Trx, Limit, St); -process_call({confirm, Trx}, #{aux_state := St}, _, _Opts) -> - process_confirm(Trx, St); -process_call({reject, Trx}, #{aux_state := St}, _, _Opts) -> - process_reject(Trx, St). - -process_repair(_RepairArgs, _Machine, _Args, _Opts) -> - erlang:error({not_implemented, repair}). - --spec process_notification(_, machine(_), handler_args(), handler_opts()) -> result(_) | no_return(). -process_notification(_Args, _Machine, _HandlerArgs, _Opts) -> - #{}. - -process_account(Trx, Range, St0) -> - case lookup_trx(get_trx_id(Trx), St0) of - error -> - St1 = record_trx(Trx, St0), - Head1 = head(St1), - case ff_range:contains(Range, ff_indef:to_range(Head1)) of - true -> - {{ok, Head1}, #{ - events => [{account, Trx}], - aux_state => St1 - }}; - false -> - {{error, {exceeded, Head1}}, #{}} - end; - {ok, Trx} -> - {{ok, head(St0)}, #{}}; - {ok, TrxWas} -> - {{error, {conflict, TrxWas}}, #{}} - end. - -process_confirm(Trx, St0) -> - case lookup_trx(get_trx_id(Trx), St0) of - {ok, Trx} -> - St1 = confirm_trx(Trx, St0), - {{ok, head(St1)}, #{ - events => [{confirm, Trx}], - aux_state => St1 - }}; - {ok, TrxWas} -> - {{error, {conflict, TrxWas}}, #{}}; - error -> - {{ok, head(St0)}, #{}} - end. - -process_reject(Trx, St0) -> - case lookup_trx(get_trx_id(Trx), St0) of - {ok, Trx} -> - St1 = reject_trx(Trx, St0), - {{ok, head(St1)}, #{ - events => [{reject, Trx}], - aux_state => St1 - }}; - {ok, TrxWas} -> - {{error, {conflict, TrxWas}}, #{}}; - error -> - {{ok, head(St0)}, #{}} - end. - -%% - -new_st(Seed) -> - #{ - head => ff_indef:new(Seed), - trxs => #{} - }. - -head(#{head := Head}) -> - Head. - -lookup_trx(TrxID, #{trxs := Trxs}) -> - maps:find(TrxID, Trxs). - -record_trx(Trx, #{head := Head, trxs := Trxs} = St) -> - St#{ - head := ff_indef:account(get_trx_dv(Trx), Head), - trxs := maps:put(get_trx_id(Trx), Trx, Trxs) - }. - -confirm_trx(Trx, #{head := Head, trxs := Trxs} = St) -> - St#{ - head := ff_indef:confirm(get_trx_dv(Trx), Head), - trxs := maps:remove(get_trx_id(Trx), Trxs) - }. - -reject_trx(Trx, #{head := Head, trxs := Trxs} = St) -> - St#{ - head := ff_indef:reject(get_trx_dv(Trx), Head), - trxs := maps:remove(get_trx_id(Trx), Trxs) - }. - -%% - -get_trx_id({ID, _Ts, _Dv}) -> - ID. - -get_trx_ts({_ID, Ts, _Dv}) -> - Ts. - -get_trx_dv({_ID, _Ts, Dv}) -> - Dv. - -get_limit_id({ID, _Range, _Span}) -> - ID. - -get_limit_range({_ID, Range, _Span}) -> - Range. - -get_limit_span({_ID, _Range, Span}) -> - Span. diff --git a/apps/fistful/src/ff_machine.erl b/apps/fistful/src/ff_machine.erl deleted file mode 100644 index 3150f952..00000000 --- a/apps/fistful/src/ff_machine.erl +++ /dev/null @@ -1,385 +0,0 @@ -%%% -%%% Generic machine -%%% -%%% TODOs -%%% -%%% - Split ctx and time tracking into different machine layers. -%%% - --module(ff_machine). - --type ctx() :: ff_entity_context:context(). --type range() :: machinery:range(). --type id() :: machinery:id(). --type namespace() :: machinery:namespace(). --type timestamp() :: machinery:timestamp(). - --type st(Model) :: #{ - model := Model, - ctx := ctx(), - times => {timestamp(), timestamp()} -}. - --type timestamped_event(T) :: - {ev, timestamp(), T}. - --type auxst() :: #{ctx := ctx()}. - --type machine(T) :: - machinery:machine(timestamped_event(T), auxst()). - --type result(T) :: - machinery:result(timestamped_event(T), auxst()). - --type migrate_params() :: #{ - ctx => ctx(), - timestamp => timestamp(), - id => id() -}. - --export_type([st/1]). --export_type([machine/1]). --export_type([result/1]). --export_type([timestamped_event/1]). --export_type([auxst/0]). --export_type([migrate_params/0]). - -%% Accessors - --export([model/1]). --export([ctx/1]). --export([created/1]). --export([updated/1]). - -%% API - --export([get/3]). --export([get/4]). --export([trace/2]). - --export([collapse/2]). --export([history/4]). - --export([emit_event/1]). --export([emit_events/1]). - -%% Machinery helpers - --export([init/4]). --export([process_timeout/3]). --export([process_call/4]). --export([process_repair/4]). --export([process_notification/4]). - -%% Model callbacks - --callback init(machinery:args(_)) -> [event()]. - --callback apply_event(event(), model()) -> model(). - --callback maybe_migrate(event(), migrate_params()) -> event(). - --callback process_call(machinery:args(_), st()) -> {machinery:response(_), [event()]}. - --callback process_repair(machinery:args(_), st()) -> - {ok, machinery:response(_), [event()]} | {error, machinery:error(_)}. - --callback process_timeout(st()) -> [event()]. - --optional_callbacks([maybe_migrate/2]). - -%% Pipeline helpers - --import(ff_pipeline, [do/1, unwrap/1]). - -%% Internal types - --type model() :: any(). --type event() :: any(). --type st() :: st(model()). --type machine() :: machine(model()). --type history() :: [machinery:event(timestamped_event(event()))]. --type trace_unit() :: map(). --type trace() :: [trace_unit()]. - -%% - --define(EPOCH_DIFF, 62167219200). - --spec model(st(Model)) -> Model. --spec ctx(st(_)) -> ctx(). --spec created(st(_)) -> timestamp() | undefined. --spec updated(st(_)) -> timestamp() | undefined. - -model(#{model := V}) -> - V. - -ctx(#{ctx := V}) -> - V. - -created(St) -> - erlang:element(1, times(St)). - -updated(St) -> - erlang:element(2, times(St)). - -times(St) -> - genlib_map:get(times, St, {undefined, undefined}). - -%% - --spec get(module(), namespace(), id()) -> - {ok, st()} - | {error, notfound}. -get(Mod, NS, Ref) -> - get(Mod, NS, Ref, {undefined, undefined, forward}). - --spec get(module(), namespace(), id(), range()) -> - {ok, st()} - | {error, notfound}. -get(Mod, NS, ID, Range) -> - do(fun() -> - Machine = unwrap(machinery:get(NS, ID, Range, fistful:backend(NS))), - collapse(Mod, Machine) - end). - --spec trace(namespace(), id()) -> {ok, trace()} | {error, term()}. -trace(NS, ID) -> - maybe - {ok, MachineTrace} ?= machinery:trace(NS, ID, fistful:backend(NS)), - Trace = unmarshal_trace(MachineTrace), - {ok, Trace} - else - {error, _} = Error -> - Error - end. - -unmarshal_trace(MachineTrace) -> - lists:map(fun(TraceUnit) -> unmarshal_trace_unit(TraceUnit) end, MachineTrace). - -unmarshal_trace_unit(TraceUnit) -> - MachineArgs = maps:get(args, TraceUnit, undefined), - MachineEvents = maps:get(events, TraceUnit, []), - OtelTraceID = extract_trace_id(TraceUnit), - Error = extract_error(TraceUnit), - maps:merge( - maps:without([response, context], TraceUnit), - #{ - args => json_compatible_value(MachineArgs), - events => unmarshal_machine_events(MachineEvents), - otel_trace_id => OtelTraceID, - error => Error - } - ). - -extract_trace_id(#{context := #{<<"otel">> := [OtelTraceID | _]}}) -> - OtelTraceID; -extract_trace_id(_) -> - null. - -extract_error(#{response := {error, Reason}}) -> - %% unification with hellgate - unicode:characters_to_binary(io_lib:format("~p", [Reason])); -extract_error(_) -> - null. - -json_compatible_value([]) -> - []; -json_compatible_value(V) when is_list(V) -> - case io_lib:printable_unicode_list(V) of - true -> - unicode:characters_to_binary(V); - false -> - [json_compatible_value(E) || E <- V] - end; -json_compatible_value(V) when is_map(V) -> - maps:fold( - fun(K, Val, Acc) -> - Acc#{json_compatible_key(K) => json_compatible_value(Val)} - end, - #{}, - V - ); -json_compatible_value({K, V}) when is_atom(K) -> - #{K => json_compatible_value(V)}; -json_compatible_value(V) when is_tuple(V) -> - [json_compatible_value(E) || E <- tuple_to_list(V)]; -json_compatible_value(true) -> - true; -json_compatible_value(false) -> - false; -json_compatible_value(null) -> - null; -json_compatible_value(undefined) -> - null; -json_compatible_value(V) when is_atom(V) -> - erlang:atom_to_binary(V); -json_compatible_value(V) when is_integer(V) -> - V; -json_compatible_value(V) when is_float(V) -> - V; -json_compatible_value(V) when is_binary(V) -> - try unicode:characters_to_binary(V) of - Binary when is_binary(Binary) -> - Binary; - _ -> - content(<<"base64">>, base64:encode(V)) - catch - _:_ -> - content(<<"base64">>, base64:encode(V)) - end; -%% default for other types (pid() | ref() | function() etc) -json_compatible_value(V) -> - CompatVal = unicode:characters_to_binary(io_lib:format("~p", [V])), - content(<<"unknown">>, CompatVal). - -json_compatible_key(K) when - is_atom(K); - is_integer(K); - is_float(K) --> - K; -json_compatible_key(K) when is_list(K) -> - case io_lib:printable_unicode_list(K) of - true -> - unicode:characters_to_binary(K); - false -> - unicode:characters_to_binary(io_lib:format("~p", [K])) - end; -json_compatible_key(K) when is_binary(K) -> - try unicode:characters_to_binary(K) of - Binary when is_binary(Binary) -> - Binary; - _ -> - base64:encode(K) - catch - _:_ -> - base64:encode(K) - end; -json_compatible_key(K) -> - unicode:characters_to_binary(io_lib:format("~p", [K])). - -content(Type, Payload) -> - #{ - <<"content_type">> => Type, - <<"content">> => Payload - }. - -unmarshal_machine_events(MachineEvents) -> - lists:map( - fun({EventID, _TsExt, {ev, Ts, Body}}) -> - #{ - event_id => EventID, - event_payload => json_compatible_value(Body), - event_timestamp => to_unix_microseconds(Ts) - } - end, - MachineEvents - ). - -to_unix_microseconds({{{_Y, _M, _D}, {_H, _Min, _S}} = DateTime, Microsec}) -> - GregorianSeconds = calendar:datetime_to_gregorian_seconds(DateTime), - (GregorianSeconds - ?EPOCH_DIFF) * 1000000 + Microsec. - --spec history(module(), namespace(), id(), range()) -> - {ok, history()} - | {error, notfound}. -history(Mod, NS, ID, Range) -> - do(fun() -> - Machine = unwrap(machinery:get(NS, ID, Range, fistful:backend(NS))), - #{history := History} = migrate_machine(Mod, Machine), - History - end). - --spec collapse(module(), machine()) -> st(). -collapse(Mod, Machine) -> - collapse_(Mod, migrate_machine(Mod, Machine)). - --spec collapse_(module(), machine()) -> st(). -collapse_(Mod, #{history := History, aux_state := #{ctx := Ctx}}) -> - collapse_history(Mod, History, #{ctx => Ctx}). - -collapse_history(Mod, History, St0) -> - lists:foldl(fun(Ev, St) -> merge_event(Mod, Ev, St) end, St0, History). - --spec migrate_history(module(), history(), migrate_params()) -> history(). -migrate_history(Mod, History, MigrateParams) -> - [migrate_event(Mod, Ev, MigrateParams) || Ev <- History]. - --spec emit_event(E) -> [timestamped_event(E)]. -emit_event(Event) -> - emit_events([Event]). - --spec emit_events([E]) -> [timestamped_event(E)]. -emit_events(Events) -> - emit_timestamped_events(Events, machinery_time:now()). - -emit_timestamped_events(Events, Ts) -> - [{ev, Ts, Body} || Body <- Events]. - -merge_event(Mod, {_ID, _Ts, TsEvent}, St0) -> - {Ev, St1} = merge_timestamped_event(TsEvent, St0), - Model1 = Mod:apply_event(Ev, maps:get(model, St1, undefined)), - St1#{model => Model1}. - -merge_timestamped_event({ev, Ts, Body}, #{times := {Created, _Updated}} = St) -> - {Body, St#{times => {Created, Ts}}}; -merge_timestamped_event({ev, Ts, Body}, #{} = St) -> - {Body, St#{times => {Ts, Ts}}}. - --spec migrate_machine(module(), machine()) -> machine(). -migrate_machine(Mod, #{history := History} = Machine) -> - MigrateParams = #{ - ctx => maps:get(ctx, maps:get(aux_state, Machine, #{}), undefined), - id => maps:get(id, Machine, undefined) - }, - Machine#{history => migrate_history(Mod, History, MigrateParams)}. - -migrate_event(Mod, {ID, Ts, {ev, EventTs, EventBody}} = Event, MigrateParams) -> - case erlang:function_exported(Mod, maybe_migrate, 2) of - true -> - {ID, Ts, {ev, EventTs, Mod:maybe_migrate(EventBody, MigrateParams#{timestamp => EventTs})}}; - false -> - Event - end. - -%% - --spec init({machinery:args(_), ctx()}, machinery:machine(E, A), module(), _) -> machinery:result(E, A). -init({Args, Ctx}, _Machine, Mod, _) -> - Events = Mod:init(Args), - #{ - events => emit_events(Events), - aux_state => #{ctx => Ctx} - }. - --spec process_timeout(machinery:machine(E, A), module(), _) -> machinery:result(E, A). -process_timeout(Machine, Mod, _) -> - Events = Mod:process_timeout(collapse(Mod, Machine)), - #{ - events => emit_events(Events) - }. - --spec process_call(machinery:args(_), machinery:machine(E, A), module(), _) -> - {machinery:response(_), machinery:result(E, A)}. -process_call(Args, Machine, Mod, _) -> - {Response, Events} = Mod:process_call(Args, collapse(Mod, Machine)), - {Response, #{ - events => emit_events(Events) - }}. - --spec process_repair(machinery:args(_), machinery:machine(E, A), module(), _) -> - {ok, machinery:response(_), machinery:result(E, A)} | {error, machinery:error(_)}. -process_repair(Args, Machine, Mod, _) -> - case Mod:process_repair(Args, collapse(Mod, Machine)) of - {ok, Response, Events} -> - {ok, Response, #{ - events => emit_events(Events) - }}; - {error, _Reason} = Error -> - Error - end. - --spec process_notification(_, machine(_), _, _) -> result(_) | no_return(). -process_notification(_Args, _Machine, _HandlerArgs, _Opts) -> - #{}. diff --git a/apps/fistful/src/ff_machine_tag.erl b/apps/fistful/src/ff_machine_tag.erl index f773e7fc..f575599a 100644 --- a/apps/fistful/src/ff_machine_tag.erl +++ b/apps/fistful/src/ff_machine_tag.erl @@ -6,12 +6,12 @@ -export([create_binding/3]). -type tag() :: binary(). --type ns() :: machinery:namespace(). +-type ns() :: prg_machine:namespace(). -type entity_id() :: binary(). -spec get_binding(ns(), tag()) -> {ok, entity_id()} | {error, not_found}. get_binding(NS, Tag) -> - WoodyContext = ff_context:get_woody_context(ff_context:load()), + WoodyContext = op_context:get_woody_context(op_context:load(op_context:key(fistful))), case bender_client:get_internal_id(tag_to_external_id(NS, Tag), WoodyContext) of {ok, EntityID} -> {ok, EntityID}; @@ -26,7 +26,7 @@ create_binding(NS, Tag, EntityID) -> %% create_binding_(NS, Tag, EntityID, Context) -> - WoodyContext = ff_context:get_woody_context(ff_context:load()), + WoodyContext = op_context:get_woody_context(op_context:load(op_context:key(fistful))), {ok, EntityID} = bender_client:gen_constant(tag_to_external_id(NS, Tag), EntityID, WoodyContext, Context), ok. diff --git a/apps/fistful/src/ff_party.erl b/apps/fistful/src/ff_party.erl index 5233c267..e7deffab 100644 --- a/apps/fistful/src/ff_party.erl +++ b/apps/fistful/src/ff_party.erl @@ -370,9 +370,9 @@ get_withdrawal_cash_flow_plan(Terms) -> %% Party management client get_party_client() -> - Context = ff_context:load(), - Client = ff_context:get_party_client(Context), - ClientContext = ff_context:get_party_client_context(Context), + Context = op_context:load(op_context:key(fistful)), + Client = op_context:get_party_client(Context), + ClientContext = op_context:get_party_client_context(Context), {Client, ClientContext}. %% Terms stuff diff --git a/apps/fistful/src/ff_repair.erl b/apps/fistful/src/ff_repair.erl index dc6c872e..8b49dda0 100644 --- a/apps/fistful/src/ff_repair.erl +++ b/apps/fistful/src/ff_repair.erl @@ -2,6 +2,7 @@ -export([apply_scenario/3]). -export([apply_scenario/4]). +-export([to_prg_machine/1]). %% Types @@ -11,13 +12,22 @@ -type scenario_id() :: atom(). -type scenario_args() :: any(). --type scenario_result(Event, AuxState) :: machinery:result(Event, AuxState). --type scenario_result() :: scenario_result(model_event(), model_aux_state()). --type scenario_error() :: machinery:error(any()). --type scenario_response() :: machinery:response(any()). + +-type timestamped_event(Body) :: {ev, prg_machine:timestamp(), Body}. + +-type repair_result() :: #{ + events := [timestamped_event(model_event())], + action => prg_action:t(), + aux_state => model_aux_state() +}. + +-type scenario_result() :: repair_result(). +-type scenario_result(_Event, _AuxState) :: repair_result(). +-type scenario_error() :: term(). +-type scenario_response() :: ok | term(). -type processor() :: fun( - (scenario_args(), machine()) -> {ok, {scenario_response(), scenario_result()}} | {error, scenario_error()} + (scenario_args(), machine()) -> {ok, {scenario_response(), repair_result()}} | {error, scenario_error()} ). -type processors() :: #{ @@ -50,14 +60,19 @@ -export_type([repair_error/0]). -export_type([repair_response/0]). -export_type([invalid_result_error/0]). +-export_type([machine/0]). %% Internal types -type model_event() :: any(). -type model_aux_state() :: any(). --type event() :: ff_machine:timestamped_event(model_event()). --type result() :: machinery:result(event(), model_aux_state()). --type machine() :: ff_machine:machine(event()). +-type result() :: repair_result(). +-type machine() :: #{ + namespace := prg_machine:namespace(), + id := prg_machine:id(), + history := [{pos_integer(), timestamped_event(model_event())}], + aux_state := model_aux_state() +}. %% Pipeline @@ -106,21 +121,39 @@ add_default_processors(Processor) -> maps:merge(Default, Processor). -spec apply_processor(processor(), scenario_args(), machine()) -> - {ok, {scenario_response(), ff_machine:result(event())}} | {error, scenario_error()}. + {ok, {scenario_response(), repair_result()}} | {error, scenario_error()}. apply_processor(Processor, Args, Machine) -> do(fun() -> {Response, #{events := Events} = Result} = unwrap(Processor(Args, Machine)), - {Response, Result#{events => ff_machine:emit_events(Events)}} + {Response, Result#{events => prg_machine:emit_events(Events)}} end). +-spec to_prg_machine(machine()) -> prg_machine:machine(). +to_prg_machine(#{namespace := NS, id := ID, history := History, aux_state := AuxSt}) -> + #{ + namespace => NS, + id => ID, + history => repair_history_to_prg(History), + aux_state => AuxSt + }. + -spec validate_result(module(), machine(), result()) -> {ok, valid} | {error, invalid_result_error()}. -validate_result(Mod, #{history := History} = Machine, #{events := NewEvents}) -> - HistoryLen = erlang:length(History), - NewEventsLen = erlang:length(NewEvents), +validate_result(Mod, RepairMachine, #{events := NewEvents}) -> + #{history := RepairHistory, aux_state := AuxSt} = RepairMachine, + PrgHistory0 = repair_history_to_prg(RepairHistory), + HistoryLen = length(PrgHistory0), + NewEventsLen = length(NewEvents), IDs = lists:seq(HistoryLen + 1, HistoryLen + NewEventsLen), - NewHistory = [{ID, machinery_time:now(), Event} || {ID, Event} <- lists:zip(IDs, NewEvents)], + PrgNewHistory = [ + {EventID, Ts, Body} + || {EventID, {ev, Ts, Body}} <- lists:zip(IDs, NewEvents) + ], + Machine = (to_prg_machine(RepairMachine))#{ + history => PrgHistory0 ++ PrgNewHistory, + aux_state => AuxSt + }, try - _ = ff_machine:collapse(Mod, Machine#{history => History ++ NewHistory}), + _ = prg_machine:collapse(Mod, Machine), {ok, valid} catch error:Error:Stack -> @@ -132,6 +165,9 @@ validate_result(Mod, #{history := History} = Machine, #{events := NewEvents}) -> {error, unexpected_failure} end. +repair_history_to_prg(History) -> + [{ID, Ts, Body} || {ID, {ev, Ts, Body}} <- History]. + -spec add_events(scenario_result(), machine()) -> {ok, {ok, scenario_result()}}. add_events(Result, _Machine) -> {ok, {ok, Result}}. diff --git a/apps/fistful/src/ff_woody_client.erl b/apps/fistful/src/ff_woody_client.erl index 9ca7b554..48412362 100644 --- a/apps/fistful/src/ff_woody_client.erl +++ b/apps/fistful/src/ff_woody_client.erl @@ -58,7 +58,7 @@ new(Url) when is_binary(Url); is_list(Url) -> {ok, woody:result()} | {exception, woody_error:business_error()}. call(ServiceIdOrClient, Request) -> - call(ServiceIdOrClient, Request, ff_context:get_woody_context(ff_context:load())). + call(ServiceIdOrClient, Request, op_context:get_woody_context(op_context:load(op_context:key(fistful)))). -spec call(service_id() | client(), request(), woody_context:ctx()) -> {ok, woody:result()} diff --git a/apps/fistful/src/fistful.app.src b/apps/fistful/src/fistful.app.src index e6e6a9e3..561fc99c 100644 --- a/apps/fistful/src/fistful.app.src +++ b/apps/fistful/src/fistful.app.src @@ -11,8 +11,8 @@ ff_core, snowflake, progressor, - machinery, - machinery_extra, + op_context, + prg_machine, woody, uuid, damsel, diff --git a/apps/fistful/src/fistful.erl b/apps/fistful/src/fistful.erl deleted file mode 100644 index 443412e6..00000000 --- a/apps/fistful/src/fistful.erl +++ /dev/null @@ -1,176 +0,0 @@ -%%% -%%% Fistful -%%% - --module(fistful). - --behaviour(machinery). --behaviour(machinery_backend). - --type namespace() :: machinery:namespace(). --type backend() :: machinery:backend(_). - --type options() :: #{ - handler := machinery:modopts(_), - party_client := party_client:client() -}. - --export([backend/1]). - --export([get/4]). --export([start/4]). --export([call/5]). --export([repair/5]). --export([notify/5]). --export([remove/3]). --export([trace/3]). - --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). - -%% - --spec backend(namespace()) -> backend(). -backend(NS) -> - {?MODULE, maps:get(NS, genlib_app:env(?MODULE, backends, #{}))}. - -%% - --type id() :: machinery:id(). --type args(T) :: machinery:args(T). --type range() :: machinery:range(). --type machine(E, A) :: machinery:machine(E, A). --type result(E, A) :: machinery:result(E, A). --type response(T) :: machinery:response(T). - --spec get(namespace(), id(), range(), machinery:backend(_)) -> {ok, machine(_, _)} | {error, notfound}. -get(NS, ID, Range, Backend) -> - machinery:get(NS, ID, Range, set_backend_context(Backend)). - --spec start(namespace(), id(), args(_), machinery:backend(_)) -> ok | {error, exists}. -start(NS, ID, Args, Backend) -> - machinery:start(NS, ID, Args, set_backend_context(Backend)). - --spec call(namespace(), id(), range(), args(_), machinery:backend(_)) -> {ok, response(_)} | {error, notfound}. -call(NS, ID, Range, Args, Backend) -> - machinery:call(NS, ID, Range, Args, set_backend_context(Backend)). - --spec repair(namespace(), id(), range(), args(_), machinery:backend(_)) -> - {ok, response(_)} | {error, notfound | working | {failed, machinery:error(_)}}. -repair(NS, ID, Range, Args, Backend) -> - machinery:repair(NS, ID, Range, Args, set_backend_context(Backend)). - --spec notify(namespace(), id(), range(), args(_), machinery:backend(_)) -> ok | {error, notfound}. -notify(NS, ID, Range, Args, Backend) -> - machinery:notify(NS, ID, Range, Args, set_backend_context(Backend)). - --spec remove(namespace(), id(), machinery:backend(_)) -> ok | {error, notfound}. -remove(NS, ID, Backend) -> - machinery:remove(NS, ID, set_backend_context(Backend)). - --spec trace(namespace(), id(), machinery:backend(_)) -> _. -trace(NS, ID, Backend) -> - machinery:trace(NS, ID, Backend). - -%% - --type handler_opts() :: machinery:handler_opts(#{ - woody_ctx := woody_context:ctx(), - otel_ctx => otel_ctx:t() -}). - --spec init(args(_), machine(E, A), options(), handler_opts()) -> result(E, A). -init(Args, Machine, #{handler := Handler} = Options, MachineryOptions) -> - _ = scope(Machine, #{activity => init}, fun() -> - ok = ff_context:save(create_context(Options, MachineryOptions)), - try - machinery:dispatch_signal({init, Args}, Machine, machinery_utils:get_handler(Handler), #{}) - after - ff_context:cleanup() - end - end). - --spec process_timeout(machine(E, A), options(), handler_opts()) -> result(E, A). -process_timeout(Machine, #{handler := Handler} = Options, MachineryOptions) -> - _ = scope(Machine, #{activity => timeout}, fun() -> - ok = ff_context:save(create_context(Options, MachineryOptions)), - try - machinery:dispatch_signal(timeout, Machine, machinery_utils:get_handler(Handler), #{}) - after - ff_context:cleanup() - end - end). - --spec process_call(args(_), machine(E, A), options(), handler_opts()) -> {response(_), result(E, A)}. -process_call(Args, Machine, #{handler := Handler} = Options, MachineryOptions) -> - _ = scope(Machine, #{activity => call}, fun() -> - ok = ff_context:save(create_context(Options, MachineryOptions)), - try - machinery:dispatch_call(Args, Machine, machinery_utils:get_handler(Handler), #{}) - after - ff_context:cleanup() - end - end). - --spec process_repair(args(_), machine(E, A), options(), handler_opts()) -> - {ok, {response(_), result(E, A)}} | {error, machinery:error(_)}. -process_repair(Args, Machine, #{handler := Handler} = Options, MachineryOptions) -> - _ = scope(Machine, #{activity => repair}, fun() -> - ok = ff_context:save(create_context(Options, MachineryOptions)), - try - machinery:dispatch_repair(Args, Machine, machinery_utils:get_handler(Handler), #{}) - after - ff_context:cleanup() - end - end). - --spec process_notification(args(_), machine(E, A), options(), handler_opts()) -> result(E, A). -process_notification(Args, Machine, #{handler := Handler} = Options, MachineryOptions) -> - _ = scope(Machine, #{activity => notification}, fun() -> - ok = ff_context:save(create_context(Options, MachineryOptions)), - try - machinery:dispatch_signal({notification, Args}, Machine, machinery_utils:get_handler(Handler), #{}) - after - ff_context:cleanup() - end - end). - -%% Internals - --spec create_context(options(), handler_opts()) -> ff_context:context(). -create_context(Options, MachineryOptions) -> - #{party_client := PartyClient} = Options, - #{woody_ctx := WoodyCtx} = MachineryOptions, - ContextOptions = #{ - woody_context => WoodyCtx, - party_client => PartyClient - }, - ff_context:create(ContextOptions). - --spec set_backend_context(machinery:backend(_)) -> machinery:backend(_). -set_backend_context(Backend) -> - %% Ensure woody context is set accordingly for composite backend. - case machinery_utils:get_backend(Backend) of - {machinery_hybrid_backend = Mod, #{primary_backend := Primary, fallback_backend := Fallback} = Opts} -> - {Mod, Opts#{ - primary_backend := set_backend_context(Primary), - fallback_backend := set_backend_context(Fallback) - }}; - {Mod, Opts} -> - {Mod, Opts#{ - woody_ctx => ff_context:get_woody_context(ff_context:load()) - }} - end. - -scope(Machine, Extra, Fun) -> - scoper:scope( - machine, - Extra#{ - namespace => maps:get(namespace, Machine), - id => maps:get(id, Machine) - }, - Fun - ). diff --git a/apps/fistful/test/ff_ct_sleepy_provider.erl b/apps/fistful/test/ff_ct_sleepy_provider.erl index 6e32506a..1dc55f5e 100644 --- a/apps/fistful/test/ff_ct_sleepy_provider.erl +++ b/apps/fistful/test/ff_ct_sleepy_provider.erl @@ -88,9 +88,9 @@ process_withdrawal(#{id := _}, nil, _Options) -> }}; process_withdrawal(#{id := WithdrawalID}, <<"sleeping">>, _Options) -> CallbackTag = <<"cb_", WithdrawalID/binary>>, - Deadline = calendar:system_time_to_universal_time(erlang:system_time(millisecond) + 5000, millisecond), + Deadline = genlib_rfc3339:format_relaxed(erlang:system_time(second) + 5, second), {ok, #{ - intent => {sleep, #{timer => {deadline, {Deadline, 0}}, tag => CallbackTag}}, + intent => {sleep, #{timer => {deadline, Deadline}, tag => CallbackTag}}, next_state => <<"callback_processing">>, transaction_info => #{id => <<"SleepyID">>, extra => #{}} }}. diff --git a/apps/fistful/test/ff_limit_SUITE.erl b/apps/fistful/test/ff_limit_SUITE.erl deleted file mode 100644 index eca2c2c1..00000000 --- a/apps/fistful/test/ff_limit_SUITE.erl +++ /dev/null @@ -1,123 +0,0 @@ --module(ff_limit_SUITE). - --export([all/0]). --export([init_per_suite/1]). --export([end_per_suite/1]). - --export([get_missing_fails/1]). --export([accounting_works/1]). --export([spanning_works/1]). - --spec get_missing_fails(config()) -> test_return(). --spec accounting_works(config()) -> test_return(). --spec spanning_works(config()) -> test_return(). - -%% - --import(ct_helper, [cfg/2]). - --type config() :: ct_helper:config(). --type test_case_name() :: ct_helper:test_case_name(). --type group_name() :: ct_helper:group_name(). --type test_return() :: _ | no_return(). - --spec all() -> [test_case_name() | {group, group_name()}]. -all() -> - [ - get_missing_fails, - accounting_works, - spanning_works - ]. - --spec init_per_suite(config()) -> config(). -init_per_suite(C) -> - {StartedApps, _StartupCtx} = ct_helper:start_apps([fistful]), - SuiteSup = ct_sup:start(), - BackendOpts = #{name => ?MODULE}, - BackendChildSpec = machinery_gensrv_backend:child_spec(ff_limit, BackendOpts), - {ok, _} = supervisor:start_child(SuiteSup, BackendChildSpec), - [ - {started_apps, StartedApps}, - {suite_sup, SuiteSup}, - {backend, machinery_gensrv_backend:new(BackendOpts)} - | C - ]. - --spec end_per_suite(config()) -> _. -end_per_suite(C) -> - ok = ct_sup:stop(cfg(suite_sup, C)), - ok = ct_helper:stop_apps(cfg(started_apps, C)), - ok. - -%% - --define(NS, ?MODULE). - -get_missing_fails(C) -> - Be = cfg(backend, C), - Limit = {<<"hurgy-gurdy">>, {infinity, infinity}, day}, - {error, notfound} = ff_limit:get(?NS, Limit, {calendar:universal_time(), 0}, Be). - -accounting_works(C) -> - Be = cfg(backend, C), - Range = {{inclusive, 0}, {exclusive, 42}}, - Limit = {genlib:unique(), Range, day}, - Date1 = ff_random:date(), - Date2 = ff_random:date(), - true = Date1 /= Date2, - ID1 = <<"H">>, - {ok, #{expected_max := 10}} = ff_limit:account(?NS, Limit, {ID1, Ts1 = rand_ts(Date1), 10}, Be), - ID2 = <<"E">>, - {ok, #{expected_max := 18}} = ff_limit:account(?NS, Limit, {ID2, Ts2 = rand_ts(Date1), 8}, Be), - {ok, #{expected_max := 18}} = ff_limit:account(?NS, Limit, {ID1, Ts1, 10}, Be), - {error, {conflict, {_, _, 8}}} = ff_limit:account(?NS, Limit, {ID2, Ts2, 10}, Be), - {error, {conflict, {_, Ts2, _}}} = ff_limit:account(?NS, Limit, {ID2, rand_ts(Date1), 8}, Be), - ID3 = <<"L">>, - {ok, #{expected_max := 32}} = ff_limit:account(?NS, Limit, {ID3, Ts3 = rand_ts(Date1), 14}, Be), - ID4 = <<"P">>, - {error, {exceeded, #{expected_max := 44}}} = ff_limit:account(?NS, Limit, {ID4, Ts4 = rand_ts(Date1), 12}, Be), - {ok, #{expected_max := 12}} = ff_limit:account(?NS, Limit, {ID4, rand_ts(Date2), 12}, Be), - {error, {exceeded, #{expected_max := 42}}} = ff_limit:account(?NS, Limit, {ID4, Ts4, 10}, Be), - {ok, #{expected_max := 41}} = ff_limit:account(?NS, Limit, {ID4, Ts4, 9}, Be), - ID5 = <<"!">>, - {error, {exceeded, #{expected_max := 50}}} = ff_limit:account(?NS, Limit, {ID5, Ts5 = rand_ts(Date1), 9}, Be), - {ok, #{expected_max := 41, current := 10}} = ff_limit:confirm(?NS, Limit, {ID1, Ts1, 10}, Be), - {ok, #{expected_max := 27, current := 10}} = ff_limit:reject(?NS, Limit, {ID3, Ts3, 14}, Be), - {ok, #{expected_max := 36}} = ff_limit:account(?NS, Limit, {ID5, Ts5, 9}, Be). - -spanning_works(C) -> - Be = cfg(backend, C), - LimID = genlib:unique(), - Range = {{inclusive, 0}, infinity}, - Lim1 = {LimID, Range, day}, - Lim2 = {LimID, Range, week}, - Lim3 = {LimID, Range, month}, - Time = ff_random:time(), - USec = rand_usec(), - Trx1 = {genlib:unique(), Ts1 = {{{2018, 06, 30}, Time}, USec}, Dv1 = rand:uniform(100)}, - % same week - Trx2 = {genlib:unique(), Ts2 = {{{2018, 07, 01}, Time}, USec}, Dv2 = rand:uniform(100)}, - % next week - Trx3 = {genlib:unique(), Ts3 = {{{2018, 07, 02}, Time}, USec}, Dv3 = rand:uniform(100)}, - _ = [ - {ok, _} = ff_limit:account(?NS, Lim, Trx, Be) - || Lim <- [Lim1, Lim2, Lim3], - Trx <- [Trx1, Trx2, Trx3] - ], - Dv12 = Dv1 + Dv2, - Dv23 = Dv2 + Dv3, - {ok, #{expected_max := Dv1}} = ff_limit:get(?NS, Lim1, Ts1, Be), - {ok, #{expected_max := Dv2}} = ff_limit:get(?NS, Lim1, Ts2, Be), - {ok, #{expected_max := Dv3}} = ff_limit:get(?NS, Lim1, Ts3, Be), - {ok, #{expected_max := Dv12}} = ff_limit:get(?NS, Lim2, Ts1, Be), - {ok, #{expected_max := Dv12}} = ff_limit:get(?NS, Lim2, Ts2, Be), - {ok, #{expected_max := Dv3}} = ff_limit:get(?NS, Lim2, Ts3, Be), - {ok, #{expected_max := Dv1}} = ff_limit:get(?NS, Lim3, Ts1, Be), - {ok, #{expected_max := Dv23}} = ff_limit:get(?NS, Lim3, Ts2, Be), - {ok, #{expected_max := Dv23}} = ff_limit:get(?NS, Lim3, Ts3, Be). - -rand_ts(Date) -> - {{Date, ff_random:time()}, rand_usec()}. - -rand_usec() -> - ff_random:from_range(0, 999999). diff --git a/apps/hellgate/include/hg_invoice.hrl b/apps/hellgate/include/hg_invoice.hrl index b7d72edc..b9032709 100644 --- a/apps/hellgate/include/hg_invoice.hrl +++ b/apps/hellgate/include/hg_invoice.hrl @@ -7,7 +7,7 @@ payments = [] :: [{hg_invoice:payment_id(), hg_invoice:payment_st()}], party :: undefined | hg_invoice:party(), party_config_ref :: undefined | hg_invoice:party_config_ref(), - latest_event_id :: undefined | hg_machine:event_id() + latest_event_id :: undefined | prg_machine:event_id() }). -endif. diff --git a/apps/hellgate/src/hellgate.app.src b/apps/hellgate/src/hellgate.app.src index fb17556b..0efc1939 100644 --- a/apps/hellgate/src/hellgate.app.src +++ b/apps/hellgate/src/hellgate.app.src @@ -10,7 +10,8 @@ fault_detector_proto, herd, progressor, - hg_progressor, + op_context, + prg_machine, hg_proto, exrates_proto, routing, diff --git a/apps/hellgate/src/hellgate.erl b/apps/hellgate/src/hellgate.erl index 23a6529e..64d934da 100644 --- a/apps/hellgate/src/hellgate.erl +++ b/apps/hellgate/src/hellgate.erl @@ -35,10 +35,6 @@ stop() -> -spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. init([]) -> - MachineHandlers = [ - hg_invoice, - hg_invoice_template - ], PartyClient = party_client:create_client(), DefaultTimeout = genlib_app:env(hellgate, default_woody_handling_timeout, ?DEFAULT_HANDLING_TIMEOUT), Opts = #{ @@ -52,19 +48,18 @@ init([]) -> %% for debugging only %% hg_profiler:get_child_spec(), party_client:child_spec(party_client, PartyClient), - hg_machine:get_child_spec(MachineHandlers), - get_api_child_spec(MachineHandlers, Opts) + prg_machine_registry:get_child_spec([hg_invoice, hg_invoice_template]), + get_api_child_spec(Opts) ] }}. -get_api_child_spec(MachineHandlers, Opts) -> +get_api_child_spec(Opts) -> {ok, Ip} = inet:parse_address(genlib_app:env(?MODULE, ip, "::")), HealthRoutes = construct_health_routes(liveness, genlib_app:env(?MODULE, health_check_liveness, #{})) ++ construct_health_routes(readiness, genlib_app:env(?MODULE, health_check_readiness, #{})), EventHandlerOpts = genlib_app:env(?MODULE, scoper_event_handler_options, #{}), PrometeusRoute = get_prometheus_route(), - ProcessTracingRoute = hg_progressor_handler:get_routes(), woody_server:child_spec( ?MODULE, #{ @@ -73,13 +68,12 @@ get_api_child_spec(MachineHandlers, Opts) -> transport_opts => genlib_app:env(?MODULE, transport_opts, #{}), protocol_opts => genlib_app:env(?MODULE, protocol_opts, #{}), event_handler => {scoper_woody_event_handler, EventHandlerOpts}, - handlers => hg_machine:get_service_handlers(MachineHandlers, Opts) ++ - [ - construct_service_handler(invoicing, hg_invoice_handler, Opts), - construct_service_handler(invoice_templating, hg_invoice_template, Opts), - construct_service_handler(proxy_host_provider, hg_proxy_host_provider, Opts) - ], - additional_routes => [PrometeusRoute | HealthRoutes] ++ ProcessTracingRoute, + handlers => [ + construct_service_handler(invoicing, hg_invoice_handler, Opts), + construct_service_handler(invoice_templating, hg_invoice_template, Opts), + construct_service_handler(proxy_host_provider, hg_proxy_host_provider, Opts) + ], + additional_routes => [PrometeusRoute | HealthRoutes], shutdown_timeout => genlib_app:env(?MODULE, shutdown_timeout, 0) } ). diff --git a/apps/hellgate/src/hg_context.erl b/apps/hellgate/src/hg_context.erl deleted file mode 100644 index e4ee246c..00000000 --- a/apps/hellgate/src/hg_context.erl +++ /dev/null @@ -1,108 +0,0 @@ --module(hg_context). - --export([create/0]). --export([create/1]). --export([save/1]). --export([load/0]). --export([cleanup/0]). - --export([get_woody_context/1]). --export([set_woody_context/2]). --export([get_party_client_context/1]). --export([set_party_client_context/2]). --export([get_party_client/1]). --export([set_party_client/2]). - --opaque context() :: #{ - woody_context := woody_context(), - party_client_context := party_client_context(), - party_client => party_client() -}. - --type options() :: #{ - party_client => party_client(), - woody_context => woody_context(), - party_client_context => party_client_context() -}. - --export_type([context/0]). --export_type([options/0]). - -%% Internal types - --type woody_context() :: woody_context:ctx(). --type party_client() :: party_client:client(). --type party_client_context() :: party_client:context(). - --define(REGISTRY_KEY, {p, l, stored_hg_context}). - -%% API - --spec create() -> context(). -create() -> - create(#{}). - --spec create(options()) -> context(). -create(Options0) -> - Options1 = ensure_woody_context_exists(Options0), - ensure_party_context_exists(Options1). - --spec save(context()) -> ok. -save(Context) -> - true = - try - gproc:reg(?REGISTRY_KEY, Context) - catch - error:badarg -> - gproc:set_value(?REGISTRY_KEY, Context) - end, - ok. - --spec load() -> context() | no_return(). -load() -> - gproc:get_value(?REGISTRY_KEY). - --spec cleanup() -> ok. -cleanup() -> - true = gproc:unreg(?REGISTRY_KEY), - ok. - --spec get_woody_context(context()) -> woody_context(). -get_woody_context(#{woody_context := WoodyContext}) -> - WoodyContext. - --spec set_woody_context(woody_context(), context()) -> context(). -set_woody_context(WoodyContext, Context) -> - Context#{woody_context => WoodyContext}. - --spec get_party_client(context()) -> party_client(). -get_party_client(#{party_client := PartyClient}) -> - PartyClient; -get_party_client(Context) -> - error(no_party_client, [Context]). - --spec set_party_client(party_client(), context()) -> context(). -set_party_client(PartyClient, Context) -> - Context#{party_client => PartyClient}. - --spec get_party_client_context(context()) -> party_client_context(). -get_party_client_context(#{party_client_context := PartyContext}) -> - PartyContext. - --spec set_party_client_context(party_client_context(), context() | options()) -> context(). -set_party_client_context(PartyContext, Context) -> - Context#{party_client_context => PartyContext}. - -%% Internal functions - --spec ensure_woody_context_exists(options()) -> options(). -ensure_woody_context_exists(#{woody_context := _WoodyContext} = Options) -> - Options; -ensure_woody_context_exists(Options) -> - Options#{woody_context => woody_context:new()}. - --spec ensure_party_context_exists(options()) -> options(). -ensure_party_context_exists(#{party_client_context := _PartyContext} = Options) -> - Options; -ensure_party_context_exists(#{woody_context := WoodyContext} = Options) -> - set_party_client_context(party_client:create_context(#{woody_context => WoodyContext}), Options). diff --git a/apps/hellgate/src/hg_customer_client.erl b/apps/hellgate/src/hg_customer_client.erl index 30828e18..1e8e3c62 100644 --- a/apps/hellgate/src/hg_customer_client.erl +++ b/apps/hellgate/src/hg_customer_client.erl @@ -149,7 +149,7 @@ call(ServiceName, Function, Args) -> Opts = hg_woody_wrapper:get_service_options(ServiceName), WoodyContext = try - hg_context:get_woody_context(hg_context:load()) + op_context:get_woody_context(op_context:load(op_context:key(hellgate))) catch error:badarg -> woody_context:new() end, diff --git a/apps/hellgate/src/hg_datetime.erl b/apps/hellgate/src/hg_datetime.erl index 51c9c651..ea4e77c4 100644 --- a/apps/hellgate/src/hg_datetime.erl +++ b/apps/hellgate/src/hg_datetime.erl @@ -4,6 +4,7 @@ -export([format_dt/1]). -export([format_ts/1]). +-export([format_ts/2]). -export([format_now/0]). -export([compare/2]). -export([between/2]). diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 64534781..0ef6f3fc 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -20,8 +20,9 @@ -include("hg_invoice.hrl"). -include_lib("damsel/include/dmsl_repair_thrift.hrl"). - --define(NS, <<"invoice">>). +-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). +-define(NS, invoice). +-define(EVENT_FORMAT_VERSION, 1). -export([process_callback/2]). -export([process_session_change_by_tag/2]). @@ -42,11 +43,10 @@ -export([marshal_invoice/1]). -export([unmarshal_invoice/1]). -export([unmarshal_history/1]). --export([collapse_history/1]). %% Machine callbacks --behaviour(hg_machine). +-behaviour(prg_machine). -export([namespace/0]). @@ -54,6 +54,12 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([process_notification/2]). +-export([marshal_event_body/2]). +-export([unmarshal_event_body/1]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% Internal @@ -88,13 +94,17 @@ invoice | {payment, payment_id()}. +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). +-type action() :: prg_action:t(). + %% API --spec get(hg_machine:id()) -> {ok, st()} | {error, notfound}. +-spec get(prg_machine:id()) -> {ok, st()} | {error, prg_machine:get_error()}. get(ID) -> - case hg_machine:get_history(?NS, ID) of - {ok, History} -> - {ok, collapse_history(unmarshal_history(History))}; + case prg_machine:get(?NS, ID) of + {ok, Machine} -> + {ok, prg_machine:collapse(?MODULE, Machine)}; Error -> Error end. @@ -137,8 +147,8 @@ get_payment_opts(Revision, #st{invoice = Invoice} = St) -> }. -spec create( - hg_machine:id(), - undefined | hg_machine:id(), + prg_machine:id(), + undefined | prg_machine:id(), invoice_params(), undefined | allocation(), [hg_invoice_mutation:mutation()], @@ -228,10 +238,12 @@ get_payment_state(PaymentSession) -> {ok, callback_response()} | {error, invalid_callback | notfound | failed} | no_return(). process_callback(Tag, Callback) -> process_with_tag(Tag, fun(MachineID) -> - case hg_machine:call(?NS, MachineID, {callback, Tag, Callback}) of - {ok, _} = Ok -> + case prg_machine:call(?NS, MachineID, {callback, Tag, Callback}) of + {ok, {ok, _} = Ok} -> Ok; - {exception, invalid_callback} -> + {ok, ok} -> + ok; + {ok, {exception, invalid_callback}} -> {error, invalid_callback}; {error, _} = Error -> Error @@ -242,11 +254,15 @@ process_callback(Tag, Callback) -> ok | {error, notfound | failed} | no_return(). process_session_change_by_tag(Tag, SessionChange) -> process_with_tag(Tag, fun(MachineID) -> - case hg_machine:call(?NS, MachineID, {session_change, Tag, SessionChange}) of - ok -> + case prg_machine:call(?NS, MachineID, {session_change, Tag, SessionChange}) of + {ok, ok} -> + ok; + {ok, {ok, _}} -> ok; - {exception, invalid_callback} -> + {ok, {exception, invalid_callback}} -> {error, notfound}; + {ok, {exception, _}} -> + {error, failed}; {error, _} = Error -> Error end @@ -262,42 +278,47 @@ process_with_tag(Tag, F) -> %% --spec fail(hg_machine:id()) -> ok. +-spec fail(prg_machine:id()) -> ok. fail(ID) -> - try hg_machine:call(?NS, ID, fail) of + case prg_machine:call(?NS, ID, fail) of {error, failed} -> ok; + {error, {exception, _, _}} -> + ok; + {error, {exception, _, _, _}} -> + ok; {error, Error} -> erlang:error({unexpected_error, Error}); {ok, Result} -> erlang:error({unexpected_result, Result}) - catch - _Class:_Term:_Trace -> - ok end. %% --spec namespace() -> hg_machine:ns(). +-spec namespace() -> prg_machine:namespace(). namespace() -> ?NS. --spec init(binary(), hg_machine:machine()) -> hg_machine:result(). +-spec init(binary(), machine()) -> prg_result(). init(Invoice, _Machine) -> UnmarshalledInvoice = unmarshal_invoice(Invoice), - % TODO ugly, better to roll state and events simultaneously, hg_party-like - handle_result(#{ - changes => [?invoice_created(UnmarshalledInvoice)], - action => set_invoice_timer(hg_machine_action:new(), #st{invoice = UnmarshalledInvoice}), - state => #st{} - }). + Changes = [?invoice_created(UnmarshalledInvoice)], + #{ + events => [Changes], + action => set_invoice_timer(idle, #st{invoice = UnmarshalledInvoice}), + auxst => #{} + }. %% --spec process_repair(hg_machine:args(), hg_machine:machine()) -> hg_machine:result() | no_return(). -process_repair(Args, #{history := History}) -> - St = collapse_history(unmarshal_history(History)), - handle_result(handle_repair(Args, St)). +-spec process_repair(prg_machine:args(), machine()) -> prg_result() | no_return(). +process_repair(Args, Machine) -> + St = prg_machine:collapse(?MODULE, Machine), + to_prg_result(handle_repair(Args, St)). + +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. handle_repair({changes, Changes, RepairAction, Params}, St) -> Result = @@ -326,9 +347,28 @@ handle_repair({scenario, Scenario}, #st{activity = {payment, PaymentID}} = St) - try_to_get_repair_state(Scenario, St) end. --spec process_signal(hg_machine:signal(), hg_machine:machine()) -> hg_machine:result(). -process_signal(Signal, #{history := History}) -> - handle_result(handle_signal(Signal, collapse_history(unmarshal_history(History)))). +construct_repair_action(undefined) -> + idle; +construct_repair_action(#repair_ComplexAction{} = CA) -> + lists:foldl( + fun merge_repair_action/2, + idle, + [{timer, CA#repair_ComplexAction.timer}, {remove, CA#repair_ComplexAction.remove}] + ). + +merge_repair_action({remove, #repair_RemoveAction{}}, _Action) -> + remove; +merge_repair_action({timer, {set_timer, #repair_SetTimerAction{timer = Timer}}}, _Action) -> + prg_action:schedule_timer(Timer); +merge_repair_action({timer, {unset_timer, #repair_UnsetTimerAction{}}}, _Action) -> + suspend; +merge_repair_action({_, undefined}, Action) -> + Action. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(Signal, Machine) -> + St = prg_machine:collapse(?MODULE, Machine), + to_prg_result(handle_signal(Signal, St)). handle_signal(timeout, #st{activity = {payment, PaymentID}} = St) -> % there's a payment pending @@ -338,24 +378,6 @@ handle_signal(timeout, #st{activity = invoice} = St) -> % invoice is expired handle_expiration(St). -construct_repair_action(CA) when CA /= undefined -> - lists:foldl( - fun merge_repair_action/2, - hg_machine_action:new(), - [{timer, CA#repair_ComplexAction.timer}, {remove, CA#repair_ComplexAction.remove}] - ); -construct_repair_action(undefined) -> - hg_machine_action:new(). - -merge_repair_action({timer, {set_timer, #repair_SetTimerAction{timer = Timer}}}, Action) -> - hg_machine_action:set_timer(Timer, Action); -merge_repair_action({timer, {unset_timer, #repair_UnsetTimerAction{}}}, Action) -> - hg_machine_action:unset_timer(Action); -merge_repair_action({remove, #repair_RemoveAction{}}, Action) -> - hg_machine_action:mark_removal(Action); -merge_repair_action({_, undefined}, Action) -> - Action. - should_validate_transitions(#payproc_InvoiceRepairParams{validate_transitions = V}) when is_boolean(V) -> V; should_validate_transitions(undefined) -> @@ -369,27 +391,48 @@ handle_expiration(St) -> %% --type thrift_call() :: hg_machine:thrift_call(). +-type thrift_call() :: {hg_proto_utils:thrift_fun_ref(), [term()]}. -type callback_call() :: {callback, tag(), callback()}. -type session_change_call() :: {session_change, tag(), session_change()}. -type call() :: thrift_call() | callback_call() | session_change_call(). -type call_result() :: #{ changes => [invoice_change()], - action => hg_machine_action:t(), + action => action(), response => ok | term(), state => st() }. +%% Result of handle_call / handle_signal / handle_repair before marshaling to progressor. +-type handler_result() :: #{ + changes => [invoice_change()], + action => action(), + response => ok | term(), + state => st(), + validate => boolean() +}. --spec process_call(call(), hg_machine:machine()) -> {hg_machine:response(), hg_machine:result()}. -process_call(Call, #{history := History}) -> - St = collapse_history(unmarshal_history(History)), +-spec process_call(call(), machine()) -> {prg_machine:response(), prg_result()}. +process_call(Call0, Machine) -> + Call = normalize_call(Call0), + St = prg_machine:collapse(?MODULE, Machine), try - handle_result(handle_call(Call, St)) + CallResult = handle_call(Call, St), + Response = maps:get(response, CallResult, ok), + {call_response(Response), to_prg_result(CallResult)} catch throw:Exception -> {{exception, Exception}, #{}} end. +%% Compat: legacy hg_machine stored pending call args as the double-wrapped +%% {thrift_call, ServiceName, FunRef, EncodedArgs}; the current form is {FunRef, Args}. +%% Only in-flight call/init tasks at deploy time hit this branch. +normalize_call({thrift_call, ServiceName, {Service, _Function} = FunRef, EncodedArgs}) -> + {Module, Service} = hg_proto:get_service(ServiceName), + Args = hg_proto_utils:deserialize_function_args({Module, FunRef}, EncodedArgs), + {FunRef, Args}; +normalize_call(Call) -> + Call. + -spec handle_call(call(), st()) -> call_result(). handle_call({{'Invoicing', 'StartPayment'}, {_InvoiceID, PaymentParams}}, St0) -> St = add_party_to_st(St0), @@ -442,7 +485,7 @@ handle_call({{'Invoicing', 'Rescind'}, {_InvoiceID, Reason}}, St0) -> #{ response => ok, changes => [?invoice_status_changed(?invoice_cancelled(hg_utils:format_reason(Reason)))], - action => hg_machine_action:unset_timer(), + action => suspend, state => St }; handle_call({{'Invoicing', 'RefundPayment'}, {_InvoiceID, PaymentID, Params}}, St0) -> @@ -511,8 +554,8 @@ assert_no_pending_payment(_) -> set_invoice_timer(Action, #st{invoice = Invoice} = St) -> set_invoice_timer(Invoice#domain_Invoice.status, Action, St). -set_invoice_timer(?invoice_unpaid(), Action, #st{invoice = #domain_Invoice{due = Due}}) -> - hg_machine_action:set_deadline(Due, Action); +set_invoice_timer(?invoice_unpaid(), _Action, #st{invoice = #domain_Invoice{due = Due}}) -> + prg_action:schedule_deadline(Due); set_invoice_timer(_Status, Action, _St) -> Action. @@ -675,28 +718,35 @@ wrap_payment_impact(PaymentID, {Response, {Changes, Action}}, St, OccurredAt) -> state => St }. -handle_result(#{} = Result) -> +-spec to_prg_result(handler_result()) -> prg_result(). +to_prg_result(Result) -> + %% Validate once (collapsing the changes) and log them, as the old handle_result + %% did for signal/call/repair alike. St = validate_changes(Result), _ = log_changes(maps:get(changes, Result, []), St), - MachineResult = handle_result_changes(Result, handle_result_action(Result, #{})), - case maps:get(response, Result, undefined) of - undefined -> - MachineResult; - ok -> - {ok, MachineResult}; - Response -> - {{ok, Response}, MachineResult} + to_prg_result_(Result). + +%% No `auxst` here: invoice sets it only in `init/2`; call/signal/repair must not touch aux_state (M1). +to_prg_result_(Result) -> + Base = + case maps:get(changes, Result, []) of + [_ | _] = Changes -> + #{events => [Changes]}; + _ -> + #{} + end, + case maps:is_key(action, Result) of + true -> + Base#{action => maps:get(action, Result)}; + false -> + Base end. -handle_result_changes(#{changes := Changes = [_ | _]}, Acc) -> - Acc#{events => [marshal_event_payload(Changes)]}; -handle_result_changes(#{}, Acc) -> - Acc. - -handle_result_action(#{action := Action}, Acc) -> - Acc#{action => Action}; -handle_result_action(#{}, Acc) -> - Acc. +-spec call_response(ok | term()) -> prg_machine:response(). +call_response(ok) -> + ok; +call_response(Response) -> + {ok, Response}. validate_changes(#{validate := false, changes := Changes = [_ | _], state := St}) -> collapse_changes(Changes, St, #{}); @@ -845,17 +895,6 @@ repair_scenario(Scenario, #st{activity = {payment, PaymentID}} = St) -> %% --spec collapse_history([hg_machine:event()]) -> st(). -collapse_history(History) -> - lists:foldl( - fun({ID, Dt, Changes}, St0) -> - St1 = collapse_changes(Changes, St0, #{timestamp => Dt}), - St1#st{latest_event_id = ID} - end, - #st{}, - History - ). - collapse_changes(Changes, St0, Opts) -> lists:foldl(fun(C, St) -> merge_change(C, St, Opts) end, St0, Changes). @@ -988,11 +1027,100 @@ get_message(invoice_created) -> get_message(invoice_status_changed) -> "Invoice status is changed". -%% Marshalling +%% prg_machine codec + +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + [invoice_change()], + st() | undefined +) -> st(). +apply_event(EventID, Ts, Changes, St0) -> + St1 = apply_event_changes(Changes, St0, event_timestamp_to_binary(Ts)), + St1#st{latest_event_id = EventID}. + +event_timestamp_to_binary(Bin) when is_binary(Bin) -> + Bin; +event_timestamp_to_binary({{_, _} = Dt, Micro}) when is_integer(Micro) -> + %% Format with microseconds, matching the legacy MG RFC3339 created_at. + USec = genlib_time:daytime_to_unixtime(Dt) * 1000000 + Micro, + hg_datetime:format_ts(USec, microsecond); +event_timestamp_to_binary(Dt) -> + hg_datetime:format_dt(Dt). + +-spec apply_event_changes([invoice_change()], st() | undefined, hg_datetime:timestamp()) -> st(). +apply_event_changes(Changes, St0, Dt) -> + St = + case St0 of + undefined -> #st{}; + _ -> St0 + end, + collapse_changes(Changes, St, #{timestamp => Dt}). + +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(_Timestamp, Changes) when is_list(Changes) -> + #{data := Data} = wrap_event_payload({invoice_changes, Changes}), + Msgp = mg_msgpack_marshalling:marshal(Data), + {?EVENT_FORMAT_VERSION, msgpack_payload_to_binary(Msgp)}. + +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + decode_event_body(Payload). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + term_to_binary(marshal_aux_st_content(AuxSt)). + +marshal_aux_st_content(AuxSt) when map_size(AuxSt) =:= 0 -> + #mg_stateproc_Content{format_version = undefined, data = {bin, <<>>}}; +marshal_aux_st_content(AuxSt) -> + #mg_stateproc_Content{ + format_version = undefined, + data = mg_msgpack_marshalling:marshal(AuxSt) + }. + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(<<>>) -> + #{}; +unmarshal_aux_state(Payload) when is_binary(Payload) -> + %% Legacy hg_progressor stored term_to_binary(#mg_stateproc_Content{data = Msgp}). + %% Keep reading bare msgpack blobs written by an intermediate branch version. + case binary_to_term(Payload) of + #mg_stateproc_Content{data = {bin, <<>>}} -> + #{}; + #mg_stateproc_Content{data = Data} -> + mg_msgpack_marshalling:unmarshal(Data); + Msgp -> + mg_msgpack_marshalling:unmarshal(Msgp) + end. + +msgpack_payload_to_binary(Msgp) -> + term_to_binary(Msgp). --spec marshal_event_payload([invoice_change()]) -> hg_machine:event_payload(). -marshal_event_payload(Changes) when is_list(Changes) -> - wrap_event_payload({invoice_changes, Changes}). +decode_event_body(Payload) -> + case try_unmarshal_msgpack_payload(Payload) of + {ok, Data} -> + changes_from_msgpack_data(Data); + {error, _} -> + unmarshal_event_payload(#{format_version => ?EVENT_FORMAT_VERSION, data => {bin, Payload}}) + end. + +try_unmarshal_msgpack_payload(Payload) -> + try + {ok, mg_msgpack_marshalling:unmarshal(binary_to_term(Payload))} + catch + _:_ -> + {error, invalid_msgpack_payload} + end. + +changes_from_msgpack_data({bin, Bin}) when is_binary(Bin) -> + unmarshal_event_payload(#{format_version => ?EVENT_FORMAT_VERSION, data => {bin, Bin}}); +changes_from_msgpack_data(#{format_version := V, data := Data}) -> + unmarshal_event_payload(#{format_version => V, data => Data}); +changes_from_msgpack_data(Changes) when is_list(Changes) -> + Changes. + +%% Marshalling -spec marshal_invoice(invoice()) -> binary(). marshal_invoice(Invoice) -> @@ -1001,15 +1129,24 @@ marshal_invoice(Invoice) -> %% Unmarshalling --spec unmarshal_history([hg_machine:event()]) -> [hg_machine:event([invoice_change()])]. +-type legacy_event_payload() :: #{ + format_version := pos_integer(), + data := {bin, binary()} | term() +}. + +-spec unmarshal_history([prg_machine:machine_event()]) -> + [{prg_machine:event_id(), hg_datetime:timestamp(), [invoice_change()]}]. unmarshal_history(Events) -> [unmarshal_event(Event) || Event <- Events]. --spec unmarshal_event(hg_machine:event()) -> hg_machine:event([invoice_change()]). +-spec unmarshal_event(prg_machine:machine_event()) -> + {prg_machine:event_id(), hg_datetime:timestamp(), [invoice_change()]}. +unmarshal_event({ID, Dt, Payload}) when is_list(Payload) -> + {ID, event_timestamp_to_binary(Dt), Payload}; unmarshal_event({ID, Dt, Payload}) -> - {ID, Dt, unmarshal_event_payload(Payload)}. + {ID, event_timestamp_to_binary(Dt), unmarshal_event_payload(Payload)}. --spec unmarshal_event_payload(hg_machine:event_payload()) -> [invoice_change()]. +-spec unmarshal_event_payload(legacy_event_payload()) -> [invoice_change()]. unmarshal_event_payload(#{format_version := 1, data := {bin, Changes}}) -> Type = {struct, union, {dmsl_payproc_thrift, 'EventPayload'}}, {invoice_changes, Buf} = hg_proto_utils:deserialize(Type, Changes), @@ -1055,4 +1192,32 @@ construct_refund_id_test() -> Refunds = lists:map(fun create_dummy_refund_with_id/1, IDs), ?assert(<<"11">> =:= construct_refund_id(Refunds)). +%% --- Golden tests: legacy HG aux_state compatibility (stage 1.4) ----------- + +-spec aux_state_roundtrip_test() -> _. +aux_state_roundtrip_test() -> + AuxSt = #{<<"k">> => <<"v">>}, + ?assertEqual(AuxSt, unmarshal_aux_state(marshal_aux_state(AuxSt))). + +-spec aux_state_empty_test() -> _. +aux_state_empty_test() -> + ?assertEqual(#{}, unmarshal_aux_state(<<>>)). + +-spec aux_state_reads_legacy_mg_content_test() -> _. +aux_state_reads_legacy_mg_content_test() -> + AuxSt = #{<<"legacy">> => 1}, + Legacy = term_to_binary(marshal_aux_st_content(AuxSt)), + ?assertEqual(Legacy, marshal_aux_state(AuxSt)), + ?assertEqual(AuxSt, unmarshal_aux_state(Legacy)). + +-spec aux_state_writes_legacy_empty_content_test() -> _. +aux_state_writes_legacy_empty_content_test() -> + Legacy = term_to_binary(#mg_stateproc_Content{format_version = undefined, data = {bin, <<>>}}), + ?assertEqual(Legacy, marshal_aux_state(#{})). + +-spec aux_state_reads_legacy_empty_content_test() -> _. +aux_state_reads_legacy_empty_content_test() -> + Legacy = term_to_binary(#mg_stateproc_Content{format_version = undefined, data = {bin, <<>>}}), + ?assertEqual(#{}, unmarshal_aux_state(Legacy)). + -endif. diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index 79d82bc5..d4bff72d 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -148,14 +148,18 @@ handle_function_('ExplainRoute', {InvoiceID, PaymentID}, _Opts) -> ensure_started(ID, TemplateID, Params, Allocation, Mutations, DomainRevision) -> Invoice = hg_invoice:create(ID, TemplateID, Params, Allocation, Mutations, DomainRevision), - case hg_machine:start(hg_invoice:namespace(), ID, hg_invoice:marshal_invoice(Invoice)) of + case prg_machine:start(hg_invoice:namespace(), ID, hg_invoice:marshal_invoice(Invoice)) of {ok, _} -> ok; {error, exists} -> ok; {error, Reason} -> erlang:error(Reason) end. call(ID, Function, Args) -> - case hg_machine:thrift_call(hg_invoice:namespace(), ID, invoicing, {'Invoicing', Function}, Args) of + case + hg_invoicing_machine_client:thrift_call( + hg_invoice:namespace(), ID, invoicing, {'Invoicing', Function}, Args + ) + of ok -> ok; {ok, Reply} -> Reply; {exception, Exception} -> erlang:throw(Exception); @@ -164,7 +168,7 @@ call(ID, Function, Args) -> end. repair(ID, Args) -> - case hg_machine:repair(hg_invoice:namespace(), ID, Args) of + case prg_machine:repair(hg_invoice:namespace(), ID, Args) of {ok, _Result} -> ok; {error, notfound} -> erlang:throw(#payproc_InvoiceNotFound{}); {error, working} -> erlang:throw(#base_InvalidRequest{errors = [<<"No need to repair">>]}); @@ -219,17 +223,28 @@ set_invoicing_meta(InvoiceID, PaymentID) -> %% get_state(ID) -> - hg_invoice:collapse_history(get_history(ID)). + case hg_invoice:get(ID) of + {ok, St} -> + St; + {error, notfound} -> + throw(#payproc_InvoiceNotFound{}); + {error, {exception, Class, Reason}} -> + _ = logger:error("invoice ~p get failed: ~p:~p", [ID, Class, Reason]), + erlang:error({process_exception, Class, Reason}) + end. get_state(ID, AfterID, Limit) -> - hg_invoice:collapse_history(get_history(ID, AfterID, Limit)). - -get_history(ID) -> - History = hg_machine:get_history(hg_invoice:namespace(), ID), - hg_invoice:unmarshal_history(map_history_error(History)). + History = get_history(ID, AfterID, Limit), + Machine = #{ + namespace => hg_invoice:namespace(), + id => ID, + history => History, + aux_state => #{} + }, + prg_machine:collapse(hg_invoice, Machine). get_history(ID, AfterID, Limit) -> - History = hg_machine:get_history(hg_invoice:namespace(), ID, AfterID, Limit), + History = prg_machine:get_history(hg_invoice:namespace(), ID, AfterID, Limit), hg_invoice:unmarshal_history(map_history_error(History)). get_public_history(InvoiceID, #payproc_EventRange{'after' = AfterID, limit = Limit}) -> diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 210de80d..5a83fea5 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -397,7 +397,7 @@ get_chargeback_opts(#st{opts = Opts} = St) -> %% -type event() :: dmsl_payproc_thrift:'InvoicePaymentChangePayload'(). --type action() :: hg_machine_action:t(). +-type action() :: prg_action:t(). -type events() :: [event()]. -type result() :: {events(), action()}. -type machine_result() :: {next | done, result()}. @@ -471,7 +471,7 @@ init_(PaymentID, Params, #{timestamp := CreatedAt} = Opts) -> [] end, Events = [?payment_started(Payment2)] ++ CascadeTokenEvents, - {collapse_changes(Events, undefined, #{}), {Events, hg_machine_action:instant()}}. + {collapse_changes(Events, undefined, #{}), {Events, timeout}}. seed_bank_card_from_parent(PartyConfigRef, BCT, #{parent_payment := ParentPayment}) -> case get_recurrent_token(ParentPayment) of @@ -994,7 +994,7 @@ total_capture(St, Reason, Cart, Allocation) -> Payment = get_payment(St), Cost = get_payment_cost(Payment), Changes = start_capture(Reason, Cost, Cart, Allocation), - {ok, {Changes, hg_machine_action:instant()}}. + {ok, {Changes, timeout}}. partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocation) -> Payment = get_payment(St0), @@ -1020,7 +1020,7 @@ partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocat }), FinalCashflow = calculate_cashflow(Context, Opts), Changes = start_partial_capture(Reason, Cost, Cart, FinalCashflow, Allocation), - {ok, {Changes, hg_machine_action:instant()}}. + {ok, {Changes, timeout}}. -spec cancel(st(), binary()) -> {ok, result()}. cancel(St, Reason) -> @@ -1028,7 +1028,7 @@ cancel(St, Reason) -> _ = assert_activity({payment, flow_waiting}, St), _ = assert_payment_flow(hold, Payment), Changes = start_session(?cancelled_with_reason(Reason)), - {ok, {Changes, hg_machine_action:instant()}}. + {ok, {Changes, timeout}}. assert_capture_cost_currency(undefined, _) -> ok; @@ -1159,7 +1159,7 @@ refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> refund => Refund, cash_flow => FinalCashflow }), - {Refund, {Changes, hg_machine_action:instant()}}. + {Refund, {Changes, timeout}}. -spec manual_refund(refund_params(), st(), opts()) -> {domain_refund(), result()}. manual_refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> @@ -1176,7 +1176,7 @@ manual_refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> cash_flow => FinalCashflow, transaction_info => TransactionInfo }), - {Refund, {Changes, hg_machine_action:instant()}}. + {Refund, {Changes, timeout}}. make_refund(Params, Payment, Revision, CreatedAt, St, Opts) -> _ = assert_no_pending_chargebacks(St), @@ -1642,7 +1642,7 @@ construct_adjustment( state = State }, Events = [?adjustment_ev(ID, ?adjustment_created(Adjustment)) | AdditionalEvents], - {Adjustment, {Events, hg_machine_action:instant()}}. + {Adjustment, {Events, timeout}}. construct_adjustment_id(#st{adjustments = As}) -> erlang:integer_to_binary(length(As) + 1). @@ -1696,7 +1696,7 @@ process_adjustment_capture(ID, _Action, St) -> ok = finalize_adjustment_cashflow(Adjustment, St, Opts), Status = ?adjustment_captured(maps:get(timestamp, Opts)), Event = ?adjustment_ev(ID, ?adjustment_status_changed(Status)), - {done, {[Event], hg_machine_action:new()}}. + {done, {[Event], idle}}. prepare_adjustment_cashflow(Adjustment, St, Options) -> PlanID = construct_adjustment_plan_id(Adjustment, St, Options), @@ -1772,7 +1772,7 @@ process_signal(timeout, St, Options) -> ). process_timeout(St) -> - Action = hg_machine_action:new(), + Action = idle, repair_process_timeout(get_activity(St), Action, St). -spec process_timeout(activity(), action(), st()) -> machine_result(). @@ -1922,19 +1922,19 @@ process_session_change(_Tag, _Payload, undefined, _St) -> %% -spec process_shop_limit_initialization(action(), st()) -> machine_result(). -process_shop_limit_initialization(Action, St) -> +process_shop_limit_initialization(_Action, St) -> Opts = get_opts(St), _ = hold_shop_limits(Opts, St), case check_shop_limits(Opts, St) of ok -> - {next, {[?shop_limit_initiated()], hg_machine_action:set_timeout(0, Action)}}; + {next, {[?shop_limit_initiated()], timeout}}; {error, {limit_overflow = Error, IDs}} -> Failure = construct_shop_limit_failure(Error, IDs), Events = [ ?shop_limit_initiated(), ?payment_rollback_started(Failure) ], - {next, {Events, hg_machine_action:set_timeout(0, Action)}} + {next, {Events, timeout}} end. construct_shop_limit_failure(limit_overflow, IDs) -> @@ -1942,16 +1942,16 @@ construct_shop_limit_failure(limit_overflow, IDs) -> Reason = genlib:format("Limits with following IDs overflowed: ~p", [IDs]), {failure, payproc_errors:construct('PaymentFailure', Error, Reason)}. -process_shop_limit_failure(Action, #st{failure = Failure} = St) -> +process_shop_limit_failure(_Action, #st{failure = Failure} = St) -> Opts = get_opts(St), _ = rollback_shop_limits(Opts, St, [ignore_business_error, ignore_not_found]), - {done, {[?payment_status_changed(?failed(Failure))], hg_machine_action:set_timeout(0, Action)}}. + {done, {[?payment_status_changed(?failed(Failure))], timeout}}. -spec process_shop_limit_finalization(action(), st()) -> machine_result(). -process_shop_limit_finalization(Action, St) -> +process_shop_limit_finalization(_Action, St) -> Opts = get_opts(St), _ = commit_shop_limits(Opts, St), - {next, {[?shop_limit_applied()], hg_machine_action:set_timeout(0, Action)}}. + {next, {[?shop_limit_applied()], timeout}}. -spec process_risk_score(action(), st()) -> machine_result(). process_risk_score(Action, St) -> @@ -1965,7 +1965,7 @@ process_risk_score(Action, St) -> Events = [?risk_score_changed(RiskScore)], case check_risk_score(RiskScore) of ok -> - {next, {Events, hg_machine_action:set_timeout(0, Action)}}; + {next, {Events, timeout}}; {error, risk_score_is_too_high = Reason} -> logger:notice("No route found, reason = ~p, varset: ~p", [Reason, VS1]), handle_choose_route_error(Reason, Events, St, Action) @@ -2002,7 +2002,7 @@ process_routing(Action, St) -> Revision, St ), - {next, {Events, hg_machine_action:set_timeout(0, Action)}} + {next, {Events, timeout}} end end. @@ -2134,7 +2134,7 @@ handle_filtered_routes_exhaustion(Result, Revision, St, Action) -> handle_choose_route_error(Error, [], St, Action); _ConsideredRoutes -> Events = produce_routing_events(hg_routing_ctx:set_error(Error, Result), Revision, St), - {next, {Events, hg_machine_action:set_timeout(0, Action)}} + {next, {Events, timeout}} end. log_rejected_route_groups(Result, VS) -> @@ -2181,7 +2181,7 @@ mk_static_error_(T, []) -> T; mk_static_error_(Sub, [Code | Codes]) -> mk_static_error_({Code, Sub}, Codes). -spec process_cash_flow_building(action(), st()) -> machine_result(). -process_cash_flow_building(Action, St) -> +process_cash_flow_building(_Action, St) -> Route = get_route(St), Opts = get_opts(St), Revision = get_payment_revision(St), @@ -2209,7 +2209,7 @@ process_cash_flow_building(Action, St) -> {1, FinalCashflow} ), Events = [?cash_flow_changed(FinalCashflow)], - {next, {Events, hg_machine_action:set_timeout(0, Action)}}. + {next, {Events, timeout}}. %% @@ -2266,9 +2266,9 @@ process_adjustment_cashflow(ID, _Action, St) -> Adjustment = get_adjustment(ID, St), ok = prepare_adjustment_cashflow(Adjustment, St, Opts), Events = [?adjustment_ev(ID, ?adjustment_status_changed(?adjustment_processed()))], - {next, {Events, hg_machine_action:instant()}}. + {next, {Events, timeout}}. -process_accounter_update(Action, #st{partial_cash_flow = FinalCashflow, capture_data = CaptureData} = St) -> +process_accounter_update(_Action, #st{partial_cash_flow = FinalCashflow, capture_data = CaptureData} = St) -> #payproc_InvoicePaymentCaptureData{ reason = Reason, cash = Cost, @@ -2283,7 +2283,7 @@ process_accounter_update(Action, #st{partial_cash_flow = FinalCashflow, capture_ ] ), Events = start_session(?captured(Reason, Cost, Cart, Allocation)), - {next, {Events, hg_machine_action:set_timeout(0, Action)}}. + {next, {Events, timeout}}. %% @@ -2313,11 +2313,11 @@ process_session(St) -> process_session(undefined, St0) -> Target = get_target(St0), TargetType = get_target_type(Target), - Action = hg_machine_action:new(), + Action = idle, case validate_processing_deadline(get_payment(St0), TargetType) of ok -> Events = start_session(Target), - Result = {Events, hg_machine_action:set_timeout(0, Action)}, + Result = {Events, timeout}, {next, Result}; Failure -> process_failure(get_activity(St0), [], Action, Failure, St0) @@ -2342,7 +2342,7 @@ finish_session_processing(Activity, {Events0, Action}, Session, St0) -> {finished, ?session_succeeded()} -> TargetType = get_target_type(hg_session:target(Session)), _ = maybe_notify_fault_detector(Activity, TargetType, finish, St0), - NewAction = hg_machine_action:set_timeout(0, Action), + NewAction = timeout, InvoiceID = get_invoice_id(get_invoice(get_opts(St0))), St1 = collapse_changes(Events1, St0, #{invoice_id => InvoiceID}), _ = @@ -2366,7 +2366,7 @@ finish_session_processing(Activity, {Events0, Action}, Session, St0) -> end. -spec finalize_payment(action(), st()) -> machine_result(). -finalize_payment(Action, St) -> +finalize_payment(_Action, St) -> Target = case get_payment_flow(get_payment(St)) of ?invoice_payment_flow_instant() -> @@ -2389,13 +2389,13 @@ finalize_payment(Action, St) -> _ -> start_session(Target) end, - {done, {StartEvents, hg_machine_action:set_timeout(0, Action)}}. + {done, {StartEvents, timeout}}. -spec process_result(action(), st()) -> machine_result(). process_result(Action, St) -> process_result(get_activity(St), Action, St). -process_result({payment, processing_accounter}, Action, #st{new_cash = Cost} = St0) when +process_result({payment, processing_accounter}, _Action, #st{new_cash = Cost} = St0) when Cost =/= undefined -> %% Rebuild cashflow for new cost @@ -2431,18 +2431,18 @@ process_result({payment, processing_accounter}, Action, #st{new_cash = Cost} = S construct_payment_plan_id(St2), get_cashflow_plan(St2) ), - {next, {[?cash_flow_changed(FinalCashflow)], hg_machine_action:set_timeout(0, Action)}}; + {next, {[?cash_flow_changed(FinalCashflow)], timeout}}; process_result({payment, processing_accounter}, Action, St) -> Target = get_target(St), NewAction = get_action(Target, Action, St), {done, {[?payment_status_changed(Target)], NewAction}}; -process_result({payment, routing_failure}, Action, #st{failure = Failure} = St) -> - NewAction = hg_machine_action:set_timeout(0, Action), +process_result({payment, routing_failure}, _Action, #st{failure = Failure} = St) -> + NewAction = timeout, Routes = get_candidate_routes(St), _ = rollback_payment_limits(Routes, get_iter(St), St, [ignore_business_error, ignore_not_found]), {done, {[?payment_status_changed(?failed(Failure))], NewAction}}; -process_result({payment, processing_failure}, Action, #st{failure = Failure} = St) -> - NewAction = hg_machine_action:set_timeout(0, Action), +process_result({payment, processing_failure}, _Action, #st{failure = Failure} = St) -> + NewAction = timeout, %% We need to rollback only current route. %% Previously used routes are supposed to have their limits already rolled back. Route = get_route(St), @@ -2622,13 +2622,13 @@ process_fatal_payment_failure(?cancelled(), _Events, _Action, Failure, _St) -> error({invalid_cancel_failure, Failure}); process_fatal_payment_failure(?captured(), _Events, _Action, Failure, _St) -> error({invalid_capture_failure, Failure}); -process_fatal_payment_failure(?processed(), Events, Action, Failure, _St) -> +process_fatal_payment_failure(?processed(), Events, _Action, Failure, _St) -> RollbackStarted = [?payment_rollback_started(Failure)], - {next, {Events ++ RollbackStarted, hg_machine_action:set_timeout(0, Action)}}. + {next, {Events ++ RollbackStarted, timeout}}. -retry_session(Action, Target, Timeout) -> +retry_session(_Action, Target, Timeout) -> NewEvents = start_session(Target), - NewAction = set_timer({timeout, Timeout}, Action), + NewAction = prg_action:schedule_timer({timeout, Timeout}), {NewEvents, NewAction}. get_actual_retry_strategy(Target, #st{retry_attempts = Attempts}) -> @@ -2676,19 +2676,16 @@ do_check_failure_type({authorization_failed, {temporarily_unavailable, _}}) -> do_check_failure_type(_Failure) -> fatal. -get_action(?processed(), Action, St) -> +get_action(?processed(), _Action, St) -> case get_payment_flow(get_payment(St)) of ?invoice_payment_flow_instant() -> - hg_machine_action:set_timeout(0, Action); + timeout; ?invoice_payment_flow_hold(_, HeldUntil) -> - hg_machine_action:set_deadline(HeldUntil, Action) + prg_action:schedule_deadline(HeldUntil) end; get_action(_Target, Action, _St) -> Action. -set_timer(Timer, Action) -> - hg_machine_action:set_timer(Timer, Action). - get_provider_payment_terms(St, Revision) -> Opts = get_opts(St), Route = get_route(St), @@ -4006,8 +4003,10 @@ get_st_meta(_) -> %% Timings -spec define_event_timestamp(change_opts()) -> integer(). -define_event_timestamp(#{timestamp := Dt}) -> +define_event_timestamp(#{timestamp := Dt}) when is_binary(Dt) -> hg_datetime:parse(Dt, millisecond); +define_event_timestamp(#{timestamp := Dt}) -> + hg_datetime:parse(hg_datetime:format_dt(Dt), millisecond); define_event_timestamp(#{}) -> erlang:system_time(millisecond). @@ -4159,9 +4158,9 @@ get_message(invoice_payment_status_changed) -> "Invoice payment status is changed". get_party_client() -> - HgContext = hg_context:load(), - Client = hg_context:get_party_client(HgContext), - Context = hg_context:get_party_client_context(HgContext), + HgContext = op_context:load(op_context:key(hellgate)), + Client = op_context:get_party_client(HgContext), + Context = op_context:get_party_client_context(HgContext), {Client, Context}. is_route_cascade_available( diff --git a/apps/hellgate/src/hg_invoice_payment_chargeback.erl b/apps/hellgate/src/hg_invoice_payment_chargeback.erl index 0dc1418e..4fa7f5da 100644 --- a/apps/hellgate/src/hg_invoice_payment_chargeback.erl +++ b/apps/hellgate/src/hg_invoice_payment_chargeback.erl @@ -139,7 +139,7 @@ dmsl_payproc_thrift:'InvoicePaymentChargebackChangePayload'(). -type action() :: - hg_machine_action:t(). + prg_action:t(). -type activity() :: preparing_initial_cash_flow @@ -271,9 +271,9 @@ merge_change(?chargeback_cash_flow_changed(CashFlow), State) -> -spec process_timeout(activity(), state(), action(), opts()) -> result(). process_timeout(preparing_initial_cash_flow, State, _Action, Opts) -> - update_cash_flow(State, hg_machine_action:new(), Opts); + update_cash_flow(State, idle, Opts); process_timeout(updating_cash_flow, State, _Action, Opts) -> - update_cash_flow(State, hg_machine_action:instant(), Opts); + update_cash_flow(State, timeout, Opts); process_timeout(finalising_accounter, State, Action, Opts) -> finalise(State, Action, Opts). @@ -301,7 +301,7 @@ do_create(Opts, CreateParams = ?chargeback_params(Levy, Body, _Reason)) -> _ = validate_eligibility_time(ServiceTerms), _ = validate_provider_terms(ProviderTerms), Chargeback = build_chargeback(Opts, CreateParams, Revision, CreatedAt), - Action = hg_machine_action:instant(), + Action = timeout, Result = {[?chargeback_created(Chargeback)], Action}, {Chargeback, Result}. @@ -311,7 +311,7 @@ do_cancel(State, ?cancel_params()) -> % there actually is a cashflow to cancel % _ = validate_cash_flow_held(State), _ = validate_chargeback_is_pending(State), - Action = hg_machine_action:instant(), + Action = timeout, Status = ?chargeback_status_cancelled(), Result = {[?chargeback_target_status_changed(Status)], Action}, {ok, Result}. @@ -378,7 +378,7 @@ build_chargeback(Opts, Params = ?chargeback_params(Levy, Body, Reason), Revision -spec build_reject_result(state(), reject_params()) -> result() | no_return(). build_reject_result(State, ?reject_params(ParamsLevy)) -> Levy = get_levy(State), - Action = hg_machine_action:instant(), + Action = timeout, LevyChange = levy_change(ParamsLevy, Levy), Status = ?chargeback_status_rejected(), StatusChange = [?chargeback_target_status_changed(Status)], @@ -389,7 +389,7 @@ build_reject_result(State, ?reject_params(ParamsLevy)) -> build_accept_result(State, ?accept_params(ParamsLevy, ParamsBody)) -> Body = get_body(State), Levy = get_levy(State), - Action = hg_machine_action:instant(), + Action = timeout, BodyChange = body_change(ParamsBody, Body), LevyChange = levy_change(ParamsLevy, Levy), Status = ?chargeback_status_accepted(), @@ -402,7 +402,7 @@ build_reopen_result(State, ?reopen_params(ParamsLevy, ParamsBody) = Params) -> Body = get_body(State), Levy = get_levy(State), Stage = get_reopen_stage(State, Params), - Action = hg_machine_action:instant(), + Action = timeout, BodyChange = body_change(ParamsBody, Body), LevyChange = levy_change(ParamsLevy, Levy), StageChange = [?chargeback_stage_changed(Stage)], diff --git a/apps/hellgate/src/hg_invoice_payment_refund.erl b/apps/hellgate/src/hg_invoice_payment_refund.erl index 7b26b013..f482a60d 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -104,7 +104,7 @@ -type event() :: dmsl_payproc_thrift:'InvoicePaymentChangePayload'(). -type event_payload() :: dmsl_payproc_thrift:'InvoicePaymentRefundChangePayload'(). -type events() :: [event()]. --type action() :: hg_machine_action:t(). +-type action() :: prg_action:t(). -type result() :: {events(), action()}. -type machine_result() :: {next | done, result()}. @@ -280,10 +280,10 @@ do_process(accounter, Refund) -> do_process(failure, Refund) -> process_failure(Refund); do_process(finished, _Refund) -> - {done, {[], hg_machine_action:new()}}. + {done, {[], idle}}. process_refund_cashflow(Refund) -> - Action = hg_machine_action:set_timeout(0, hg_machine_action:new()), + Action = timeout, PartyConfigRef = get_injected_party_config_ref(Refund), ShopConfigRef = get_injected_shop_config_ref(Refund), Shop = get_injected_shop(Refund), @@ -325,7 +325,7 @@ finish_session_processing({Events0, Action}, Session, Refund) -> Events1 = hg_session:wrap_events(Events0, Session), case {hg_session:status(Session), hg_session:result(Session)} of {finished, ?session_succeeded()} -> - NewAction = hg_machine_action:set_timeout(0, Action), + NewAction = timeout, {next, {Events1, NewAction}}; {finished, ?session_failed(Failure)} -> case check_retry_possibility(Failure, Refund) of @@ -335,7 +335,7 @@ finish_session_processing({Events0, Action}, Session, Refund) -> {next, {Events1 ++ SessionEvents, SessionAction}}; fatal -> RollbackStarted = [?refund_rollback_started(Failure)], - {next, {Events1 ++ RollbackStarted, hg_machine_action:set_timeout(0, Action)}} + {next, {Events1 ++ RollbackStarted, timeout}} end; _ -> {next, {Events1, Action}} @@ -344,14 +344,14 @@ finish_session_processing({Events0, Action}, Session, Refund) -> process_accounter(Refund) -> _ = commit_refund_limits(Refund), _PostingPlanLog = commit_refund_cashflow(Refund), - {done, {[?refund_status_changed(?refund_succeeded())], hg_machine_action:new()}}. + {done, {[?refund_status_changed(?refund_succeeded())], idle}}. process_failure(Refund) -> Failure = failure(Refund), _ = rollback_refund_limits(Refund), _PostingPlanLog = rollback_refund_cashflow(Refund), Events = [?refund_status_changed(?refund_failed(Failure))], - {done, {Events, hg_machine_action:new()}}. + {done, {Events, idle}}. hold_refund_limits(Refund) -> DomainRefund = refund(Refund), @@ -460,9 +460,9 @@ get_manual_refund_events(#{transaction_info := TransactionInfo}) -> get_manual_refund_events(_) -> []. -retry_session(Action, Timeout) -> +retry_session(_Action, Timeout) -> NewEvents = [hg_session:wrap_event(?refunded(), hg_session:create())], - NewAction = hg_machine_action:set_timer({timeout, Timeout}, Action), + NewAction = prg_action:schedule_timer({timeout, Timeout}), {NewEvents, NewAction}. -spec check_retry_possibility(failure(), t()) -> diff --git a/apps/hellgate/src/hg_invoice_registered_payment.erl b/apps/hellgate/src/hg_invoice_registered_payment.erl index 8414ccaf..8ed04b26 100644 --- a/apps/hellgate/src/hg_invoice_registered_payment.erl +++ b/apps/hellgate/src/hg_invoice_registered_payment.erl @@ -109,7 +109,7 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> ChangeOpts = #{ invoice_id => Invoice#domain_Invoice.id }, - {collapse_changes(Events, undefined, ChangeOpts), {Events, hg_machine_action:instant()}}. + {collapse_changes(Events, undefined, ChangeOpts), {Events, timeout}}. -spec merge_change( hg_invoice_payment:change(), @@ -147,19 +147,18 @@ process_signal(timeout, St, Options) -> ). process_timeout(St) -> - Action = hg_machine_action:new(), + Action = idle, process_timeout(hg_invoice_payment:get_activity(St), Action, St). -process_timeout({payment, processing_capture}, Action, St) -> +process_timeout({payment, processing_capture}, _Action, St) -> %% It is an intermediate activity in hg_invoice_payment, but is used to initiate holds, %% due to need to save Transaction Info during initiation. - process_processing_capture(Action, St); + process_processing_capture(St); process_timeout(Activity, Action, St) -> hg_invoice_payment:process_timeout(Activity, Action, St). --spec process_processing_capture(hg_invoice_payment:action(), hg_invoice_payment:st()) -> - hg_invoice_payment:machine_result(). -process_processing_capture(Action, St) -> +-spec process_processing_capture(hg_invoice_payment:st()) -> hg_invoice_payment:machine_result(). +process_processing_capture(St) -> Opts = hg_invoice_payment:get_opts(St), Invoice = hg_invoice_payment:get_invoice(Opts), #domain_InvoicePayment{ @@ -172,7 +171,7 @@ process_processing_capture(Action, St) -> hg_session:wrap_event(?captured(?CAPTURE_REASON, Cost), hg_session:create()), hg_session:wrap_event(?captured(?CAPTURE_REASON, Cost), ?session_finished(?session_succeeded())) ], - {next, {Events, hg_machine_action:set_timeout(0, Action)}}. + {next, {Events, timeout}}. hold_payment_cashflow(St) -> PlanID = hg_invoice_payment:construct_payment_plan_id(St), diff --git a/apps/hellgate/src/hg_invoice_repair.erl b/apps/hellgate/src/hg_invoice_repair.erl index 3a644f08..6bd989da 100644 --- a/apps/hellgate/src/hg_invoice_repair.erl +++ b/apps/hellgate/src/hg_invoice_repair.erl @@ -41,7 +41,7 @@ check_for_action( fail_pre_processing, {fail_pre_processing, #payproc_InvoiceRepairFailPreProcessing{failure = Failure}} ) -> - {result, {done, {[?payment_status_changed(?failed({failure, Failure}))], hg_machine_action:instant()}}}; + {result, {done, {[?payment_status_changed(?failed({failure, Failure}))], timeout}}}; check_for_action(skip_inspector, {skip_inspector, #payproc_InvoiceRepairSkipInspector{risk_score = RiskScore}}) -> {result, RiskScore}; check_for_action(repair_session, {fail_session, #payproc_InvoiceRepairFailSession{failure = Failure, trx = Trx}}) -> diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index addd13ca..da688eee 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -5,8 +5,10 @@ -include_lib("damsel/include/dmsl_base_thrift.hrl"). -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). --define(NS, <<"invoice_template">>). +-define(NS, invoice_template). +-define(EVENT_FORMAT_VERSION, 1). %% Woody handler called by hg_woody_service_wrapper -behaviour(hg_woody_service_wrapper). @@ -14,7 +16,7 @@ -export([handle_function/3]). %% Machine callbacks --behaviour(hg_machine). +-behaviour(prg_machine). -export([namespace/0]). @@ -22,6 +24,12 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([process_notification/2]). +-export([marshal_event_body/2]). +-export([unmarshal_event_body/1]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% API @@ -34,6 +42,8 @@ %% Internal types -type invoice_template_change() :: dmsl_payproc_thrift:'InvoiceTemplateChange'(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). %% API @@ -42,9 +52,13 @@ get(TplID) -> get_invoice_template(TplID). get_invoice_template(ID) -> - History = get_history(ID), - _ = assert_invoice_template_not_deleted(lists:last(History)), - collapse_history(History). + case prg_machine:get(?NS, ID) of + {ok, Machine = #{history := History}} -> + _ = assert_invoice_template_not_deleted(lists:last(History)), + prg_machine:collapse(?MODULE, Machine); + {error, notfound} -> + throw(#payproc_InvoiceTemplateNotFound{}) + end. %% Woody handler @@ -167,10 +181,14 @@ validate_price({unlim, _}, _Shop) -> start(ID, Params) -> EncodedParams = marshal_invoice_template_params(Params), - map_start_error(hg_machine:start(?NS, ID, EncodedParams)). + map_start_error(prg_machine:start(?NS, ID, EncodedParams)). call(ID, Function, Args) -> - case hg_machine:thrift_call(?NS, ID, invoice_templating, {'InvoiceTemplating', Function}, Args) of + case + hg_invoicing_machine_client:thrift_call( + ?NS, ID, invoice_templating, {'InvoiceTemplating', Function}, Args + ) + of ok -> ok; {ok, Reply} -> @@ -181,9 +199,6 @@ call(ID, Function, Args) -> map_error(Error) end. -get_history(TplID) -> - unmarshal_history(map_history_error(hg_machine:get_history(?NS, TplID))). - -spec map_error(notfound | any()) -> no_return(). map_error(notfound) -> throw(#payproc_InvoiceTemplateNotFound{}); @@ -195,15 +210,10 @@ map_start_error({ok, _}) -> map_start_error({error, Reason}) -> error(Reason). -map_history_error({ok, Result}) -> - Result; -map_history_error({error, notfound}) -> - throw(#payproc_InvoiceTemplateNotFound{}). - %% Machine -type create_params() :: dmsl_payproc_thrift:'InvoiceTemplateCreateParams'(). --type call() :: hg_machine:thrift_call(). +-type call() :: {{atom(), atom()}, woody:args()}. -define(tpl_created(InvoiceTpl), {invoice_template_created, #payproc_InvoiceTemplateCreated{invoice_template = InvoiceTpl}} @@ -222,15 +232,15 @@ assert_invoice_template_not_deleted({_, _, [?tpl_deleted()]}) -> assert_invoice_template_not_deleted(_) -> ok. --spec namespace() -> hg_machine:ns(). +-spec namespace() -> prg_machine:namespace(). namespace() -> ?NS. --spec init(binary(), hg_machine:machine()) -> hg_machine:result(). +-spec init(binary(), machine()) -> prg_result(). init(EncodedParams, #{id := ID}) -> Params = unmarshal_invoice_template_params(EncodedParams), Tpl = create_invoice_template(ID, Params), - #{events => [marshal_event_payload([?tpl_created(Tpl)])]}. + #{events => [[?tpl_created(Tpl)]]}. create_invoice_template(ID, P) -> #domain_InvoiceTemplate{ @@ -247,24 +257,26 @@ create_invoice_template(ID, P) -> mutations = P#payproc_InvoiceTemplateCreateParams.mutations }. --spec process_repair(hg_machine:args(), hg_machine:machine()) -> no_return(). +-spec process_repair(prg_machine:args(), machine()) -> no_return(). process_repair(_Args, _Machine) -> erlang:error({not_implemented, repair}). --spec process_signal(hg_machine:signal(), hg_machine:machine()) -> hg_machine:result(). +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, _Machine) -> - #{}; -process_signal({repair, _}, _Machine) -> #{}. --spec process_call(call(), hg_machine:machine()) -> {hg_machine:response(), hg_machine:result()}. -process_call(Call, #{history := History}) -> - St = collapse_history(unmarshal_history(History)), +-spec process_call(call(), machine()) -> {prg_machine:response(), prg_result()}. +process_call(Call, Machine) -> + St = prg_machine:collapse(?MODULE, Machine), try handle_call(Call, St) of {ok, Changes} -> - {ok, #{events => [marshal_event_payload(Changes)]}}; + {ok, #{events => [Changes]}}; {Reply, Changes} -> - {{ok, Reply}, #{events => [marshal_event_payload(Changes)]}} + {{ok, Reply}, #{events => [Changes]}} catch throw:Exception -> {{exception, Exception}, #{}} @@ -276,12 +288,14 @@ handle_call({{'InvoiceTemplating', 'Update'}, {_TplID, Params}}, Tpl) -> handle_call({{'InvoiceTemplating', 'Delete'}, {_TplID}}, _Tpl) -> {ok, [?tpl_deleted()]}. -collapse_history(History) -> - lists:foldl( - fun({_ID, _, Ev}, Tpl) -> merge_changes(Ev, Tpl) end, - undefined, - History - ). +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + [invoice_template_change()], + tpl() | undefined +) -> tpl(). +apply_event(_EventID, _Ts, Changes, Tpl) -> + merge_changes(Changes, Tpl). merge_changes([?tpl_created(Tpl)], _) -> Tpl; @@ -334,9 +348,68 @@ marshal_invoice_template_params(Params) -> Type = {struct, struct, {dmsl_payproc_thrift, 'InvoiceTemplateCreateParams'}}, hg_proto_utils:serialize(Type, Params). --spec marshal_event_payload([invoice_template_change()]) -> hg_machine:event_payload(). -marshal_event_payload(Changes) when is_list(Changes) -> - wrap_event_payload({invoice_template_changes, Changes}). +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(_Timestamp, Changes) when is_list(Changes) -> + #{data := Data} = wrap_event_payload({invoice_template_changes, Changes}), + Msgp = mg_msgpack_marshalling:marshal(Data), + {?EVENT_FORMAT_VERSION, msgpack_payload_to_binary(Msgp)}. + +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + decode_event_body(Payload). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + term_to_binary(marshal_aux_st_content(AuxSt)). + +marshal_aux_st_content(AuxSt) when map_size(AuxSt) =:= 0 -> + #mg_stateproc_Content{format_version = undefined, data = {bin, <<>>}}; +marshal_aux_st_content(AuxSt) -> + #mg_stateproc_Content{ + format_version = undefined, + data = mg_msgpack_marshalling:marshal(AuxSt) + }. + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(<<>>) -> + #{}; +unmarshal_aux_state(Payload) when is_binary(Payload) -> + %% Legacy hg_progressor stored term_to_binary(#mg_stateproc_Content{data = Msgp}). + %% Keep reading bare msgpack blobs written by an intermediate branch version. + case binary_to_term(Payload) of + #mg_stateproc_Content{data = {bin, <<>>}} -> + #{}; + #mg_stateproc_Content{data = Data} -> + mg_msgpack_marshalling:unmarshal(Data); + Msgp -> + mg_msgpack_marshalling:unmarshal(Msgp) + end. + +msgpack_payload_to_binary(Msgp) -> + term_to_binary(Msgp). + +decode_event_body(Payload) -> + case try_unmarshal_msgpack_payload(Payload) of + {ok, Data} -> + changes_from_msgpack_data(Data); + {error, _} -> + unmarshal_event_payload(#{format_version => ?EVENT_FORMAT_VERSION, data => {bin, Payload}}) + end. + +try_unmarshal_msgpack_payload(Payload) -> + try + {ok, mg_msgpack_marshalling:unmarshal(binary_to_term(Payload))} + catch + _:_ -> + {error, invalid_msgpack_payload} + end. + +changes_from_msgpack_data({bin, Bin}) when is_binary(Bin) -> + unmarshal_event_payload(#{format_version => ?EVENT_FORMAT_VERSION, data => {bin, Bin}}); +changes_from_msgpack_data(#{format_version := V, data := Data}) -> + unmarshal_event_payload(#{format_version => V, data => Data}); +changes_from_msgpack_data(Changes) when is_list(Changes) -> + Changes. wrap_event_payload(Payload) -> Type = {struct, union, {dmsl_payproc_thrift, 'EventPayload'}}, @@ -353,16 +426,32 @@ unmarshal_invoice_template_params(EncodedParams) -> Type = {struct, struct, {dmsl_payproc_thrift, 'InvoiceTemplateCreateParams'}}, hg_proto_utils:deserialize(Type, EncodedParams). --spec unmarshal_history([hg_machine:event()]) -> [hg_machine:event([invoice_template_change()])]. -unmarshal_history(Events) -> - [unmarshal_event(Event) || Event <- Events]. - --spec unmarshal_event(hg_machine:event()) -> hg_machine:event([invoice_template_change()]). -unmarshal_event({ID, Dt, Payload}) -> - {ID, Dt, unmarshal_event_payload(Payload)}. - --spec unmarshal_event_payload(hg_machine:event_payload()) -> [invoice_template_change()]. +-spec unmarshal_event_payload(map()) -> [invoice_template_change()]. unmarshal_event_payload(#{format_version := 1, data := {bin, Changes}}) -> Type = {struct, union, {dmsl_payproc_thrift, 'EventPayload'}}, {invoice_template_changes, Buf} = hg_proto_utils:deserialize(Type, Changes), Buf. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec aux_state_roundtrip_test() -> _. +aux_state_roundtrip_test() -> + AuxSt = #{<<"k">> => <<"v">>}, + ?assertEqual(AuxSt, unmarshal_aux_state(marshal_aux_state(AuxSt))). + +-spec aux_state_writes_legacy_empty_content_test() -> _. +aux_state_writes_legacy_empty_content_test() -> + Legacy = term_to_binary(#mg_stateproc_Content{format_version = undefined, data = {bin, <<>>}}), + ?assertEqual(Legacy, marshal_aux_state(#{})). + +-spec aux_state_reads_legacy_mg_content_test() -> _. +aux_state_reads_legacy_mg_content_test() -> + AuxSt = #{<<"legacy">> => 1}, + Legacy = term_to_binary(marshal_aux_st_content(AuxSt)), + ?assertEqual(Legacy, marshal_aux_state(AuxSt)), + ?assertEqual(AuxSt, unmarshal_aux_state(Legacy)). + +-endif. diff --git a/apps/hellgate/src/hg_invoicing_machine_client.erl b/apps/hellgate/src/hg_invoicing_machine_client.erl new file mode 100644 index 00000000..84ed5118 --- /dev/null +++ b/apps/hellgate/src/hg_invoicing_machine_client.erl @@ -0,0 +1,36 @@ +-module(hg_invoicing_machine_client). + +%%% Thrift RPC to invoicing machines via progressor. +%%% Call args are Erlang thrift terms; prg_machine encodes them with term_to_binary. +%%% hg_proto stays in apps/hellgate (not in prg_machine). + +-export([thrift_call/5]). + +-type namespace() :: prg_machine:namespace(). +-type id() :: prg_machine:id(). +-type service_name() :: atom(). +-type function_ref() :: hg_proto_utils:thrift_fun_ref(). +-type args() :: woody:args(). +-type response() :: prg_machine:response(). + +-spec thrift_call(namespace(), id(), service_name(), function_ref(), args()) -> + response() | {error, notfound | failed}. +thrift_call(NS, ID, _ServiceName, FunRef, Args) -> + case prg_machine:call(NS, ID, {FunRef, Args}) of + {ok, Response} -> + normalize_response(Response); + {error, notfound} -> + {error, notfound}; + {error, failed} -> + {error, failed}; + {error, _} = Error -> + Error + end. + +-spec normalize_response(prg_machine:response()) -> response(). +normalize_response(ok) -> + ok; +normalize_response({ok, Reply}) -> + {ok, Reply}; +normalize_response({exception, Exception}) -> + {exception, Exception}. diff --git a/apps/hellgate/src/hg_machine.erl b/apps/hellgate/src/hg_machine.erl deleted file mode 100644 index 91a98829..00000000 --- a/apps/hellgate/src/hg_machine.erl +++ /dev/null @@ -1,569 +0,0 @@ --module(hg_machine). - --include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). - --type msgp() :: mg_msgpack_marshalling:msgpack_value(). - --type id() :: mg_proto_base_thrift:'ID'(). --type ns() :: mg_proto_base_thrift:'Namespace'(). --type args() :: _. - --type event(T) :: {event_id(), timestamp(), T}. --type event() :: event(event_payload()). --type event_id() :: mg_proto_base_thrift:'EventID'(). --type event_payload() :: #{ - data := msgp(), - format_version := pos_integer() | undefined -}. - --type timestamp() :: mg_proto_base_thrift:'Timestamp'(). --type history() :: [event()]. --type auxst() :: msgp(). - --type history_range() :: mg_proto_state_processing_thrift:'HistoryRange'(). --type direction() :: mg_proto_state_processing_thrift:'Direction'(). --type descriptor() :: mg_proto_state_processing_thrift:'MachineDescriptor'(). - --type machine() :: #{ - id := id(), - history := history(), - aux_state := auxst() -}. - --type result() :: #{ - events => [event_payload()], - action => hg_machine_action:t(), - auxst => auxst() -}. - --type backend() :: - machinegun - | progressor - | hybrid. - --callback namespace() -> ns(). - --callback init(args(), machine()) -> result(). - --type signal() :: - timeout | {repair, args()}. - --callback process_signal(signal(), machine()) -> result(). - --type call() :: _. --type thrift_call() :: {hg_proto_utils:thrift_fun_ref(), Args :: [term()]}. --type response() :: ok | {ok, term()} | {exception, term()}. - --callback process_call(call(), machine()) -> {response(), result()}. - --callback process_repair(args(), machine()) -> result(). - --type context() :: #{ - client_context => woody_context:ctx() -}. - --export_type([id/0]). --export_type([ns/0]). --export_type([args/0]). --export_type([event_id/0]). --export_type([event_payload/0]). --export_type([event/0]). --export_type([event/1]). --export_type([history/0]). --export_type([auxst/0]). --export_type([signal/0]). --export_type([call/0]). --export_type([thrift_call/0]). --export_type([result/0]). --export_type([context/0]). --export_type([response/0]). --export_type([machine/0]). - --export([start/3]). --export([call/3]). --export([call/6]). --export([thrift_call/5]). --export([thrift_call/8]). --export([repair/3]). --export([get_history/2]). --export([get_history/4]). --export([get_history/5]). --export([get_machine/5]). - --export([call_automaton/3]). - -%% Dispatch - --export([get_child_spec/1]). --export([get_service_handlers/2]). --export([get_handler_module/1]). - --export([start_link/1]). --export([init/1]). - -%% Woody handler called by hg_woody_service_wrapper - --behaviour(hg_woody_service_wrapper). - --export([handle_function/3]). - -%% Internal types - --type mg_event() :: mg_proto_state_processing_thrift:'Event'(). --type mg_event_payload() :: mg_proto_state_processing_thrift:'EventBody'(). --type function_ref() :: hg_proto_utils:thrift_fun_ref(). --type service_name() :: atom(). - -%% - --spec start(ns(), id(), term()) -> {ok, term()} | {error, exists | term()} | no_return(). -start(NS, ID, Args) -> - call_automaton('Start', {NS, ID, wrap_args(Args)}). - --spec thrift_call(ns(), id(), service_name(), function_ref(), args()) -> response() | {error, notfound | failed}. -thrift_call(NS, ID, Service, FunRef, Args) -> - thrift_call(NS, ID, Service, FunRef, Args, undefined, undefined, forward). - --spec thrift_call(NS, ID, Service, FunRef, Args, After, Limit, Direction) -> Result when - NS :: ns(), - ID :: id(), - Service :: service_name(), - FunRef :: function_ref(), - Args :: args(), - After :: event_id() | undefined, - Limit :: integer() | undefined, - Direction :: forward | backward, - Result :: response() | {error, notfound | failed}. -thrift_call(NS, ID, Service, FunRef, Args, After, Limit, Direction) -> - EncodedArgs = marshal_thrift_args(Service, FunRef, Args), - Call = {thrift_call, Service, FunRef, EncodedArgs}, - case do_call(NS, ID, Call, After, Limit, Direction) of - {ok, Response} -> - % should be specific to a processing interface already - unmarshal_thrift_response(Service, FunRef, Response); - {error, _} = Error -> - Error - end. - --spec call(ns(), id(), Args :: term()) -> response() | {error, notfound | failed}. -call(NS, ID, Args) -> - call(NS, ID, Args, undefined, undefined, forward). - --spec call(NS, ID, Args, After, Limit, Direction) -> Result when - NS :: ns(), - ID :: id(), - Args :: args(), - After :: event_id() | undefined, - Limit :: integer() | undefined, - Direction :: forward | backward, - Result :: response() | {error, notfound | failed}. -call(NS, ID, Args, After, Limit, Direction) -> - case do_call(NS, ID, {schemaless_call, Args}, After, Limit, Direction) of - {ok, Response} -> - unmarshal_schemaless_response(Response); - {error, _} = Error -> - Error - end. - --spec repair(ns(), id(), term()) -> - {ok, term()} | {error, notfound | failed | working | {repair, {failed, binary()}}} | no_return(). -repair(NS, ID, Args) -> - Descriptor = prepare_descriptor(NS, ID, #mg_stateproc_HistoryRange{}), - call_automaton('Repair', {Descriptor, wrap_args(Args)}). - --spec get_history(ns(), id()) -> {ok, history()} | {error, notfound} | no_return(). -get_history(NS, ID) -> - get_history(NS, ID, undefined, undefined, forward). - --spec get_history(ns(), id(), undefined | event_id(), undefined | non_neg_integer()) -> - {ok, history()} | {error, notfound} | no_return(). -get_history(NS, ID, AfterID, Limit) -> - get_history(NS, ID, AfterID, Limit, forward). - --spec get_history(ns(), id(), undefined | event_id(), undefined | non_neg_integer(), direction()) -> - {ok, history()} | {error, notfound} | no_return(). -get_history(NS, ID, AfterID, Limit, Direction) -> - case get_machine(NS, ID, AfterID, Limit, Direction) of - {ok, #{history := History}} -> - {ok, History}; - Error -> - Error - end. - --spec get_machine(ns(), id(), undefined | event_id(), undefined | non_neg_integer(), direction()) -> - {ok, machine()} | {error, notfound} | no_return(). -get_machine(NS, ID, AfterID, Limit, Direction) -> - Range = #mg_stateproc_HistoryRange{'after' = AfterID, limit = Limit, direction = Direction}, - Descriptor = prepare_descriptor(NS, ID, Range), - case call_automaton('GetMachine', {Descriptor}) of - {ok, #mg_stateproc_Machine{} = Machine} -> - {ok, unmarshal_machine(Machine)}; - Error -> - Error - end. - -%% - --spec do_call(NS, ID, Args, After, Limit, Direction) -> Result when - NS :: ns(), - ID :: id(), - Args :: args(), - After :: event_id() | undefined, - Limit :: integer() | undefined, - Direction :: forward | backward, - Result :: {ok, response()} | {error, notfound | failed}. -do_call(NS, ID, Args, After, Limit, Direction) -> - HistoryRange = #mg_stateproc_HistoryRange{ - 'after' = After, - 'limit' = Limit, - 'direction' = Direction - }, - Descriptor = prepare_descriptor(NS, ID, HistoryRange), - case call_automaton('Call', {Descriptor, wrap_args(Args)}) of - {ok, Response} -> - {ok, unmarshal_response(Response)}; - {error, _} = Error -> - Error - end. - -call_automaton(Function, Args) -> - call_automaton(Function, Args, application:get_env(hellgate, backend, machinegun)). - --spec call_automaton(woody:func(), woody:args(), backend()) -> term(). -call_automaton(Function, Args, machinegun) -> - case hg_woody_wrapper:call(automaton, Function, Args) of - {ok, _} = Result -> - Result; - {exception, #mg_stateproc_MachineAlreadyExists{}} -> - {error, exists}; - {exception, #mg_stateproc_MachineNotFound{}} -> - {error, notfound}; - {exception, #mg_stateproc_MachineFailed{}} -> - {error, failed}; - {exception, #mg_stateproc_MachineAlreadyWorking{}} -> - {error, working}; - {exception, #mg_stateproc_RepairFailed{reason = Reason}} -> - {error, {repair, {failed, Reason}}} - end; -call_automaton(Function, Args, progressor) -> - hg_progressor:call_automaton(Function, Args); -call_automaton(Function, Args, hybrid) -> - hg_hybrid:call_automaton(Function, Args). - -%% - --type func() :: 'ProcessSignal' | 'ProcessCall' | 'ProcessRepair'. - --spec handle_function(func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return(). -handle_function(Func, Args, Opts) -> - scoper:scope( - machine, - fun() -> handle_function_(Func, Args, Opts) end - ). - --spec handle_function_(func(), woody:args(), #{ns := ns()}) -> term() | no_return(). -handle_function_('ProcessSignal', {Args}, #{ns := NS} = _Opts) -> - #mg_stateproc_SignalArgs{signal = {Type, Signal}, machine = #mg_stateproc_Machine{id = ID} = Machine} = Args, - scoper:add_meta(#{ - namespace => NS, - id => ID, - activity => signal, - signal => Type - }), - dispatch_signal(NS, Signal, unmarshal_machine(Machine)); -handle_function_('ProcessCall', {Args}, #{ns := NS} = _Opts) -> - #mg_stateproc_CallArgs{arg = Payload, machine = #mg_stateproc_Machine{id = ID} = Machine} = Args, - scoper:add_meta(#{ - namespace => NS, - id => ID, - activity => call - }), - dispatch_call(NS, Payload, unmarshal_machine(Machine)); -handle_function_('ProcessRepair', {Args}, #{ns := NS} = _Opts) -> - #mg_stateproc_RepairArgs{arg = Payload, machine = #mg_stateproc_Machine{id = ID} = Machine} = Args, - scoper:add_meta(#{ - namespace => NS, - id => ID, - activity => repair - }), - dispatch_repair(NS, Payload, unmarshal_machine(Machine)). - -%% - --spec dispatch_signal(ns(), Signal, machine()) -> Result when - Signal :: - mg_proto_state_processing_thrift:'InitSignal'() - | mg_proto_state_processing_thrift:'TimeoutSignal'(), - Result :: - mg_proto_state_processing_thrift:'SignalResult'(). -dispatch_signal(NS, #mg_stateproc_InitSignal{arg = Payload}, Machine) -> - Args = unwrap_args(Payload), - _ = log_dispatch(init, Args, Machine), - Module = get_handler_module(NS), - Result = Module:init(Args, Machine), - marshal_signal_result(Result, Machine); -dispatch_signal(NS, #mg_stateproc_TimeoutSignal{}, Machine) -> - _ = log_dispatch(timeout, Machine), - Module = get_handler_module(NS), - Result = Module:process_signal(timeout, Machine), - marshal_signal_result(Result, Machine). - -marshal_signal_result(#{} = Result, #{aux_state := AuxStWas}) -> - _ = logger:debug("signal result = ~p", [Result]), - Change = #mg_stateproc_MachineStateChange{ - events = marshal_events(maps:get(events, Result, [])), - aux_state = marshal_aux_st_format(maps:get(auxst, Result, AuxStWas)) - }, - #mg_stateproc_SignalResult{ - change = Change, - action = maps:get(action, Result, hg_machine_action:new()) - }. - --spec dispatch_call(ns(), Call, machine()) -> Result when - Call :: mg_proto_state_processing_thrift:'Args'(), - Result :: mg_proto_state_processing_thrift:'CallResult'(). -dispatch_call(NS, Payload, Machine) -> - Args = unwrap_args(Payload), - _ = log_dispatch(call, Args, Machine), - Module = get_handler_module(NS), - do_dispatch_call(Module, Args, Machine). - -do_dispatch_call(Module, {schemaless_call, Args}, Machine) -> - {Response, Result} = Module:process_call(Args, Machine), - marshal_call_result(marshal_schemaless_response(Response), Result, Machine); -do_dispatch_call(Module, {thrift_call, ServiceName, FunctionRef, EncodedArgs}, Machine) -> - Args = unmarshal_thrift_args(ServiceName, FunctionRef, EncodedArgs), - {Response, Result} = Module:process_call({FunctionRef, Args}, Machine), - EncodedResponse = marshal_thrift_response(ServiceName, FunctionRef, Response), - marshal_call_result(EncodedResponse, Result, Machine). - -marshal_call_result(Response, Result, #{aux_state := AuxStWas}) -> - _ = logger:debug("call response = ~p with result = ~p", [Response, Result]), - Change = #mg_stateproc_MachineStateChange{ - events = marshal_events(maps:get(events, Result, [])), - aux_state = marshal_aux_st_format(maps:get(auxst, Result, AuxStWas)) - }, - #mg_stateproc_CallResult{ - change = Change, - action = maps:get(action, Result, hg_machine_action:new()), - response = marshal_response(Response) - }. - --spec dispatch_repair(ns(), Args, machine()) -> Result when - Args :: mg_proto_state_processing_thrift:'Args'(), - Result :: mg_proto_state_processing_thrift:'RepairResult'(). -dispatch_repair(NS, Payload, Machine) -> - Args = unwrap_args(Payload), - _ = log_dispatch(repair, Args, Machine), - Module = get_handler_module(NS), - try - Result = Module:process_repair(Args, Machine), - marshal_repair_result(ok, Result, Machine) - catch - throw:{exception, Reason} = Error -> - logger:notice("Process repair failed, ~p", [Reason]), - woody_error:raise(business, marshal_repair_failed(Error)) - end. - -marshal_repair_result(Response, #{} = RepairResult, #{aux_state := AuxStWas}) -> - _ = logger:debug("repair response = ~p with result = ~p", [Response, RepairResult]), - Change = #mg_stateproc_MachineStateChange{ - events = marshal_events(maps:get(events, RepairResult, [])), - aux_state = marshal_aux_st_format(maps:get(auxst, RepairResult, AuxStWas)) - }, - #mg_stateproc_RepairResult{ - change = Change, - action = maps:get(action, RepairResult, hg_machine_action:new()), - response = marshal_response(Response) - }. - -marshal_repair_failed({exception, _} = Error) -> - #mg_stateproc_RepairFailed{ - reason = marshal_response(Error) - }. - -%% - --type service_handler() :: - {Path :: string(), {woody:service(), {module(), hg_woody_service_wrapper:handler_opts()}}}. - --spec get_child_spec([MachineHandler :: module()]) -> supervisor:child_spec(). -get_child_spec(MachineHandlers) -> - #{ - id => hg_machine_dispatch, - start => {?MODULE, start_link, [MachineHandlers]}, - type => supervisor - }. - --spec get_service_handlers([MachineHandler :: module()], map()) -> [service_handler()]. -get_service_handlers(MachineHandlers, Opts) -> - [get_service_handler(H, Opts) || H <- MachineHandlers]. - -get_service_handler(MachineHandler, Opts) -> - NS = MachineHandler:namespace(), - FullOpts = maps:merge(#{ns => NS, handler => ?MODULE}, Opts), - {Path, Service} = hg_proto:get_service_spec(processor, #{namespace => NS}), - {Path, {Service, {hg_woody_service_wrapper, FullOpts}}}. - -%% - --define(TABLE, hg_machine_dispatch). - --spec start_link([module()]) -> {ok, pid()}. -start_link(MachineHandlers) -> - supervisor:start_link(?MODULE, MachineHandlers). - --spec init([module()]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. -init(MachineHandlers) -> - _ = ets:new(?TABLE, [named_table, {read_concurrency, true}]), - true = ets:insert_new(?TABLE, [{MH:namespace(), MH} || MH <- MachineHandlers]), - {ok, {#{}, []}}. - -%% - --spec get_handler_module(ns()) -> module(). -get_handler_module(NS) -> - ets:lookup_element(?TABLE, NS, 2). - -log_dispatch(Operation, #{id := ID, history := History, aux_state := AuxSt}) -> - logger:debug( - "dispatch ~p with id = ~p, history = ~p, aux state = ~p", - [Operation, ID, History, AuxSt] - ). - -log_dispatch(Operation, Args, #{id := ID, history := History, aux_state := AuxSt}) -> - logger:debug( - "dispatch ~p with id = ~p, args = ~p, history = ~p, aux state = ~p", - [Operation, ID, Args, History, AuxSt] - ). - -unmarshal_machine(#mg_stateproc_Machine{id = ID, history = History} = Machine) -> - AuxState = get_aux_state(Machine), - #{ - id => ID, - history => unmarshal_events(History), - aux_state => AuxState - }. - --spec marshal_events([event_payload()]) -> [mg_event_payload()]. -marshal_events(Events) when is_list(Events) -> - [marshal_event(Event) || Event <- Events]. - --spec marshal_event(event_payload()) -> mg_event_payload(). -marshal_event(#{format_version := Format, data := Data}) -> - #mg_stateproc_Content{ - format_version = Format, - data = mg_msgpack_marshalling:marshal(Data) - }. - -marshal_aux_st_format(AuxSt) -> - #mg_stateproc_Content{ - format_version = undefined, - data = mg_msgpack_marshalling:marshal(AuxSt) - }. - --spec marshal_thrift_args(service_name(), function_ref(), args()) -> binary(). -marshal_thrift_args(ServiceName, FunctionRef, Args) -> - {Service, _Function} = FunctionRef, - {Module, Service} = hg_proto:get_service(ServiceName), - FullFunctionRef = {Module, FunctionRef}, - hg_proto_utils:serialize_function_args(FullFunctionRef, Args). - --spec unmarshal_thrift_args(service_name(), function_ref(), binary()) -> args(). -unmarshal_thrift_args(ServiceName, FunctionRef, Args) -> - {Service, _Function} = FunctionRef, - {Module, Service} = hg_proto:get_service(ServiceName), - FullFunctionRef = {Module, FunctionRef}, - hg_proto_utils:deserialize_function_args(FullFunctionRef, Args). - --spec marshal_thrift_response(service_name(), function_ref(), response()) -> response(). -marshal_thrift_response(ServiceName, FunctionRef, Response) -> - {Service, _Function} = FunctionRef, - {Module, Service} = hg_proto:get_service(ServiceName), - FullFunctionRef = {Module, FunctionRef}, - case Response of - ok -> - ok; - {ok, Reply} -> - EncodedReply = hg_proto_utils:serialize_function_reply(FullFunctionRef, Reply), - {ok, EncodedReply}; - {exception, Exception} -> - EncodedException = hg_proto_utils:serialize_function_exception(FullFunctionRef, Exception), - {exception, EncodedException} - end. - --spec unmarshal_thrift_response(service_name(), function_ref(), response()) -> response(). -unmarshal_thrift_response(ServiceName, FunctionRef, Response) -> - {Service, _Function} = FunctionRef, - {Module, Service} = hg_proto:get_service(ServiceName), - FullFunctionRef = {Module, FunctionRef}, - case Response of - ok -> - ok; - {ok, EncodedReply} -> - Reply = hg_proto_utils:deserialize_function_reply(FullFunctionRef, EncodedReply), - {ok, Reply}; - {exception, EncodedException} -> - Exception = hg_proto_utils:deserialize_function_exception(FullFunctionRef, EncodedException), - {exception, Exception} - end. - --spec marshal_schemaless_response(response()) -> response(). -marshal_schemaless_response(ok) -> - ok; -marshal_schemaless_response({ok, _Reply} = Response) -> - Response; -marshal_schemaless_response({exception, _Exception} = Response) -> - Response. - --spec unmarshal_schemaless_response(response()) -> response(). -unmarshal_schemaless_response(ok) -> - ok; -unmarshal_schemaless_response({ok, _Reply} = Response) -> - Response; -unmarshal_schemaless_response({exception, _Exception} = Response) -> - Response. - -marshal_response(ok = Response) -> - marshal_term(Response); -marshal_response({ok, _Reply} = Response) -> - marshal_term(Response); -marshal_response({exception, _Exception} = Response) -> - marshal_term(Response). - -unmarshal_response(Response) -> - unmarshal_term(Response). - --spec unmarshal_events([mg_event()]) -> [event()]. -unmarshal_events(Events) when is_list(Events) -> - [unmarshal_event(Event) || Event <- Events]. - --spec unmarshal_event(mg_event()) -> event(). -unmarshal_event(#mg_stateproc_Event{id = ID, created_at = Dt, format_version = Format, data = Payload}) -> - {ID, Dt, #{format_version => Format, data => mg_msgpack_marshalling:unmarshal(Payload)}}. - -unmarshal_aux_st(Data) -> - mg_msgpack_marshalling:unmarshal(Data). - -get_aux_state(#mg_stateproc_Machine{aux_state = #mg_stateproc_Content{format_version = undefined, data = Data}}) -> - unmarshal_aux_st(Data). - -wrap_args(Args) -> - marshal_term(Args). - -unwrap_args(Payload) -> - unmarshal_term(Payload). - -marshal_term(V) -> - {bin, term_to_binary(V)}. - -unmarshal_term({bin, B}) -> - binary_to_term(B). - --spec prepare_descriptor(ns(), id(), history_range()) -> descriptor(). -prepare_descriptor(NS, ID, Range) -> - #mg_stateproc_MachineDescriptor{ - ns = NS, - ref = {id, ID}, - range = Range - }. diff --git a/apps/hellgate/src/hg_machine_action.erl b/apps/hellgate/src/hg_machine_action.erl deleted file mode 100644 index 29984a43..00000000 --- a/apps/hellgate/src/hg_machine_action.erl +++ /dev/null @@ -1,81 +0,0 @@ --module(hg_machine_action). - --export([new/0]). --export([instant/0]). --export([set_timeout/1]). --export([set_timeout/2]). --export([set_deadline/1]). --export([set_deadline/2]). --export([set_timer/1]). --export([set_timer/2]). --export([unset_timer/0]). --export([unset_timer/1]). --export([mark_removal/1]). - --include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). - -%% - --type seconds() :: non_neg_integer(). --type datetime_rfc3339() :: binary(). --type datetime() :: calendar:datetime() | datetime_rfc3339(). - --type timer() :: mg_proto_base_thrift:'Timer'(). --type t() :: mg_proto_state_processing_thrift:'ComplexAction'(). - --export_type([t/0]). - -%% - --spec new() -> t(). -new() -> - #mg_stateproc_ComplexAction{}. - --spec instant() -> t(). -instant() -> - set_timeout(0, new()). - --spec set_timeout(seconds()) -> t(). -set_timeout(Seconds) -> - set_timeout(Seconds, new()). - --spec set_timeout(seconds(), t()) -> t(). -set_timeout(Seconds, Action) when is_integer(Seconds) andalso Seconds >= 0 -> - set_timer({timeout, Seconds}, Action). - --spec set_deadline(datetime()) -> t(). -set_deadline(Deadline) -> - set_deadline(Deadline, new()). - --spec set_deadline(datetime(), t()) -> t(). -set_deadline(Deadline, Action) -> - set_timer({deadline, try_format_dt(Deadline)}, Action). - --spec set_timer(timer()) -> t(). -set_timer(Timer) -> - set_timer(Timer, new()). - --spec set_timer(timer(), t()) -> t(). -set_timer(Timer, #mg_stateproc_ComplexAction{} = Action) -> - % TODO pass range and processing timeout explicitly too - Action#mg_stateproc_ComplexAction{timer = {set_timer, #mg_stateproc_SetTimerAction{timer = Timer}}}. - --spec unset_timer() -> t(). -unset_timer() -> - unset_timer(new()). - --spec unset_timer(t()) -> t(). -unset_timer(#mg_stateproc_ComplexAction{} = Action) -> - Action#mg_stateproc_ComplexAction{timer = {unset_timer, #mg_stateproc_UnsetTimerAction{}}}. - --spec mark_removal(t()) -> t(). -mark_removal(#mg_stateproc_ComplexAction{} = Action) -> - Action#mg_stateproc_ComplexAction{remove = #mg_stateproc_RemoveAction{}}. - -%% - -try_format_dt({_, _} = Datetime) -> - Seconds = genlib_time:daytime_to_unixtime(Datetime), - genlib_rfc3339:format(Seconds, second); -try_format_dt(Datetime) when is_binary(Datetime) -> - Datetime. diff --git a/apps/hellgate/src/hg_machine_tag.erl b/apps/hellgate/src/hg_machine_tag.erl index f0b3896c..7fa90c98 100644 --- a/apps/hellgate/src/hg_machine_tag.erl +++ b/apps/hellgate/src/hg_machine_tag.erl @@ -7,13 +7,13 @@ -export([create_binding/4]). -type tag() :: dmsl_base_thrift:'Tag'(). --type ns() :: hg_machine:ns(). +-type ns() :: prg_machine:namespace(). -type entity_id() :: dmsl_base_thrift:'ID'(). --type machine_id() :: hg_machine:id(). +-type machine_id() :: prg_machine:id(). -spec get_binding(ns(), tag()) -> {ok, entity_id(), machine_id()} | {error, notfound}. get_binding(NS, Tag) -> - WoodyContext = hg_context:get_woody_context(hg_context:load()), + WoodyContext = op_context:get_woody_context(op_context:load(op_context:key(hellgate))), case bender_client:get_internal_id(tag_to_external_id(NS, Tag), WoodyContext) of {ok, EntityID} -> {ok, EntityID, EntityID}; @@ -34,7 +34,7 @@ create_binding(NS, Tag, EntityID, MachineID) -> %% create_binding_(NS, Tag, EntityID, Context) -> - WoodyContext = hg_context:get_woody_context(hg_context:load()), + WoodyContext = op_context:get_woody_context(op_context:load(op_context:key(hellgate))), case bender_client:gen_constant(tag_to_external_id(NS, Tag), EntityID, WoodyContext, Context) of {ok, EntityID} -> ok; @@ -43,4 +43,5 @@ create_binding_(NS, Tag, EntityID, Context) -> end. tag_to_external_id(NS, Tag) -> - <>. + BinNS = atom_to_binary(NS, utf8), + <>. diff --git a/apps/hellgate/src/hg_party.erl b/apps/hellgate/src/hg_party.erl index a6b967f8..c718f727 100644 --- a/apps/hellgate/src/hg_party.erl +++ b/apps/hellgate/src/hg_party.erl @@ -51,9 +51,9 @@ get_route_provision_terms(?route(ProviderRef, TerminalRef), VS, Revision) -> TermsSet. get_party_client() -> - HgContext = hg_context:load(), - Client = hg_context:get_party_client(HgContext), - Context = hg_context:get_party_client_context(HgContext), + HgContext = op_context:load(op_context:key(hellgate)), + Client = op_context:get_party_client(HgContext), + Context = op_context:get_party_client_context(HgContext), {Client, Context}. -spec get_party(party_config_ref()) -> {party_config_ref(), party()} | hg_domain:get_error(). diff --git a/apps/hellgate/src/hg_payment_institution.erl b/apps/hellgate/src/hg_payment_institution.erl index b295cc6e..79903540 100644 --- a/apps/hellgate/src/hg_payment_institution.erl +++ b/apps/hellgate/src/hg_payment_institution.erl @@ -81,7 +81,7 @@ choose_external_account(Currency, VS, Revision) -> end. get_party_client() -> - HgContext = hg_context:load(), - Client = hg_context:get_party_client(HgContext), - Context = hg_context:get_party_client_context(HgContext), + HgContext = op_context:load(op_context:key(hellgate)), + Client = op_context:get_party_client(HgContext), + Context = op_context:get_party_client_context(HgContext), {Client, Context}. diff --git a/apps/hellgate/src/hg_session.erl b/apps/hellgate/src/hg_session.erl index a84ee8e4..3896c633 100644 --- a/apps/hellgate/src/hg_session.erl +++ b/apps/hellgate/src/hg_session.erl @@ -98,7 +98,7 @@ dmsl_payproc_thrift:'SessionChangePayload'() | {invoice_payment_rec_token_acquired, dmsl_payproc_thrift:'InvoicePaymentRecTokenAcquired'()}. -type events() :: [event()]. --type action() :: hg_machine_action:t(). +-type action() :: prg_action:t(). -type result() :: {events(), action()}. -type callback() :: dmsl_proxy_provider_thrift:'Callback'(). @@ -210,7 +210,7 @@ process_change(#proxy_provider_PaymentSessionChange{status = {failure, Failure}} ?session_activated(), ?session_finished(?session_failed({failure, Failure})) ], - Result = {SessionEvents, hg_machine_action:instant()}, + Result = {SessionEvents, timeout}, apply_result(Result, Session); process_change(_Change, _Session) -> %% NOTE For now there is no other applicable change defined in protocol. @@ -233,7 +233,7 @@ do_process(active, Session) -> do_process(suspended, Session) -> process_callback_timeout(Session); do_process(finished, Session) -> - {{[], hg_machine_action:new()}, Session}. + {{[], idle}, Session}. repair(#{repair_scenario := {result, ProxyResult}} = Session) -> Result = handle_proxy_result(ProxyResult, Session), @@ -252,7 +252,7 @@ process_callback_timeout(Session) -> apply_result(Result, Session); {operation_failure, OperationFailure} -> SessionEvents = [?session_finished(?session_failed(OperationFailure))], - Result = {SessionEvents, hg_machine_action:new()}, + Result = {SessionEvents, idle}, apply_result(Result, Session) end. @@ -315,7 +315,7 @@ handle_proxy_result( Events1 = hg_proxy_provider:bind_transaction(Trx, Session), Events2 = hg_proxy_provider:update_proxy_state(ProxyState, Session), Events3 = hg_proxy_provider:handle_interaction_intent({Type, Intent}, Session), - {Events4, Action} = handle_proxy_intent(Intent, hg_machine_action:new(), Session), + {Events4, Action} = handle_proxy_intent(Intent, idle, Session), {lists:flatten([Events1, Events2, Events3, Events4]), Action}. handle_callback_result( @@ -332,7 +332,7 @@ handle_proxy_callback_result( Events1 = hg_proxy_provider:bind_transaction(Trx, Session), Events2 = hg_proxy_provider:update_proxy_state(ProxyState, Session), Events3 = hg_proxy_provider:handle_interaction_intent({Type, Intent}, Session), - {Events4, Action} = handle_proxy_intent(Intent, hg_machine_action:unset_timer(hg_machine_action:new()), Session), + {Events4, Action} = handle_proxy_intent(Intent, suspend, Session), {lists:flatten([Events0, Events1, Events2, Events3, Events4]), Action}; handle_proxy_callback_result( #proxy_provider_PaymentCallbackProxyResult{intent = undefined, trx = Trx, next_state = ProxyState}, @@ -340,7 +340,7 @@ handle_proxy_callback_result( ) -> Events1 = hg_proxy_provider:bind_transaction(Trx, Session), Events2 = hg_proxy_provider:update_proxy_state(ProxyState, Session), - {Events1 ++ Events2, hg_machine_action:new()}. + {Events1 ++ Events2, idle}. apply_result({Events, _Action} = Result, T) -> {Result, update_state_with(Events, T)}. @@ -366,17 +366,17 @@ handle_proxy_intent(#proxy_provider_FinishIntent{status = {success, Success}}, A handle_proxy_intent(#proxy_provider_FinishIntent{status = {failure, Failure}}, Action, _Session) -> Events = [?session_finished(?session_failed({failure, Failure}))], {Events, Action}; -handle_proxy_intent(#proxy_provider_SleepIntent{timer = Timer}, Action0, _Session) -> - Action1 = hg_machine_action:set_timer(Timer, Action0), +handle_proxy_intent(#proxy_provider_SleepIntent{timer = Timer}, _Action0, _Session) -> + Action1 = prg_action:schedule_timer(Timer), {[], Action1}; handle_proxy_intent( #proxy_provider_SuspendIntent{tag = Tag, timeout = Timer, timeout_behaviour = TimeoutBehaviour}, - Action0, + _Action0, Session ) -> #{payment_id := PaymentID, invoice_id := InvoiceID} = tag_context(Session), ok = hg_machine_tag:create_binding(hg_invoice:namespace(), Tag, PaymentID, InvoiceID), - Action1 = hg_machine_action:set_timer(Timer, Action0), + Action1 = prg_action:schedule_timer(Timer), Events = [?session_suspended(Tag, TimeoutBehaviour)], {Events, Action1}. diff --git a/apps/hellgate/test/hg_ct_fixture.erl b/apps/hellgate/test/hg_ct_fixture.erl index 16321519..265d9aac 100644 --- a/apps/hellgate/test/hg_ct_fixture.erl +++ b/apps/hellgate/test/hg_ct_fixture.erl @@ -159,7 +159,7 @@ construct_inspector(Ref, Name, ProxyRef, Additional, FallBackScore) -> -spec construct_provider_account_set([currency()]) -> dmsl_domain_thrift:'ProviderAccountSet'(). construct_provider_account_set(Currencies) -> - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), AccountSet = lists:foldl( fun(Cur = ?cur(Code), Acc) -> Acc#{Cur => ?prvacc(hg_accounting:create_account(Code))} @@ -167,7 +167,7 @@ construct_provider_account_set(Currencies) -> #{}, Currencies ), - _ = hg_context:cleanup(), + _ = op_context:cleanup(hellgate), AccountSet. -spec construct_system_account_set(system_account_set()) -> @@ -178,10 +178,10 @@ construct_system_account_set(Ref) -> -spec construct_system_account_set(system_account_set(), name(), currency()) -> {system_account_set, dmsl_domain_thrift:'SystemAccountSetObject'()}. construct_system_account_set(Ref, Name, ?cur(CurrencyCode)) -> - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), SettlementAccountID = hg_accounting:create_account(CurrencyCode), SubagentAccountID = hg_accounting:create_account(CurrencyCode), - hg_context:cleanup(), + op_context:cleanup(hellgate), {system_account_set, #domain_SystemAccountSetObject{ ref = Ref, data = #domain_SystemAccountSet{ @@ -204,10 +204,10 @@ construct_external_account_set(Ref) -> -spec construct_external_account_set(external_account_set(), name(), currency()) -> {external_account_set, dmsl_domain_thrift:'ExternalAccountSetObject'()}. construct_external_account_set(Ref, Name, ?cur(CurrencyCode)) -> - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), AccountID1 = hg_accounting:create_account(CurrencyCode), AccountID2 = hg_accounting:create_account(CurrencyCode), - hg_context:cleanup(), + op_context:cleanup(hellgate), {external_account_set, #domain_ExternalAccountSetObject{ ref = Ref, data = #domain_ExternalAccountSet{ diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index 0bceec03..ae1f45f7 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -58,6 +58,8 @@ -export([make_trace_id/1]). +-export([cleanup_progressor_namespaces/0]). + -include("hg_ct_domain.hrl"). -include("hg_ct_json.hrl"). @@ -320,42 +322,18 @@ start_app(progressor = AppName) -> {namespaces, #{ invoice => #{ processor => #{ - client => hg_progressor, + client => prg_machine, options => #{ - party_client => #{}, - ns => <<"invoice">>, - handler => hg_machine + ns => invoice } }, worker_pool_size => 150 }, invoice_template => #{ processor => #{ - client => hg_progressor, + client => prg_machine, options => #{ - party_client => #{}, - ns => <<"invoice_template">>, - handler => hg_machine - } - } - }, - customer => #{ - processor => #{ - client => hg_progressor, - options => #{ - party_client => #{}, - ns => <<"customer">>, - handler => hg_machine - } - } - }, - recurrent_paytools => #{ - processor => #{ - client => hg_progressor, - options => #{ - party_client => #{}, - ns => <<"recurrent_paytools">>, - handler => hg_machine + ns => invoice_template } } } @@ -1050,3 +1028,10 @@ make_due_date(LifetimeSeconds) -> make_trace_id(Prefix) -> B = genlib:to_binary(Prefix), iolist_to_binary([binary:part(B, 0, min(byte_size(B), 20)), $., hg_utils:unique_id()]). + +-spec cleanup_progressor_namespaces() -> ok. +cleanup_progressor_namespaces() -> + lists:foreach( + fun(Ns) -> prg_test_utils:cleanup(#{ns => Ns}) end, + [invoice, invoice_template] + ). diff --git a/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl b/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl index 546dba6d..3088b821 100644 --- a/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl +++ b/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl @@ -135,7 +135,7 @@ init_per_suite(C) -> PartyClient = {party_client:create_client(), party_client:create_context()}, _ = hg_ct_helper:create_party(PartyConfigRef, PartyClient), _ = hg_ct_helper:create_party(AnotherPartyConfigRef, PartyClient), - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), Shop1ConfigRef = hg_ct_helper:create_shop( PartyConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), undefined, PartyClient ), @@ -145,7 +145,7 @@ init_per_suite(C) -> AnotherPartyShopConfigRef = hg_ct_helper:create_shop( AnotherPartyConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), undefined, PartyClient ), - ok = hg_context:cleanup(), + ok = op_context:cleanup(hellgate), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), C1 = [ @@ -166,7 +166,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_ct_helper:cleanup_progressor_namespaces(), [application:stop(App) || App <- cfg(apps, C)]. -spec init_per_group(group_name(), config()) -> config(). diff --git a/apps/hellgate/test/hg_dummy_provider.erl b/apps/hellgate/test/hg_dummy_provider.erl index badda0ce..a74a2e2a 100644 --- a/apps/hellgate/test/hg_dummy_provider.erl +++ b/apps/hellgate/test/hg_dummy_provider.erl @@ -204,8 +204,9 @@ process_payment(?processed(), undefined, PaymentInfo, CtxOpts, _) -> empty_cvv -> %% simple workflow without 3DS result(?sleep(0), <<"sleeping">>); - preauth_3ds_offsite -> + Scenario when Scenario =:= preauth_3ds_offsite; Scenario =:= preauth_3ds_offsite_fail -> %% user interaction in sleep intent + reset_offsite_preauth_state(PaymentInfo), Uri = get_callback_url(), UserInteraction = ?redirect( Uri, @@ -279,10 +280,11 @@ process_payment(?processed(), <<"sleeping_with_user_interaction">>, PaymentInfo, Key = {get_invoice_id(PaymentInfo), get_payment_id(PaymentInfo)}, TrxID = hg_utils:construct_complex_id([get_payment_id(PaymentInfo), get_ctx_opts_override(CtxOpts)]), Trx = mk_trx(TrxID, PaymentInfo), + MaxPending = offsite_preauth_max_pending_polls(get_payment_info_scenario(PaymentInfo)), case get_transaction_state(Key) of processed -> finish(success(PaymentInfo), Trx); - {pending, Count} when Count > 2 -> + {pending, Count} when is_integer(MaxPending), Count > MaxPending -> finish(failure(authorization_failed), undefined); {pending, Count} -> set_transaction_state(Key, {pending, Count + 1}), @@ -612,6 +614,8 @@ get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"preauth_3ds: {preauth_3ds, erlang:binary_to_integer(Timeout)}; get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"preauth_3ds_offsite">>}}) -> preauth_3ds_offsite; +get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"preauth_3ds_offsite_fail">>}}) -> + preauth_3ds_offsite_fail; get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"preauth_3ds_sleep:timeout=", Timeout/binary>>}}) -> {preauth_3ds_sleep, erlang:binary_to_integer(Timeout)}; get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"forbidden">>}}) -> @@ -667,6 +671,7 @@ get_payment_tool_scenario({'bank_card', #domain_BankCard{token = Token} = BCard} | {mobile_commerce, failure} | {mobile_commerce, success} | preauth_3ds_offsite + | preauth_3ds_offsite_fail | change_cash_increase | change_cash_decrease | change_currency_and_increase @@ -694,6 +699,7 @@ make_payment_tool(Code, PSys) when Code =:= no_preauth_timeout_failure orelse Code =:= no_preauth_suspend_default orelse Code =:= preauth_3ds_offsite orelse + Code =:= preauth_3ds_offsite_fail orelse Code =:= change_cash_increase orelse Code =:= change_cash_decrease orelse Code =:= forbidden orelse @@ -894,6 +900,15 @@ set_transaction_state(Key, Value) -> get_transaction_state(Key) -> hg_kv_store:get(Key). +offsite_preauth_max_pending_polls(preauth_3ds_offsite) -> + unlimited; +offsite_preauth_max_pending_polls(_) -> + 2. + +reset_offsite_preauth_state(PaymentInfo) -> + Key = {get_invoice_id(PaymentInfo), get_payment_id(PaymentInfo)}, + set_transaction_state(Key, undefined). + maybe_sleep(#{<<"sleep_ms">> := TimeMs}) when is_binary(TimeMs) -> timer:sleep(binary_to_integer(TimeMs)); maybe_sleep(_Opts) -> diff --git a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl index c94655af..f2994a61 100644 --- a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl @@ -77,7 +77,7 @@ init_per_suite(C) -> _ = hg_domain:upsert(hg_invoice_dummy_data:construct_domain_fixture()), PartyConfigRef = #domain_PartyConfigRef{id = hg_utils:unique_id()}, PartyClient = {party_client:create_client(), party_client:create_context()}, - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), %% все магазины рублёвые, но каждая категория роутится на терминалы с раными валютами ShopConfigRef = hg_ct_helper:create_party_and_shop( PartyConfigRef, ?cat(2), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient @@ -91,7 +91,7 @@ init_per_suite(C) -> ShopConfigRefCny = hg_ct_helper:create_party_and_shop( PartyConfigRef, ?cat(5), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient ), - ok = hg_context:cleanup(), + ok = op_context:cleanup(hellgate), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), ok = hg_invoice_helper:start_kv_store(SupPid), @@ -114,7 +114,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_ct_helper:cleanup_progressor_namespaces(), _ = [application:stop(App) || App <- cfg(apps, C)], hg_invoice_helper:stop_kv_store(cfg(test_sup, C)), exit(cfg(test_sup, C), shutdown). @@ -131,7 +131,7 @@ end_per_group(_Group, _C) -> init_per_testcase(_, C) -> ApiClient = hg_ct_helper:create_client(hg_ct_helper:cfg(root_url, C)), Client = hg_client_invoicing:start_link(ApiClient), - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), [ {client, Client} | C diff --git a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl index e5fc2586..03cba0ed 100644 --- a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl @@ -16,6 +16,7 @@ -export([payment_ok_test/1]). -export([payment_start_idempotency/1]). -export([payment_success/1]). +-export([payment_success_trace/1]). -export([payment_w_first_blacklisted_success/1]). -export([payment_w_all_blacklisted/1]). -export([register_payment_success/1]). @@ -27,7 +28,6 @@ -export([payment_success_empty_cvv/1]). -export([payment_has_optional_fields/1]). -export([payment_last_trx_correct/1]). --export([payment_success_trace/1]). -type config() :: hg_ct_helper:config(). -type test_case_name() :: hg_ct_helper:test_case_name(). @@ -100,11 +100,11 @@ init_per_suite(C) -> _ = hg_domain:upsert(construct_domain_fixture()), PartyConfigRef = #domain_PartyConfigRef{id = hg_utils:unique_id()}, PartyClient = {party_client:create_client(), party_client:create_context()}, - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), ShopConfigRef = hg_ct_helper:create_party_and_shop( PartyConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient ), - ok = hg_context:cleanup(), + ok = op_context:cleanup(hellgate), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), ok = hg_invoice_helper:start_kv_store(SupPid), @@ -123,7 +123,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_ct_helper:cleanup_progressor_namespaces(), _ = [application:stop(App) || App <- cfg(apps, C)], hg_invoice_helper:stop_kv_store(cfg(test_sup, C)), exit(cfg(test_sup, C), shutdown). @@ -147,7 +147,7 @@ end_per_group(_Group, _C) -> init_per_testcase(_, C) -> ApiClient = hg_ct_helper:create_client(hg_ct_helper:cfg(root_url, C)), Client = hg_client_invoicing:start_link(ApiClient), - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), [ {client, Client} | C @@ -257,152 +257,16 @@ payment_success(C) -> payment_success_trace(C) -> Client = cfg(client, C), InvoiceID = start_invoice(<<"rubberduck">>, make_due_date(10), 42000, C), - Context = #base_Content{ - type = <<"application/x-erlang-binary">>, - data = erlang:term_to_binary({you, 643, "not", [<<"welcome">>, here]}) - }, - PayerSessionInfo = #domain_PayerSessionInfo{ - redirect_url = <<"https://redirectly.io/merchant">> - }, - PaymentParams = (make_payment_params(?pmt_sys(<<"visa-ref">>)))#payproc_InvoicePaymentParams{ - payer_session_info = PayerSessionInfo, - context = Context - }, + PaymentParams = make_payment_params(?pmt_sys(<<"visa-ref">>)), PaymentID = process_payment(InvoiceID, PaymentParams, Client), PaymentID = await_payment_capture(InvoiceID, PaymentID, Client), - - RootUrl = unicode:characters_to_binary(cfg(root_url, C)), - UrlInternal = <>, - UrlJaeger = <>, - {ok, _Status, _Headers, RefInternal} = hackney:get(UrlInternal), - {ok, BodyInternal} = hackney:body(RefInternal), - [ - #{ - <<"args">> := #{ - <<"content_type">> := <<"thrift_call">>, - <<"content">> := #{ - <<"call">> := #{ - <<"function">> := <<"Create">>, - <<"service">> := <<"Invoicing">> - }, - <<"params">> := _ - } - }, - <<"error">> := null, - <<"events">> := [ - #{ - <<"event_id">> := 1, - <<"event_payload">> := _, - <<"event_timestamp">> := _ - } - ], - <<"finished">> := _, - <<"otel_trace_id">> := _, - <<"retry_attempts">> := 0, - <<"retry_interval">> := 0, - <<"running">> := _, - <<"scheduled">> := _, - <<"task_id">> := _, - <<"task_metadata">> := #{<<"range">> := #{}}, - <<"task_status">> := <<"finished">>, - <<"task_type">> := <<"init">> - }, - #{<<"task_type">> := <<"call">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"finished">>}, - #{<<"task_type">> := <<"timeout">>, <<"task_status">> := <<"cancelled">>} - ] = json:decode(BodyInternal), - {ok, _Status2, _Headers2, RefJaeger} = hackney:get(UrlJaeger), - {ok, BodyJaeger} = hackney:body(RefJaeger), - #{ - <<"data">> := [ - #{ - <<"traceId">> := _, - <<"processes">> := #{ - InvoiceID := #{ - <<"service_name">> := <<"hellgate_invoice">>, - <<"tags">> := [] - } - }, - <<"spans">> := [ - #{ - <<"operationName">> := <<"init">>, - <<"process">> := #{ - <<"service_name">> := <<"hellgate_invoice">>, - <<"tags">> := [] - }, - <<"processID">> := InvoiceID, - <<"spanId">> := _, - <<"traceId">> := _, - <<"startTime">> := _, - <<"duration">> := _, - <<"tags">> := [ - #{ - <<"key">> := <<"task.status">>, - <<"type">> := <<"string">>, - <<"value">> := <<"finished">> - }, - #{ - <<"key">> := <<"task.retries">>, - <<"type">> := <<"int64">>, - <<"value">> := 0 - }, - #{ - <<"key">> := <<"task.input">>, - <<"type">> := <<"string">>, - <<"value">> := _NestedJsonArgs - } - ], - <<"logs">> := [ - #{ - <<"timestamp">> := _, - <<"fields">> := [ - #{ - <<"key">> := <<"event.id">>, - <<"type">> := <<"int64">>, - <<"value">> := 1 - }, - #{ - <<"key">> := <<"event.payload">>, - <<"type">> := <<"string">>, - <<"value">> := _NestedJsonEvent - } - ] - } - ] - }, - #{<<"operationName">> := <<"call">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>}, - #{<<"operationName">> := <<"timeout">>} - ] - } - ] - } = json:decode(BodyJaeger), - BadInvoiceUrl = <>, - {ok, 404, _, _} = hackney:get(BadInvoiceUrl), - BadFormatUrl = <>, - {ok, 400, _, _} = hackney:get(BadFormatUrl), + {ok, Trace} = progressor:trace(#{ns => invoice, id => InvoiceID}), + TaskTypes = [maps:get(task_type, Unit) || Unit <- Trace], + ?assert(lists:member(<<"init">>, TaskTypes)), + ?assert(lists:member(<<"call">>, TaskTypes)), + ?assert(lists:member(<<"timeout">>, TaskTypes)), + [#{task_type := <<"init">>, task_status := <<"finished">>, events := [_ | _]} | _] = + [Unit || Unit <- Trace, maps:get(task_type, Unit) =:= <<"init">>], ok. -spec payment_w_first_blacklisted_success(config()) -> test_return(). diff --git a/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl index 31e0347d..5392d799 100644 --- a/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl @@ -95,10 +95,10 @@ init_per_suite(C) -> RootUrl = maps:get(hellgate_root_url, Ret), PartyConfigRef = #domain_PartyConfigRef{id = hg_utils:unique_id()}, Client = {party_client:create_client(), party_client:create_context()}, - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), ShopConfigRef = hg_ct_helper:create_party_and_shop(PartyConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), Client), - ok = hg_context:cleanup(), + ok = op_context:cleanup(hellgate), [ {party_config_ref, PartyConfigRef}, {party_client, Client}, @@ -112,7 +112,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_ct_helper:cleanup_progressor_namespaces(), [application:stop(App) || App <- cfg(apps, C)]. %% tests diff --git a/apps/hellgate/test/hg_invoice_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_tests_SUITE.erl index f7699961..21856b5e 100644 --- a/apps/hellgate/test/hg_invoice_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_tests_SUITE.erl @@ -541,14 +541,14 @@ init_per_suite(C) -> _BaseRevision = hg_domain:upsert(construct_domain_fixture(BaseLimitsRevision)), - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), ShopConfigRef = hg_ct_helper:create_party_and_shop( PartyConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient ), Shop2ConfigRef = hg_ct_helper:create_party_and_shop( Party2ConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient2 ), - ok = hg_context:cleanup(), + ok = op_context:cleanup(hellgate), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), @@ -575,7 +575,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_ct_helper:cleanup_progressor_namespaces(), _ = [application:stop(App) || App <- cfg(apps, C)], _ = hg_invoice_helper:stop_kv_store(cfg(test_sup, C)), exit(cfg(test_sup, C), shutdown). @@ -764,7 +764,7 @@ init_per_testcase_(Name, C) -> ApiClient = hg_ct_helper:create_client(cfg(root_url, C)), Client = hg_client_invoicing:start_link(ApiClient), ClientTpl = hg_client_invoice_templating:start_link(ApiClient), - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), [{client, Client}, {client_tpl, ClientTpl} | trace_testcase(Name, C)]. trace_testcase(Name, C) -> @@ -783,7 +783,7 @@ end_per_testcase(repair_fail_cash_flow_building_succeeded, C) -> end_per_testcase(default, C); end_per_testcase(_Name, C) -> ok = maybe_end_trace(C), - ok = hg_context:cleanup(), + ok = op_context:cleanup(hellgate), _ = case cfg(original_domain_revision, C) of Revision when is_integer(Revision) -> @@ -5625,7 +5625,7 @@ payment_with_offsite_preauth_success(C) -> payment_with_offsite_preauth_failed(C) -> Client = cfg(client, C), InvoiceID = start_invoice(<<"rubberduck">>, make_due_date(3), 42000, C), - {PaymentTool, Session} = hg_dummy_provider:make_payment_tool(preauth_3ds_offsite, ?pmt_sys(<<"jcb-ref">>)), + {PaymentTool, Session} = hg_dummy_provider:make_payment_tool(preauth_3ds_offsite_fail, ?pmt_sys(<<"jcb-ref">>)), PaymentParams = make_payment_params(PaymentTool, Session, instant), PaymentID = start_payment(InvoiceID, PaymentParams, Client), _UserInteraction = await_payment_process_interaction(InvoiceID, PaymentID, Client), @@ -6161,7 +6161,7 @@ init_route_cascading_group(C1) -> PartyConfigRef = cfg(party_config_ref, C1), PartyClient = cfg(party_client, C1), Revision = hg_domain:head(), - ok = hg_context:save(hg_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), _ = hg_domain:upsert(cascade_fixture_pre_shop_create(Revision, C1)), C2 = [ { @@ -6256,7 +6256,7 @@ init_route_cascading_group(C1) -> } | C1 ], - ok = hg_context:cleanup(), + ok = op_context:cleanup(hellgate), _ = hg_domain:upsert(cascade_fixture(Revision, C2)), [{base_limits_domain_revision, Revision} | C2]. diff --git a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl index 35c11b10..db7b7a94 100644 --- a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl +++ b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl @@ -119,7 +119,7 @@ init_per_suite(C) -> end_per_suite(C) -> SupPid = cfg(suite_test_sup, C), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_ct_helper:cleanup_progressor_namespaces(), hg_mock_helper:stop_sup(SupPid). -spec init_per_group(group_name(), config()) -> config(). @@ -132,13 +132,13 @@ end_per_group(_GroupName, _C) -> -spec init_per_testcase(test_case_name(), config()) -> config(). init_per_testcase(_, C) -> - Ctx = hg_context:set_party_client(cfg(party_client, C), hg_context:create()), - ok = hg_context:save(Ctx), + Ctx = op_context:set_party_client(cfg(party_client, C), op_context:create()), + ok = op_context:save(op_context:key(hellgate), Ctx), C. -spec end_per_testcase(test_case_name(), config()) -> ok. end_per_testcase(_Name, _C) -> - ok = hg_context:cleanup(), + ok = op_context:cleanup(hellgate), ok. cfg(Key, C) -> diff --git a/apps/hg_progressor/rebar.config b/apps/hg_progressor/rebar.config deleted file mode 100644 index f618f3e4..00000000 --- a/apps/hg_progressor/rebar.config +++ /dev/null @@ -1,2 +0,0 @@ -{erl_opts, [debug_info]}. -{deps, []}. \ No newline at end of file diff --git a/apps/hg_progressor/src/hg_hybrid.erl b/apps/hg_progressor/src/hg_hybrid.erl deleted file mode 100644 index 9bd64efc..00000000 --- a/apps/hg_progressor/src/hg_hybrid.erl +++ /dev/null @@ -1,215 +0,0 @@ --module(hg_hybrid). - --include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). - --export([call_automaton/2]). - --spec call_automaton(woody:func(), woody:args()) -> term(). -call_automaton('Start' = Func, {NS, ID, _} = Args) -> - MachineDesc = prepare_descriptor(NS, ID), - case hg_machine:call_automaton('GetMachine', {MachineDesc}, machinegun) of - {ok, Machine} -> - ok = migrate(unmarshal(machine, Machine), unmarshal(descriptor, MachineDesc)), - {error, exists}; - {error, notfound} -> - hg_progressor:call_automaton(Func, Args) - end; -call_automaton(Func, Args) -> - MachineDesc = extract_descriptor(Args), - case hg_progressor:call_automaton(Func, Args) of - {error, notfound} -> - maybe_retry_call_backend(maybe_migrate_machine(MachineDesc), Func, Args); - Result -> - Result - end. - -%% Internal functions - -maybe_migrate_machine(MachineDesc) -> - case hg_machine:call_automaton('GetMachine', {MachineDesc}, machinegun) of - {error, notfound} = Error -> - Error; - {ok, Machine} -> - migrate(unmarshal(machine, Machine), unmarshal(descriptor, MachineDesc)) - end. - -maybe_retry_call_backend(ok, Func, Args) -> - hg_progressor:call_automaton(Func, Args); -maybe_retry_call_backend({error, _Reason} = Error, _Func, _Args) -> - erlang:error(Error). - -migrate(MigrateArgs, Req0) -> - Req = Req0#{args => MigrateArgs}, - case progressor:put(Req) of - {ok, _} -> - ok; - {error, <<"process already exists">>} -> - ok; - {error, Reason} -> - {error, {migration_failed, Reason}} - end. - -unmarshal(machine, #mg_stateproc_Machine{ - ns = NS, - id = ID, - history = Events, - status = Status, - aux_state = AuxState, - timer = Timestamp -}) -> - Process = genlib_map:compact(#{ - namespace => unmarshal(atom, NS), - process_id => unmarshal(string, ID), - history => maybe_unmarshal({list, {event, ID}}, Events), - status => unmarshal(status, Status), - aux_state => maybe_unmarshal(term, AuxState) - }), - Action = maybe_unmarshal(action, Timestamp), - #{ - process => Process, - action => Action - }; -unmarshal({event, ProcessID}, #mg_stateproc_Event{ - id = EventID, - created_at = CreatedAt, - format_version = Ver, - data = Payload -}) -> - genlib_map:compact(#{ - process_id => ProcessID, - event_id => EventID, - timestamp => unmarshal(timestamp_sec, CreatedAt), - metadata => unmarshal(metadata, [{<<"format_version">>, Ver}]), - payload => maybe_unmarshal(term, Payload) - }); -unmarshal(action, Timestamp) -> - #{set_timer => unmarshal(timestamp_sec, Timestamp)}; -unmarshal(metadata, List) -> - lists:foldl( - fun - ({_K, undefined}, Acc) -> Acc; - ({K, V}, Acc) -> Acc#{K => V} - end, - #{}, - List - ); -unmarshal(status, {failed, _}) -> - <<"error">>; -unmarshal(status, _) -> - <<"running">>; -unmarshal(timestamp_sec, TimestampBin) when is_binary(TimestampBin) -> - genlib_rfc3339:parse(TimestampBin, second); -unmarshal({list, T}, List) -> - lists:map(fun(V) -> unmarshal(T, V) end, List); -unmarshal(string, V) when is_binary(V) -> - V; -unmarshal(atom, V) when is_binary(V) -> - erlang:binary_to_atom(V, utf8); -unmarshal(descriptor, #mg_stateproc_MachineDescriptor{ns = NS, ref = {id, ID}}) -> - #{ - ns => unmarshal(atom, NS), - id => unmarshal(string, ID) - }; -unmarshal(term, V) -> - term_to_binary(V). - -maybe_unmarshal(_, undefined) -> - undefined; -maybe_unmarshal(T, V) -> - unmarshal(T, V). - -prepare_descriptor(NS, ID) -> - prepare_descriptor(NS, ID, #mg_stateproc_HistoryRange{ - direction = forward - }). - -prepare_descriptor(NS, ID, Range) -> - #mg_stateproc_MachineDescriptor{ - ns = NS, - ref = {id, ID}, - range = Range - }. - -extract_descriptor({MachineDescriptor}) -> - MachineDescriptor; -extract_descriptor({MachineDescriptor, _}) -> - MachineDescriptor. - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). - --define(TEST_MACHINE, #mg_stateproc_Machine{ - ns = <<"invoice">>, - id = <<"24Dbt7gfCnw">>, - status = {working, {mg_stateproc_MachineStatusWorking}}, - aux_state = #mg_stateproc_Content{ - format_version = undefined, - data = {bin, <<>>} - }, - timer = <<"2025-02-10T16:07:21Z">>, - history_range = #mg_stateproc_HistoryRange{}, - history = [ - #mg_stateproc_Event{ - id = 1, - created_at = <<"2025-02-10T16:07:21Z">>, - format_version = 1, - data = {bin, <<>>} - }, - #mg_stateproc_Event{ - id = 2, - created_at = <<"2025-02-10T16:07:21Z">>, - format_version = 1, - data = {bin, <<>>} - }, - #mg_stateproc_Event{ - id = 3, - created_at = <<"2025-02-10T16:07:21Z">>, - format_version = 1, - data = {bin, <<>>} - } - ] -}). - --spec test() -> _. - --spec unmarshal_test() -> _. -unmarshal_test() -> - Unmarshalled = unmarshal(machine, ?TEST_MACHINE), - Expected = #{ - process => #{ - process_id => <<"24Dbt7gfCnw">>, - status => <<"running">>, - history => [ - #{ - timestamp => 1739203641, - metadata => #{<<"format_version">> => 1}, - process_id => <<"24Dbt7gfCnw">>, - event_id => 1, - payload => <<131, 104, 2, 119, 3, 98, 105, 110, 109, 0, 0, 0, 0>> - }, - #{ - timestamp => 1739203641, - metadata => #{<<"format_version">> => 1}, - process_id => <<"24Dbt7gfCnw">>, - event_id => 2, - payload => <<131, 104, 2, 119, 3, 98, 105, 110, 109, 0, 0, 0, 0>> - }, - #{ - timestamp => 1739203641, - metadata => #{<<"format_version">> => 1}, - process_id => <<"24Dbt7gfCnw">>, - event_id => 3, - payload => <<131, 104, 2, 119, 3, 98, 105, 110, 109, 0, 0, 0, 0>> - } - ], - namespace => invoice, - aux_state => - <<131, 104, 3, 119, 20, 109, 103, 95, 115, 116, 97, 116, 101, 112, 114, 111, 99, 95, 67, 111, 110, 116, - 101, 110, 116, 119, 9, 117, 110, 100, 101, 102, 105, 110, 101, 100, 104, 2, 119, 3, 98, 105, 110, - 109, 0, 0, 0, 0>> - }, - action => #{set_timer => 1739203641} - }, - ?assertEqual(Expected, Unmarshalled). - --endif. diff --git a/apps/hg_progressor/src/hg_progressor.app.src b/apps/hg_progressor/src/hg_progressor.app.src deleted file mode 100644 index feb05522..00000000 --- a/apps/hg_progressor/src/hg_progressor.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, hg_progressor, [ - {description, "An OTP library"}, - {vsn, "0.1.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {env, []}, - {modules, []}, - - {licenses, ["Apache-2.0"]}, - {links, []} -]}. diff --git a/apps/hg_progressor/src/hg_progressor.erl b/apps/hg_progressor/src/hg_progressor.erl deleted file mode 100644 index 024b836a..00000000 --- a/apps/hg_progressor/src/hg_progressor.erl +++ /dev/null @@ -1,381 +0,0 @@ --module(hg_progressor). - --include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). --include_lib("progressor/include/progressor.hrl"). - -%% automaton call wrapper --export([call_automaton/2]). - -%% processor call wrapper --export([process/3]). - -%-ifdef(TEST). --export([cleanup/0]). -%-endif. - --type encoded_args() :: binary(). --type encoded_ctx() :: binary(). - --define(EMPTY_CONTENT, #mg_stateproc_Content{data = {bin, <<>>}}). - --spec call_automaton(woody:func(), woody:args()) -> term(). -call_automaton('Start', {NS, ID, Args}) -> - Req = #{ - ns => erlang:binary_to_atom(NS), - id => ID, - args => maybe_unmarshal(term, Args), - context => get_context() - }, - case progressor:init(Req) of - {ok, ok} = Result -> - Result; - {error, <<"process already exists">>} -> - {error, exists}; - {error, {exception, _, _} = Exception} -> - handle_exception(Exception) - end; -call_automaton('Call', {MachineDesc, Args}) -> - #mg_stateproc_MachineDescriptor{ - ns = NS, - ref = {id, ID}, - range = HistoryRange - } = MachineDesc, - Req = #{ - ns => erlang:binary_to_atom(NS), - id => ID, - args => maybe_unmarshal(term, Args), - context => get_context(), - range => unmarshal(history_range, HistoryRange) - }, - case progressor:call(Req) of - {ok, _Response} = Ok -> - Ok; - {error, <<"process not found">>} -> - {error, notfound}; - {error, <<"process is init">>} -> - {error, notfound}; - {error, <<"process is error">>} -> - {error, failed}; - {error, {exception, _, _} = Exception} -> - handle_exception(Exception) - end; -call_automaton('GetMachine', {MachineDesc}) -> - #mg_stateproc_MachineDescriptor{ - ns = NS, - ref = {id, ID}, - range = HistoryRange - } = MachineDesc, - Req = #{ - ns => erlang:binary_to_atom(NS), - id => ID, - range => unmarshal(history_range, HistoryRange) - }, - case progressor:get(Req) of - {ok, Process} -> - Machine = marshal(process, Process#{ns => NS}), - {ok, Machine}; - {error, <<"process not found">>} -> - {error, notfound}; - {error, {exception, _, _} = Exception} -> - handle_exception(Exception) - end; -call_automaton('Repair', {MachineDesc, Args}) -> - #mg_stateproc_MachineDescriptor{ - ns = NS, - ref = {id, ID} - } = MachineDesc, - Req = #{ - ns => erlang:binary_to_atom(NS), - id => ID, - args => maybe_unmarshal(term, Args), - context => get_context() - }, - case progressor:repair(Req) of - {ok, _Response} = Ok -> - Ok; - {error, <<"process not found">>} -> - {error, notfound}; - {error, <<"process is init">>} -> - {error, notfound}; - {error, <<"process is running">>} -> - {error, working}; - {error, <<"process is error">>} -> - {error, failed}; - {error, {exception, _, _} = Exception} -> - handle_exception(Exception) - end. - -%-ifdef(TEST). - --spec cleanup() -> _. -cleanup() -> - Namespaces = [ - invoice, - invoice_template, - customer, - recurrent_paytools - ], - lists:foreach(fun(NsID) -> prg_test_utils:cleanup(#{ns => NsID}) end, Namespaces). - -%-endif. - -%% Processor - --spec process({task_t(), encoded_args(), process()}, hg_woody_service_wrapper:handler_opts(), encoded_ctx()) -> - process_result(). -process({CallType, BinArgs, Process}, #{ns := NS} = Options, BinCtx) -> - {WoodyContext0, OtelCtx} = decode_rpc_context(BinCtx), - ok = woody_rpc_helper:attach_otel_context(OtelCtx), - #{last_event_id := LastEventID} = Process, - Machine = marshal(process, Process#{ns => NS}), - Func = marshal(function, CallType), - Args = marshal(args, {CallType, BinArgs, Machine}), - WoodyContext = hg_woody_service_wrapper:ensure_woody_deadline_set(WoodyContext0, Options), - ok = hg_context:save(hg_woody_service_wrapper:create_context(WoodyContext, Options)), - try - handle_result(hg_machine:handle_function(Func, {Args}, Options), LastEventID) - after - hg_context:cleanup() - end. - -%% Internal functions - -decode_rpc_context(<<>>) -> - woody_rpc_helper:decode_rpc_context(#{}); -decode_rpc_context(BinCtx) -> - woody_rpc_helper:decode_rpc_context(marshal(term, BinCtx)). - -handle_result( - #mg_stateproc_SignalResult{ - change = #'mg_stateproc_MachineStateChange'{ - aux_state = AuxState, - events = Events - }, - action = Action - }, - LastEventID -) -> - {ok, - genlib_map:compact(#{ - events => unmarshal(events, {Events, LastEventID}), - aux_state => maybe_unmarshal(term, AuxState), - action => maybe_unmarshal(action, Action) - })}; -handle_result( - #mg_stateproc_CallResult{ - response = Response, - change = #'mg_stateproc_MachineStateChange'{ - aux_state = AuxState, - events = Events - }, - action = Action - }, - LastEventID -) -> - {ok, - genlib_map:compact(#{ - response => Response, - events => unmarshal(events, {Events, LastEventID}), - aux_state => maybe_unmarshal(term, AuxState), - action => maybe_unmarshal(action, Action) - })}; -handle_result( - #mg_stateproc_RepairResult{ - response = Response, - change = #'mg_stateproc_MachineStateChange'{ - aux_state = AuxState, - events = Events - }, - action = Action - }, - LastEventID -) -> - {ok, - genlib_map:compact(#{ - response => Response, - events => unmarshal(events, {Events, LastEventID}), - aux_state => maybe_unmarshal(term, AuxState), - action => maybe_unmarshal(action, Action) - })}; -handle_result(_Unexpected, _LastEventID) -> - {error, <<"unexpected result">>}. - --spec handle_exception(_) -> no_return(). -handle_exception({exception, Class, Reason}) -> - erlang:raise(Class, Reason, []). - -get_context() -> - WoodyContext = - try hg_context:load() of - Ctx -> - hg_context:get_woody_context(Ctx) - catch - Class:Reason -> - _ = logger:warning("Failed to load context with error class '~s' and reason: ~p", [Class, Reason]), - _ = logger:info("Creating empty fallback context"), - woody_context:new() - end, - unmarshal(term, woody_rpc_helper:encode_rpc_context(WoodyContext, otel_ctx:get_current())). - -%% Marshalling - -maybe_marshal(_, undefined) -> - undefined; -maybe_marshal(Type, Value) -> - marshal(Type, Value). - -marshal( - process, - #{ - ns := NS, - process_id := ID, - status := Status, - history := History - } = Process -) -> - Range = maps:get(range, Process, #{}), - AuxState = maps:get(aux_state, Process, term_to_binary(?EMPTY_CONTENT)), - Detail = maps:get(detail, Process, undefined), - MarshalledEvents = lists:map(fun(Ev) -> marshal(event, Ev) end, History), - #mg_stateproc_Machine{ - ns = NS, - id = ID, - history = MarshalledEvents, - history_range = marshal(history_range, Range), - status = marshal(status, {Status, Detail}), - aux_state = maybe_marshal(term, AuxState) - }; -marshal( - event, - #{ - event_id := EventID, - timestamp := Timestamp, - payload := Payload - } = Event -) -> - Meta = maps:get(metadata, Event, #{}), - #mg_stateproc_Event{ - id = EventID, - created_at = marshal(timestamp, Timestamp), - format_version = format_version(Meta), - data = marshal(term, Payload) - }; -marshal(history_range, Range) -> - #mg_stateproc_HistoryRange{ - 'after' = maps:get(offset, Range, undefined), - limit = maps:get(limit, Range, undefined), - direction = maps:get(direction, Range, forward) - }; -marshal(status, {<<"init">>, _Detail}) -> - {'working', #mg_stateproc_MachineStatusWorking{}}; -marshal(status, {<<"running">>, _Detail}) -> - {'working', #mg_stateproc_MachineStatusWorking{}}; -marshal(status, {<<"error">>, Detail}) -> - {'failed', #mg_stateproc_MachineStatusFailed{reason = Detail}}; -marshal(timestamp, Timestamp) -> - unicode:characters_to_binary(calendar:system_time_to_rfc3339(Timestamp, [{offset, "Z"}, {unit, microsecond}])); -marshal(term, Term) -> - binary_to_term(Term); -marshal(function, init) -> - 'ProcessSignal'; -marshal(function, call) -> - 'ProcessCall'; -marshal(function, repair) -> - 'ProcessRepair'; -marshal(function, timeout) -> - 'ProcessSignal'; -marshal(args, {init, BinArgs, Machine}) -> - #mg_stateproc_SignalArgs{ - signal = {init, #mg_stateproc_InitSignal{arg = maybe_marshal(term, BinArgs)}}, - machine = Machine - }; -marshal(args, {call, BinArgs, Machine}) -> - #mg_stateproc_CallArgs{ - arg = maybe_marshal(term, BinArgs), - machine = Machine - }; -marshal(args, {repair, BinArgs, Machine}) -> - #mg_stateproc_RepairArgs{ - arg = maybe_marshal(term, BinArgs), - machine = Machine - }; -marshal(args, {timeout, _BinArgs, Machine}) -> - #mg_stateproc_SignalArgs{ - signal = {timeout, #mg_stateproc_TimeoutSignal{}}, - machine = Machine - }. - -maybe_unmarshal(_, undefined) -> - undefined; -maybe_unmarshal(Type, Value) -> - unmarshal(Type, Value). - -unmarshal(events, {undefined, _ID}) -> - []; -unmarshal(events, {[], _}) -> - []; -unmarshal(events, {Events, LastEventID}) -> - Ts = erlang:system_time(microsecond), - lists:foldl( - fun(#mg_stateproc_Content{format_version = Ver, data = Payload}, Acc) -> - PrevID = - case Acc of - [] -> LastEventID; - [#{event_id := ID} | _] -> ID - end, - [ - genlib_map:compact(#{ - event_id => PrevID + 1, - timestamp => Ts, - metadata => #{<<"format_version">> => Ver}, - payload => unmarshal(term, Payload) - }) - | Acc - ] - end, - [], - Events - ); -unmarshal(action, #mg_stateproc_ComplexAction{ - timer = {set_timer, #mg_stateproc_SetTimerAction{timer = Timer}}, - remove = RemoveAction -}) when Timer =/= undefined -> - genlib_map:compact(#{ - set_timer => unmarshal(timer, Timer), - remove => maybe_unmarshal(remove_action, RemoveAction) - }); -unmarshal(action, #mg_stateproc_ComplexAction{ - timer = {set_timer, #mg_stateproc_SetTimerAction{timeout = Timeout}}, - remove = RemoveAction -}) when Timeout =/= undefined -> - genlib_map:compact(#{ - set_timer => erlang:system_time(microsecond) + (Timeout * 1000000), - remove => maybe_unmarshal(remove_action, RemoveAction) - }); -unmarshal(action, #mg_stateproc_ComplexAction{timer = {unset_timer, #'mg_stateproc_UnsetTimerAction'{}}}) -> - unset_timer; -unmarshal(action, #mg_stateproc_ComplexAction{remove = #mg_stateproc_RemoveAction{}}) -> - #{remove => true}; -unmarshal(action, #mg_stateproc_ComplexAction{remove = undefined}) -> - undefined; -unmarshal(timer, {deadline, DateTimeRFC3339}) -> - calendar:rfc3339_to_system_time(unicode:characters_to_list(DateTimeRFC3339), [{unit, microsecond}]); -unmarshal(timer, {timeout, Timeout}) -> - erlang:system_time(microsecond) + (Timeout * 1000000); -unmarshal(term, Term) -> - erlang:term_to_binary(Term); -unmarshal(remove_action, #mg_stateproc_RemoveAction{}) -> - true; -unmarshal(history_range, undefined) -> - #{}; -unmarshal(history_range, #mg_stateproc_HistoryRange{'after' = Offset, limit = Limit, direction = Direction}) -> - genlib_map:compact(#{ - offset => Offset, - limit => Limit, - direction => Direction - }). - -format_version(#{<<"format_version">> := Version}) -> - Version; -format_version(_) -> - undefined. diff --git a/apps/hg_progressor/src/hg_progressor_handler.erl b/apps/hg_progressor/src/hg_progressor_handler.erl deleted file mode 100644 index 8c3b6095..00000000 --- a/apps/hg_progressor/src/hg_progressor_handler.erl +++ /dev/null @@ -1,384 +0,0 @@ --module(hg_progressor_handler). - --export([init/2, terminate/3]). --export([get_routes/0]). - --spec get_routes() -> _. -get_routes() -> - [ - {"/traces/[:format]/invoice/[:process_id]", ?MODULE, #{namespace => invoice}}, - {"/traces/[:format]/invoice_template/[:process_id]", ?MODULE, #{namespace => invoice_template}} - ]. - --spec init(cowboy_req:req(), cowboy_http:opts()) -> - {ok, cowboy_req:req(), undefined}. -init(Request, Opts) -> - Method = cowboy_req:method(Request), - NS = maps:get(namespace, Opts), - Format = cowboy_req:binding(format, Request), - ProcessID = cowboy_req:binding(process_id, Request), - maybe - {format_is_valid, true} ?= {format_is_valid, Format =:= <<"internal">> orelse Format =:= <<"jaeger">>}, - {process_id_is_valid, true} ?= {process_id_is_valid, is_binary(ProcessID)}, - Req = handle(Method, NS, ProcessID, Format, Request), - {ok, Req, undefined} - else - {format_is_valid, false} -> - Req1 = cowboy_req:reply(400, #{}, <<"Invalid Format">>, Request), - {ok, Req1, undefined}; - {process_id_is_valid, false} -> - Req2 = cowboy_req:reply(400, #{}, <<"Invalid ProcessID">>, Request), - {ok, Req2, undefined} - end. - --spec terminate(term(), cowboy_req:req(), undefined) -> - ok. -terminate(_Reason, _Req, _State) -> - ok. - --spec handle(_, _, _, _, _) -> _. -handle(<<"GET">>, NS, ProcessID, Format, Request) -> - case progressor:trace(#{ns => NS, id => ProcessID}) of - {ok, RawTrace} -> - Trace = unmarshal_trace(NS, ProcessID, RawTrace, Format), - Body = unicode:characters_to_binary(json:encode(Trace)), - cowboy_req:reply(200, #{}, Body, Request); - {error, <<"process not found">>} = _Error -> - cowboy_req:reply(404, #{}, <<"Unknown process">>, Request) - end; -handle(_, _NS, _ProcessID, _Format, Request) -> - cowboy_req:reply(405, #{}, <<"Method Not Allowed">>, Request). - -unmarshal_trace(NS, ProcessID, RawTrace, <<"internal">> = Format) -> - lists:map(fun(RawTraceUnit) -> unmarshal_trace_unit(NS, ProcessID, RawTraceUnit, Format) end, RawTrace); -unmarshal_trace(NS, ProcessID, RawTrace, <<"jaeger">> = Format) -> - Spans = lists:map(fun(RawTraceUnit) -> unmarshal_trace_unit(NS, ProcessID, RawTraceUnit, Format) end, RawTrace), - #{ - data => [ - #{ - traceId => trace_id(NS, ProcessID), - spans => Spans, - processes => #{ - ProcessID => #{ - service_name => service_name(NS), - tags => [] - } - } - } - ] - }. - -unmarshal_trace_unit(NS, _ProcessID, #{task_type := TaskType} = TraceUnit, <<"internal">> = Format) -> - BinArgs = maps:get(args, TraceUnit, <<>>), - BinEvents = maps:get(events, TraceUnit, []), - OtelTraceID = extract_trace_id(TraceUnit), - Error = extract_error(TraceUnit), - (maps:without([response, context], TraceUnit))#{ - args => unmarshal_args(NS, TaskType, BinArgs), - events => unmarshal_events(BinEvents, Format), - otel_trace_id => OtelTraceID, - error => Error - }; -unmarshal_trace_unit(NS, ProcessID, #{task_type := TaskType, task_id := TaskID} = TraceUnit, <<"jaeger">> = Format) -> - BinArgs = maps:get(args, TraceUnit, <<>>), - BinEvents = maps:get(events, TraceUnit, []), - #{ - processID => ProcessID, - process => #{ - service_name => service_name(NS), - tags => [] - }, - warnings => [], - traceId => trace_id(NS, ProcessID), - spanId => integer_to_binary(TaskID), - operationName => TaskType, - startTime => start_time(TraceUnit), - duration => duration(TraceUnit), - tags => [ - #{ - key => <<"task.status">>, - type => <<"string">>, - value => maps:get(task_status, TraceUnit) - }, - #{ - key => <<"task.retries">>, - type => <<"int64">>, - value => maps:get(retry_attempts, TraceUnit) - }, - #{ - key => <<"task.input">>, - type => <<"string">>, - value => unicode:characters_to_binary(json:encode(unmarshal_args(NS, TaskType, BinArgs))) - } - ] ++ error_tag(TraceUnit), - logs => unmarshal_events(BinEvents, Format) - }. - -unmarshal_args(_, _, <<>>) -> - <<>>; -unmarshal_args(invoice, <<"init">> = _TaskType, BinArgs) -> - {bin, B} = binary_to_term(BinArgs), - UnwrappedArgs = binary_to_term(B), - Type = {struct, struct, {dmsl_domain_thrift, 'Invoice'}}, - Args = hg_invoice:unmarshal_invoice(UnwrappedArgs), - #{ - content_type => <<"thrift_call">>, - content => #{ - call => #{service => 'Invoicing', function => 'Create'}, - params => to_maps(term_to_object(Args, Type)) - } - }; -unmarshal_args(invoice_template, <<"init">> = _TaskType, BinArgs) -> - {bin, B} = binary_to_term(BinArgs), - UnwrappedArgs = binary_to_term(B), - Type = {struct, struct, {dmsl_payproc_thrift, 'InvoiceTemplateCreateParams'}}, - Args = hg_invoice_template:unmarshal_invoice_template_params(UnwrappedArgs), - #{ - content_type => <<"thrift_call">>, - content => #{ - call => #{service => 'InvoiceTemplating', function => 'Create'}, - params => to_maps(term_to_object(Args, Type)) - } - }; -unmarshal_args(_, TaskType, BinArgs) when - TaskType =:= <<"call">>; - TaskType =:= <<"repair">>; - TaskType =:= <<"timeout">> --> - {bin, B} = binary_to_term(BinArgs), - case binary_to_term(B) of - {schemaless_call, Args} -> - maybe_format(Args); - {thrift_call, ServiceName, FunctionRef, EncodedArgs} -> - {Service, Function} = FunctionRef, - {Module, Service} = hg_proto:get_service(ServiceName), - Type = Module:function_info(Service, Function, params_type), - Args = hg_proto_utils:deserialize(Type, EncodedArgs), - #{ - content_type => <<"thrift_call">>, - content => #{ - call => #{service => Service, function => Function}, - params => to_maps(term_to_object(Args, Type)) - } - }; - Args -> - maybe_format(Args) - end. - -unmarshal_events(BinEvents, Format) -> - lists:map( - fun(#{event_payload := BinPayload} = Event) -> - {bin, BinChanges} = binary_to_term(BinPayload), - Type = {struct, union, {dmsl_payproc_thrift, 'EventPayload'}}, - Changes = hg_proto_utils:deserialize(Type, BinChanges), - Payload = to_maps(term_to_object(Changes, Type)), - unmarshal_event(Event, Payload, Format) - end, - BinEvents - ). - -unmarshal_event(#{event_timestamp := Ts} = Event, Payload, <<"internal">>) -> - Event#{event_payload => Payload, event_timestamp => prg_utils:to_microseconds(Ts)}; -unmarshal_event(#{event_id := EventID, event_timestamp := Ts}, Payload, <<"jaeger">>) -> - #{ - timestamp => prg_utils:to_microseconds(Ts), - fields => [ - #{ - key => <<"event.id">>, - type => <<"int64">>, - value => EventID - }, - #{ - key => <<"event.payload">>, - type => <<"string">>, - value => unicode:characters_to_binary(json:encode(Payload)) - } - ] - }. - -maybe_format(Data) when is_binary(Data) -> - case is_printable_string(Data) of - true -> - Data; - false -> - to_maps(term_to_object_content(Data)) - end; -maybe_format(Data) -> - #{ - content_type => <<"unknown">>, - content => format(Data) - }. - -format(Data) -> - unicode:characters_to_binary(io_lib:format("~p", [Data])). - -extract_trace_id(#{context := <<>>}) -> - null; -extract_trace_id(#{context := BinContext}) -> - try binary_to_term(BinContext) of - #{<<"otel">> := [TraceID | _]} -> - TraceID; - _ -> - null - catch - _:_ -> - null - end. - -extract_error(#{task_status := <<"error">>, response := {error, ReasonTerm}}) -> - #{content := Content} = maybe_format(ReasonTerm), - Content; -extract_error(_) -> - null. - -service_name(invoice) -> - <<"hellgate_invoice">>; -service_name(invoice_template) -> - <<"hellgate_invoice_template">>. - -trace_id(NS, ProcessID) -> - NsBin = erlang:atom_to_binary(NS), - HexList = [io_lib:format("~2.16.0b", [B]) || <> <= <>], - Hex = lists:flatten(HexList), - case length(Hex) of - Len when Len < 32 -> unicode:characters_to_binary(lists:duplicate(32 - Len, $0) ++ Hex); - Len when Len > 32 -> unicode:characters_to_binary(string:slice(Hex, 0, 32)); - _ -> unicode:characters_to_binary(Hex) - end. - -start_time(#{running := Ts}) -> - Ts; -start_time(_) -> - null. - -duration(#{running := Running, finished := Finished}) -> - Finished - Running; -duration(_) -> - null. - -error_tag(#{task_status := <<"error">>, response := {error, ReasonTerm}}) -> - #{content := Content} = maybe_format(ReasonTerm), - [ - #{ - key => <<"task.error">>, - type => <<"string">>, - value => Content - } - ]; -error_tag(_) -> - []. - --define(is_integer(T), (T == byte orelse T == i8 orelse T == i16 orelse T == i32 orelse T == i64)). --define(is_number(T), (?is_integer(T) orelse T == double)). --define(is_scalar(T), (?is_number(T) orelse T == string orelse element(1, T) == enum)). - --spec term_to_object(term(), hg_proto_utils:thrift_type()) -> jsone:json_value(). -term_to_object(Term, Type) -> - term_to_object(Term, Type, []). - -term_to_object(Term, {list, Type}, Stack) when is_list(Term) -> - [term_to_object(T, Type, [N | Stack]) || {N, T} <- enumerate(0, Term)]; -term_to_object(Term, {set, Type}, Stack) -> - term_to_object(ordsets:to_list(Term), {list, Type}, Stack); -term_to_object(Term, {map, KType, VType}, Stack) when is_map(Term), ?is_scalar(KType) -> - maps:fold( - fun(K, V, A) -> - [{genlib:to_binary(K), term_to_object(V, VType, [value, V | Stack])} | A] - end, - [], - Term - ); -term_to_object(Term, {map, KType, VType}, Stack) when is_map(Term) -> - maps:fold( - fun(K, V, A) -> - [ - [ - {<<"key">>, term_to_object(K, KType, [key, K | Stack])}, - {<<"value">>, term_to_object(V, VType, [value, V | Stack])} - ] - | A - ] - end, - [], - Term - ); -term_to_object(Term, {struct, union, {Mod, Name}}, Stack) when is_atom(Mod), is_atom(Name) -> - {struct, _, StructDef} = Mod:struct_info(Name), - union_to_object(Term, StructDef, Stack); -term_to_object(Term, {struct, _, {Mod, Name}}, Stack) when is_atom(Mod), is_atom(Name), is_tuple(Term) -> - {struct, _, StructDef} = Mod:struct_info(Name), - struct_to_object(Term, StructDef, Stack); -term_to_object(Term, {struct, struct, List}, Stack) when is_tuple(Term), is_list(List) -> - Data = lists:zip(List, tuple_to_list(Term)), - [{atom_to_binary(Name), term_to_object(V, Type, Stack)} || {{_Pos, _, Type, Name, _}, V} <- Data]; -term_to_object(Term, {enum, _}, _Stack) when is_atom(Term) -> - Term; -term_to_object(Term, Type, _Stack) when is_integer(Term), ?is_integer(Type) -> - Term; -term_to_object(Term, double, _Stack) when is_number(Term) -> - float(Term); -term_to_object(Term, string, _Stack) when is_binary(Term) -> - case is_printable_string(Term) of - true -> - Term; - false -> - term_to_object_content(Term) - end; -term_to_object(Term, bool, _Stack) when is_boolean(Term) -> - Term; -term_to_object(Term, Type, _Stack) -> - erlang:error({badarg, Term, Type}). - -union_to_object({Fn, Term}, StructDef, Stack) -> - {_N, _Req, Type, Fn, _Def} = lists:keyfind(Fn, 4, StructDef), - [{Fn, term_to_object(Term, Type, [Fn | Stack])}]. - -struct_to_object(Struct, StructDef, Stack) -> - [_ | Fields] = tuple_to_list(Struct), - lists:foldr( - fun - ({undefined, _}, A) -> - A; - ({Term, {_N, _Req, Type, Fn, _Def}}, A) -> - [{Fn, term_to_object(Term, Type, [Fn | Stack])} | A] - end, - [], - lists:zip(Fields, StructDef) - ). - -term_to_object_content(Term) -> - term_to_object_content(<<"base64">>, base64:encode(Term)). - -term_to_object_content(CType, Term) -> - [ - {<<"content_type">>, CType}, - {<<"content">>, Term} - ]. - -enumerate(_, []) -> - []; -enumerate(N, [H | T]) -> - [{N, H} | enumerate(N + 1, T)]. - -is_printable_string(V) -> - try unicode:characters_to_binary(V) of - B when is_binary(B) -> - true; - _ -> - false - catch - _:_ -> - false - end. - -to_maps(Data) -> - to_maps(Data, #{}). - -to_maps([], Acc) -> - Acc; -to_maps([{K, [{_, _} | _] = V} | Rest], Acc) -> - to_maps(Rest, Acc#{K => to_maps(V)}); -to_maps([{K, V} | Rest], Acc) when is_list(V) -> - to_maps(Rest, Acc#{K => lists:map(fun(E) -> to_maps(E) end, V)}); -to_maps([{K, V} | Rest], Acc) -> - to_maps(Rest, Acc#{K => V}). diff --git a/apps/hg_proto/src/hg_woody_service_wrapper.erl b/apps/hg_proto/src/hg_woody_service_wrapper.erl index 911871fa..761bc1b8 100644 --- a/apps/hg_proto/src/hg_woody_service_wrapper.erl +++ b/apps/hg_proto/src/hg_woody_service_wrapper.erl @@ -32,7 +32,7 @@ {ok, term()} | no_return(). handle_function(Func, Args, WoodyContext0, #{handler := Handler} = Opts) -> WoodyContext = ensure_woody_deadline_set(WoodyContext0, Opts), - ok = hg_context:save(create_context(WoodyContext, Opts)), + ok = op_context:save(op_context:key(hellgate), create_context(WoodyContext, Opts)), try Result = Handler:handle_function( Func, @@ -44,23 +44,23 @@ handle_function(Func, Args, WoodyContext0, #{handler := Handler} = Opts) -> throw:Reason -> raise(Reason) after - hg_context:cleanup() + op_context:cleanup(hellgate) end. -spec raise(term()) -> no_return(). raise(Exception) -> woody_error:raise(business, Exception). --spec create_context(woody_context:ctx(), handler_opts()) -> hg_context:context(). +-spec create_context(woody_context:ctx(), handler_opts()) -> op_context:context(). create_context(WoodyContext, Opts) -> ContextOptions = #{ woody_context => WoodyContext }, - Context = hg_context:create(ContextOptions), + Context = op_context:create(ContextOptions), configure_party_client(Context, Opts). configure_party_client(Context0, #{party_client := PartyClient}) -> - hg_context:set_party_client(PartyClient, Context0); + op_context:set_party_client(PartyClient, Context0); configure_party_client(Context, _Opts) -> Context. diff --git a/apps/hg_proto/src/hg_woody_wrapper.erl b/apps/hg_proto/src/hg_woody_wrapper.erl index c0b865ed..bfdc4075 100644 --- a/apps/hg_proto/src/hg_woody_wrapper.erl +++ b/apps/hg_proto/src/hg_woody_wrapper.erl @@ -30,7 +30,7 @@ call(ServiceName, Function, Args, Opts) -> -spec call(atom(), woody:func(), woody:args(), client_opts(), woody_deadline:deadline()) -> term(). call(ServiceName, Function, Args, Opts, Deadline) -> Service = get_service_modname(ServiceName), - Context = hg_context:get_woody_context(hg_context:load()), + Context = op_context:get_woody_context(op_context:load(op_context:key(hellgate))), Request = {Service, Function, Args}, woody_client:call( Request, diff --git a/apps/machinery_extra/src/machinery_extra.app.src b/apps/machinery_extra/src/machinery_extra.app.src deleted file mode 100644 index 092e05ff..00000000 --- a/apps/machinery_extra/src/machinery_extra.app.src +++ /dev/null @@ -1,17 +0,0 @@ -{application, machinery_extra, [ - {description, "Machinery extra backends / utilities"}, - {vsn, "1"}, - {registered, []}, - {applications, [ - kernel, - stdlib, - genlib, - machinery, - gproc - ]}, - {env, []}, - {modules, []}, - {maintainers, []}, - {licenses, ["Apache 2.0"]}, - {links, ["https://github.com/valitydev/fistful-server"]} -]}. diff --git a/apps/machinery_extra/src/machinery_gensrv_backend.erl b/apps/machinery_extra/src/machinery_gensrv_backend.erl deleted file mode 100644 index 6db3e3bc..00000000 --- a/apps/machinery_extra/src/machinery_gensrv_backend.erl +++ /dev/null @@ -1,346 +0,0 @@ -%%% -%%% Machinery gen_server backend -%%% -%%% TODO -%%% - Notion of _failed_ machines - --module(machinery_gensrv_backend). - --type namespace() :: machinery:namespace(). --type id() :: machinery:id(). --type range() :: machinery:range(). --type machine(E, A) :: machinery:machine(E, A). --type args(T) :: machinery:args(T). --type response(T) :: machinery:response(T). --type logic_handler(T) :: machinery:logic_handler(T). --type timestamp() :: machinery:timestamp(). - --type backend_opts() :: - machinery:backend_opts(#{ - name := atom() - }). - --type backend() :: {?MODULE, backend_opts()}. - --export_type([backend_opts/0]). --export_type([backend/0]). - -%% API - --export([new/1]). --export([child_spec/2]). - -%% Machinery backend - --behaviour(machinery_backend). - --export([start/4]). --export([call/5]). --export([repair/5]). --export([get/4]). --export([notify/5]). --export([remove/3]). --export([trace/3]). - -%% Gen Server - --behaviour(gen_server). - --export([start_machine_link/4]). - --export([init/1]). --export([handle_call/3]). --export([handle_cast/2]). --export([handle_info/2]). --export([terminate/2]). --export([code_change/3]). - -%% API - --spec new(backend_opts()) -> backend(). -new(#{name := _} = Opts) -> - {?MODULE, Opts}. - --spec child_spec(logic_handler(_), backend_opts()) -> supervisor:child_spec(). -child_spec(Handler0, Opts) -> - Handler = machinery_utils:get_handler(Handler0), - MFA = {?MODULE, start_machine_link, [Handler]}, - #{ - id => get_sup_name(Opts), - start => {machinery_gensrv_backend_sup, start_link, [get_sup_ref(Opts), MFA]}, - type => supervisor - }. - -%% Machinery backend - --spec start(namespace(), id(), args(_), backend_opts()) -> ok | {error, exists}. -start(NS, ID, Args, Opts) -> - _ = logger:debug("[machinery/gensrv][client][~s:~s] starting with args: ~p", [NS, ID, Args]), - case supervisor:start_child(get_sup_ref(Opts), [NS, ID, Args]) of - {ok, PID} -> - _ = logger:debug("[machinery/gensrv][client][~s:~s] started as: ~p", [NS, ID, PID]), - ok; - {error, {already_started, _}} -> - report_exists(NS, ID); - {error, already_present} -> - report_exists(NS, ID) - end. - --spec call(namespace(), id(), range(), args(_), backend_opts()) -> {ok, response(_)} | {error, notfound}. -call(NS, ID, Range, Args, _Opts) -> - _ = logger:debug("[machinery/gensrv][client][~s:~s] calling with range ~p and args: ~p", [NS, ID, Range, Args]), - try gen_server:call(get_machine_ref(NS, ID), {call, Range, Args}) of - Response -> - _ = logger:debug("[machinery/gensrv][client][~s:~s] response: ~p", [NS, ID, Response]), - {ok, Response} - catch - exit:noproc -> - report_notfound(NS, ID); - exit:{noproc, {gen_server, call, _}} -> - report_notfound(NS, ID) - end. - --spec repair(namespace(), id(), range(), args(_), backend_opts()) -> no_return(). -repair(_NS, _ID, _Range, _Args, _Opts) -> - % Machine state has been removed after machine process failed. Nothing to repair. - erlang:error({not_implemented, repair}). - --spec get(namespace(), id(), range(), backend_opts()) -> {ok, machine(_, _)} | {error, notfound}. -get(NS, ID, Range, _Opts) -> - _ = logger:debug("[machinery/gensrv][client][~s:~s] getting with range: ~p", [NS, ID, Range]), - try gen_server:call(get_machine_ref(NS, ID), {get, Range}) of - Machine -> - _ = logger:debug("[machinery/gensrv][client][~s:~s] machine: ~p", [NS, ID, Machine]), - {ok, Machine} - catch - exit:noproc -> - report_notfound(NS, ID); - exit:{noproc, {gen_server, call, _}} -> - report_notfound(NS, ID) - end. - -report_exists(NS, ID) -> - _ = _ = logger:debug("[machinery/gensrv][client][~s:~s] exists already", [NS, ID]), - {error, exists}. - -report_notfound(NS, ID) -> - _ = _ = logger:debug("[machinery/gensrv][client][~s:~s] not found", [NS, ID]), - {error, notfound}. - --spec notify(namespace(), id(), range(), args(_), backend_opts()) -> no_return(). -notify(_NS, _ID, _Range, _Args, _Opts) -> - erlang:error({not_implemented, notify}). - --spec remove(namespace(), id(), backend_opts()) -> no_return(). -remove(_Namespace, _ID, _Opts) -> - erlang:error({not_implemented, remove}). - --spec trace(namespace(), id(), backend_opts()) -> no_return(). -trace(_Namespace, _ID, _Opts) -> - erlang:error({not_implemented, trace}). - -%% Gen Server + Supervisor - --spec start_machine_link(logic_handler(_), namespace(), id(), args(_)) -> {ok, pid()}. -start_machine_link(Handler, NS, ID, Args) -> - gen_server:start_link(get_machine_ref(NS, ID), ?MODULE, {machine, Handler, NS, ID, Args}, []). - -%% - --type st(E, Aux, Args) :: #{ - machine := machine(E, Aux), - handler := logic_handler(Args), - deadline => timestamp() -}. - --spec init({machine, logic_handler(Args), namespace(), id(), args(_)}) -> - ignore - | {ok, st(_, _, Args), timeout()}. -% Gen Server -init({machine, Handler, NS, ID, Args}) -> - St0 = #{machine => construct_machine(NS, ID), handler => Handler}, - _ = logger:debug("[machinery/gensrv][server][~s:~s] dispatching init: ~p with state: ~p", [NS, ID, Args, St0]), - Result = dispatch_signal({init, Args}, St0), - case apply_result(Result, St0) of - St1 = #{} -> - _ = logger:debug("[machinery/gensrv][server][~s:~s] started with: ~p", [NS, ID, St1]), - {ok, St1, compute_timeout(St1)}; - removed -> - _ = logger:debug("[machinery/gensrv][server][~s:~s] removed", [NS, ID]), - ignore - end. - -construct_machine(NS, ID) -> - #{ - namespace => NS, - id => ID, - history => [], - aux_state => undefined - }. - --spec handle_call({call, range(), args(_)}, {pid(), reference()}, st(E, Aux, Args)) -> - {reply, response(_), st(E, Aux, Args), timeout()} - | {stop, normal, st(E, Aux, Args)}. -handle_call({call, Range, Args}, _From, #{machine := #{namespace := NS, id := ID}} = St0) -> - St1 = apply_range(Range, St0), - _ = logger:debug("[machinery/gensrv][server][~s:~s] dispatching call: ~p with state: ~p", [NS, ID, Args, St1]), - {Response, Result} = dispatch_call(Args, St0), - case apply_result(Result, St0) of - St2 = #{} -> - _ = logger:debug("[machinery/gensrv][server][~s:~s] responded: ~p, new state: ~p", [NS, ID, Response, St2]), - {reply, Response, St2, compute_timeout(St2)}; - removed -> - _ = logger:debug("[machinery/gensrv][server][~s:~s] responded: ~p, removed", [NS, ID, Response]), - {stop, normal, Response, St0} - end; -handle_call({get, Range}, _From, #{machine := M} = St) -> - {reply, apply_range(Range, M), St, compute_timeout(St)}; -handle_call(Call, _From, _St) -> - error({badcall, Call}). - --spec handle_cast(_Cast, st(_, _, _)) -> no_return(). -handle_cast(Cast, _St) -> - error({badcast, Cast}). - --spec handle_info(timeout, st(E, Aux, Args)) -> - {noreply, st(E, Aux, Args), timeout()} - | {stop, normal, st(E, Aux, Args)}. -handle_info(timeout, #{machine := #{namespace := NS, id := ID}} = St0) -> - _ = logger:debug("[machinery/gensrv][server][~s:~s] dispatching timeout with state: ~p", [NS, ID, St0]), - Result = dispatch_signal(timeout, St0), - case apply_result(Result, St0) of - St1 = #{} -> - _ = logger:debug("[machinery/gensrv][server][~s:~s] new state: ~p", [NS, ID, St1]), - {noreply, St1, compute_timeout(St1)}; - removed -> - _ = logger:debug("[machinery/gensrv][server][~s:~s] removed", [NS, ID]), - {stop, normal, St0} - end; -handle_info(Info, _St) -> - error({badinfo, Info}). - --spec terminate(_Reason, st(_, _, _)) -> ok. -terminate(_Reason, _St) -> - ok. - --spec code_change(_OldVsn, st(E, Aux, Args), _Extra) -> {ok, st(E, Aux, Args)}. -code_change(_OldVsn, St, _Extra) -> - {ok, St}. - -%% - -apply_range(Range, #{machine := M} = St) -> - St#{machine := apply_range(Range, M)}; -apply_range(Range, #{history := H} = M) -> - M#{history := select_range(Range, H)}. - -apply_result(#{action := As} = R, St) -> - apply_result( - maps:remove(action, R), - apply_actions(As, St) - ); -apply_result(#{events := Es} = R, #{machine := M} = St) -> - apply_result( - maps:remove(events, R), - St#{machine := apply_events(Es, M)} - ); -apply_result(#{aux_state := Aux} = R, #{machine := M} = St) -> - apply_result( - maps:remove(aux_state, R), - St#{machine := apply_auxst(Aux, M)} - ); -apply_result(#{}, St) -> - St. - -apply_actions(As, St) when is_list(As) -> - lists:foldl(fun apply_action/2, St, As); -apply_actions(A, St) -> - apply_action(A, St). - -apply_action({set_timer, Timer}, St) -> - St#{deadline => compute_deadline(Timer)}; -apply_action(unset_timer, St) -> - maps:without([deadline], St); -apply_action(continue, St) -> - St#{deadline => machinery_time:now()}; -apply_action(remove, _St) -> - removed. - -apply_events(Es, #{history := Hs} = M) -> - Ts = machinery_time:now(), - Hl = length(Hs), - M#{ - history := Hs ++ - [ - {ID, Ts, Eb} - || {ID, Eb} <- lists:zip(lists:seq(Hl + 1, Hl + length(Es)), Es) - ] - }. - -apply_auxst(Aux, #{} = M) -> - M#{aux_state := Aux}. - -compute_deadline({timeout, V}) -> - machinery_time:add_seconds(V, machinery_time:now()); -compute_deadline({deadline, V}) -> - V. - -compute_timeout(#{deadline := Deadline}) -> - erlang:max(0, machinery_time:interval(Deadline, machinery_time:now())); -compute_timeout(#{}) -> - infinity. - -%% Utils - -get_name(#{name := V}) -> - V. - -get_sup_ref(Opts) -> - {via, gproc, construct_gproc_ref(get_sup_name(Opts))}. - -get_sup_name(Opts) -> - {?MODULE, {sup, get_name(Opts)}}. - -get_machine_ref(NS, ID) -> - {via, gproc, construct_gproc_ref(get_machine_name(NS, ID))}. - -get_machine_name(NS, ID) -> - {?MODULE, {machine, NS, ID}}. - -construct_gproc_ref(Name) -> - {n, l, Name}. - -dispatch_signal(Signal, #{machine := Machine, handler := Handler}) -> - machinery:dispatch_signal(Signal, Machine, Handler, undefined). - -dispatch_call(Args, #{machine := Machine, handler := Handler}) -> - machinery:dispatch_call(Args, Machine, Handler, undefined). - -%% - -select_range({Ec, N, Dir}, H) -> - R0 = {1, length(H)}, - R1 = intersect_range(Ec, Dir, R0), - R2 = limit_range(N, Dir, R1), - query_range(R2, Dir, H). - -intersect_range(undefined, _, R) -> - R; -intersect_range(Ec, forward, {A, B}) -> - {erlang:max(A, Ec + 1), B}; -intersect_range(Ec, backward, {A, B}) -> - {A, erlang:min(B, Ec - 1)}. - -limit_range(undefined, _, R) -> - R; -limit_range(N, forward, {A, B}) -> - {A, min(A + N - 1, B)}; -limit_range(N, backward, {A, B}) -> - {max(B - N + 1, A), B}. - -query_range({A, B}, _, _) when A > B -> - []; -query_range({A, B}, forward, H) -> - lists:sublist(H, A, B - A + 1); -query_range({A, B}, backward, H) -> - lists:reverse(lists:sublist(H, A, B - A + 1)). diff --git a/apps/machinery_extra/src/machinery_gensrv_backend_sup.erl b/apps/machinery_extra/src/machinery_gensrv_backend_sup.erl deleted file mode 100644 index b30779b2..00000000 --- a/apps/machinery_extra/src/machinery_gensrv_backend_sup.erl +++ /dev/null @@ -1,43 +0,0 @@ -%%% -%%% Machinery gen_server backend -%%% -%%% A supervisor for machine processes. -%%% - --module(machinery_gensrv_backend_sup). - -%% API - --export([start_link/2]). - -%% Supervisor - --behaviour(supervisor). - --export([init/1]). - -%% - --type sup_name() :: {via, module(), any()}. --type mfargs() :: {module(), atom(), [_]}. - --spec start_link(sup_name(), mfargs()) -> {ok, pid()} | {error, {already_started, pid()}}. -start_link(SupName, Args) -> - supervisor:start_link(SupName, ?MODULE, Args). - -%% - --spec init({supervisor, mfargs()}) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. -init(MFA) -> - {ok, - { - #{strategy => simple_one_for_one}, - [ - #{ - id => machine, - start => MFA, - type => worker, - restart => temporary - } - ] - }}. diff --git a/apps/machinery_extra/src/machinery_time.erl b/apps/machinery_extra/src/machinery_time.erl deleted file mode 100644 index a0724328..00000000 --- a/apps/machinery_extra/src/machinery_time.erl +++ /dev/null @@ -1,33 +0,0 @@ -%%% -%%% - --module(machinery_time). - --type timestamp() :: machinery:timestamp(). - --export([now/0]). --export([add_seconds/2]). --export([interval/2]). - -%% - --spec now() -> timestamp(). -now() -> - Now = {_, _, USec} = os:timestamp(), - {calendar:now_to_universal_time(Now), USec}. - --spec add_seconds(integer(), timestamp()) -> timestamp(). -add_seconds(V, {Dt, USec}) -> - {gs2dt(dt2gs(Dt) + V), USec}. - --spec interval(timestamp(), timestamp()) -> timeout(). -interval({Dt1, USec1}, {Dt2, USec2}) -> - (dt2gs(Dt1) - dt2gs(Dt2)) * 1000 + (USec1 - USec2) div 1000. - -%% - -dt2gs(Dt) -> - calendar:datetime_to_gregorian_seconds(Dt). - -gs2dt(Dt) -> - calendar:gregorian_seconds_to_datetime(Dt). diff --git a/apps/op_context/rebar.config b/apps/op_context/rebar.config new file mode 100644 index 00000000..1031f8fc --- /dev/null +++ b/apps/op_context/rebar.config @@ -0,0 +1,13 @@ +{src_dirs, ["src"]}. + +{erl_opts, [ + debug_info, + warnings_as_errors, + warn_missing_spec +]}. + +{deps, [ + {gproc, "0.9.0"}, + {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.2"}}}, + {party_client, {git, "https://github.com/valitydev/party-client-erlang.git", {tag, "v2.0.1"}}} +]}. diff --git a/apps/op_context/src/op_context.app.src b/apps/op_context/src/op_context.app.src new file mode 100644 index 00000000..9104cc91 --- /dev/null +++ b/apps/op_context/src/op_context.app.src @@ -0,0 +1,16 @@ +{application, op_context, [ + {description, "Process-scoped operation context (woody + party_client) for HG and FF"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + gproc, + woody, + party_client + ]}, + {env, []}, + {modules, []}, + {licenses, ["Apache-2.0"]}, + {links, []} +]}. diff --git a/apps/op_context/src/op_context.erl b/apps/op_context/src/op_context.erl new file mode 100644 index 00000000..bef1b74c --- /dev/null +++ b/apps/op_context/src/op_context.erl @@ -0,0 +1,259 @@ +-module(op_context). + +-export([create/0]). +-export([create/1]). +-export([save/2]). +-export([load/1]). +-export([cleanup/1]). +-export([cleanup/2]). + +-export([key/1]). +-export([binding/1]). +-export([scope_for_namespace/1]). +-export([binding_for_namespace/1]). + +-export([get_woody_context/1]). +-export([get_party_client_context/1]). +-export([set_party_client_context/2]). +-export([get_party_client/1]). +-export([set_party_client/2]). + +-export([env_enter/2]). +-export([env_leave/1]). +-export([current_woody_context/0]). + +-type scope() :: hellgate | fistful. +-type registry_key() :: {p, l, term()}. +-type cleanup_mode() :: strict | lenient. + +-type binding() :: #{ + registry_key := registry_key(), + cleanup_mode := cleanup_mode() +}. + +-type context() :: #{ + woody_context := woody_context(), + party_client_context := party_client_context(), + party_client => party_client() +}. + +-type options() :: #{ + woody_context => woody_context(), + party_client_context => party_client_context(), + party_client => party_client() +}. + +-export_type([ + scope/0, + registry_key/0, + cleanup_mode/0, + binding/0, + context/0, + options/0 +]). + +%% Internal types + +-type woody_context() :: woody_context:ctx(). +-type party_client() :: party_client:client(). +-type party_client_context() :: party_client:context(). + +%% API + +-spec create() -> context(). +create() -> + create(#{}). + +-spec create(options()) -> context(). +create(Options0) -> + Options1 = ensure_woody_context_exists(Options0), + ensure_party_context_exists(Options1). + +-spec save(registry_key(), context()) -> ok. +save(RegistryKey, Context) -> + true = + try + gproc:reg(RegistryKey, Context) + catch + error:badarg -> + gproc:set_value(RegistryKey, Context) + end, + ok. + +-spec load(registry_key()) -> context() | no_return(). +load(RegistryKey) -> + gproc:get_value(RegistryKey). + +-spec cleanup(scope()) -> ok. +cleanup(Scope) when Scope =:= hellgate; Scope =:= fistful -> + cleanup(key(Scope), cleanup_mode(Scope)). + +-spec cleanup(registry_key(), cleanup_mode()) -> ok. +cleanup(RegistryKey, strict) -> + true = gproc:unreg(RegistryKey), + ok; +cleanup(RegistryKey, lenient) -> + try + true = gproc:unreg(RegistryKey) + catch + _:_ -> ok + end, + ok. + +-spec key(scope()) -> registry_key(). +key(hellgate) -> + {p, l, stored_hg_context}; +key(fistful) -> + {p, l, {ff_context, stored_context}}. + +-spec binding(scope()) -> binding(). +binding(Scope) -> + #{ + registry_key => key(Scope), + cleanup_mode => cleanup_mode(Scope) + }. + +%% prg_machine namespace → scope; single source for worker env scoping. +-spec scope_for_namespace(atom()) -> scope(). +scope_for_namespace(NS) -> + case atom_to_binary(NS, utf8) of + <<"ff/", _/binary>> -> + fistful; + _ -> + hellgate + end. + +-spec binding_for_namespace(atom()) -> binding(). +binding_for_namespace(NS) -> + binding(scope_for_namespace(NS)). + +-spec env_enter(woody_context(), binding()) -> ok. +env_enter(WoodyCtx, #{registry_key := RegistryKey}) -> + ok = save( + RegistryKey, + create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + }) + ). + +-spec env_leave(binding()) -> ok. +env_leave(#{registry_key := RegistryKey, cleanup_mode := CleanupMode}) -> + cleanup(RegistryKey, CleanupMode). + +%% Resolve the woody context bound to the current process: try the hellgate +%% binding first, then the fistful one (their gproc keys differ, so there is no +%% collision), falling back to a fresh context with a warning. Replaces the old +%% global prg_machine woody_context_loader app-env hook. +-spec current_woody_context() -> woody_context(). +current_woody_context() -> + case try_load_woody_context([key(hellgate), key(fistful)]) of + {ok, WoodyContext} -> + WoodyContext; + error -> + _ = logger:warning( + "op_context: no woody context bound to the current process, using a fresh one" + ), + woody_context:new() + end. + +-spec try_load_woody_context([registry_key()]) -> {ok, woody_context()} | error. +try_load_woody_context([]) -> + error; +try_load_woody_context([Key | Rest]) -> + try get_woody_context(load(Key)) of + WoodyContext -> {ok, WoodyContext} + catch + _:_ -> try_load_woody_context(Rest) + end. + +-spec get_woody_context(context()) -> woody_context(). +get_woody_context(#{woody_context := WoodyContext}) -> + WoodyContext. + +-spec get_party_client(context()) -> party_client(). +get_party_client(#{party_client := PartyClient}) -> + PartyClient; +get_party_client(Context) -> + error(no_party_client, [Context]). + +-spec set_party_client(party_client(), context()) -> context(). +set_party_client(PartyClient, Context) -> + Context#{party_client => PartyClient}. + +-spec get_party_client_context(context()) -> party_client_context(). +get_party_client_context(#{party_client_context := PartyContext}) -> + PartyContext. + +-spec set_party_client_context(party_client_context(), context() | options()) -> context(). +set_party_client_context(PartyContext, Context) -> + Context#{party_client_context => PartyContext}. + +%% Internal functions + +-spec cleanup_mode(scope()) -> cleanup_mode(). +cleanup_mode(hellgate) -> + strict; +cleanup_mode(fistful) -> + lenient. + +-spec ensure_woody_context_exists(options()) -> options(). +ensure_woody_context_exists(#{woody_context := _WoodyContext} = Options) -> + Options; +ensure_woody_context_exists(Options) -> + Options#{woody_context => woody_context:new()}. + +-spec ensure_party_context_exists(options()) -> context(). +ensure_party_context_exists(#{party_client_context := _PartyContext} = Options) -> + Options; +ensure_party_context_exists(#{woody_context := WoodyContext} = Options) -> + set_party_client_context(party_client:create_context(#{woody_context => WoodyContext}), Options). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec colocated_keys_isolated_test() -> _. +colocated_keys_isolated_test() -> + {ok, _} = application:ensure_all_started(gproc), + {ok, _} = application:ensure_all_started(woody), + WoodyHg = woody_context:add_meta(woody_context:new(), #{<<"app">> => <<"hg">>}), + WoodyFf = woody_context:add_meta(woody_context:new(), #{<<"app">> => <<"ff">>}), + try + CtxHg = create(#{woody_context => WoodyHg}), + CtxFf = create(#{woody_context => WoodyFf}), + ok = save(key(hellgate), CtxHg), + ok = save(key(fistful), CtxFf), + CtxHgLoaded = load(key(hellgate)), + CtxFfLoaded = load(key(fistful)), + ?assertEqual(WoodyHg, get_woody_context(CtxHgLoaded)), + ?assertEqual(WoodyFf, get_woody_context(CtxFfLoaded)), + ?assertNotEqual( + get_party_client_context(CtxHgLoaded), + get_party_client_context(CtxFfLoaded) + ), + ok = cleanup(hellgate), + CtxFfAfterHgCleanup = load(key(fistful)), + ?assertEqual(WoodyFf, get_woody_context(CtxFfAfterHgCleanup)), + ok = cleanup(fistful) + after + cleanup(key(hellgate), lenient), + cleanup(fistful) + end. + +-spec scoped_helpers_test() -> _. +scoped_helpers_test() -> + {ok, _} = application:ensure_all_started(gproc), + {ok, _} = application:ensure_all_started(woody), + WoodyCtx = woody_context:new(), + try + ok = save(key(fistful), create(#{woody_context => WoodyCtx})), + ?assertEqual(WoodyCtx, get_woody_context(load(key(fistful)))), + ok = cleanup(fistful), + ok = cleanup(fistful) + after + cleanup(fistful) + end. + +-endif. diff --git a/apps/prg_machine/src/prg_action.erl b/apps/prg_machine/src/prg_action.erl new file mode 100644 index 00000000..720f77f0 --- /dev/null +++ b/apps/prg_machine/src/prg_action.erl @@ -0,0 +1,60 @@ +-module(prg_action). + +%%% Wire `action()` scheduling helpers for domain handlers. + +-include_lib("progressor/include/progressor.hrl"). + +-export([marshal_timer/1, schedule_timer/1, schedule_deadline/1]). + +-export_type([t/0, timer/0, seconds/0]). + +-type seconds() :: timeout_sec(). +-type datetime() :: calendar:datetime() | {calendar:datetime(), non_neg_integer()} | binary(). +-type timer() :: {timeout, seconds()} | {deadline, datetime()}. +-type t() :: action(). + +-spec schedule_timer(timer()) -> t(). +schedule_timer({timeout, 0}) -> + timeout; +schedule_timer(Timer) -> + {schedule, #{at => marshal_timer(Timer), action => timeout}}. + +-spec schedule_deadline(datetime()) -> t(). +schedule_deadline(Deadline) -> + {schedule, #{at => marshal_timer({deadline, Deadline}), action => timeout}}. + +-spec marshal_timer(timer()) -> timestamp_us(). +marshal_timer({timeout, Seconds}) when is_integer(Seconds), Seconds =< 0 -> + erlang:system_time(microsecond); +marshal_timer({timeout, Seconds}) when is_integer(Seconds), Seconds > 0 -> + erlang:system_time(microsecond) + Seconds * 1000000; +marshal_timer({deadline, {{{_, _, _}, {_, _, _}} = Dt, USec}}) when is_integer(USec) -> + datetime_to_microseconds(Dt, USec); +marshal_timer({deadline, {{_, _, _}, {_, _, _}} = Dt}) -> + datetime_to_microseconds(Dt, 0); +marshal_timer({deadline, Bin}) when is_binary(Bin) -> + calendar:rfc3339_to_system_time(unicode:characters_to_list(Bin), [{unit, microsecond}]); +marshal_timer(Other) -> + error({invalid_timer, Other}). + +%% + +datetime_to_microseconds(Dt, USec) -> + Sec = calendar:datetime_to_gregorian_seconds(Dt) - ?EPOCH_DIFF, + Sec * 1000000 + USec. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec marshal_timer_machinery_deadline_test() -> _. +marshal_timer_machinery_deadline_test() -> + Dt = {{2099, 6, 13}, {12, 34, 56}}, + USec = 789000, + Sec = calendar:datetime_to_gregorian_seconds(Dt) - ?EPOCH_DIFF, + Expected = Sec * 1000000 + USec, + ?assertEqual(Expected, marshal_timer({deadline, {Dt, USec}})), + ?assertEqual(marshal_timer({deadline, {Dt, 0}}), marshal_timer({deadline, Dt})). + +-endif. diff --git a/apps/prg_machine/src/prg_machine.app.src b/apps/prg_machine/src/prg_machine.app.src new file mode 100644 index 00000000..37cd3092 --- /dev/null +++ b/apps/prg_machine/src/prg_machine.app.src @@ -0,0 +1,18 @@ +{application, prg_machine, [ + {description, "Unified progressor machine runtime for HG and FF"}, + {vsn, "0.1.0"}, + {registered, [prg_machine_registry]}, + {applications, [ + kernel, + stdlib, + genlib, + woody, + scoper, + progressor, + op_context + ]}, + {env, []}, + {modules, []}, + {licenses, ["Apache-2.0"]}, + {links, []} +]}. diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl new file mode 100644 index 00000000..7a6137c3 --- /dev/null +++ b/apps/prg_machine/src/prg_machine.erl @@ -0,0 +1,801 @@ +-module(prg_machine). + +%%% Unified runtime: HTTP/woody handlers -> domain (-behaviour(prg_machine)) -> progressor. +%%% Replaces hg_machine, ff_machine, machinery client/backend stack for progressor. + +-include_lib("progressor/include/progressor.hrl"). + +%% Types + +-type namespace() :: namespace_id(). +-type args() :: term(). +-type call() :: term(). +-type response() :: ok | {ok, term()} | {error, term()} | {exception, term()}. + +%% Machinery timestamp format: a UTC datetime plus a microsecond remainder. +-type timestamp() :: {calendar:datetime(), non_neg_integer()}. +-type event_body() :: term(). +%% Domain history tuple (not progressor storage event() map). +-type machine_event() :: {event_id(), timestamp(), event_body()}. +-type history() :: [machine_event()]. +-type processor_error() :: {exception, atom(), term()}. + +-type get_error() :: + notfound + | timeout + | {unknown_namespace, namespace()} + | processor_error(). + +-type repair_error() :: + notfound + | timeout + | working + | failed + | {unknown_namespace, namespace()} + | processor_error() + | {repair, {failed, term()}}. + +-type machine() :: #{ + namespace := namespace(), + id := id(), + history := history(), + aux_state := term(), + range => history_range() +}. + +-type signal() :: timeout. +-type result() :: #{ + events => [event_body()], + action => action(), + auxst => term() +}. + +-type process_options() :: #{ + ns := namespace(), + default_handling_timeout => timeout() +}. + +-export_type([ + namespace/0, + id/0, + event_id/0, + history_range/0, + args/0, + call/0, + response/0, + timestamp/0, + event_body/0, + machine_event/0, + history/0, + machine/0, + get_error/0, + processor_error/0, + repair_error/0, + signal/0, + result/0, + process_options/0 +]). + +%% Domain behaviour + +-callback namespace() -> namespace(). + +-callback init(args(), machine()) -> result(). + +-callback process_signal(signal(), machine()) -> result(). + +-callback process_call(call(), machine()) -> {response(), result()}. + +-callback process_repair(args(), machine()) -> result() | {error, term()}. + +-callback process_notification(args(), machine()) -> result(). + +-callback marshal_event_body(timestamp(), event_body()) -> {undefined | pos_integer(), binary()}. + +-callback unmarshal_event_body(binary()) -> event_body(). + +-callback marshal_aux_state(term()) -> binary(). + +-callback unmarshal_aux_state(binary()) -> term(). + +-callback apply_event(event_id(), timestamp(), event_body(), term()) -> term(). + +%% Client API + +-export([start/3]). +-export([call/3]). +-export([call/6]). +-export([repair/3]). +-export([get/2]). +-export([get/3]). +-export([get_history/2]). +-export([get_history/4]). +-export([get_history/5]). +-export([notify/3]). +-export([history_range/3]). + +%% Progressor processor + +-export([process/3]). + +%% Registry (namespace -> handler module) + +-export([handler_namespace/1]). + +%% Event-sourcing helpers (replaces ff_machine) + +-export([collapse/2]). +-export([emit_events/1]). +-export([timestamp/0]). + +-export([unmarshal_event_body/2]). + +%% + +-spec handler_namespace(module()) -> namespace(). +handler_namespace(Handler) -> + Handler:namespace(). + +%% Client API + +-spec start(namespace(), id(), args()) -> + {ok, ok} | {error, exists | timeout | {unknown_namespace, namespace()} | term()}. +start(NS, ID, Args) -> + Req = #{ + ns => NS, + id => ID, + args => encode_term(Args), + context => encode_rpc_context() + }, + case progressor:init(Req) of + {ok, ok} = Ok -> + Ok; + {error, <<"process already exists">>} -> + {error, exists}; + Error -> + map_client_error(NS, Error) + end. + +-spec call(namespace(), id(), call()) -> + {ok, response()} | {error, notfound | failed | timeout | {unknown_namespace, namespace()} | term()}. +call(NS, ID, CallArgs) -> + call(NS, ID, CallArgs, undefined, undefined, forward). + +-spec call(namespace(), id(), call(), event_id() | undefined, non_neg_integer() | undefined, forward | backward) -> + {ok, response()} | {error, notfound | failed | timeout | {unknown_namespace, namespace()} | term()}. +call(NS, ID, CallArgs, After, Limit, Direction) -> + Req = request(NS, ID, CallArgs, encode_range(After, Limit, Direction)), + case progressor:call(Req) of + {ok, Response} -> + {ok, decode_term(Response)}; + {error, <<"process not found">>} -> + {error, notfound}; + {error, <<"process is init">>} -> + {error, notfound}; + {error, <<"process is error">>} -> + {error, failed}; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception}; + {error, {exception, Class, Reason, _Stacktrace}} -> + {error, {exception, Class, Reason}}; + Error -> + map_client_error(NS, Error) + end. + +-spec repair(namespace(), id(), args()) -> + {ok, term()} | {error, repair_error()}. +repair(NS, ID, Args) -> + Req = #{ + ns => NS, + id => ID, + args => encode_term(Args), + context => encode_rpc_context() + }, + case progressor:repair(Req) of + {ok, Response} -> + {ok, decode_term(Response)}; + {error, <<"process not found">>} -> + {error, notfound}; + {error, <<"process is init">>} -> + {error, notfound}; + {error, <<"process is running">>} -> + {error, working}; + {error, <<"process is error">>} -> + {error, failed}; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception}; + {error, {exception, Class, Reason, _Stacktrace}} -> + {error, {exception, Class, Reason}}; + Error -> + map_client_error(repair, NS, Error) + end. + +-spec get(namespace(), id()) -> {ok, machine()} | {error, get_error()}. +get(NS, ID) -> + get(NS, ID, #{direction => forward}). + +-spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, get_error()}. +get(NS, ID, Range) -> + case prg_machine_registry:lookup(NS) of + {ok, Handler} -> + Req = request(NS, ID, undefined, Range), + case progressor:get(Req) of + {ok, Process} -> + {ok, unmarshal_machine(Handler, NS, Process)}; + {error, <<"process not found">>} -> + {error, notfound}; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception}; + {error, {exception, Class, Reason, _Stacktrace}} -> + {error, {exception, Class, Reason}}; + Error -> + map_client_error(NS, Error) + end; + {error, _} = Error -> + Error + end. + +-spec get_history(namespace(), id()) -> {ok, history()} | {error, get_error()}. +get_history(NS, ID) -> + get_history(NS, ID, undefined, undefined, forward). + +-spec get_history(namespace(), id(), event_id() | undefined, non_neg_integer() | undefined) -> + {ok, history()} | {error, get_error()}. +get_history(NS, ID, After, Limit) -> + get_history(NS, ID, After, Limit, forward). + +-spec get_history(namespace(), id(), event_id() | undefined, non_neg_integer() | undefined, forward | backward) -> + {ok, history()} | {error, get_error()}. +get_history(NS, ID, After, Limit, Direction) -> + case get(NS, ID, history_range(After, Limit, Direction)) of + {ok, #{history := History}} -> + {ok, History}; + Error -> + Error + end. + +-spec notify(namespace(), id(), args()) -> + ok | {error, notfound | failed | timeout | {unknown_namespace, namespace()} | processor_error() | term()}. +notify(NS, ID, Args) -> + case call(NS, ID, {notify, Args}) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. + +-spec history_range(undefined | event_id(), undefined | non_neg_integer(), forward | backward) -> + history_range(). +history_range(Offset, Limit, Direction) -> + encode_range(Offset, Limit, Direction). + +%% Progressor processor callback. +%% progressor config: #{client => prg_machine, options => #{ns => invoice, ...}} + +-spec process({init | call | repair | notify | timeout, binary(), map()}, process_options(), binary()) -> + {ok, map()} | {error, term()}. +process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> + try + case prg_machine_registry:lookup(NS) of + {error, _} = Error -> + Error; + {ok, Handler} -> + {WoodyCtx0, OtelCtx} = decode_rpc_context(BinCtx), + ok = woody_rpc_helper:attach_otel_context(OtelCtx), + WoodyCtx = ensure_deadline_set(WoodyCtx0, Opts), + run_scoped(Opts, WoodyCtx, fun() -> + LastEventID = maps:get(last_event_id, Process), + Machine = unmarshal_machine(Handler, NS, Process), + Result = dispatch(Handler, CallType, BinArgs, Machine), + marshal_process_result(Handler, LastEventID, Result) + end) + end + catch + Class:Reason:Stacktrace -> + Exception = {exception, Class, Reason}, + logger:error( + "prg_machine process failed: ~p:~p", + [Class, Reason], + #{stacktrace => Stacktrace, exception => Exception} + ), + {error, Exception} + end. + +%% Default woody deadline (30s, configurable per namespace via opts), restoring the +%% old hg_progressor behaviour (hg_woody_service_wrapper:ensure_woody_deadline_set/2). +-define(DEFAULT_HANDLING_TIMEOUT, 30000). + +ensure_deadline_set(WoodyCtx, Opts) -> + case woody_context:get_deadline(WoodyCtx) of + undefined -> + Timeout = maps:get(default_handling_timeout, Opts, ?DEFAULT_HANDLING_TIMEOUT), + woody_context:set_deadline(woody_deadline:from_timeout(Timeout), WoodyCtx); + _Set -> + WoodyCtx + end. + +%% Event-sourcing + +-spec collapse(module(), machine()) -> term(). +collapse(Handler, #{history := History, aux_state := AuxState}) -> + lists:foldl( + fun({EventID, Ts, Body}, Model) -> + dispatch_apply_event(Handler, EventID, Ts, Body, Model) + end, + initial_model(Handler, AuxState), + History + ). + +-spec emit_events([term()]) -> [{ev, timestamp(), term()}]. +emit_events(Events) -> + Ts = timestamp(), + [{ev, Ts, Body} || Body <- Events]. + +-spec timestamp() -> timestamp(). +timestamp() -> + Now = erlang:system_time(microsecond), + {Seconds, Micro} = prg_utils:split_timestamp(Now), + {calendar:system_time_to_universal_time(Seconds, second), Micro}. + +%% Internals — dispatch + +dispatch(Handler, init, BinArgs, Machine) -> + Args = decode_term(BinArgs), + Handler:init(Args, Machine); +dispatch(Handler, timeout, _BinArgs, Machine) -> + Handler:process_signal(timeout, Machine); +dispatch(Handler, notify, BinArgs, Machine) -> + Args = decode_term(BinArgs), + Handler:process_notification(Args, Machine); +dispatch(Handler, call, BinArgs, Machine) -> + case decode_term(BinArgs) of + {notify, Args} -> + Handler:process_notification(Args, Machine); + remove -> + #{events => [], action => remove, auxst => maps:get(aux_state, Machine)}; + Call -> + Handler:process_call(Call, Machine) + end; +dispatch(Handler, repair, BinArgs, Machine) -> + Args = decode_term(BinArgs), + case Handler:process_repair(Args, Machine) of + {error, Reason} -> + {error, Reason}; + Result when is_map(Result) -> + Result + end. + +marshal_process_result(Handler, LastEventID, {Response, Result}) when is_map(Result) -> + Intent = marshal_intent(Handler, LastEventID, Result), + {ok, Intent#{response => encode_term(Response)}}; +marshal_process_result(Handler, LastEventID, Result) when is_map(Result) -> + {ok, marshal_intent(Handler, LastEventID, Result)}; +marshal_process_result(_Handler, _LastEventID, {error, Reason}) -> + {error, encode_term(Reason)}. + +marshal_intent(Handler, LastEventID, Result) when is_map(Result) -> + Base0 = #{events => marshal_new_events(Handler, LastEventID, maps:get(events, Result, []))}, + Base1 = + case maps:get(action, Result, idle) of + idle -> + Base0; + Action -> + Base0#{action => Action} + end, + case maps:is_key(auxst, Result) of + true -> + Base1#{aux_state => marshal_aux_state(Handler, maps:get(auxst, Result))}; + false -> + Base1 + end. + +%% Internals — progressor <-> machine + +unmarshal_machine(Handler, NS, #{process_id := ID, history := RawHistory} = Process) -> + Range = range_from_process(Process), + History = [unmarshal_event(Handler, Ev) || Ev <- RawHistory], + AuxState = unmarshal_aux_state(Handler, maps:get(aux_state, Process, undefined)), + #{ + namespace => NS, + id => ID, + history => History, + aux_state => AuxState, + range => Range + }. + +unmarshal_event(Handler, #{ + event_id := EventID, + timestamp := TsMicroSec, + payload := Payload +}) -> + Body = unmarshal_event_body(Handler, Payload), + {EventID, event_timestamp_to_datetime(TsMicroSec), Body}; +unmarshal_event(_Handler, #{event_id := EventID} = Ev) -> + erlang:error({missing_event_payload, EventID, maps:keys(Ev)}). + +marshal_new_events(Handler, LastEventID, Bodies) -> + %% One timestamp for the whole batch (as the old emit_events did). + BatchTs = timestamp(), + StorageTs = erlang:system_time(microsecond), + lists:zipwith( + fun(EventID, Body) -> + {Format, Bin} = marshal_event_body(Handler, BatchTs, Body), + #{ + event_id => EventID, + timestamp => StorageTs, + metadata => event_metadata(Format), + payload => Bin + } + end, + lists:seq(LastEventID + 1, LastEventID + length(Bodies)), + Bodies + ). + +marshal_event_body(Handler, Timestamp, Body) -> + Handler:marshal_event_body(Timestamp, Body). + +-spec unmarshal_event_body(module(), binary()) -> event_body(). +unmarshal_event_body(Handler, Payload) -> + Handler:unmarshal_event_body(Payload). + +marshal_aux_state(Handler, AuxSt) -> + Handler:marshal_aux_state(AuxSt). + +unmarshal_aux_state(_Handler, undefined) -> + undefined; +unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> + Handler:unmarshal_aux_state(Bin). + +%% Write both legacy keys: old HG reader expects <<"format_version">>, +%% old FF reader expects <<"format">>. Keeping both keeps rollback safe for +%% both stacks and feeds the event sink (prg_notifier reads <<"format_version">>). +event_metadata(undefined) -> + event_metadata(0); +event_metadata(Format) when is_integer(Format) -> + #{<<"format_version">> => Format, <<"format">> => Format}. + +%% Already in machinery format {datetime, micro}. +event_timestamp_to_datetime({{{_, _, _}, {_, _, _}}, Micro} = DtMicro) when is_integer(Micro) -> + DtMicro; +%% Bare datetime (defensive) — assume zero microseconds. +event_timestamp_to_datetime({{_, _, _}, {_, _, _}} = Dt) -> + {Dt, 0}; +%% Integer timestamp stored by progressor — split into seconds + microseconds. +event_timestamp_to_datetime(Ts) when is_integer(Ts) -> + {Seconds, Micro} = prg_utils:split_timestamp(Ts), + {calendar:system_time_to_universal_time(Seconds, second), Micro}. + +dispatch_apply_event(Handler, EventID, Ts, Body, Model) -> + Handler:apply_event(EventID, Ts, Body, Model). + +initial_model(_Handler, AuxState) when is_map(AuxState) -> + maps:get(model, AuxState, undefined); +initial_model(_Handler, _AuxState) -> + undefined. + +%% RPC / terms + +request(NS, ID, Args, Range) -> + genlib_map:compact(#{ + ns => NS, + id => ID, + args => encode_term(Args), + context => encode_rpc_context(), + range => Range + }). + +encode_rpc_context() -> + WoodyContext = op_context:current_woody_context(), + encode_term(woody_rpc_helper:encode_rpc_context(WoodyContext, otel_ctx:get_current())). + +decode_rpc_context(<<>>) -> + woody_rpc_helper:decode_rpc_context(#{}); +decode_rpc_context(Bin) -> + woody_rpc_helper:decode_rpc_context(decode_term(Bin)). + +run_scoped(#{ns := NS}, WoodyCtx, Fun) when is_function(Fun, 0) -> + Binding = op_context:binding_for_namespace(NS), + ok = op_context:env_enter(WoodyCtx, Binding), + try + Fun() + after + safe_env_leave(Binding) + end. + +safe_env_leave(Binding) -> + try + op_context:env_leave(Binding) + catch + Class:Reason:Stacktrace -> + logger:error( + "prg_machine env_leave failed: ~p:~p", + [Class, Reason], + #{stacktrace => Stacktrace} + ) + end. + +encode_term(Term) -> + term_to_binary(Term). + +decode_term(Term) when is_binary(Term) -> + case binary_to_term(Term) of + %% Legacy double envelope: old hg_machine wrote + %% term_to_binary({bin, term_to_binary(Args)}) for call/init args. + {bin, Bin} when is_binary(Bin) -> + binary_to_term(Bin); + Decoded -> + Decoded + end; +decode_term(Term) -> + Term. + +encode_range(After, Limit, Direction) -> + genlib_map:compact(#{ + offset => After, + limit => Limit, + direction => Direction + }). + +range_from_process(#{range := Range = #{}}) -> + Range; +range_from_process(_) -> + #{direction => forward}. + +map_client_error(NS, Error) -> + map_client_error(common, NS, Error). + +map_client_error(_Type, NS, {error, <<"namespace not found">>}) -> + {error, {unknown_namespace, NS}}; +map_client_error(_Type, _NS, {error, <<"timeout">>}) -> + {error, timeout}; +map_client_error(repair, _NS, {error, Reason}) -> + {error, {repair, {failed, decode_term(Reason)}}}; +map_client_error(_Type, _NS, Error) -> + Error. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-define(TEST_NS, env_test_ns). +-define(TEST_FF_NS, 'ff/env_test_ns'). +-define(TABLE, prg_machine_dispatch). + +-spec test() -> _. + +-spec context_binding_scopes_process_test_() -> _. +context_binding_scopes_process_test_() -> + {setup, fun setup_env_hook_test/0, fun cleanup_env_hook_test/1, [ + ?_test(context_binding_scopes_process(hellgate)), + ?_test(context_binding_scopes_process(fistful)) + ]}. + +-spec aux_state_runtime_test_() -> _. +aux_state_runtime_test_() -> + {setup, fun setup_aux_state_test/0, fun cleanup_aux_state_test/1, [ + ?_test(marshal_intent_omits_aux_state_without_auxst()), + ?_test(collapse_survives_non_map_aux_state()), + ?_test(business_exception_then_signal_does_not_corrupt_aux_state()), + ?_test(notify_noop_omits_aux_state()) + ]}. + +-spec registry_runtime_test_() -> _. +registry_runtime_test_() -> + {setup, fun setup_registry_test/0, fun cleanup_registry_test/1, [ + ?_test(lookup_unknown_namespace_returns_error()), + ?_test(process_unknown_namespace_returns_error()) + ]}. + +-spec process_exception_test_() -> _. +process_exception_test_() -> + {setup, fun setup_aux_state_test/0, fun cleanup_aux_state_test/1, [ + ?_test(process_crash_conforms_progressor_exception()) + ]}. + +-spec context_binding_scopes_process(hellgate | fistful) -> _. +context_binding_scopes_process(Scope) -> + ok = ensure_woody_available(), + ok = prg_machine_env_mock_context:reset(), + NS = + case Scope of + hellgate -> ?TEST_NS; + fistful -> ?TEST_FF_NS + end, + ?assertMatch({ok, _}, run_env_hook_process(#{ns => NS})), + ?assertEqual([{context_bound, Scope}], prg_machine_env_mock_context:events()). + +-spec setup_env_hook_test() -> ok. +setup_env_hook_test() -> + _ = application:load(prg_machine), + {ok, _} = application:ensure_all_started(gproc), + {ok, _} = application:ensure_all_started(snowflake), + {ok, _} = application:ensure_all_started(woody), + {ok, _} = application:ensure_all_started(scoper), + {ok, _} = application:ensure_all_started(party_client), + {ok, _} = application:ensure_all_started(opentelemetry_api), + {ok, _} = application:ensure_all_started(opentelemetry), + {ok, _} = application:ensure_all_started(op_context), + _ = ensure_env_hook_dispatch_table(), + true = ets:insert(?TABLE, [ + {?TEST_NS, prg_machine_env_mock_handler}, + {?TEST_FF_NS, prg_machine_env_mock_handler} + ]), + ok = prg_machine_env_mock_context:reset(), + ok. + +-spec cleanup_env_hook_test(_) -> ok. +cleanup_env_hook_test(_) -> + _ = ets:delete(?TABLE, ?TEST_NS), + _ = ets:delete(?TABLE, ?TEST_FF_NS), + ok = op_context:cleanup(op_context:key(hellgate), lenient), + ok = op_context:cleanup(fistful), + ok. + +-spec ensure_woody_available() -> ok. +ensure_woody_available() -> + {ok, _} = application:ensure_all_started(snowflake), + _ = woody_context:new(), + ok. + +-spec ensure_env_hook_dispatch_table() -> ok. +ensure_env_hook_dispatch_table() -> + prg_machine_registry:ensure_table(). + +-spec run_env_hook_process(process_options()) -> _. +run_env_hook_process(Opts) -> + run_env_hook_process(Opts, <<>>). + +-spec run_env_hook_process(process_options(), binary()) -> _. +run_env_hook_process(Opts, BinCtx) -> + Process = #{ + process_id => <<"env-hook-test">>, + last_event_id => 0, + history => [], + aux_state => undefined + }, + process({init, term_to_binary(#{}), Process}, Opts, BinCtx). + +-define(AUX_STATE_TEST_NS, aux_state_test_ns). + +-spec setup_aux_state_test() -> ok. +setup_aux_state_test() -> + _ = application:load(prg_machine), + {ok, _} = application:ensure_all_started(progressor), + _ = ensure_env_hook_dispatch_table(), + true = ets:insert(?TABLE, {?AUX_STATE_TEST_NS, prg_machine_aux_state_test_handler}), + ok. + +-spec cleanup_aux_state_test(_) -> ok. +cleanup_aux_state_test(_) -> + _ = ets:delete(?TABLE, ?AUX_STATE_TEST_NS), + ok. + +-spec marshal_intent_omits_aux_state_without_auxst() -> _. +marshal_intent_omits_aux_state_without_auxst() -> + Intent = marshal_intent(prg_machine_aux_state_test_handler, 0, #{}), + ?assertNot(maps:is_key(aux_state, Intent)), + ?assertEqual([], maps:get(events, Intent)). + +-spec collapse_survives_non_map_aux_state() -> _. +collapse_survives_non_map_aux_state() -> + Machine = #{ + namespace => ?AUX_STATE_TEST_NS, + id => <<"collapse-test">>, + history => [], + aux_state => {corrupt, undefined} + }, + ?assertEqual(undefined, collapse(prg_machine_aux_state_test_handler, Machine)). + +-spec business_exception_then_signal_does_not_corrupt_aux_state() -> _. +business_exception_then_signal_does_not_corrupt_aux_state() -> + Opts = #{ns => ?AUX_STATE_TEST_NS}, + Process0 = #{ + process_id => <<"invoice-exception-test">>, + last_event_id => 0, + history => [], + aux_state => undefined + }, + {ok, InitIntent} = process({init, term_to_binary(#{}), Process0}, Opts, <<>>), + ?assert(maps:is_key(aux_state, InitIntent)), + AuxAfterInit = maps:get(aux_state, InitIntent), + Process1 = Process0#{ + aux_state => AuxAfterInit, + last_event_id => 0 + }, + {ok, ExceptionIntent} = process( + {call, term_to_binary(business_exception), Process1}, + Opts, + <<>> + ), + ?assertNot(maps:is_key(aux_state, ExceptionIntent)), + Process2 = Process1#{aux_state => AuxAfterInit}, + {ok, TimeoutIntent} = process({timeout, <<>>, Process2}, Opts, <<>>), + ?assertNot(maps:is_key(aux_state, TimeoutIntent)), + {ok, RecheckIntent} = process( + {call, term_to_binary(recheck), Process2}, + Opts, + <<>> + ), + ?assert(maps:is_key(aux_state, RecheckIntent)), + Rechecked = binary_to_term(maps:get(aux_state, RecheckIntent), [safe]), + ?assertEqual(#{model => initialized}, Rechecked). + +-spec notify_noop_omits_aux_state() -> _. +notify_noop_omits_aux_state() -> + Opts = #{ns => ?AUX_STATE_TEST_NS}, + AuxBin = prg_machine_aux_state_test_handler:marshal_aux_state(#{model => initialized}), + Process = #{ + process_id => <<"notify-test">>, + last_event_id => 0, + history => [], + aux_state => AuxBin + }, + {ok, NotifyIntent} = process({notify, term_to_binary(#{payload => test}), Process}, Opts, <<>>), + ?assertNot(maps:is_key(aux_state, NotifyIntent)). + +-spec setup_registry_test() -> ok. +setup_registry_test() -> + ok = prg_machine_registry:ensure_table(), + ok. + +-spec cleanup_registry_test(_) -> ok. +cleanup_registry_test(_) -> + ok. + +-spec lookup_unknown_namespace_returns_error() -> _. +lookup_unknown_namespace_returns_error() -> + ?assertEqual( + {error, {unknown_namespace, unknown_ns}}, + prg_machine_registry:lookup(unknown_ns) + ). + +-spec process_unknown_namespace_returns_error() -> _. +process_unknown_namespace_returns_error() -> + Opts = #{ns => unknown_ns}, + Process = #{ + process_id => <<"unknown-ns-test">>, + last_event_id => 0, + history => [], + aux_state => undefined + }, + ?assertEqual( + {error, {unknown_namespace, unknown_ns}}, + process({init, term_to_binary(#{}), Process}, Opts, <<>>) + ). + +-spec process_crash_conforms_progressor_exception() -> _. +process_crash_conforms_progressor_exception() -> + Opts = #{ns => ?AUX_STATE_TEST_NS}, + Process = #{ + process_id => <<"crash-test">>, + last_event_id => 0, + history => [], + aux_state => undefined + }, + {ok, _} = process({init, term_to_binary(#{}), Process}, Opts, <<>>), + ?assertEqual( + {error, {exception, error, deliberate_crash}}, + process({call, term_to_binary(crash), Process}, Opts, <<>>) + ). + +%% --- Golden tests: legacy format compatibility (stage 1) ------------------- + +-spec event_metadata_writes_both_keys_test() -> _. +event_metadata_writes_both_keys_test() -> + %% Old HG reader expects <<"format_version">>, old FF reader expects + %% <<"format">>; we must keep both so a rollback to either stack still reads. + ?assertEqual(#{<<"format_version">> => 1, <<"format">> => 1}, event_metadata(1)), + ?assertEqual(#{<<"format_version">> => 0, <<"format">> => 0}, event_metadata(undefined)). + +-spec decode_term_reads_legacy_double_envelope_test() -> _. +decode_term_reads_legacy_double_envelope_test() -> + Args = #{<<"some">> => <<"args">>, n => 42}, + %% Legacy hg_machine wrapped call/init args as + %% term_to_binary({bin, term_to_binary(Args)}). + Legacy = term_to_binary({bin, term_to_binary(Args)}), + ?assertEqual(Args, decode_term(Legacy)), + %% New single envelope still works (rollback/forward invariant). + ?assertEqual(Args, decode_term(encode_term(Args))), + %% A genuine {bin, Bin} payload that is not double-wrapped term is returned + %% as the inner term only when the inner binary decodes — guard keeps us safe + %% for non-binary tuples. + ?assertEqual({bin, not_a_binary}, decode_term(term_to_binary({bin, not_a_binary}))). + +-endif. diff --git a/apps/prg_machine/src/prg_machine_registry.erl b/apps/prg_machine/src/prg_machine_registry.erl new file mode 100644 index 00000000..09e3548c --- /dev/null +++ b/apps/prg_machine/src/prg_machine_registry.erl @@ -0,0 +1,73 @@ +-module(prg_machine_registry). + +%%% Namespace -> handler module registry (ETS owner). + +-behaviour(gen_server). + +-define(TABLE, prg_machine_dispatch). +-define(SERVER, ?MODULE). + +-export([get_child_spec/1]). +-export([start_link/1]). +-export([lookup/1]). +-export([ensure_table/0]). + +-export([init/1, handle_call/3, handle_cast/2, handle_info/2]). + +-record(state, { + handlers :: [module()] +}). + +-type state() :: #state{}. + +-spec get_child_spec([module()]) -> supervisor:child_spec(). +get_child_spec(Handlers) -> + #{ + id => prg_machine_registry, + start => {?MODULE, start_link, [Handlers]}, + restart => permanent, + shutdown => 5000, + type => worker, + modules => [?MODULE] + }. + +-spec start_link([module()]) -> {ok, pid()} | {error, term()}. +start_link(Handlers) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, Handlers, []). + +-spec lookup(prg_machine:namespace()) -> {ok, module()} | {error, {unknown_namespace, prg_machine:namespace()}}. +lookup(NS) -> + case ets:lookup(?TABLE, NS) of + [{NS, Handler}] -> + {ok, Handler}; + [] -> + {error, {unknown_namespace, NS}} + end. + +-spec ensure_table() -> ok. +ensure_table() -> + case ets:info(?TABLE) of + undefined -> + _ = ets:new(?TABLE, [named_table, set, protected, {read_concurrency, true}]), + ok; + _ -> + ok + end. + +-spec init([module()]) -> {ok, state()}. +init(Handlers) -> + ok = ensure_table(), + true = ets:insert(?TABLE, [{prg_machine:handler_namespace(H), H} || H <- Handlers]), + {ok, #state{handlers = Handlers}}. + +-spec handle_call(term(), {pid(), term()}, state()) -> {reply, term(), state()}. +handle_call(_Request, _From, State) -> + {reply, {error, unsupported}, State}. + +-spec handle_cast(term(), state()) -> {noreply, state()}. +handle_cast(_Msg, State) -> + {noreply, State}. + +-spec handle_info(term(), state()) -> {noreply, state()}. +handle_info(_Info, State) -> + {noreply, State}. diff --git a/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl b/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl new file mode 100644 index 00000000..b2789eb5 --- /dev/null +++ b/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl @@ -0,0 +1,86 @@ +-module(prg_machine_aux_state_test_handler). + +%%% Test handler: aux_state corruption scenarios (H1/H2/M1). + +-behaviour(prg_machine). + +-export([ + namespace/0, + init/2, + process_signal/2, + process_call/2, + process_repair/2, + process_notification/2, + marshal_event_body/2, + unmarshal_event_body/1, + marshal_aux_state/1, + unmarshal_aux_state/1, + apply_event/4 +]). + +-define(NS, aux_state_test_ns). + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). +init(_Args, _Machine) -> + #{ + events => [], + action => idle, + auxst => #{model => initialized} + }. + +-spec process_signal(prg_machine:signal(), prg_machine:machine()) -> prg_machine:result(). +process_signal(timeout, Machine) -> + _ = prg_machine:collapse(?MODULE, Machine), + #{events => [], action => idle}. + +-spec process_call(prg_machine:call(), prg_machine:machine()) -> + {prg_machine:response(), prg_machine:result()}. +process_call(business_exception, _Machine) -> + {{exception, {business, rejected}}, #{}}; +process_call(crash, _Machine) -> + erlang:error(deliberate_crash); +process_call(recheck, Machine) -> + Model = prg_machine:collapse(?MODULE, Machine), + {ok, #{events => [], action => idle, auxst => #{model => Model}}}. + +-spec process_repair(prg_machine:args(), prg_machine:machine()) -> prg_machine:result() | {error, term()}. +process_repair(_Args, _Machine) -> + #{events => [], action => idle}. + +-spec process_notification(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {undefined, binary()}. +marshal_event_body(_Timestamp, Body) -> + {undefined, term_to_binary(Body)}. + +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + binary_to_term(Payload, [safe]). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(undefined) -> + %% Mimics hg_invoice: marshaling undefined yields a non-empty corrupting binary. + term_to_binary({corrupt, undefined}); +marshal_aux_state(AuxSt) -> + term_to_binary(AuxSt). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(<<>>) -> + #{}; +unmarshal_aux_state(Bin) when is_binary(Bin) -> + binary_to_term(Bin, [safe]). + +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + prg_machine:event_body(), + term() +) -> term(). +apply_event(_EventID, _Ts, _Body, Model) -> + Model. diff --git a/apps/prg_machine/test/prg_machine_env_mock_context.erl b/apps/prg_machine/test/prg_machine_env_mock_context.erl new file mode 100644 index 00000000..7b8d5fb7 --- /dev/null +++ b/apps/prg_machine/test/prg_machine_env_mock_context.erl @@ -0,0 +1,17 @@ +-module(prg_machine_env_mock_context). + +-export([reset/0, events/0, record/1]). + +-spec reset() -> ok. +reset() -> + persistent_term:put({?MODULE, events}, []), + ok. + +-spec events() -> [{context_bound, op_context:scope()}]. +events() -> + persistent_term:get({?MODULE, events}, []). + +-spec record({context_bound, op_context:scope()}) -> ok. +record(Event) -> + Events = persistent_term:get({?MODULE, events}, []), + persistent_term:put({?MODULE, events}, Events ++ [Event]). diff --git a/apps/prg_machine/test/prg_machine_env_mock_handler.erl b/apps/prg_machine/test/prg_machine_env_mock_handler.erl new file mode 100644 index 00000000..f67c338f --- /dev/null +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -0,0 +1,76 @@ +-module(prg_machine_env_mock_handler). + +-behaviour(prg_machine). + +-export([ + namespace/0, + init/2, + process_signal/2, + process_call/2, + process_repair/2, + process_notification/2, + marshal_event_body/2, + unmarshal_event_body/1, + marshal_aux_state/1, + unmarshal_aux_state/1, + apply_event/4 +]). + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + env_test_ns. + +-spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). +init(_Args, #{namespace := NS}) -> + Scope = op_context:scope_for_namespace(NS), + try + _ = op_context:load(op_context:key(Scope)), + prg_machine_env_mock_context:record({context_bound, Scope}) + catch + _:_ -> + ok + end, + #{events => [], action => idle}. + +-spec process_signal(prg_machine:signal(), prg_machine:machine()) -> prg_machine:result(). +process_signal(_Signal, _Machine) -> + #{events => [], action => idle}. + +-spec process_call(prg_machine:call(), prg_machine:machine()) -> {prg_machine:response(), prg_machine:result()}. +process_call(_Call, _Machine) -> + {ok, #{events => [], action => idle}}. + +-spec process_repair(prg_machine:args(), prg_machine:machine()) -> prg_machine:result() | {error, term()}. +process_repair(_Args, _Machine) -> + #{events => [], action => idle}. + +-spec process_notification(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec marshal_event_body(prg_machine:timestamp(), prg_machine:event_body()) -> {undefined, binary()}. +marshal_event_body(_Timestamp, Body) -> + {undefined, term_to_binary(Body)}. + +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + binary_to_term(Payload, [safe]). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + term_to_binary(AuxSt). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(<<>>) -> + #{}; +unmarshal_aux_state(Bin) when is_binary(Bin) -> + binary_to_term(Bin, [safe]). + +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + prg_machine:event_body(), + term() +) -> term(). +apply_event(_EventID, _Ts, _Body, Model) -> + Model. diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index 64adecfb..79bb7bf8 100644 --- a/apps/routing/src/hg_route_collector.erl +++ b/apps/routing/src/hg_route_collector.erl @@ -60,11 +60,11 @@ fill_blacklist(_BlCtx, []) -> fill_blacklist(BlCtx, [Route]) -> [hg_inspector:fill_blacklist(Route, BlCtx)]; fill_blacklist(BlCtx, Routes) -> - HgContext = hg_context:load(), + HgContext = op_context:load(op_context:key(hellgate)), try genlib_pmap:map( fun(Route) -> - ok = hg_context:save(HgContext), + ok = op_context:save(op_context:key(hellgate), HgContext), hg_inspector:fill_blacklist(Route, BlCtx) end, Routes, @@ -272,9 +272,9 @@ acceptable_terminal(Predestination, Route, VS0, Revision) -> end. get_party_client() -> - HgContext = hg_context:load(), - Client = hg_context:get_party_client(HgContext), - Context = hg_context:get_party_client_context(HgContext), + HgContext = op_context:load(op_context:key(hellgate)), + Client = op_context:get_party_client(HgContext), + Context = op_context:get_party_client_context(HgContext), {Client, Context}. maybe_currency_conversion( @@ -494,7 +494,7 @@ setup_fill_blacklist_test() -> -spec cleanup_fill_blacklist_test(_) -> ok. cleanup_fill_blacklist_test(_Ok) -> try - hg_context:cleanup() + op_context:cleanup(hellgate) catch _:_ -> ok end, @@ -507,15 +507,15 @@ cleanup_fill_blacklist_test(_Ok) -> -spec fill_blacklist_preserves_hg_context_in_workers() -> _. fill_blacklist_preserves_hg_context_in_workers() -> - HgCtx = hg_context:create(#{woody_context => woody_context:new()}), - ok = hg_context:save(HgCtx), + HgCtx = op_context:create(#{woody_context => woody_context:new()}), + ok = op_context:save(op_context:key(hellgate), HgCtx), Parent = self(), Routes = [test_route(N) || N <- [1, 2, 3]], BlCtx = test_blacklist_context(hd(Routes)), Ref = make_ref(), ok = meck:new(hg_inspector, [passthrough]), ok = meck:expect(hg_inspector, fill_blacklist, fun(Route, _BlCtx) -> - ?assertEqual(HgCtx, hg_context:load()), + ?assertEqual(HgCtx, op_context:load(op_context:key(hellgate))), Parent ! {worker_done, self(), Ref}, Route end), @@ -526,15 +526,15 @@ fill_blacklist_preserves_hg_context_in_workers() -> ?assert(lists:all(fun(Pid) -> Pid =/= Parent end, WorkerPids)) after ok = meck:unload(hg_inspector), - ok = hg_context:cleanup() + ok = op_context:cleanup(hellgate) end. -spec fill_blacklist_timeout_raises_transient_error() -> _. fill_blacklist_timeout_raises_transient_error() -> Routes = [test_route(1), test_route(2)], BlCtx = test_blacklist_context(hd(Routes)), - HgCtx = hg_context:create(#{woody_context => woody_context:new()}), - ok = hg_context:save(HgCtx), + HgCtx = op_context:create(#{woody_context => woody_context:new()}), + ok = op_context:save(op_context:key(hellgate), HgCtx), PrevTimeout = application:get_env(hellgate, inspect_timeout, infinity), PrevLimit = application:get_env(hellgate, inspect_parallel_limit, 10), ok = application:set_env(hellgate, inspect_timeout, 100), @@ -553,7 +553,7 @@ fill_blacklist_timeout_raises_transient_error() -> ok = application:set_env(hellgate, inspect_timeout, PrevTimeout), ok = application:set_env(hellgate, inspect_parallel_limit, PrevLimit), ok = meck:unload(hg_inspector), - ok = hg_context:cleanup() + ok = op_context:cleanup(hellgate) end. -spec collect_worker_pids(reference(), non_neg_integer()) -> [pid()]. diff --git a/config/sys.config b/config/sys.config index 0812ea52..8647be8e 100644 --- a/config/sys.config +++ b/config/sys.config @@ -362,11 +362,9 @@ {namespaces, #{ 'invoice' => #{ processor => #{ - client => hg_progressor, + client => prg_machine, options => #{ - party_client => #{}, - ns => <<"invoice">>, - handler => hg_machine + ns => invoice } }, storage => #{ @@ -381,111 +379,50 @@ }, 'invoice_template' => #{ processor => #{ - client => hg_progressor, + client => prg_machine, options => #{ - party_client => #{}, - ns => <<"invoice_template">>, - handler => hg_machine + ns => invoice_template } }, worker_pool_size => 5 }, - 'customer' => #{ - processor => #{ - client => hg_progressor, - options => #{ - party_client => #{}, - ns => <<"customer">>, - handler => hg_machine - } - }, - worker_pool_size => 5 - }, - 'recurrent_paytools' => #{ - processor => #{ - client => hg_progressor, - options => #{ - party_client => #{}, - ns => <<"recurrent_paytools">>, - handler => hg_machine - } - }, - worker_pool_size => 5 - }, - 'ff/identity' => #{ - processor => #{ - client => machinery_prg_backend, - options => #{ - namespace => 'ff/identity', - %% TODO Party client create - handler => {fistful, #{handler => ff_identity_machine, party_client => #{}}}, - schema => ff_identity_machinery_schema - } - } - }, - 'ff/wallet_v2' => #{ - processor => #{ - client => machinery_prg_backend, - options => #{ - namespace => 'ff/wallet_v2', - %% TODO Party client create - handler => {fistful, #{handler => ff_wallet_machine, party_client => #{}}}, - schema => ff_wallet_machinery_schema - } - } - }, 'ff/source_v1' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/source_v1', - %% TODO Party client create - handler => {fistful, #{handler => ff_source_machine, party_client => #{}}}, - schema => ff_source_machinery_schema + ns => 'ff/source_v1' } } }, 'ff/destination_v2' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/destination_v2', - %% TODO Party client create - handler => {fistful, #{handler => ff_destination_machine, party_client => #{}}}, - schema => ff_destination_machinery_schema + ns => 'ff/destination_v2' } } }, 'ff/deposit_v1' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/deposit_v1', - %% TODO Party client create - handler => {fistful, #{handler => ff_deposit_machine, party_client => #{}}}, - schema => ff_deposit_machinery_schema + ns => 'ff/deposit_v1' } } }, 'ff/withdrawal_v2' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/withdrawal_v2', - %% TODO Party client create - handler => {fistful, #{handler => ff_withdrawal_machine, party_client => #{}}}, - schema => ff_withdrawal_machinery_schema + ns => 'ff/withdrawal_v2' } } }, 'ff/withdrawal/session_v2' => #{ processor => #{ - client => machinery_prg_backend, + client => prg_machine, options => #{ - namespace => 'ff/withdrawal/session_v2', - %% TODO Party client create - handler => {fistful, #{handler => ff_withdrawal_session_machine, party_client => #{}}}, - schema => ff_withdrawal_session_machinery_schema + ns => 'ff/withdrawal/session_v2' } } } diff --git a/docs/prg-machine.md b/docs/prg-machine.md new file mode 100644 index 00000000..5203b3f2 --- /dev/null +++ b/docs/prg-machine.md @@ -0,0 +1,263 @@ +# `prg_machine` в Hellgate / Fistful + +Единый runtime поверх progressor для HG и FF. Контракт `action()` в progressor: `progressor/docs/step-effect-migration.md`. + +Бэклог упрощений после миграции: [refactor-local.md](refactor-local.md) (понятные задачи), [refactor-architecture.md](refactor-architecture.md) (крупные итерации). + +*Обновлено: 2026-06-17.* + +--- + +## 1. Поток данных + +``` +woody handler (hg_*_handler, ff_*_handler) + → prg_machine:start | call | get | repair | notify | remove + → progressor + → prg_machine:process/3 + → domain handler (-behaviour(prg_machine)) +``` + +```mermaid +sequenceDiagram + participant WH as woody handler + participant PM as prg_machine + participant PR as progressor + participant DM as domain handler + + WH->>PM: call(NS, ID, Args) + PM->>PR: progressor:call + PR->>PM: process({call, ...}) + PM->>DM: process_call + DM-->>PM: {Response, #{events, action, auxst}} + PM-->>PR: marshal_intent + PR-->>WH: decode_term +``` + +**Убрано из prod:** `hg_machine`, `ff_machine`, `hg_progressor_handler`, `machinery_prg_backend` в `config/sys.config`, `progressor_action`, legacy map в intent, отдельные модули `prg_machine_client` / `prg_machine_processor` / `prg_machine_events` / `prg_machine_env` / `prg_machine_codec` (логика сведена в `prg_machine.erl`). + +**Trace (FF only):** HTTP JSON через `ff_machine_handler` → `ff_machine_trace` (не client API `prg_machine`). Thrift — `docs/trace-api-thrift.md`. + +**Вне scope:** `{machinery, …}` в `rebar.config` — только docker-sidecar тесты (`test/bender`, `test/party-management`). + +--- + +## 2. Модули `apps/prg_machine` + +| Модуль | Роль | +|--------|------| +| `prg_machine` | behaviour, client API (`start` / `call` / `get` / `repair` / `notify` / `remove`), `process/3`, `collapse` / `emit_events`, term codec, event marshal, env scoping | +| `prg_machine_registry` | ETS `{Namespace, Handler}`; owner-процесс + `get_child_spec/1` | +| `prg_action` | `{timeout, Sec}` / `{deadline, Dt}` → wire `action()`; scheduling helpers | +| `ff_machine_lib` | общие FF-хелперы: create/get/repair/history, `to_prg_result`, event/aux_state codec | + +Связанные модули вне приложения: `op_context` (woody/party context, `env_enter` / `env_leave`, `current_woody_context/0`), `ff_machine_codec` (FF event/aux_state marshal, legacy sniff). + +### `process/3` + +``` +registry lookup + → decode_rpc_context + attach otel + → ensure_deadline_set (default 30s, `default_handling_timeout` в opts) + → run_scoped: op_context:env_enter / env_leave (если `context_binding` в opts) + → unmarshal_machine + → dispatch (init | call | repair | notify | timeout) + → marshal_process_result +``` + +- Неизвестный namespace → `{error, {unknown_namespace, NS}}`. +- Исключение в домене → `{error, {exception, Class, Reason}}` + log (stacktrace только в логах). +- `env_leave` в `after`, ошибки leave логируются, не маскируют результат dispatch. + +**Контекст RPC:** `op_context:current_woody_context/0` — hellgate-binding, затем fistful-binding, иначе fresh ctx + warning (заменяет старый `woody_context_loader` app-env hook). В `process/3` — `env_enter`/`env_leave` по `context_binding` из `sys.config` (HG strict / FF lenient). + +**События:** timestamp пишется в progressor как microsecond integer; при чтении — machinery-формат `{calendar:datetime(), Micro}`. Metadata пишет оба ключа `<<"format_version">>` и `<<"format">>`. FF payload — legacy `term_to_binary({bin, ThriftBin})`; HG payload — `term_to_binary(msgpack)`. + +**aux_state:** домен возвращает ключ `auxst`; в intent progressor пишется `aux_state` **только** при явном `auxst` в result map. Пропуск ключа сохраняет существующий aux_state в storage (важно после business-exception и noop notify). + +--- + +## 3. Контракт домена + +### Callbacks + +`namespace/0`, `init/2`, `process_signal/2`, `process_call/2`, `process_repair/2`, marshal/unmarshal event + aux_state. Опционально: `process_notification/2`. + +`collapse/2` вызывает только канонический `apply_event/4`: + +```erlang +apply_event(EventID, Timestamp, EventBody, Model) +``` + +Legacy-формы fold'а адаптируются в доменном модуле. Runtime больше не выбирает между `apply_event/2` и `apply_event/4`. + +### `result()` + +```erlang +#{ + events => [EventBody, ...], + action => action(), %% progressor.hrl; omit = idle + auxst => term() %% ключ пишется только при явном обновлении +} +``` + +### Wire `action()` + +```erlang +idle | suspend | timeout | remove +| {schedule, #{at := timestamp_us(), action := timeout | remove}} +``` + +| Было (legacy) | Стало | +|---------------|-------| +| `instant()` / `set_timeout(0, _)` | `timeout` | +| `unset_timer` | `suspend` | +| `remove()` | `remove` | +| `set_timeout(N, _)` / deadline | `prg_action:schedule_timer/1`, `schedule_deadline/1` | + +`hg_invoice:construct_repair_action/1` — damsel `#repair_ComplexAction{}` → wire в HG repair. FF repairer `#repairer_ComplexAction{}` → wire в `ff_codec:unmarshal_repairer_complex_action/2`. `prg_action:marshal_timer/1` принимает `{deadline, {calendar:datetime(), Micro}}` (machinery-формат из `ff_codec:unmarshal(timer, ...)`). + +FF домен возвращает `prg_action:t()` напрямую; `*_machine` оборачивает через `ff_machine_lib:to_prg_result/1`. + +### Prod namespaces (7) + +| NS | Handler (registry) | Доменная логика | Registry | +|----|--------------------|-----------------|----------| +| `invoice` | `hg_invoice` | `hg_invoice` | `hellgate.erl` | +| `invoice_template` | `hg_invoice_template` | `hg_invoice_template` | `hellgate.erl` | +| `ff/deposit_v1` | `ff_deposit_machine` | `ff_deposit` | `ff_server.erl` | +| `ff/source_v1` | `ff_source_machine` | `ff_source` | `ff_server.erl` | +| `ff/destination_v2` | `ff_destination_machine` | `ff_destination` | `ff_server.erl` | +| `ff/withdrawal_v2` | `ff_withdrawal_machine` | `ff_withdrawal` | `ff_server.erl` | +| `ff/withdrawal/session_v2` | `ff_withdrawal_session_machine` | `ff_withdrawal_session` | `ff_server.erl` | + +HG регистрирует доменные модули напрямую. FF — тонкие `*_machine` (behaviour + codec + делегирование в домен). + +Orphan NS (`ff/identity`, `ff/wallet_v2`, HG `customer`, `recurrent_paytools`) убраны из config. + +### `sys.config` (шаблон) + +```erlang +processor => #{ + client => prg_machine, + options => #{ + ns => , + context_binding => #{registry_key => ..., cleanup_mode => strict | lenient}, + default_handling_timeout => 30000 %% optional, woody deadline default + } +} +``` + +--- + +## 4. Ошибки: три слоя + +| Слой | Пример | Где | +|------|--------|-----| +| **A** Progressor API | `{error, <<"process is waiting">>}` | `prg_machine:call` | +| **B** Processor response | `{ok, {error, invalid_callback}}` | домен в теле ответа | +| **C** Woody throw | `#payproc_InvoiceNotFound{}` | handler | + +Путаница A vs B — типичный источник регрессий после миграции с machinery. + +### `prg_machine` client API (целевой контракт) + +| Progressor | `call` / `start` | +|------------|------------------| +| `<<"process not found">>` / `<<"process is init">>` | `{error, notfound}` | +| `<<"process is error">>` | `{error, failed}` | +| `{exception, Class, Reason}` (3- или 4-tuple) | **pass-through** `{error, {exception, Class, Reason}}` (stacktrace срезается) | +| прочие guard (`<<"process is waiting">>`, …) | **pass-through** `{error, Reason}` | +| `<<"process already exists">>` (`start`) | `{error, exists}` | + +`get`: + `{error, {unknown_namespace, NS}}` при отсутствии handler в registry. + +`repair`: + `{error, working}` для `<<"process is running">>`; прочие term-reasons → `{error, {repair, {failed, Reason}}}`. + +**Антипаттерн:** catch-all `{error, _} -> {error, failed}` — ломает HG CT (waiting/running превращаются в `failed`). + +**Machinery (история):** guard-ошибки на call возвращались как `{ok, {error, Reason}}`. Полная эмуляция в `prg_machine` не нужна — достаточно pass-through + `{error, _} = Error -> Error` в `hg_invoicing_machine_client` и `ff_*_machine`. + +**Слой B** обрабатывается в домене (`hg_invoice:process_callback` → `{error, failed}` для `{ok, {error,_}}`), не в `prg_machine`. + +### FF CT: meck `prg_machine:process/3` + +```erlang +meck:new(prg_machine, [no_link, passthrough]), +meck:expect(prg_machine, process, fun process/3). +%% внутри: 'prg_machine_meck_original':process(Call, Opts, BinCtx). +``` + +Хелпер: `ff_ct_machine` (timeout hooks + passthrough). Processor crash в тестах: `{error, {exception, _, _}}`, не атом `failed`. + +### Runtime eunit в `prg_machine.erl` + +- `context_binding` scopes `env_enter`/`env_leave` +- aux_state omit при noop notify / business exception +- `{unknown_namespace, NS}` на lookup и process +- exception conform progressor wire format + +--- + +## 5. Техдолг + +### До релиза + +- Progressor: CHANGELOG + tag `vX.Y.0` +- Hellgate: bump tag в `rebar.config` (сейчас branch `add_action_module`) + +### HG invoice — двойной collapse + +Реплей: `prg_machine:collapse` (lenient). После call/signal/repair: `to_prg_result/1` → один `validate_changes` + `log_changes` (как старый `handle_result`). Остаётся отдельный strict-фолд `collapse_changes` **мимо** `prg_machine:collapse/2` при валидации новых changes — цель: один фолд с параметром strict/lenient. Только HG invoice; FF адаптирует старый fold за `apply_event/4`. + +### Прочее (низкий приоритет) + +- Registry без ETS `heir` — краткое окно при рестарте owner-процесса +- Фиктивная обёртка `{ev, Ts, Body}` в event payload +- Trace: сейчас HTTP JSON (`ff_machine_trace`); Thrift — `docs/trace-api-thrift.md` +- Единый конверт HG+FF (format 2) + +--- + +## 6. Новый namespace + +1. `sys.config` — `client => prg_machine`, `context_binding` +2. `-behaviour(prg_machine)` + callbacks (`apply_event/4` для `collapse/2`) +3. HG: доменный модуль в `prg_machine_registry:get_child_spec/1`. FF: тонкий `*_machine` + домен +4. Клиентский слой — только `prg_machine:*` (или `ff_machine_lib` для FF) +5. Handler в `get_child_spec` (`hellgate.erl` / `ff_server.erl`) +6. CT suite + +--- + +## 7. Grep-инварианты + +```bash +rg 'progressor_action|hg_machine_action' apps/ # 0 +rg '#{set_timer' apps/ --glob '*.erl' # 0 +rg 'machinery_prg_backend|ff_machine:' apps/fistful apps/ff_transfer apps/ff_server --glob '*.erl' # 0 +rg "client => machinery_prg_backend" config/sys.config # 0 +rg 'woody_context_loader' apps/hellgate apps/ff_server # 0 +rg 'prg_machine_client|prg_machine_processor|prg_machine_env' apps/ # 0 +``` + +--- + +## 8. Точки входа в коде + +| Путь | Зачем | +|------|-------| +| `apps/prg_machine/src/prg_machine.erl` | behaviour, client API, `process/3`, marshal_intent, eunit | +| `apps/prg_machine/src/prg_machine_registry.erl` | namespace → handler | +| `apps/prg_machine/src/prg_action.erl` | timer → wire | +| `apps/op_context/src/op_context.erl` | woody/party context, env scoping | +| `apps/ff_transfer/src/ff_machine_lib.erl` | FF repair/history/helpers | +| `apps/ff_transfer/src/ff_machine_codec.erl` | FF event/aux_state marshal, legacy sniff | +| `apps/hellgate/src/hg_invoice.erl` | HG behaviour, repair, `to_prg_result` | +| `apps/ff_transfer/src/ff_deposit_machine.erl` | FF thin handler (образец) | +| `apps/hellgate/src/hg_invoicing_machine_client.erl` | Thrift → prg_machine | +| `apps/ff_server/src/ff_machine_handler.erl` | HTTP trace routes | +| `apps/ff_transfer/src/ff_machine_trace.erl` | progressor trace → JSON | +| `apps/fistful/src/ff_repair.erl` | repair scenarios | +| `apps/ff_server/src/ff_codec.erl` | repair thrift unmarshal | +| `apps/ff_transfer/test/ff_ct_machine.erl` | meck hooks | diff --git a/docs/trace-api-thrift.md b/docs/trace-api-thrift.md new file mode 100644 index 00000000..12af6604 --- /dev/null +++ b/docs/trace-api-thrift.md @@ -0,0 +1,264 @@ +# Trace API: переход с JSON на Thrift + +Документ описывает текущую реализацию ручек получения трейсов из progressor в FF и HG, различия между ними и план перехода на Thrift как формат ответа. + +**Ограничения и допущения:** + +- Формат **Jaeger выпиливается полностью** (маршруты, unmarshaling, тесты). +- **Dual format / deprecation JSON не нужны** — функциональность не запущена и не используется в проде; можно сразу заменить JSON на Thrift. +- **Единый raw pipeline** (HG через machinery schema, как FF) — за скобками, отдельная задача. + +--- + +## 1. Общая цепочка данных + +Оба сервиса читают трейс из **progressor** (`progressor:trace/1` → `prg_storage:process_trace/3`). + +Сырой ответ — список **task unit**’ов (агрегация по `task_id` внутри progressor), у каждого unit примерно: + +| Поле | Смысл | +|------|--------| +| `task_id`, `task_type`, `task_status` | Идентификация и тип задачи (`init`, `call`, `repair`, `timeout`, …) | +| `scheduled`, `running`, `finished` | Тайминги (микросекунды) | +| `retry_attempts`, `retry_interval`, `task_metadata` | Ретраи и метаданные | +| `args`, `context`, `response` | Бинарные blob’ы (Erlang term / thrift на storage) | +| `events` | Список `{event_id, event_timestamp, event_payload, …}` | + +Дальше FF и HG расходятся в **декодировании** и **сериализации на HTTP**. + +--- + +## 2. FF (ff_server) + +### 2.1. Текущая схема + +| Слой | Модуль / путь | +|------|----------------| +| HTTP | Cowboy (не Woody): `GET /traces/internal/{entity}/{process_id}` | +| Handler | `apps/ff_server/src/ff_machine_handler.erl` | +| Домен | `ff_machine_handler` → `progressor:trace/1` (сырой trace из storage) | +| Декодирование | codec доменов (`ff_deposit_codec`, …) в handler | +| Ответ | `json:encode/1` (временный формат до Thrift) | + +**Зависимость:** bump тега `damsel` в `rebar.config` после релиза `progressor_trace.thrift` в damsel (см. комментарий у `{damsel, …}`). + +Маршруты (`ff_machine_handler:get_routes/0`): + +``` +/traces/internal/source_v1/:process_id → ff/source_v1 +/traces/internal/destination_v2/:process_id → ff/destination_v2 +/traces/internal/deposit_v1/:process_id → ff/deposit_v1 +/traces/internal/withdrawal_v2/:process_id → ff/withdrawal_v2 +/traces/internal/withdrawal_session_v2/:process_id → ff/withdrawal/session_v2 +``` + +Подключение: `apps/ff_server/src/ff_server.erl` → `additional_routes` ++ `ff_machine_handler:get_routes()`. + +### 2.2. Особенности + +- На **storage** события уже в Thrift (`machinery_mg_schema` + `ff_proto_utils:serialize/2`). +- На **выдаче в trace** типизация теряется: `json_compatible_value/1` превращает Erlang-термы в произвольные JSON-map’ы (base64 для бинарников, `~p` для неизвестного). +- Паттерн «нормального» API в FF — **Woody + Thrift** (`Management`, `Repairer` на `/v1/...`); trace — исключение. + +### 2.3. Пример контракта (из тестов) + +`apps/ff_server/test/ff_deposit_handler_SUITE.erl` — массив span’ов с `task_type`, `task_status`, `args`, `events` (`event_id`, `event_payload`, `event_timestamp`). + +--- + +## 3. HG (hellgate) + +### 3.1. Текущая схема + +| Слой | Модуль / путь | +|------|----------------| +| HTTP | Cowboy: `GET /traces/{format}/{entity}/{process_id}` | +| Handler | `apps/hg_progressor/src/hg_progressor_handler.erl` | +| Домен | Напрямую `progressor:trace/1` (**без** machinery schema) | +| Декодирование | Ручное в handler: `binary_to_term`, `hg_proto_utils:deserialize`, `term_to_object` | +| Ответ | `json:encode/1` | + +Маршруты: + +``` +/traces/{format}/invoice/:process_id +/traces/{format}/invoice_template/:process_id +``` + +`format`: `internal` | `jaeger` (валидация в `init/2`). + +Подключение: `apps/hellgate/src/hellgate.erl` → `hg_progressor_handler:get_routes()`. + +### 3.2. Особенности + +- **`internal`**: список task unit’ов; args часто как `{content_type: thrift_call, content: {call, params}}` — thrift уже раскрыт, но на wire отдаётся JSON map. +- **`jaeger`**: отдельная JSON-схема под Jaeger UI — **подлежит удалению** (см. раздел 6). +- Namespace-специфичная логика в `unmarshal_args/4` (`invoice`, `invoice_template`, `call` / `repair` / `timeout`). + +### 3.3. Тесты + +`apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl` — `payment_success_trace/1`: проверки `internal` и `jaeger` URL. + +--- + +## 4. Сравнение FF vs HG + +| Аспект | FF | HG | +|--------|----|----| +| Доступ к progressor | `machinery` + schema | напрямую `progressor:trace` | +| Типизация до HTTP | выше (codec + schema) | ad hoc в handler | +| Транспорт | Cowboy GET + JSON | Cowboy GET + JSON | +| Woody на wire | нет | нет | +| Thrift IDL для trace | нет | нет | +| Jaeger | нет | есть (удалить) | + +--- + +## 5. Переход JSON → Thrift + +### 5.1. Чего нет сейчас + +- Нет thrift-типов `Trace`, `TraceUnit`, `TraceEvent` в damsel / fistful / progressor. +- Нет thrift-метода вроде `GetTrace` — только cowboy routes с JSON. + +### 5.2. Главная сложность — полиморфные payload’ы + +`args` и `event_payload` зависят от namespace и `task_type`. + +Варианты в IDL: + +1. **Union per domain** — `DepositTraceEvent`, `InvoiceTraceArgs`, … (строго, больше IDL). +2. **Opaque + typed sidecar** — `ThriftCall { service, function, params }` + `binary fallback` (близко к тому, что HG уже отдаёт в JSON как `thrift_call`). +3. **Только binary** — минимальный IDL, клиент декодирует сам (хуже для отладки). + +Рекомендация: формализовать вариант 2 (как текущий `internal` JSON в HG) + domain unions для событий там, где уже есть thrift `Change` / `EventPayload`. + +### 5.3. Доставка Thrift + +| Путь | Плюсы | Минусы | +|------|--------|--------| +| **A. Woody service** | Как `Repairer` / `Management`, woody errors, codegen-клиенты | Новый path, не «голый» GET в браузере | +| **B. Cowboy + binary thrift body** | Можно оставить GET | Дублирование с woody, ручной Content-Type | + +**Рекомендация:** Woody (путь A), единообразно с остальным FF/HG API. + +Пример для FF: + +``` +/v1/trace/deposit → TraceViewer для deposit_v1 +/v1/trace/withdrawal → … +``` + +Для HG — отдельный service или методы на sidecar-сервисе для `invoice` / `invoice_template`. + +### 5.4. Переиспользование кода + +**FF:** после `machinery_prg_backend:trace` данные уже в доменных Erlang-термах → **marshal** через `ff_*_codec` в новые thrift-структуры (аналог `marshal_event` в `ff_*_machinery_schema`). + +**HG:** логику из `hg_progressor_handler` (`unmarshal_args`, `unmarshal_events`, `term_to_object`) перенести в **`hg_trace_codec`** с marshal в thrift вместо JSON map. + +**Общий слой (в рамках задачи):** marshal общих полей `TraceUnit` (timestamps, task meta, `otel_trace_id`, `error`); domain-specific — в codec по NS. + +### 5.5. Миграция + +Так как API **не используется**, допустимо **сразу**: + +- удалить JSON-encode и cowboy trace routes (или заменить на woody); +- не вводить `format=internal|thrift` и не держать параллельные эндпоинты; +- обновить CT под thrift-клиент. + +--- + +## 6. Удаление Jaeger + +Убрать полностью: + +| Место | Что удалить | +|-------|-------------| +| `hg_progressor_handler.erl` | валидация `format=jaeger`, `unmarshal_trace/4` и clauses для `jaeger`, `unmarshal_trace_unit` для jaeger, `unmarshal_event` для jaeger, `service_name/1`, `trace_id/2`, `error_tag/1` если используются только jaeger | +| `hg_progressor_handler:get_routes/0` | сегмент `[:format]` → фиксированный internal-only path или переход на woody без format | +| `hg_invoice_lite_tests_SUITE.erl` | `UrlJaeger`, проверки jaeger body | +| Документация / compose | `compose.tracing.yaml` и образ jaeger — **не трогать**, если используются для OTEL в dev; это не HTTP trace API | + +После удаления маршрут HG упрощается, например: + +``` +/traces/invoice/:process_id +/traces/invoice_template/:process_id +``` + +или полностью заменяется woody path без HTTP format. + +--- + +## 7. Реализация (HG, invoice / invoice_template) + +**IDL:** `damsel/proto/progressor_trace.thrift` — сервисы `InvoiceTrace`, `InvoiceTemplateTrace`, метод `GetTrace`. + +**Woody:** + +| Сервис | Path | +|--------|------| +| `InvoiceTrace` | `/v1/trace/invoice` | +| `InvoiceTemplateTrace` | `/v1/trace/invoice_template` | + +**Модули:** + +- `hg_progressor_trace` — `progressor:trace` + marshal +- `hg_trace_codec` — raw progressor → thrift +- `hg_progressor_trace_handler` — woody handler + +**Удалено:** `hg_progressor_handler` (cowboy JSON, jaeger). + +**Зависимость:** после мержа `progressor_trace.thrift` в damsel — поднять tag в `rebar.config` (сейчас `v2.2.33` + TODO). + +--- + +## 8. План работ (остальное) + +1. **IDL (FF)** — по аналогии в fistful / отдельный thrift: + - `Trace = list` + - `TraceUnit` — поля progressor + `TraceArgs` + `list` + - `TraceArgs` / payload — `ThriftCall` + fallback `Content` / `binary` + +2. **Codegen** — подключить в `fistful_proto` / damsel, rebar. + +3. **FF** + - `ff_trace_codec` — marshal из результата `ff_machine:trace/2` (убрать `json_compatible_*` из trace path). + - Woody handler + `ff_services` (path + service spec). + - Удалить `ff_machine_handler` routes или модуль целиком. + - Обновить CT (`ff_deposit_handler_SUITE`, `ff_withdrawal_handler_SUITE`, …). + +4. **HG** + - `hg_trace_codec` — вынести логику из `hg_progressor_handler`. + - Woody handler вместо/вместе с cowboy JSON. + - Удалить jaeger и `term_to_object` для trace (оставить только thrift marshal). + - Обновить `hg_invoice_lite_tests_SUITE`. + +5. **Не в scope сейчас** + - Единый pipeline HG через machinery schema. + - Dual JSON/Thrift. + - Jaeger HTTP format. + +--- + +## 9. Затрагиваемые файлы + +| Компонент | Сейчас | Целевое состояние | +|-----------|--------|-------------------| +| FF HTTP | `ff_machine_handler.erl` | woody trace handler | +| FF домен | `ff_machine.erl` (`json_compatible_*` в trace) | `ff_trace_codec.erl` | +| HG HTTP | `hg_progressor_handler.erl` | woody + `hg_trace_codec` (jaeger удалён) | +| HG регистрация | `hellgate.erl` | woody routes вместо cowboy trace | +| IDL | — | новый `.thrift` | +| Тесты | `ff_*_handler_SUITE`, `hg_invoice_lite_tests_SUITE` | thrift client | + +--- + +## 10. Вывод + +- Источник данных один — **progressor**; FF декодирует через **machinery schema**, HG — кастомным handler’ом в JSON. +- Переход на Thrift — это **IDL + codec + woody**, а не замена одной строки `json:encode`. +- FF проще (данные ближе к доменным thrift-типам); HG — перенос unmarshaling из handler в `hg_trace_codec` + marshal. +- **Jaeger HTTP format удаляется**; OTEL/Jaeger в compose для dev — отдельная история. +- **Обратная совместимость JSON не требуется** — можно резать сразу. diff --git a/elvis.config b/elvis.config index b616a910..bff14eb7 100644 --- a/elvis.config +++ b/elvis.config @@ -29,7 +29,6 @@ %% Project settings {elvis_style, invalid_dynamic_call, #{ ignore => [ - hg_progressor_handler, hg_proto_utils ] }}, @@ -44,21 +43,13 @@ }}, {elvis_style, state_record_and_type, #{ ignore => [ - hg_profiler, - machinery_gensrv_backend + hg_profiler ] }}, {elvis_style, dont_repeat_yourself, #{ min_complexity => 32, ignore => [ - hg_routing, - ff_source_machinery_schema, - ff_deposit_machinery_schema, - ff_destination_machinery_schema, - ff_identity_machinery_schema, - ff_wallet_machinery_schema, - ff_withdrawal_machinery_schema, - ff_withdrawal_session_machinery_schema + hg_routing ] }}, {elvis_style, max_function_arity, #{max_arity => 10}}, diff --git a/rebar.config b/rebar.config index e4fe4ed5..8c367d87 100644 --- a/rebar.config +++ b/rebar.config @@ -47,7 +47,7 @@ {fault_detector_proto, {git, "https://github.com/valitydev/fault-detector-proto.git", {branch, "master"}}}, {limiter_proto, {git, "https://github.com/valitydev/limiter-proto.git", {tag, "v2.1.1"}}}, {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, - {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.24"}}}, + {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.25"}}}, {machinery, {git, "https://github.com/valitydev/machinery-erlang.git", {tag, "v1.1.22"}}}, {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {tag, "v2.0.2"}}}, {binbase_proto, {git, "https://github.com/valitydev/binbase-proto.git", {branch, "master"}}}, @@ -169,5 +169,5 @@ {shell, [ {config, "config/sys.config"}, - {apps, [hellgate, hg_client, hg_progressor, hg_proto, routing, recon]} + {apps, [hellgate, hg_client, hg_proto, routing, recon]} ]}. diff --git a/rebar.lock b/rebar.lock index f1a84ab2..0a6e64cf 100644 --- a/rebar.lock +++ b/rebar.lock @@ -121,7 +121,7 @@ 0}, {<<"progressor">>, {git,"https://github.com/valitydev/progressor.git", - {ref,"90f46570007a1cbf7cac960059fe6c8018702489"}}, + {ref,"8624e58627e33633e6eb72c5b9defb208b06a169"}}, 0}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.11.0">>},0}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.9">>},0}, diff --git a/test/bender/sys.config b/test/bender/sys.config index 5993022f..332696ec 100644 --- a/test/bender/sys.config +++ b/test/bender/sys.config @@ -119,6 +119,7 @@ worker_pool_size => 100, process_step_timeout => 30 }}, + {namespaces, #{ 'bender_generator' => #{ processor => #{ diff --git a/test/party-management/sys.config b/test/party-management/sys.config index 732034a0..b3c514ab 100644 --- a/test/party-management/sys.config +++ b/test/party-management/sys.config @@ -17,7 +17,6 @@ ]}, {party_management, [ - {machinery_backend, progressor}, {scoper_event_handler_options, #{ event_handler_opts => #{ formatter_opts => #{ @@ -80,6 +79,7 @@ worker_pool_size => 100, process_step_timeout => 30 }}, + {namespaces, #{ 'party' => #{ processor => #{