From 92e1b72c915e387d67388077705b96413fe04b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 5 Jun 2026 14:43:20 +0300 Subject: [PATCH 01/62] Refactors machinery integration to use prg_machine across multiple modules, removing obsolete machinery schemas and updating related function signatures. Enhances deposit and destination machine implementations to align with the new prg_machine client structure. --- apps/ff_cth/src/ct_payment_system.erl | 80 ++- apps/ff_server/src/ff_codec.erl | 2 +- apps/ff_server/src/ff_deposit_handler.erl | 4 + .../src/ff_deposit_machinery_schema.erl | 88 --- .../src/ff_destination_machinery_schema.erl | 84 --- apps/ff_server/src/ff_machine_handler.erl | 2 +- apps/ff_server/src/ff_server.app.src | 1 + apps/ff_server/src/ff_server.erl | 61 +- .../src/ff_source_machinery_schema.erl | 84 --- .../src/ff_withdrawal_machinery_schema.erl | 90 --- ...ff_withdrawal_session_machinery_schema.erl | 87 --- .../ff_withdrawal_session_repair_SUITE.erl | 2 +- .../ff_transfer/src/ff_adapter_withdrawal.erl | 2 +- apps/ff_transfer/src/ff_adjustment.erl | 2 +- apps/ff_transfer/src/ff_adjustment_utils.erl | 2 +- apps/ff_transfer/src/ff_deposit.erl | 135 ++++- apps/ff_transfer/src/ff_deposit_machine.erl | 148 ++--- apps/ff_transfer/src/ff_destination.erl | 112 ++++ .../src/ff_destination_machine.erl | 125 ++-- apps/ff_transfer/src/ff_machine_codec.erl | 92 +++ apps/ff_transfer/src/ff_source.erl | 112 ++++ apps/ff_transfer/src/ff_source_machine.erl | 126 ++-- apps/ff_transfer/src/ff_transfer.app.src | 1 + apps/ff_transfer/src/ff_withdrawal.erl | 152 +++++ .../ff_transfer/src/ff_withdrawal_machine.erl | 191 +++--- .../ff_transfer/src/ff_withdrawal_session.erl | 164 ++++- .../src/ff_withdrawal_session_machine.erl | 209 +++---- apps/ff_transfer/test/ff_ct_machine.erl | 53 +- .../test/ff_withdrawal_limits_SUITE.erl | 4 +- apps/fistful/src/ff_context.erl | 2 +- apps/fistful/src/ff_machine.erl | 385 ------------ apps/fistful/src/ff_machine_tag.erl | 2 +- apps/fistful/src/ff_repair.erl | 48 +- apps/fistful/src/fistful.app.src | 1 + apps/fistful/src/fistful.erl | 176 ------ apps/hellgate/include/hg_invoice.hrl | 2 +- apps/hellgate/src/hellgate.app.src | 1 + apps/hellgate/src/hellgate.erl | 37 +- apps/hellgate/src/hg_invoice.erl | 285 ++++++--- apps/hellgate/src/hg_invoice_handler.erl | 19 +- apps/hellgate/src/hg_invoice_payment.erl | 66 +- .../src/hg_invoice_payment_chargeback.erl | 16 +- .../src/hg_invoice_payment_refund.erl | 16 +- .../src/hg_invoice_registered_payment.erl | 6 +- apps/hellgate/src/hg_invoice_repair.erl | 2 +- apps/hellgate/src/hg_invoice_template.erl | 106 +++- .../src/hg_invoicing_machine_client.erl | 76 +++ apps/hellgate/src/hg_machine.erl | 569 ------------------ apps/hellgate/src/hg_machine_action.erl | 81 --- apps/hellgate/src/hg_machine_tag.erl | 4 +- apps/hellgate/src/hg_session.erl | 18 +- apps/hellgate/test/hg_ct_helper.erl | 46 +- .../test/hg_invoice_lite_tests_SUITE.erl | 154 ----- apps/hg_progressor/src/hg_hybrid.erl | 18 +- apps/hg_progressor/src/hg_progressor.app.src | 6 +- apps/hg_progressor/src/hg_progressor.erl | 173 +----- .../src/hg_progressor_handler.erl | 384 ------------ apps/hg_proto/src/hg_proto.erl | 12 +- apps/prg_machine/src/prg_machine.app.src | 17 + apps/prg_machine/src/prg_machine.erl | 544 +++++++++++++++++ apps/prg_machine/src/prg_machine_action.erl | 98 +++ config/sys.config | 140 ++--- docs/prg-machine-migration-context.md | 315 ++++++++++ docs/trace-api-thrift.md | 262 ++++++++ elvis.config | 8 +- rebar.config | 2 + 66 files changed, 3088 insertions(+), 3224 deletions(-) delete mode 100644 apps/ff_server/src/ff_deposit_machinery_schema.erl delete mode 100644 apps/ff_server/src/ff_destination_machinery_schema.erl delete mode 100644 apps/ff_server/src/ff_source_machinery_schema.erl delete mode 100644 apps/ff_server/src/ff_withdrawal_machinery_schema.erl delete mode 100644 apps/ff_server/src/ff_withdrawal_session_machinery_schema.erl create mode 100644 apps/ff_transfer/src/ff_machine_codec.erl delete mode 100644 apps/fistful/src/ff_machine.erl delete mode 100644 apps/fistful/src/fistful.erl create mode 100644 apps/hellgate/src/hg_invoicing_machine_client.erl delete mode 100644 apps/hellgate/src/hg_machine.erl delete mode 100644 apps/hellgate/src/hg_machine_action.erl delete mode 100644 apps/hg_progressor/src/hg_progressor_handler.erl create mode 100644 apps/prg_machine/src/prg_machine.app.src create mode 100644 apps/prg_machine/src/prg_machine.erl create mode 100644 apps/prg_machine/src/prg_machine_action.erl create mode 100644 docs/prg-machine-migration-context.md create mode 100644 docs/trace-api-thrift.md diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index c9edc197..fa0b52e4 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -206,10 +206,18 @@ services(Options) -> }, maps:get(services, Options, Default). +postgres_host() -> + case inet:gethostbyname("db") of + {ok, _} -> + "db"; + {error, _} -> + "127.0.0.1" + end. + epg_databases() -> #{ default_db => #{ - host => "db", + host => postgres_host(), port => 5432, database => "fistful", username => "fistful", @@ -250,56 +258,76 @@ 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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } }, '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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } }, '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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } }, '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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } }, '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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } } diff --git a/apps/ff_server/src/ff_codec.erl b/apps/ff_server/src/ff_codec.erl index 42449f1a..6efbb94d 100644 --- a/apps/ff_server/src/ff_codec.erl +++ b/apps/ff_server/src/ff_codec.erl @@ -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()) -> prg_machine:timestamp(). parse_timestamp(Bin) -> try MicroSeconds = genlib_rfc3339:parse(Bin, microsecond), diff --git a/apps/ff_server/src/ff_deposit_handler.erl b/apps/ff_server/src/ff_deposit_handler.erl index 74c3481d..0f97b048 100644 --- a/apps/ff_server/src/ff_deposit_handler.erl +++ b/apps/ff_server/src/ff_deposit_handler.erl @@ -1,3 +1,7 @@ +%%% +%%% Deposit woody handler — ff_deposit_machine (prg_machine runtime). +%%% + -module(ff_deposit_handler). -behaviour(ff_woody_wrapper). 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_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..d305f97c 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} ?= prg_machine: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..e85407f9 100644 --- a/apps/ff_server/src/ff_server.erl +++ b/apps/ff_server/src/ff_server.erl @@ -39,6 +39,7 @@ start() -> -spec start(normal, any()) -> {ok, pid()} | {error, any()}. start(_StartType, _StartArgs) -> ok = setup_metrics(), + ok = application:set_env(prg_machine, woody_context_loader, fun woody_rpc_context/0), supervisor:start_link({local, ?MODULE}, ?MODULE, []). -spec stop(any()) -> ok. @@ -65,15 +66,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 +99,16 @@ init([]) -> ) ), PartyClientSpec = party_client:child_spec(party_client, PartyClient), + PrgMachineSpec = prg_machine:get_child_spec([ + ff_deposit, + ff_source, + ff_destination, + ff_withdrawal, + ff_withdrawal_session + ]), % 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 +124,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}. @@ -164,3 +133,15 @@ wrap_handler(Handler, WrapperOpts) -> setup_metrics() -> ok = woody_ranch_prometheus_collector:setup(), ok = woody_hackney_prometheus_collector:setup(). + +-spec woody_rpc_context() -> woody_context:ctx(). +woody_rpc_context() -> + try ff_context:load() of + Ctx -> + ff_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. 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_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_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/test/ff_withdrawal_session_repair_SUITE.erl b/apps/ff_server/test/ff_withdrawal_session_repair_SUITE.erl index b5873257..0d082838 100644 --- a/apps/ff_server/test/ff_withdrawal_session_repair_SUITE.erl +++ b/apps/ff_server/test/ff_withdrawal_session_repair_SUITE.erl @@ -216,7 +216,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..7857f46e 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_machine_action:timer(). -type transaction_info() :: ff_adapter:transaction_info(). -type failure() :: ff_adapter:failure(). diff --git a/apps/ff_transfer/src/ff_adjustment.erl b/apps/ff_transfer/src/ff_adjustment.erl index 0ea5e808..9434a53f 100644 --- a/apps/ff_transfer/src/ff_adjustment.erl +++ b/apps/ff_transfer/src/ff_adjustment.erl @@ -91,7 +91,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_machine_action:t() | undefined. -type process_result() :: {action(), [event()]}. -type legacy_event() :: any(). -type external_id() :: id(). diff --git a/apps/ff_transfer/src/ff_adjustment_utils.erl b/apps/ff_transfer/src/ff_adjustment_utils.erl index 783a8ccf..7f05fee7 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() :: prg_machine_action:t() | undefined. -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..05705d24 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -4,8 +4,13 @@ -module(ff_deposit). +-behaviour(prg_machine). + -include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-define(NS, 'ff/deposit_v1'). +-define(EVENT_FORMAT_VERSION, 1). + -type id() :: binary(). -type description() :: binary(). @@ -118,6 +123,18 @@ -export([apply_event/2]). +%% prg_machine + +-export([namespace/0]). +-export([init/2]). +-export([process_signal/2]). +-export([process_call/2]). +-export([process_repair/2]). +-export([marshal_event_body/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). @@ -134,7 +151,10 @@ -type is_negative() :: boolean(). -type cash() :: ff_cash:cash(). -type cash_range() :: ff_range:range(cash()). --type action() :: machinery:action() | undefined. +-type action() :: prg_machine_action:t() | undefined. +-type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). -type p_transfer() :: ff_postings_transfer:transfer(). -type currency_id() :: ff_currency:id(). -type external_id() :: id(). @@ -300,6 +320,61 @@ is_finished(#{status := {failed, _}}) -> is_finished(#{status := pending}) -> false. +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init({[event()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + #{ + events => Events, + action => prg_machine_action:instant(), + auxst => #{ctx => Ctx} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Deposit = prg_machine:collapse(?MODULE, Machine), + process_transfer_result(process_transfer(Deposit), Machine); +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, + Encoded = ff_machine_codec:marshal_event(deposit, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(deposit, ?EVENT_FORMAT_VERSION, Payload), + event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + %% Events utils -spec apply_event(event(), deposit_state() | undefined) -> deposit_state(). @@ -582,3 +657,61 @@ build_failure(limit_check, Deposit) -> code => <<"amount">> } }. + +%% prg_machine helpers + +-spec process_transfer_result(process_result(), machine()) -> prg_result(). +process_transfer_result({Action, Events}, Machine) -> + #{ + events => Events, + action => map_action(Action), + auxst => maps:get(aux_state, Machine, #{}) + }. + +-type repair_result() :: #{ + events := [term()], + action => continue | undefined, + aux_state => term() +}. + +-spec from_repair_result(repair_result(), machine()) -> prg_result(). +from_repair_result(#{events := Events} = Result, Machine) -> + #{ + events => repair_events_to_domain(Events), + action => map_action(maps:get(action, Result, undefined)), + auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) + }. + +-spec map_action(action()) -> prg_machine_action:t() | undefined. +map_action(undefined) -> + undefined; +map_action(continue) -> + prg_machine_action:instant(); +map_action(sleep) -> + prg_machine_action:instant(); +map_action({setup_timer, Timer}) -> + prg_machine_action:set_timer(Timer). + +-spec repair_events_to_domain([term()]) -> [event()]. +repair_events_to_domain(undefined) -> + []; +repair_events_to_domain(Events) -> + [event_body_from_timestamped(E) || E <- Events]. + +-spec event_body_from_timestamped(term()) -> event(). +event_body_from_timestamped({ev, _Timestamp, Change}) -> + Change; +event_body_from_timestamped(Change) -> + Change. + +-type repair_machine() :: #{ + history := [{pos_integer(), {ev, non_neg_integer(), event()}}], + aux_state := term() +}. + +-spec to_repair_machine(machine()) -> repair_machine(). +to_repair_machine(#{history := History, aux_state := AuxState}) -> + #{ + history => [{EventID, {ev, Timestamp, Body}} || {EventID, Timestamp, Body} <- History], + aux_state => AuxState + }. diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index b55985da..f4a9b0b3 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -1,17 +1,21 @@ %%% -%%% Deposit machine +%%% Deposit machine — thin prg_machine client %%% -module(ff_deposit_machine). --behaviour(machinery). - %% 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(), + times => {timestamp() | undefined, timestamp() | undefined} +}. -type deposit() :: ff_deposit:deposit_state(). -type external_id() :: id(). -type event_range() :: {After :: non_neg_integer() | undefined, Limit :: non_neg_integer() | undefined}. @@ -27,6 +31,12 @@ -type unknown_deposit_error() :: {unknown_deposit, id()}. +-type action() :: + continue + | sleep + | {setup_timer, prg_machine_action:timer()} + | undefined. + -export_type([id/0]). -export_type([st/0]). -export_type([change/0]). @@ -50,14 +60,7 @@ -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]). +-export([map_action/1]). %% Pipeline @@ -78,7 +81,7 @@ create(Params, Ctx) -> do(fun() -> #{id := ID} = Params, Events = unwrap(ff_deposit:create(Params)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) + unwrap(prg_machine:start(?NS, ID, {Events, Ctx})) end). -spec get(id()) -> @@ -91,9 +94,9 @@ 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; + case prg_machine:get(?NS, ID, {After, Limit, forward}) of + {ok, Machine} -> + {ok, machine_to_st(Machine)}; {error, notfound} -> {error, {unknown_deposit, ID}} end. @@ -102,9 +105,9 @@ get(ID, {After, Limit}) -> {ok, [event()]} | {error, unknown_deposit_error()}. events(ID, {After, Limit}) -> - case ff_machine:history(ff_deposit, ?NS, ID, {After, Limit, forward}) of + case prg_machine:get_history(?NS, ID, After, Limit, forward) of {ok, History} -> - {ok, [{EventID, TsEv} || {EventID, _, TsEv} <- History]}; + {ok, history_to_events(History)}; {error, notfound} -> {error, {unknown_deposit, ID}} end. @@ -112,64 +115,73 @@ events(ID, {After, Limit}) -> -spec repair(id(), ff_repair:scenario()) -> {ok, repair_response()} | {error, notfound | working | {failed, repair_error()}}. repair(ID, Scenario) -> - machinery:repair(?NS, ID, Scenario, backend()). + case prg_machine:repair(?NS, ID, Scenario) of + {ok, Response} -> + {ok, Response}; + {error, notfound} -> + {error, notfound}; + {error, working} -> + {error, working}; + {error, failed} -> + {error, {failed, {invalid_result, unexpected_failure}}}; + {error, {repair, {failed, _Reason}}} = Error -> + Error + end. %% 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 +%% Internals --type machine() :: ff_machine:machine(event()). --type result() :: ff_machine:result(event()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). +-compile({nowarn_unused_function, [map_action/1]}). --spec init({[event()], ctx()}, machine(), handler_args(), handler_opts()) -> result(). -init({Events, Ctx}, #{}, _, _Opts) -> +-spec machine_to_st(prg_machine:machine()) -> st(). +machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> + Model = prg_machine:collapse(ff_deposit, Machine), + Ctx = maps:get(ctx, AuxState, #{}), #{ - events => ff_machine:emit_events(Events), - action => continue, - aux_state => #{ctx => Ctx} + model => Model, + ctx => Ctx, + times => history_times(History) }. --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_call(_CallArgs, machine(), handler_args(), handler_opts()) -> no_return(). -process_call(CallArgs, _Machine, _, _Opts) -> - 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_notification(_, machine(), handler_args(), handler_opts()) -> result() | no_return(). -process_notification(_Args, _Machine, _HandlerArgs, _Opts) -> - #{}. - -%% Internals - -backend() -> - fistful:backend(?NS). - -process_result({Action, Events}) -> - genlib_map:compact(#{ - events => set_events(Events), - action => Action - }). - -set_events([]) -> +-spec history_to_events(prg_machine:history()) -> [event()]. +history_to_events(History) -> + [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. + +-spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +history_times([]) -> + {undefined, undefined}; +history_times(History) -> + lists:foldl( + fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> + case Created of + undefined -> {Timestamp, Timestamp}; + _ -> {Created, Timestamp} + end + end, + {undefined, undefined}, + History + ). + +-spec map_action(action()) -> prg_machine_action:t() | undefined. +map_action(undefined) -> undefined; -set_events(Events) -> - ff_machine:emit_events(Events). +map_action(continue) -> + prg_machine_action:instant(); +map_action(sleep) -> + prg_machine_action:instant(); +map_action({setup_timer, Timer}) -> + prg_machine_action:set_timer(Timer). + +codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> + {DateTime, USec} = Timestamp; +codec_timestamp(DateTime) -> + {DateTime, 0}. diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index eb4b210d..21cac757 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -8,6 +8,11 @@ -module(ff_destination). +-behaviour(prg_machine). + +-define(NS, 'ff/destination_v2'). +-define(EVENT_FORMAT_VERSION, 1). + -type id() :: binary(). -type token() :: binary(). -type name() :: binary(). @@ -109,10 +114,27 @@ -export([is_accessible/1]). -export([apply_event/2]). +%% prg_machine + +-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/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). +-type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). + %% Accessors -spec party_id(destination_state()) -> party_id(). @@ -222,3 +244,93 @@ 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}). + +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init({[event()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + #{ + events => Events, + action => prg_machine_action:instant(), + auxst => #{ctx => Ctx} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, _Machine) -> + #{}; +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_notification(term(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, ff_time:now(), Body}, + Encoded = ff_machine_codec:marshal_event(destination, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(destination, ?EVENT_FORMAT_VERSION, Payload), + event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + +-spec from_repair_result(map(), machine()) -> prg_result(). +from_repair_result(#{events := Events} = Result, Machine) -> + #{ + events => repair_events_to_domain(Events), + action => undefined, + auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) + }. + +-spec repair_events_to_domain([term()]) -> [event()]. +repair_events_to_domain(undefined) -> + []; +repair_events_to_domain(Events) -> + [event_body_from_timestamped(E) || E <- Events]. + +-spec event_body_from_timestamped(term()) -> event(). +event_body_from_timestamped({ev, _Timestamp, Change}) -> + Change; +event_body_from_timestamped(Change) -> + Change. + +-type repair_machine() :: #{ + history := [{pos_integer(), {ev, non_neg_integer(), event()}}], + aux_state := term() +}. + +-spec to_repair_machine(machine()) -> repair_machine(). +to_repair_machine(#{history := History, aux_state := AuxState}) -> + #{ + history => [{EventID, {ev, Timestamp, Body}} || {EventID, Timestamp, Body} <- History], + aux_state => AuxState + }. diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index 5ae942c3..5886bc9c 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -1,21 +1,27 @@ %%% -%%% Destination machine +%%% Destination machine — thin prg_machine client %%% -module(ff_destination_machine). %% 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(), + times => {timestamp() | undefined, timestamp() | undefined} +}. -type repair_error() :: ff_repair:repair_error(). -type repair_response() :: ff_repair:repair_response(). @@ -40,99 +46,94 @@ -export([destination/1]). -export([ctx/1]). -%% Machinery - --behaviour(machinery). - --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). - %% Pipeline -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) -> +create(Params, Ctx) -> do(fun() -> + #{id := ID} = Params, Events = unwrap(ff_destination:create(Params)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) + unwrap(prg_machine:start(?NS, ID, {Events, Ctx})) end). -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}). + case prg_machine:get(?NS, ID, {After, Limit, forward}) of + {ok, Machine} -> + {ok, machine_to_st(Machine)}; + {error, notfound} -> + {error, notfound} + end. -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). + case prg_machine:get_history(?NS, ID, After, Limit, forward) of + {ok, History} -> + {ok, history_to_events(History)}; + {error, notfound} -> + {error, notfound} + end. %% 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). +ctx(#{ctx := Ctx}) -> + Ctx. -%% Machinery - --type machine() :: ff_machine:machine(change()). --type result() :: ff_machine:result(change()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). +%% Internals --spec init({[change()], ctx()}, machine(), _, handler_opts()) -> result(). -init({Events, Ctx}, #{}, _, _Opts) -> +-spec machine_to_st(prg_machine:machine()) -> st(). +machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> + Model = prg_machine:collapse(ff_destination, Machine), + Ctx = maps:get(ctx, AuxState, #{}), #{ - events => ff_machine:emit_events(Events), - aux_state => #{ctx => Ctx} + model => Model, + ctx => Ctx, + times => history_times(History) }. -%% - --spec process_timeout(machine(), handler_args(), handler_opts()) -> result(). -process_timeout(_Machine, _, _Opts) -> - #{}. - -%% - --spec process_call(_CallArgs, machine(), handler_args(), handler_opts()) -> {ok, result()}. -process_call(_CallArgs, #{}, _, _Opts) -> - {ok, #{}}. - --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) -> - #{}. - -%% Internals - -backend() -> - fistful:backend(?NS). +-spec history_to_events(prg_machine:history()) -> [event()]. +history_to_events(History) -> + [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. + +-spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +history_times([]) -> + {undefined, undefined}; +history_times(History) -> + lists:foldl( + fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> + case Created of + undefined -> {Timestamp, Timestamp}; + _ -> {Created, Timestamp} + end + end, + {undefined, undefined}, + History + ). + +codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> + {DateTime, USec} = Timestamp; +codec_timestamp(DateTime) -> + {DateTime, 0}. 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..8430e354 --- /dev/null +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -0,0 +1,92 @@ +-module(ff_machine_codec). + +-export([marshal_event/3]). +-export([unmarshal_event/3]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([payload_to_binary/1]). + +-type domain() :: deposit | source | destination | withdrawal | withdrawal_session. +-type format_version() :: pos_integer(). +-type timestamped_event() :: {ev, term(), term()}. + +-spec marshal_event(domain(), format_version(), timestamped_event()) -> machinery_msgpack:t(). +marshal_event(deposit, 1, Timestamped) -> + marshal_thrift_event(Timestamped, ff_deposit_codec, timestamped_change, fistful_deposit_thrift, 'TimestampedChange'); +marshal_event(source, 1, Timestamped) -> + marshal_thrift_event(Timestamped, ff_source_codec, timestamped_change, fistful_source_thrift, 'TimestampedChange'); +marshal_event(destination, 1, Timestamped) -> + marshal_thrift_event( + Timestamped, ff_destination_codec, timestamped_change, fistful_destination_thrift, 'TimestampedChange' + ); +marshal_event(withdrawal, 1, Timestamped) -> + marshal_thrift_event(Timestamped, ff_withdrawal_codec, timestamped_change, fistful_wthd_thrift, 'TimestampedChange'); +marshal_event(withdrawal_session, 1, Timestamped) -> + marshal_thrift_event( + Timestamped, ff_withdrawal_session_codec, timestamped_change, fistful_wthd_session_thrift, 'TimestampedChange' + ); +marshal_event(Domain, Format, _Timestamped) -> + erlang:error({unknown_event_format, Domain, Format}). + +-spec unmarshal_event(domain(), format_version(), binary()) -> timestamped_event(). +unmarshal_event(deposit, 1, Payload) -> + unmarshal_thrift_event(Payload, ff_deposit_codec, timestamped_change, fistful_deposit_thrift, 'TimestampedChange'); +unmarshal_event(source, 1, Payload) -> + unmarshal_thrift_event(Payload, ff_source_codec, timestamped_change, fistful_source_thrift, 'TimestampedChange'); +unmarshal_event(destination, 1, Payload) -> + unmarshal_thrift_event( + Payload, ff_destination_codec, timestamped_change, fistful_destination_thrift, 'TimestampedChange' + ); +unmarshal_event(withdrawal, 1, Payload) -> + unmarshal_thrift_event(Payload, ff_withdrawal_codec, timestamped_change, fistful_wthd_thrift, 'TimestampedChange'); +unmarshal_event(withdrawal_session, 1, Payload) -> + unmarshal_thrift_event( + Payload, ff_withdrawal_session_codec, timestamped_change, fistful_wthd_session_thrift, 'TimestampedChange' + ); +unmarshal_event(Domain, Format, _Payload) -> + erlang:error({unknown_event_format, Domain, Format}). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + {Encoded, _} = machinery_mg_schema_generic:marshal({aux_state, undefined}, AuxSt, #{}), + payload_to_binary(Encoded). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(Payload) when is_binary(Payload) -> + {AuxSt, _} = machinery_mg_schema_generic:unmarshal( + {aux_state, undefined}, + {bin, Payload}, + #{} + ), + AuxSt. + +-spec payload_to_binary(machinery_msgpack:t()) -> binary(). +payload_to_binary({bin, Bin}) when is_binary(Bin) -> + Bin; +payload_to_binary(Payload) -> + {ok, Bin} = machinery_msgpack:pack(Payload), + Bin. + +-spec marshal_thrift_event( + timestamped_event(), + module(), + atom(), + atom(), + atom() +) -> machinery_msgpack:t(). +marshal_thrift_event(Timestamped, Codec, Tag, ThriftModule, ThriftStruct) -> + ThriftChange = Codec:marshal(Tag, Timestamped), + Type = {struct, struct, {ThriftModule, ThriftStruct}}, + {bin, ff_proto_utils:serialize(Type, ThriftChange)}. + +-spec unmarshal_thrift_event( + binary(), + module(), + atom(), + atom(), + atom() +) -> timestamped_event(). +unmarshal_thrift_event(Payload, Codec, Tag, ThriftModule, ThriftStruct) -> + Type = {struct, struct, {ThriftModule, ThriftStruct}}, + ThriftChange = ff_proto_utils:deserialize(Type, Payload), + Codec:unmarshal(Tag, ThriftChange). diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index e5b2c3bb..268b4712 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -7,6 +7,11 @@ -module(ff_source). +-behaviour(prg_machine). + +-define(NS, 'ff/source_v1'). +-define(EVENT_FORMAT_VERSION, 1). + -type id() :: binary(). -type name() :: binary(). -type account() :: ff_account:account(). @@ -95,10 +100,27 @@ -export([is_accessible/1]). -export([apply_event/2]). +%% prg_machine + +-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/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). +-type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). + %% Accessors -spec id(source_state()) -> id(). @@ -198,3 +220,93 @@ 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}). + +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init({[event()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + #{ + events => Events, + action => prg_machine_action:instant(), + auxst => #{ctx => Ctx} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, _Machine) -> + #{}; +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_notification(term(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, ff_time:now(), Body}, + Encoded = ff_machine_codec:marshal_event(source, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(source, ?EVENT_FORMAT_VERSION, Payload), + event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + +-spec from_repair_result(map(), machine()) -> prg_result(). +from_repair_result(#{events := Events} = Result, Machine) -> + #{ + events => repair_events_to_domain(Events), + action => undefined, + auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) + }. + +-spec repair_events_to_domain([term()]) -> [event()]. +repair_events_to_domain(undefined) -> + []; +repair_events_to_domain(Events) -> + [event_body_from_timestamped(E) || E <- Events]. + +-spec event_body_from_timestamped(term()) -> event(). +event_body_from_timestamped({ev, _Timestamp, Change}) -> + Change; +event_body_from_timestamped(Change) -> + Change. + +-type repair_machine() :: #{ + history := [{pos_integer(), {ev, non_neg_integer(), event()}}], + aux_state := term() +}. + +-spec to_repair_machine(machine()) -> repair_machine(). +to_repair_machine(#{history := History, aux_state := AuxState}) -> + #{ + history => [{EventID, {ev, Timestamp, Body}} || {EventID, Timestamp, Body} <- History], + aux_state => AuxState + }. diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index 04449e56..f974198f 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -1,21 +1,27 @@ %%% -%%% Source machine +%%% Source machine — thin prg_machine client %%% -module(ff_source_machine). %% 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(), + times => {timestamp() | undefined, timestamp() | undefined} +}. -type repair_error() :: ff_repair:repair_error(). -type repair_response() :: ff_repair:repair_response(). @@ -40,100 +46,94 @@ -export([source/1]). -export([ctx/1]). -%% Machinery - --behaviour(machinery). - --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). - %% Pipeline -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) -> +create(Params, Ctx) -> do(fun() -> + #{id := ID} = Params, Events = unwrap(ff_source:create(Params)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) + unwrap(prg_machine:start(?NS, ID, {Events, Ctx})) end). -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}). + case prg_machine:get(?NS, ID, {After, Limit, forward}) of + {ok, Machine} -> + {ok, machine_to_st(Machine)}; + {error, notfound} -> + {error, notfound} + end. -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). + case prg_machine:get_history(?NS, ID, After, Limit, forward) of + {ok, History} -> + {ok, history_to_events(History)}; + {error, notfound} -> + {error, notfound} + end. %% 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). +ctx(#{ctx := Ctx}) -> + Ctx. -%% Machinery - --type machine() :: ff_machine:machine(change()). --type result() :: ff_machine:result(change()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). +%% Internals --spec init({[change()], ctx()}, machine(), _, handler_opts()) -> result(). -init({Events, Ctx}, #{}, _, _Opts) -> +-spec machine_to_st(prg_machine:machine()) -> st(). +machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> + Model = prg_machine:collapse(ff_source, Machine), + Ctx = maps:get(ctx, AuxState, #{}), #{ - events => ff_machine:emit_events(Events), - action => continue, - aux_state => #{ctx => Ctx} + model => Model, + ctx => Ctx, + times => history_times(History) }. -%% - --spec process_timeout(machine(), handler_args(), handler_opts()) -> result(). -process_timeout(_Machine, _, _Opts) -> - #{}. - -%% - --spec process_call(_CallArgs, machine(), handler_args(), handler_opts()) -> {ok, result()}. -process_call(_CallArgs, #{}, _, _Opts) -> - {ok, #{}}. - --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) -> - #{}. - -%% Internals - -backend() -> - fistful:backend(?NS). +-spec history_to_events(prg_machine:history()) -> [event()]. +history_to_events(History) -> + [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. + +-spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +history_times([]) -> + {undefined, undefined}; +history_times(History) -> + lists:foldl( + fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> + case Created of + undefined -> {Timestamp, Timestamp}; + _ -> {Created, Timestamp} + end + end, + {undefined, undefined}, + History + ). + +codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> + {DateTime, USec} = Timestamp; +codec_timestamp(DateTime) -> + {DateTime, 0}. diff --git a/apps/ff_transfer/src/ff_transfer.app.src b/apps/ff_transfer/src/ff_transfer.app.src index 2fc2fff2..ca8dcbdd 100644 --- a/apps/ff_transfer/src/ff_transfer.app.src +++ b/apps/ff_transfer/src/ff_transfer.app.src @@ -8,6 +8,7 @@ genlib, ff_core, progressor, + prg_machine, machinery, machinery_extra, damsel, diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 9d650624..375032c6 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -4,8 +4,13 @@ -module(ff_withdrawal). +-behaviour(prg_machine). + -include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-define(NS, 'ff/withdrawal_v2'). +-define(EVENT_FORMAT_VERSION, 1). + -type id() :: binary(). -define(ACTUAL_FORMAT_VERSION, 4). @@ -250,6 +255,19 @@ -export([apply_event/2]). +%% prg_machine + +-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/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). @@ -290,6 +308,10 @@ -type legacy_event() :: any(). +-type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). + -type transfer_params() :: #{ party_id := party_id(), wallet_id := wallet_id(), @@ -1813,6 +1835,136 @@ get_quote_field(provider_id, #{route := Route}) -> get_quote_field(terminal_id, #{route := Route}) -> ff_withdrawal_routing:get_terminal(Route). +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init({[event()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + #{ + events => Events, + action => prg_machine_action:instant(), + auxst => #{ctx => Ctx} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Withdrawal = prg_machine:collapse(?MODULE, Machine), + process_transfer_result(process_transfer(Withdrawal), Machine); +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-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 start_adjustment(Params, Withdrawal) of + {ok, Result} -> + {ok, process_transfer_result(Result, Machine)}; + {error, _Reason} = Error -> + {Error, #{}} + end; +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_notification({session_finished, session_id(), session_result()}, machine()) -> prg_result(). +process_notification({session_finished, SessionID, SessionResult}, Machine) -> + Withdrawal = prg_machine:collapse(?MODULE, Machine), + case finalize_session(SessionID, SessionResult, Withdrawal) of + {ok, Result} -> + process_transfer_result(Result, Machine); + {error, Reason} -> + erlang:error({unable_to_finalize_session, Reason}) + end. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, ff_time:now(), Body}, + Encoded = ff_machine_codec:marshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Payload), + event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + +-spec process_transfer_result(process_result(), machine()) -> prg_result(). +process_transfer_result({Action, Events}, Machine) -> + #{ + events => Events, + action => map_action(Action), + auxst => maps:get(aux_state, Machine, #{}) + }. + +-type repair_result() :: #{ + events := [term()], + action => action() | undefined, + aux_state => term() +}. + +-spec from_repair_result(repair_result(), machine()) -> prg_result(). +from_repair_result(#{events := Events} = Result, Machine) -> + #{ + events => repair_events_to_domain(Events), + action => map_action(maps:get(action, Result, undefined)), + auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) + }. + +-spec map_action(action()) -> prg_machine_action:t() | undefined. +map_action(undefined) -> + undefined; +map_action(continue) -> + prg_machine_action:instant(); +map_action(sleep) -> + prg_machine_action:unset_timer(); +map_action({setup_timer, Timer}) -> + prg_machine_action:set_timer(Timer). + +-spec repair_events_to_domain([term()]) -> [event()]. +repair_events_to_domain(undefined) -> + []; +repair_events_to_domain(Events) -> + [event_body_from_timestamped(E) || E <- Events]. + +-spec event_body_from_timestamped(term()) -> event(). +event_body_from_timestamped({ev, _Timestamp, Change}) -> + Change; +event_body_from_timestamped(Change) -> + Change. + +-type repair_machine() :: #{ + history := [{pos_integer(), {ev, non_neg_integer(), event()}}], + aux_state := term() +}. + +-spec to_repair_machine(machine()) -> repair_machine(). +to_repair_machine(#{history := History, aux_state := AuxState}) -> + #{ + history => [{EventID, {ev, Timestamp, Body}} || {EventID, Timestamp, Body} <- History], + aux_state => AuxState + }. + %% -spec apply_event(event() | legacy_event(), ff_maybe:'maybe'(withdrawal_state())) -> withdrawal_state(). diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 9b492914..9d1007d9 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -1,20 +1,23 @@ %%% -%%% Withdrawal machine +%%% Withdrawal machine — thin prg_machine client %%% -module(ff_withdrawal_machine). --behaviour(machinery). - %% 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(), + times => {timestamp() | undefined, timestamp() | undefined} +}. -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,18 +25,28 @@ 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 unknown_withdrawal_error() :: {unknown_withdrawal, id()}. --type repair_error() :: ff_repair:repair_error(). --type repair_response() :: ff_repair:repair_response(). +-type action() :: + continue + | sleep + | undefined. + +-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 session_id() :: ff_withdrawal_session:id(). +-type session_result() :: ff_withdrawal_session:session_result(). + -export_type([id/0]). -export_type([st/0]). -export_type([action/0]). @@ -56,7 +69,6 @@ -export([events/2]). -export([repair/2]). -export([notify/2]). - -export([start_adjustment/2]). %% Accessors @@ -64,14 +76,6 @@ -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]). - %% Pipeline -import(ff_pipeline, [do/1, unwrap/1]). @@ -80,14 +84,6 @@ -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()}. - -define(NS, 'ff/withdrawal_v2'). %% API @@ -99,7 +95,7 @@ create(Params, Ctx) -> do(fun() -> #{id := ID} = Params, Events = unwrap(ff_withdrawal:create(Params)), - unwrap(machinery:start(?NS, ID, {Events, Ctx}, backend())) + unwrap(prg_machine:start(?NS, ID, {Events, Ctx})) end). -spec get(id()) -> @@ -112,9 +108,9 @@ 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; + case prg_machine:get(?NS, ID, {After, Limit, forward}) of + {ok, Machine} -> + {ok, machine_to_st(Machine)}; {error, notfound} -> {error, {unknown_withdrawal, ID}} end. @@ -123,9 +119,9 @@ get(ID, {After, Limit}) -> {ok, [event()]} | {error, unknown_withdrawal_error()}. events(ID, {After, Limit}) -> - case ff_machine:history(ff_withdrawal, ?NS, ID, {After, Limit, forward}) of + case prg_machine:get_history(?NS, ID, After, Limit, forward) of {ok, History} -> - {ok, [{EventID, TsEv} || {EventID, _, TsEv} <- History]}; + {ok, history_to_events(History)}; {error, notfound} -> {error, {unknown_withdrawal, ID}} end. @@ -133,7 +129,18 @@ events(ID, {After, Limit}) -> -spec repair(id(), ff_repair:scenario()) -> {ok, repair_response()} | {error, notfound | working | {failed, repair_error()}}. repair(ID, Scenario) -> - machinery:repair(?NS, ID, Scenario, backend()). + case prg_machine:repair(?NS, ID, Scenario) of + {ok, Response} -> + {ok, Response}; + {error, notfound} -> + {error, notfound}; + {error, working} -> + {error, working}; + {error, failed} -> + {error, {failed, {invalid_result, unexpected_failure}}}; + {error, {repair, {failed, _Reason}}} = Error -> + Error + end. -spec start_adjustment(id(), adjustment_params()) -> ok @@ -144,96 +151,58 @@ start_adjustment(WithdrawalID, Params) -> -spec notify(id(), notify_args()) -> ok | {error, notfound} | 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). +ctx(#{ctx := Ctx}) -> + Ctx. -%% Machinery +%% Internals --type machine() :: ff_machine:machine(event()). --type result() :: ff_machine:result(event()). --type handler_opts() :: machinery:handler_opts(_). --type handler_args() :: machinery:handler_args(_). - -backend() -> - fistful:backend(?NS). - --spec init({[event()], ctx()}, machine(), handler_args(), handler_opts()) -> result(). -init({Events, Ctx}, #{}, _, _Opts) -> +-spec machine_to_st(prg_machine:machine()) -> st(). +machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> + Model = prg_machine:collapse(ff_withdrawal, Machine), + Ctx = maps:get(ctx, AuxState, #{}), #{ - events => ff_machine:emit_events(Events), - action => continue, - aux_state => #{ctx => Ctx} + model => Model, + ctx => Ctx, + times => history_times(History) }. --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_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) -> - 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_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 - {ok, Result} -> - process_result(Result, St); - {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. - -process_result({Action, Events}, St) -> - genlib_map:compact(#{ - events => set_events(Events), - action => set_action(Action, St) - }). - -set_events([]) -> - undefined; -set_events(Events) -> - ff_machine:emit_events(Events). - -set_action(continue, _St) -> - continue; -set_action(undefined, _St) -> - undefined; -set_action(sleep, _St) -> - unset_timer. +-spec history_to_events(prg_machine:history()) -> [event()]. +history_to_events(History) -> + [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. + +-spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +history_times([]) -> + {undefined, undefined}; +history_times(History) -> + lists:foldl( + fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> + case Created of + undefined -> {Timestamp, Timestamp}; + _ -> {Created, Timestamp} + end + end, + {undefined, undefined}, + History + ). 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}} end. + +codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> + {DateTime, USec} = Timestamp; +codec_timestamp(DateTime) -> + {DateTime, 0}. diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index a830d426..1ab3c0ee 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -4,6 +4,11 @@ -module(ff_withdrawal_session). +-behaviour(prg_machine). + +-define(NS, 'ff/withdrawal/session_v2'). +-define(EVENT_FORMAT_VERSION, 1). + %% Accessors -export([id/1]). @@ -29,6 +34,19 @@ %% ff_repair -export([set_session_result/2]). +%% prg_machine + +-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/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% %% Types %% @@ -96,13 +114,15 @@ opts := ff_adapter:opts() }. --type id() :: machinery:id(). +-type id() :: binary(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). -type action() :: undefined | continue - | {setup_callback, ff_withdrawal_callback:tag(), machinery:timer()} - | {setup_timer, machinery:timer()} + | {setup_callback, ff_withdrawal_callback:tag(), prg_machine_action:timer()} + | {setup_timer, prg_machine_action:timer()} | retry | finish. @@ -369,3 +389,141 @@ create_adapter_withdrawal( -spec set_callbacks_index(callbacks_index(), session_state()) -> session_state(). set_callbacks_index(Callbacks, Session) -> Session#{callbacks => Callbacks}. + +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init([event()], machine()) -> prg_result(). +init(Events, _Machine) -> + #{ + events => Events, + action => prg_machine_action:instant(), + auxst => #{ctx => ff_entity_context:new()} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Session = prg_machine:collapse(?MODULE, Machine), + process_session_result(process_session(Session), Machine); +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-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 process_callback(Params, Session) of + {ok, {Response, Result}} -> + {{ok, Response}, process_session_result(Result, Machine)}; + {error, {Reason, _Result}} -> + {{error, Reason}, #{}} + end; +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + ScenarioProcessors = #{ + set_session_result => fun(Args, RMachine) -> + Session = prg_machine:collapse(?MODULE, RMachine), + {Action, Events} = set_session_result(Args, Session), + {ok, {ok, #{action => Action, events => Events}}} + end + }, + case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario, ScenarioProcessors) of + {ok, {_Response, Result}} -> + from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_notification(term(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, ff_time:now(), Body}, + Encoded = ff_machine_codec:marshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Payload), + event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + +-spec process_session_result(process_result(), machine()) -> prg_result(). +process_session_result({Action, Events}, Machine) -> + Session = prg_machine:collapse(?MODULE, Machine), + #{ + events => Events, + action => map_action(Action, Session), + auxst => maps:get(aux_state, Machine, #{}) + }. + +-type repair_result() :: #{ + events := [term()], + action => action() | undefined, + aux_state => term() +}. + +-spec from_repair_result(repair_result(), machine()) -> prg_result(). +from_repair_result(#{events := Events} = Result, Machine) -> + Session = prg_machine:collapse(?MODULE, Machine), + #{ + events => repair_events_to_domain(Events), + action => map_action(maps:get(action, Result, undefined), Session), + auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) + }. + +-spec map_action(action(), session_state()) -> prg_machine_action:t() | undefined. +map_action(undefined, _Session) -> + undefined; +map_action(continue, _Session) -> + prg_machine_action:instant(); +map_action({setup_callback, Tag, Timer}, Session) -> + ok = ff_machine_tag:create_binding(?NS, Tag, id(Session)), + prg_machine_action:set_timer(Timer); +map_action({setup_timer, Timer}, _Session) -> + prg_machine_action:set_timer(Timer); +map_action(finish, _Session) -> + prg_machine_action:unset_timer(); +map_action(retry, _Session) -> + prg_machine_action:instant(). + +-spec repair_events_to_domain([term()]) -> [event()]. +repair_events_to_domain(undefined) -> + []; +repair_events_to_domain(Events) -> + [event_body_from_timestamped(E) || E <- Events]. + +-spec event_body_from_timestamped(term()) -> event(). +event_body_from_timestamped({ev, _Timestamp, Change}) -> + Change; +event_body_from_timestamped(Change) -> + Change. + +-type repair_machine() :: #{ + history := [{pos_integer(), {ev, non_neg_integer(), event()}}], + aux_state := term() +}. + +-spec to_repair_machine(machine()) -> repair_machine(). +to_repair_machine(#{history := History, aux_state := AuxState}) -> + #{ + history => [{EventID, {ev, Timestamp, Body}} || {EventID, Timestamp, Body} <- History], + aux_state => AuxState + }. diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 25abaf0d..61c06759 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -1,17 +1,9 @@ %%% -%%% Withdrawal session machine -%%% -%%% TODOs -%%% -%%% - The way we ask `fistful` for a machinery backend smells like a circular -%%% dependency injection. -%%% - Dehydrate events upon saving. +%%% Withdrawal session machine — thin prg_machine client %%% -module(ff_withdrawal_session_machine). --behaviour(machinery). - -define(NS, 'ff/withdrawal/session_v2'). %% API @@ -26,14 +18,6 @@ -export([repair/2]). -export([process_callback/1]). -%% machinery - --export([init/4]). --export([process_timeout/3]). --export([process_repair/4]). --export([process_call/4]). --export([process_notification/4]). - %% %% Types %% @@ -44,24 +28,20 @@ -export_type([repair_error/0]). -export_type([repair_response/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 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(), + times => {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined} +}. -type session() :: ff_withdrawal_session:session_state(). -type event() :: ff_withdrawal_session:event(). --type action() :: ff_withdrawal_session:action(). -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,8 +49,6 @@ {unknown_session, {tag, id()}} | ff_withdrawal_session:process_callback_error(). --type process_result() :: ff_withdrawal_session:process_result(). - -type ctx() :: ff_entity_context:context(). %% Pipeline @@ -81,51 +59,64 @@ %% 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}). + case prg_machine:get(?NS, ID, {After, Limit, forward}) of + {ok, Machine} -> + {ok, machine_to_st(Machine)}; + {error, notfound} -> + {error, notfound} + end. -spec events(id(), event_range()) -> - {ok, [{integer(), ff_machine:timestamped_event(event())}]} + {ok, [{integer(), timestamped_event(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). + case prg_machine:get_history(?NS, ID, After, Limit, forward) of + {ok, History} -> + {ok, history_to_events(History)}; + {error, notfound} -> + {error, notfound} + end. -spec repair(id(), ff_repair:scenario()) -> {ok, repair_response()} | {error, notfound | working | {failed, repair_error()}}. repair(ID, Scenario) -> - machinery:repair(?NS, ID, Scenario, backend()). + case prg_machine:repair(?NS, ID, Scenario) of + {ok, Response} -> + {ok, Response}; + {error, notfound} -> + {error, notfound}; + {error, working} -> + {error, working}; + {error, failed} -> + {error, {failed, {invalid_result, unexpected_failure}}}; + {error, {repair, {failed, _Reason}}} = Error -> + Error + end. -spec process_callback(callback_params()) -> {ok, process_callback_response()} @@ -138,104 +129,48 @@ 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) -> - 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) -> - 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}}} - end - }, - ff_repair:apply_scenario(ff_withdrawal_session, Machine, Scenario, ScenarioProcessors). - --spec process_notification(_, machine(), handler_args(), handler_opts()) -> result() | no_return(). -process_notification(_Args, _Machine, _HandlerArgs, _Opts) -> - #{}. - %% %% Internals %% --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 timer_action(machinery:timer()) -> machinery:action(). -timer_action(Timer) -> - {set_timer, Timer}. +-spec machine_to_st(prg_machine:machine()) -> st(). +machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> + Model = prg_machine:collapse(ff_withdrawal_session, Machine), + Ctx = maps:get(ctx, AuxState, #{}), + #{ + model => Model, + ctx => Ctx, + times => history_times(History) + }. -backend() -> - fistful:backend(?NS). +-spec history_to_events(prg_machine:history()) -> [{integer(), timestamped_event(event())}]. +history_to_events(History) -> + [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. + +-spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +history_times([]) -> + {undefined, undefined}; +history_times(History) -> + lists:foldl( + fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> + case Created of + undefined -> {Timestamp, Timestamp}; + _ -> {Created, Timestamp} + end + end, + {undefined, undefined}, + History + ). 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}, #{}} - end. +codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> + {DateTime, USec} = Timestamp; +codec_timestamp(DateTime) -> + {DateTime, 0}. diff --git a/apps/ff_transfer/test/ff_ct_machine.erl b/apps/ff_transfer/test/ff_ct_machine.erl index f40d5c63..b6be7ef0 100644 --- a/apps/ff_transfer/test/ff_ct_machine.erl +++ b/apps/ff_transfer/test/ff_ct_machine.erl @@ -1,28 +1,27 @@ %%% -%%% 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). + meck:new(prg_machine, [no_link, passthrough]), + meck:expect(prg_machine, process, fun process/3). -spec unload_per_suite() -> ok. unload_per_suite() -> - meck:unload(machinery). + meck:unload(prg_machine). --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 +32,23 @@ 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), + meck:passthrough([prg_machine, process, [Call, Opts, BinCtx]]); + undefined -> + meck:passthrough([prg_machine, process, [Call, Opts, BinCtx]]) + end; +process(Call, Opts, BinCtx) -> + meck:passthrough([prg_machine, process, [Call, Opts, BinCtx]]). + +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..4cbec99c 100644 --- a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl @@ -546,8 +546,8 @@ await_provider_retry(FirstAmount, SecondAmount, TotalAmount, C) -> ok = ff_ct_machine:set_hook( timeout, fun - (Machine, ff_withdrawal_machine, _Args) -> - Withdrawal = ff_machine:model(ff_machine:collapse(ff_withdrawal, Machine)), + (Machine, ff_withdrawal, _Args) -> + Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), case {ff_withdrawal:id(Withdrawal), ff_withdrawal:activity(Withdrawal)} of {WithdrawalID1, Activity} -> ff_ct_barrier:enter(Barrier, _Timeout = 10000); diff --git a/apps/fistful/src/ff_context.erl b/apps/fistful/src/ff_context.erl index 4af3aec8..b22fa4a8 100644 --- a/apps/fistful/src/ff_context.erl +++ b/apps/fistful/src/ff_context.erl @@ -61,7 +61,7 @@ load() -> -spec cleanup() -> ok. cleanup() -> - true = gproc:unreg(?REGISTRY_KEY), + _ = catch gproc:unreg(?REGISTRY_KEY), ok. -spec get_woody_context(context()) -> woody_context(). 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..52e18b9f 100644 --- a/apps/fistful/src/ff_machine_tag.erl +++ b/apps/fistful/src/ff_machine_tag.erl @@ -6,7 +6,7 @@ -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}. diff --git a/apps/fistful/src/ff_repair.erl b/apps/fistful/src/ff_repair.erl index dc6c872e..80641b77 100644 --- a/apps/fistful/src/ff_repair.erl +++ b/apps/fistful/src/ff_repair.erl @@ -11,13 +11,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 => term(), + 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() :: #{ @@ -55,9 +64,11 @@ -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() :: #{ + history := [{pos_integer(), timestamped_event(model_event())}], + aux_state := model_aux_state() +}. %% Pipeline @@ -106,21 +117,25 @@ 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 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, #{history := RepairHistory, aux_state := AuxSt}, #{events := NewEvents}) -> + 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 = [ + {ID, Ts, Body} + || {ID, {ev, Ts, Body}} <- lists:zip(IDs, NewEvents) + ], try - _ = ff_machine:collapse(Mod, Machine#{history => History ++ NewHistory}), + _ = prg_machine:collapse(Mod, #{history => PrgHistory0 ++ PrgNewHistory, aux_state => AuxSt}), {ok, valid} catch error:Error:Stack -> @@ -132,6 +147,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/fistful.app.src b/apps/fistful/src/fistful.app.src index e6e6a9e3..92771480 100644 --- a/apps/fistful/src/fistful.app.src +++ b/apps/fistful/src/fistful.app.src @@ -11,6 +11,7 @@ ff_core, snowflake, progressor, + prg_machine, machinery, machinery_extra, woody, 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/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 d5b33e3e..49515373 100644 --- a/apps/hellgate/src/hellgate.app.src +++ b/apps/hellgate/src/hellgate.app.src @@ -10,6 +10,7 @@ fault_detector_proto, herd, progressor, + prg_machine, hg_progressor, hg_proto, routing, diff --git a/apps/hellgate/src/hellgate.erl b/apps/hellgate/src/hellgate.erl index 23a6529e..7fc09c67 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: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) } ). @@ -107,6 +101,7 @@ get_prometheus_route() -> -spec start(normal, any()) -> {ok, pid()} | {error, any()}. start(_StartType, _StartArgs) -> ok = setup_metrics(), + ok = application:set_env(prg_machine, woody_context_loader, fun woody_rpc_context/0), supervisor:start_link(?MODULE, []). -spec stop(any()) -> ok. @@ -118,3 +113,15 @@ stop(_State) -> setup_metrics() -> ok = woody_ranch_prometheus_collector:setup(), ok = woody_hackney_prometheus_collector:setup(). + +-spec woody_rpc_context() -> woody_context:ctx(). +woody_rpc_context() -> + 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. diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 64534781..0592e6f7 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -20,8 +20,10 @@ -include("hg_invoice.hrl"). -include_lib("damsel/include/dmsl_repair_thrift.hrl"). +-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). --define(NS, <<"invoice">>). +-define(NS, invoice). +-define(EVENT_FORMAT_VERSION, 1). -export([process_callback/2]). -export([process_session_change_by_tag/2]). @@ -46,7 +48,7 @@ %% Machine callbacks --behaviour(hg_machine). +-behaviour(prg_machine). -export([namespace/0]). @@ -54,6 +56,11 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([marshal_event_body/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). +-export([apply_event/2]). %% Internal @@ -88,13 +95,17 @@ invoice | {payment, payment_id()}. +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). +-type action() :: prg_machine_action:t(). + %% API --spec get(hg_machine:id()) -> {ok, st()} | {error, notfound}. +-spec get(prg_machine:id()) -> {ok, st()} | {error, notfound}. get(ID) -> - case hg_machine:get_history(?NS, ID) of + case prg_machine:get_history(?NS, ID) of {ok, History} -> - {ok, collapse_history(unmarshal_history(History))}; + {ok, collapse_history(History)}; Error -> Error end. @@ -137,8 +148,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,7 +239,7 @@ 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 + case prg_machine:call(?NS, MachineID, {callback, Tag, Callback}) of {ok, _} = Ok -> Ok; {exception, invalid_callback} -> @@ -242,7 +253,7 @@ 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 + case prg_machine:call(?NS, MachineID, {session_change, Tag, SessionChange}) of ok -> ok; {exception, invalid_callback} -> @@ -262,9 +273,9 @@ 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 + try prg_machine:call(?NS, ID, fail) of {error, failed} -> ok; {error, Error} -> @@ -278,26 +289,26 @@ fail(ID) -> %% --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(prg_machine_action:new(), #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 = collapse_st(Machine), + to_prg_result(handle_repair(Args, St)). handle_repair({changes, Changes, RepairAction, Params}, St) -> Result = @@ -326,9 +337,10 @@ 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)))). +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(Signal, Machine) -> + St = collapse_st(Machine), + to_prg_result(handle_signal(Signal, St)). handle_signal(timeout, #st{activity = {payment, PaymentID}} = St) -> % there's a payment pending @@ -341,18 +353,18 @@ handle_signal(timeout, #st{activity = invoice} = St) -> construct_repair_action(CA) when CA /= undefined -> lists:foldl( fun merge_repair_action/2, - hg_machine_action:new(), + prg_machine_action:new(), [{timer, CA#repair_ComplexAction.timer}, {remove, CA#repair_ComplexAction.remove}] ); construct_repair_action(undefined) -> - hg_machine_action:new(). + prg_machine_action:new(). merge_repair_action({timer, {set_timer, #repair_SetTimerAction{timer = Timer}}}, Action) -> - hg_machine_action:set_timer(Timer, Action); + prg_machine_action:set_timer(Timer, Action); merge_repair_action({timer, {unset_timer, #repair_UnsetTimerAction{}}}, Action) -> - hg_machine_action:unset_timer(Action); + prg_machine_action:unset_timer(Action); merge_repair_action({remove, #repair_RemoveAction{}}, Action) -> - hg_machine_action:mark_removal(Action); + prg_machine_action:mark_removal(Action); merge_repair_action({_, undefined}, Action) -> Action. @@ -369,22 +381,25 @@ 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() }. --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 = collapse_st(Machine), try - handle_result(handle_call(Call, St)) + CallResult = handle_call(Call, St), + _ = log_changes(maps:get(changes, CallResult, []), validate_changes(CallResult)), + Response = maps:get(response, CallResult, ok), + {call_response(Response), to_prg_result(CallResult)} catch throw:Exception -> {{exception, Exception}, #{}} @@ -413,7 +428,7 @@ handle_call({{'Invoicing', 'CapturePayment'}, {_InvoiceID, PaymentID, Params}}, #{ response => ok, changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => Action, + action => action_to_prg(Action), state => St }; handle_call({{'Invoicing', 'CancelPayment'}, {_InvoiceID, PaymentID, Reason}}, St0) -> @@ -424,7 +439,7 @@ handle_call({{'Invoicing', 'CancelPayment'}, {_InvoiceID, PaymentID, Reason}}, S #{ response => ok, changes => wrap_payment_changes(PaymentID, Changes, hg_datetime:format_now()), - action => Action, + action => action_to_prg(Action), state => St }; handle_call({{'Invoicing', 'Fulfill'}, {_InvoiceID, Reason}}, St0) -> @@ -442,7 +457,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 => prg_machine_action:unset_timer(), state => St }; handle_call({{'Invoicing', 'RefundPayment'}, {_InvoiceID, PaymentID, Params}}, St0) -> @@ -512,7 +527,7 @@ 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); + prg_machine_action:set_deadline(Due, Action); set_invoice_timer(_Status, Action, _St) -> Action. @@ -561,7 +576,7 @@ do_register_payment(PaymentID, PaymentParams, St) -> #{ response => get_payment_state(PaymentSession), changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => Action, + action => action_to_prg(Action), state => St }. @@ -574,7 +589,7 @@ do_start_payment(PaymentID, PaymentParams, St) -> #{ response => get_payment_state(PaymentSession), changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => Action, + action => action_to_prg(Action), state => St }. @@ -605,7 +620,7 @@ handle_payment_result({next, {Changes, Action}}, PaymentID, _PaymentSession, St, #{timestamp := OccurredAt} = Opts, #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => Action, + action => action_to_prg(Action), state => St }; handle_payment_result({done, {Changes, Action}}, PaymentID, PaymentSession, St, Opts) -> @@ -618,7 +633,7 @@ handle_payment_result({done, {Changes, Action}}, PaymentID, PaymentSession, St, ?processed() -> #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => Action, + action => action_to_prg(Action), state => St }; ?captured() -> @@ -631,7 +646,7 @@ handle_payment_result({done, {Changes, Action}}, PaymentID, PaymentSession, St, end, #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt) ++ MaybePaid, - action => Action, + action => action_to_prg(Action), state => St }; ?refunded() -> @@ -647,13 +662,13 @@ handle_payment_result({done, {Changes, Action}}, PaymentID, PaymentSession, St, ?failed(_) -> #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => set_invoice_timer(Action, St), + action => set_invoice_timer(action_to_prg(Action), St), state => St }; ?cancelled() -> #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => set_invoice_timer(Action, St), + action => set_invoice_timer(action_to_prg(Action), St), state => St } end. @@ -671,32 +686,34 @@ wrap_payment_impact(PaymentID, {Response, {Changes, Action}}, St, OccurredAt) -> #{ response => Response, changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => Action, + action => action_to_prg(Action), state => St }. -handle_result(#{} = Result) -> - 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} - end. - -handle_result_changes(#{changes := Changes = [_ | _]}, Acc) -> - Acc#{events => [marshal_event_payload(Changes)]}; -handle_result_changes(#{}, Acc) -> - Acc. +-spec to_prg_result(map()) -> prg_result(). +to_prg_result(Result) -> + _ = validate_changes(Result), + to_prg_result_(Result). + +to_prg_result_(#{changes := Changes = [_ | _]} = Result) -> + genlib_map:compact(#{ + events => [Changes], + action => maps:get(action, Result, undefined), + auxst => maps:get(auxst, Result, #{}) + }); +to_prg_result_(#{action := Action} = Result) -> + genlib_map:compact(#{ + action => Action, + auxst => maps:get(auxst, Result, #{}) + }); +to_prg_result_(#{}) -> + #{auxst => #{}}. -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,11 +862,27 @@ repair_scenario(Scenario, #st{activity = {payment, PaymentID}} = St) -> %% --spec collapse_history([hg_machine:event()]) -> st(). +-spec collapse_st(machine()) -> st(). +collapse_st(#{history := History}) -> + lists:foldl( + fun({ID, Dt, Changes}, St0) -> + St1 = apply_event(Changes, St0, event_timestamp_to_binary(Dt)), + St1#st{latest_event_id = ID} + end, + #st{}, + History + ). + +event_timestamp_to_binary(Bin) when is_binary(Bin) -> + Bin; +event_timestamp_to_binary(Dt) -> + hg_datetime:format_dt(Dt). + +-spec collapse_history([prg_machine:event()]) -> st(). collapse_history(History) -> lists:foldl( fun({ID, Dt, Changes}, St0) -> - St1 = collapse_changes(Changes, St0, #{timestamp => Dt}), + St1 = collapse_changes(Changes, St0, #{timestamp => event_timestamp_to_binary(Dt)}), St1#st{latest_event_id = ID} end, #st{}, @@ -988,11 +1021,98 @@ get_message(invoice_created) -> get_message(invoice_status_changed) -> "Invoice status is changed". -%% Marshalling +%% prg_machine codec + +-spec apply_event([invoice_change()], st() | undefined) -> st(). +apply_event(Changes, St) -> + apply_event(Changes, St, undefined). + +-spec apply_event([invoice_change()], st() | undefined, event_timestamp() | undefined) -> st(). +apply_event(Changes, St0, Dt) -> + St = case St0 of undefined -> #st{}; _ -> St0 end, + Opts = + case Dt of + undefined -> #{}; + Bin when is_binary(Bin) -> #{timestamp => Bin}; + CalDt -> #{timestamp => hg_datetime:format_dt(CalDt)} + end, + collapse_changes(Changes, St, Opts). + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(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(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + decode_event_body(Payload); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + msgpack_payload_to_binary(mg_msgpack_marshalling:marshal(AuxSt)). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(<<>>) -> + #{}; +unmarshal_aux_state(Payload) when is_binary(Payload) -> + try + mg_msgpack_marshalling:unmarshal(binary_to_term(Payload, [safe])) + catch + _:_ -> + binary_to_term(Payload, [safe]) + 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. --spec marshal_event_payload([invoice_change()]) -> hg_machine:event_payload(). -marshal_event_payload(Changes) when is_list(Changes) -> - wrap_event_payload({invoice_changes, Changes}). +try_unmarshal_msgpack_payload(Payload) -> + try + {ok, mg_msgpack_marshalling:unmarshal(binary_to_term(Payload, [safe]))} + 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(Data) -> + unmarshal_event_payload(#{format_version => ?EVENT_FORMAT_VERSION, data => Data}). + +-type event_timestamp() :: calendar:datetime(). + +-spec action_to_prg(prg_machine_action:t() | undefined) -> action(). +action_to_prg(#mg_stateproc_ComplexAction{timer = Timer, remove = Remove}) -> + Action0 = prg_machine_action:new(), + Action1 = + case Timer of + undefined -> + Action0; + {set_timer, #mg_stateproc_SetTimerAction{timer = T}} -> + prg_machine_action:set_timer(T, Action0); + {unset_timer, #mg_stateproc_UnsetTimerAction{}} -> + prg_machine_action:unset_timer(Action0) + end, + case Remove of + undefined -> + Action1; + #mg_stateproc_RemoveAction{} -> + prg_machine_action:mark_removal(Action1) + end; +action_to_prg(Action) -> + Action. + +%% Marshalling -spec marshal_invoice(invoice()) -> binary(). marshal_invoice(Invoice) -> @@ -1001,15 +1121,22 @@ 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:event()]) -> [prg_machine:event([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:event()) -> prg_machine:event([invoice_change()]). +unmarshal_event({ID, Dt, Payload}) when is_list(Payload) -> + {ID, Dt, Payload}; unmarshal_event({ID, Dt, Payload}) -> {ID, 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), diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index 79d82bc5..48652984 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -148,14 +148,16 @@ 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 +166,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">>]}); @@ -225,11 +227,11 @@ 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), + History = prg_machine:get_history(hg_invoice:namespace(), ID), hg_invoice:unmarshal_history(map_history_error(History)). 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}) -> @@ -239,10 +241,15 @@ publish_invoice_event(InvoiceID, {ID, Dt, Event}) -> #payproc_Event{ id = ID, source = {invoice_id, InvoiceID}, - created_at = Dt, + created_at = format_event_timestamp(Dt), payload = ?invoice_ev(Event) }. +format_event_timestamp(Dt) when is_binary(Dt) -> + Dt; +format_event_timestamp(Dt) -> + hg_datetime:format_dt(Dt). + map_history_error({ok, Result}) -> Result; map_history_error({error, notfound}) -> diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 493c5d6d..a9a27c8d 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -388,7 +388,7 @@ get_chargeback_opts(#st{opts = Opts} = St) -> %% -type event() :: dmsl_payproc_thrift:'InvoicePaymentChangePayload'(). --type action() :: hg_machine_action:t(). +-type action() :: prg_machine_action:t(). -type events() :: [event()]. -type result() :: {events(), action()}. -type machine_result() :: {next | done, result()}. @@ -462,7 +462,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, prg_machine_action:instant()}}. seed_bank_card_from_parent(PartyConfigRef, BCT, #{parent_payment := ParentPayment}) -> case get_recurrent_token(ParentPayment) of @@ -985,7 +985,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, prg_machine_action:instant()}}. partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocation) -> Payment = get_payment(St0), @@ -1009,7 +1009,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, prg_machine_action:instant()}}. -spec cancel(st(), binary()) -> {ok, result()}. cancel(St, Reason) -> @@ -1017,7 +1017,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, prg_machine_action:instant()}}. assert_capture_cost_currency(undefined, _) -> ok; @@ -1148,7 +1148,7 @@ refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> refund => Refund, cash_flow => FinalCashflow }), - {Refund, {Changes, hg_machine_action:instant()}}. + {Refund, {Changes, prg_machine_action:instant()}}. -spec manual_refund(refund_params(), st(), opts()) -> {domain_refund(), result()}. manual_refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> @@ -1165,7 +1165,7 @@ manual_refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> cash_flow => FinalCashflow, transaction_info => TransactionInfo }), - {Refund, {Changes, hg_machine_action:instant()}}. + {Refund, {Changes, prg_machine_action:instant()}}. make_refund(Params, Payment, Revision, CreatedAt, St, Opts) -> _ = assert_no_pending_chargebacks(St), @@ -1625,7 +1625,7 @@ construct_adjustment( state = State }, Events = [?adjustment_ev(ID, ?adjustment_created(Adjustment)) | AdditionalEvents], - {Adjustment, {Events, hg_machine_action:instant()}}. + {Adjustment, {Events, prg_machine_action:instant()}}. construct_adjustment_id(#st{adjustments = As}) -> erlang:integer_to_binary(length(As) + 1). @@ -1679,7 +1679,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], prg_machine_action:new()}}. prepare_adjustment_cashflow(Adjustment, St, Options) -> PlanID = construct_adjustment_plan_id(Adjustment, St, Options), @@ -1755,7 +1755,7 @@ process_signal(timeout, St, Options) -> ). process_timeout(St) -> - Action = hg_machine_action:new(), + Action = prg_machine_action:new(), repair_process_timeout(get_activity(St), Action, St). -spec process_timeout(activity(), action(), st()) -> machine_result(). @@ -1909,14 +1909,14 @@ process_shop_limit_initialization(Action, 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()], prg_machine_action:set_timeout(0, Action)}}; {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, prg_machine_action:set_timeout(0, Action)}} end. construct_shop_limit_failure(limit_overflow, IDs) -> @@ -1927,13 +1927,13 @@ construct_shop_limit_failure(limit_overflow, IDs) -> 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))], prg_machine_action:set_timeout(0, Action)}}. -spec process_shop_limit_finalization(action(), st()) -> machine_result(). 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()], prg_machine_action:set_timeout(0, Action)}}. -spec process_risk_score(action(), st()) -> machine_result(). process_risk_score(Action, St) -> @@ -1947,7 +1947,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, prg_machine_action:set_timeout(0, Action)}}; {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) @@ -1984,7 +1984,7 @@ process_routing(Action, St) -> Revision, St ), - {next, {Events, hg_machine_action:set_timeout(0, Action)}} + {next, {Events, prg_machine_action:set_timeout(0, Action)}} end end. @@ -2109,7 +2109,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, prg_machine_action:set_timeout(0, Action)}} end. log_rejected_route_groups(Result, VS) -> @@ -2182,7 +2182,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, prg_machine_action:set_timeout(0, Action)}}. %% @@ -2239,7 +2239,7 @@ 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, prg_machine_action:instant()}}. process_accounter_update(Action, #st{partial_cash_flow = FinalCashflow, capture_data = CaptureData} = St) -> #payproc_InvoicePaymentCaptureData{ @@ -2256,7 +2256,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, prg_machine_action:set_timeout(0, Action)}}. %% @@ -2281,11 +2281,11 @@ process_session(St) -> process_session(undefined, St0) -> Target = get_target(St0), TargetType = get_target_type(Target), - Action = hg_machine_action:new(), + Action = prg_machine_action:new(), 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, prg_machine_action:set_timeout(0, Action)}, {next, Result}; Failure -> process_failure(get_activity(St0), [], Action, Failure, St0) @@ -2310,7 +2310,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 = prg_machine_action:set_timeout(0, Action), InvoiceID = get_invoice_id(get_invoice(get_opts(St0))), St1 = collapse_changes(Events1, St0, #{invoice_id => InvoiceID}), _ = @@ -2357,7 +2357,7 @@ finalize_payment(Action, St) -> _ -> start_session(Target) end, - {done, {StartEvents, hg_machine_action:set_timeout(0, Action)}}. + {done, {StartEvents, prg_machine_action:set_timeout(0, Action)}}. -spec process_result(action(), st()) -> machine_result(). process_result(Action, St) -> @@ -2397,18 +2397,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)], prg_machine_action:set_timeout(0, Action)}}; 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), + NewAction = prg_machine_action:set_timeout(0, Action), 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), + NewAction = prg_machine_action:set_timeout(0, Action), %% We need to rollback only current route. %% Previously used routes are supposed to have their limits already rolled back. Route = get_route(St), @@ -2590,7 +2590,7 @@ process_fatal_payment_failure(?captured(), _Events, _Action, Failure, _St) -> error({invalid_capture_failure, Failure}); 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, prg_machine_action:set_timeout(0, Action)}}. retry_session(Action, Target, Timeout) -> NewEvents = start_session(Target), @@ -2645,15 +2645,15 @@ do_check_failure_type(_Failure) -> get_action(?processed(), Action, St) -> case get_payment_flow(get_payment(St)) of ?invoice_payment_flow_instant() -> - hg_machine_action:set_timeout(0, Action); + prg_machine_action:set_timeout(0, Action); ?invoice_payment_flow_hold(_, HeldUntil) -> - hg_machine_action:set_deadline(HeldUntil, Action) + prg_machine_action:set_deadline(HeldUntil, Action) end; get_action(_Target, Action, _St) -> Action. set_timer(Timer, Action) -> - hg_machine_action:set_timer(Timer, Action). + prg_machine_action:set_timer(Timer, Action). get_provider_payment_terms(St, Revision) -> Opts = get_opts(St), @@ -3943,8 +3943,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). diff --git a/apps/hellgate/src/hg_invoice_payment_chargeback.erl b/apps/hellgate/src/hg_invoice_payment_chargeback.erl index 80adbf8e..ed4b9487 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_machine_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, prg_machine_action:new(), Opts); process_timeout(updating_cash_flow, State, _Action, Opts) -> - update_cash_flow(State, hg_machine_action:instant(), Opts); + update_cash_flow(State, prg_machine_action:instant(), 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 = prg_machine_action:instant(), 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 = prg_machine_action:instant(), 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 = prg_machine_action:instant(), 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 = prg_machine_action:instant(), 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 = prg_machine_action:instant(), 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 6c18a766..9f5908c4 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -103,7 +103,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_machine_action:t(). -type result() :: {events(), action()}. -type machine_result() :: {next | done, result()}. @@ -271,10 +271,10 @@ do_process(accounter, Refund) -> do_process(failure, Refund) -> process_failure(Refund); do_process(finished, _Refund) -> - {done, {[], hg_machine_action:new()}}. + {done, {[], prg_machine_action:new()}}. process_refund_cashflow(Refund) -> - Action = hg_machine_action:set_timeout(0, hg_machine_action:new()), + Action = prg_machine_action:set_timeout(0, prg_machine_action:new()), PartyConfigRef = get_injected_party_config_ref(Refund), ShopConfigRef = get_injected_shop_config_ref(Refund), Shop = get_injected_shop(Refund), @@ -316,7 +316,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 = prg_machine_action:set_timeout(0, Action), {next, {Events1, NewAction}}; {finished, ?session_failed(Failure)} -> case check_retry_possibility(Failure, Refund) of @@ -326,7 +326,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, prg_machine_action:set_timeout(0, Action)}} end; _ -> {next, {Events1, Action}} @@ -335,14 +335,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())], prg_machine_action:new()}}. 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, prg_machine_action:new()}}. hold_refund_limits(Refund) -> DomainRefund = refund(Refund), @@ -453,7 +453,7 @@ get_manual_refund_events(_) -> retry_session(Action, Timeout) -> NewEvents = [hg_session:wrap_event(?refunded(), hg_session:create())], - NewAction = hg_machine_action:set_timer({timeout, Timeout}, Action), + NewAction = prg_machine_action:set_timer({timeout, Timeout}, Action), {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..ba173500 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, prg_machine_action:instant()}}. -spec merge_change( hg_invoice_payment:change(), @@ -147,7 +147,7 @@ process_signal(timeout, St, Options) -> ). process_timeout(St) -> - Action = hg_machine_action:new(), + Action = prg_machine_action:new(), process_timeout(hg_invoice_payment:get_activity(St), Action, St). process_timeout({payment, processing_capture}, Action, St) -> @@ -172,7 +172,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, prg_machine_action:set_timeout(0, Action)}}. 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..55b4ad41 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}))], prg_machine_action:instant()}}}; 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..5dbce709 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -6,7 +6,8 @@ -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -include_lib("damsel/include/dmsl_payproc_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 +15,7 @@ -export([handle_function/3]). %% Machine callbacks --behaviour(hg_machine). +-behaviour(prg_machine). -export([namespace/0]). @@ -22,6 +23,10 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([marshal_event_body/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). %% API @@ -34,6 +39,8 @@ %% Internal types -type invoice_template_change() :: dmsl_payproc_thrift:'InvoiceTemplateChange'(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). %% API @@ -167,10 +174,12 @@ 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} -> @@ -182,7 +191,7 @@ call(ID, Function, Args) -> end. get_history(TplID) -> - unmarshal_history(map_history_error(hg_machine:get_history(?NS, TplID))). + map_history_error(prg_machine:get_history(?NS, TplID)). -spec map_error(notfound | any()) -> no_return(). map_error(notfound) -> @@ -198,12 +207,14 @@ map_start_error({error, Reason}) -> map_history_error({ok, Result}) -> Result; map_history_error({error, notfound}) -> - throw(#payproc_InvoiceTemplateNotFound{}). + throw(#payproc_InvoiceTemplateNotFound{}); +map_history_error({error, Reason}) -> + error(Reason). %% 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 +233,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 +258,24 @@ 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_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()}. +-spec process_call(call(), machine()) -> {prg_machine:response(), prg_result()}. process_call(Call, #{history := History}) -> - St = collapse_history(unmarshal_history(History)), + St = collapse_history(History), 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}, #{}} @@ -334,9 +345,56 @@ 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:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(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(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + decode_event_body(Payload); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-spec marshal_aux_state(term()) -> binary(). +marshal_aux_state(AuxSt) -> + msgpack_payload_to_binary(mg_msgpack_marshalling:marshal(AuxSt)). + +-spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(<<>>) -> + #{}; +unmarshal_aux_state(Payload) when is_binary(Payload) -> + try + mg_msgpack_marshalling:unmarshal(binary_to_term(Payload, [safe])) + catch + _:_ -> + binary_to_term(Payload, [safe]) + 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, [safe]))} + 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(Data) -> + unmarshal_event_payload(#{format_version => ?EVENT_FORMAT_VERSION, data => Data}). wrap_event_payload(Payload) -> Type = {struct, union, {dmsl_payproc_thrift, 'EventPayload'}}, @@ -353,15 +411,7 @@ 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), 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..9d20d976 --- /dev/null +++ b/apps/hellgate/src/hg_invoicing_machine_client.erl @@ -0,0 +1,76 @@ +-module(hg_invoicing_machine_client). + +%%% Thrift RPC to invoicing machines via progressor. +%%% Encode/decode with hg_proto_utils; transport via prg_machine:call/6. +%%% hg_proto stays in apps/hellgate (not in prg_machine). + +-export([thrift_call/5]). +-export([thrift_call/8]). + +-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 event_id() :: prg_machine:event_id(). +-type response() :: prg_machine:response(). + +-spec thrift_call(namespace(), 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( + namespace(), + id(), + service_name(), + function_ref(), + args(), + event_id() | undefined, + non_neg_integer() | undefined, + forward | backward +) -> response() | {error, notfound | failed}. +thrift_call(NS, ID, ServiceName, FunRef, Args, After, Limit, Direction) -> + EncodedArgs = marshal_thrift_args(ServiceName, FunRef, Args), + MachineCall = {FunRef, unmarshal_thrift_args(ServiceName, FunRef, EncodedArgs)}, + case prg_machine:call(NS, ID, MachineCall, After, Limit, Direction) of + {ok, Response} -> + unmarshal_thrift_response(ServiceName, FunRef, Response); + {error, notfound} -> + {error, notfound}; + {error, failed} -> + {error, failed}; + {error, _} = Error -> + Error + end. + +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). + +unmarshal_thrift_args(ServiceName, FunctionRef, EncodedArgs) -> + {Service, _Function} = FunctionRef, + {Module, Service} = hg_proto:get_service(ServiceName), + FullFunctionRef = {Module, FunctionRef}, + hg_proto_utils:deserialize_function_args(FullFunctionRef, EncodedArgs). + +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} when is_binary(EncodedReply) -> + Reply = hg_proto_utils:deserialize_function_reply(FullFunctionRef, EncodedReply), + {ok, Reply}; + {ok, Reply} -> + {ok, Reply}; + {exception, EncodedException} when is_binary(EncodedException) -> + Exception = hg_proto_utils:deserialize_function_exception(FullFunctionRef, EncodedException), + {exception, Exception}; + {exception, Exception} -> + {exception, Exception} + end. 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..8dd632e5 100644 --- a/apps/hellgate/src/hg_machine_tag.erl +++ b/apps/hellgate/src/hg_machine_tag.erl @@ -7,9 +7,9 @@ -export([create_binding/4]). -type tag() :: dmsl_base_thrift:'Tag'(). --type ns() :: hg_machine:ns(). +-type ns() :: hg_stateproc_types:ns(). -type entity_id() :: dmsl_base_thrift:'ID'(). --type machine_id() :: hg_machine:id(). +-type machine_id() :: hg_stateproc_types:id(). -spec get_binding(ns(), tag()) -> {ok, entity_id(), machine_id()} | {error, notfound}. get_binding(NS, Tag) -> diff --git a/apps/hellgate/src/hg_session.erl b/apps/hellgate/src/hg_session.erl index a84ee8e4..f3f6d482 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_machine_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, prg_machine_action:instant()}, 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}. + {{[], prg_machine_action:new()}, 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, prg_machine_action:new()}, 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, prg_machine_action:new(), 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, prg_machine_action:unset_timer(prg_machine_action:new()), 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, prg_machine_action:new()}. apply_result({Events, _Action} = Result, T) -> {Result, update_state_with(Events, T)}. @@ -367,7 +367,7 @@ handle_proxy_intent(#proxy_provider_FinishIntent{status = {failure, Failure}}, A 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), + Action1 = prg_machine_action:set_timer(Timer, Action0), {[], Action1}; handle_proxy_intent( #proxy_provider_SuspendIntent{tag = Tag, timeout = Timer, timeout_behaviour = TimeoutBehaviour}, @@ -376,7 +376,7 @@ handle_proxy_intent( ) -> #{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_machine_action:set_timer(Timer, Action0), Events = [?session_suspended(Tag, TimeoutBehaviour)], {Events, Action1}. diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index 37e6b445..0397de08 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -319,42 +319,32 @@ start_app(progressor = AppName) -> {namespaces, #{ invoice => #{ processor => #{ - client => hg_progressor, + client => prg_machine, options => #{ - party_client => #{}, - ns => <<"invoice">>, - handler => hg_machine + ns => invoice, + env_enter => fun(WoodyCtx) -> + ok = hg_context:save(hg_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> hg_context:cleanup() end } }, 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, + env_enter => fun(WoodyCtx) -> + ok = hg_context:save(hg_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> hg_context:cleanup() end } } } diff --git a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl index e5fc2586..362f2d41 100644 --- a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl @@ -27,7 +27,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(). @@ -64,7 +63,6 @@ groups() -> {payments, [parallel], [ payment_start_idempotency, payment_success, - payment_success_trace, payment_w_first_blacklisted_success, payment_w_all_blacklisted, register_payment_success, @@ -253,158 +251,6 @@ payment_success(C) -> Trx ). --spec payment_success_trace(config()) -> test_return(). -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 - }, - 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. - -spec payment_w_first_blacklisted_success(config()) -> test_return(). payment_w_first_blacklisted_success(C) -> Client = cfg(client, C), diff --git a/apps/hg_progressor/src/hg_hybrid.erl b/apps/hg_progressor/src/hg_hybrid.erl index 9bd64efc..a710241a 100644 --- a/apps/hg_progressor/src/hg_hybrid.erl +++ b/apps/hg_progressor/src/hg_hybrid.erl @@ -7,7 +7,7 @@ -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 + case call_machinegun('GetMachine', {MachineDesc}) of {ok, Machine} -> ok = migrate(unmarshal(machine, Machine), unmarshal(descriptor, MachineDesc)), {error, exists}; @@ -26,7 +26,7 @@ call_automaton(Func, Args) -> %% Internal functions maybe_migrate_machine(MachineDesc) -> - case hg_machine:call_automaton('GetMachine', {MachineDesc}, machinegun) of + case call_machinegun('GetMachine', {MachineDesc}) of {error, notfound} = Error -> Error; {ok, Machine} -> @@ -135,6 +135,20 @@ extract_descriptor({MachineDescriptor}) -> extract_descriptor({MachineDescriptor, _}) -> MachineDescriptor. +call_machinegun(Function, Args) -> + case hg_woody_wrapper:call(automaton, Function, Args) of + {ok, _} = Result -> + Result; + {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. + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/apps/hg_progressor/src/hg_progressor.app.src b/apps/hg_progressor/src/hg_progressor.app.src index feb05522..69109259 100644 --- a/apps/hg_progressor/src/hg_progressor.app.src +++ b/apps/hg_progressor/src/hg_progressor.app.src @@ -4,7 +4,11 @@ {registered, []}, {applications, [ kernel, - stdlib + stdlib, + damsel, + progressor, + prg_machine, + hg_proto ]}, {env, []}, {modules, []}, diff --git a/apps/hg_progressor/src/hg_progressor.erl b/apps/hg_progressor/src/hg_progressor.erl index 024b836a..267f6a81 100644 --- a/apps/hg_progressor/src/hg_progressor.erl +++ b/apps/hg_progressor/src/hg_progressor.erl @@ -6,16 +6,10 @@ %% 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(). @@ -111,95 +105,14 @@ call_automaton('Repair', {MachineDesc, Args}) -> cleanup() -> Namespaces = [ invoice, - invoice_template, - customer, - recurrent_paytools + invoice_template ], 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, []). @@ -275,97 +188,15 @@ marshal(status, {<<"error">>, 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 - }. + binary_to_term(Term). 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}) -> 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_proto.erl b/apps/hg_proto/src/hg_proto.erl index 2da3e413..387b7797 100644 --- a/apps/hg_proto/src/hg_proto.erl +++ b/apps/hg_proto/src/hg_proto.erl @@ -45,7 +45,11 @@ get_service(party_config) -> get_service(customer_management) -> {dmsl_customer_thrift, 'CustomerManagement'}; get_service(bank_card_storage) -> - {dmsl_customer_thrift, 'BankCardStorage'}. + {dmsl_customer_thrift, 'BankCardStorage'}; +get_service(invoice_trace) -> + {dmsl_progressor_trace_thrift, 'InvoiceTrace'}; +get_service(invoice_template_trace) -> + {dmsl_progressor_trace_thrift, 'InvoiceTemplateTrace'}. -spec get_service_spec(Name :: atom()) -> service_spec(). get_service_spec(Name) -> @@ -59,4 +63,8 @@ get_service_spec(invoice_templating = Name, #{}) -> get_service_spec(processor = Name, #{namespace := Ns}) when is_binary(Ns) -> {?VERSION_PREFIX ++ "/stateproc/" ++ binary_to_list(Ns), get_service(Name)}; get_service_spec(proxy_host_provider = Name, #{}) -> - {?VERSION_PREFIX ++ "/proxyhost/provider", get_service(Name)}. + {?VERSION_PREFIX ++ "/proxyhost/provider", get_service(Name)}; +get_service_spec(invoice_trace = Name, #{}) -> + {?VERSION_PREFIX ++ "/trace/invoice", get_service(Name)}; +get_service_spec(invoice_template_trace = Name, #{}) -> + {?VERSION_PREFIX ++ "/trace/invoice_template", get_service(Name)}. 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..59d9ae33 --- /dev/null +++ b/apps/prg_machine/src/prg_machine.app.src @@ -0,0 +1,17 @@ +{application, prg_machine, [ + {description, "Unified progressor machine runtime for HG and FF"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + genlib, + woody, + scoper, + progressor + ]}, + {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..74b95a02 --- /dev/null +++ b/apps/prg_machine/src/prg_machine.erl @@ -0,0 +1,544 @@ +-module(prg_machine). + +%%% Unified runtime: HTTP/woody handlers -> domain (-behaviour(prg_machine)) -> progressor. +%%% Replaces hg_machine, ff_machine, machinery client/backend stack for progressor. + +-define(TABLE, prg_machine_dispatch). +-define(PROCESSOR_EXCEPTION(Class, Reason, _Stacktrace), {exception, Class, Reason}). + +%% Types + +-type namespace() :: atom(). +-type id() :: binary(). +-type args() :: term(). +-type call() :: term(). +-type response() :: ok | {ok, term()} | {exception, term()}. + +-type event_id() :: pos_integer(). +-type timestamp() :: calendar:datetime(). +-type event_body() :: term(). +-type event() :: {event_id(), timestamp(), event_body()}. +-type history() :: [event()]. + +-type range() :: {undefined | event_id(), undefined | non_neg_integer(), forward | backward}. + +-type machine() :: #{ + namespace := namespace(), + id := id(), + history := history(), + aux_state := term(), + range => range() +}. + +-type signal() :: timeout | {repair, args()}. +-type result() :: #{ + events => [event_body()], + action => prg_machine_action:t(), + auxst => term() +}. + +-type env_enter_fun() :: fun(() -> ok) | fun((woody_context:ctx()) -> ok). + +-type processor_opts() :: #{ + ns := namespace(), + env_enter => env_enter_fun(), + env_leave => fun(() -> ok) +}. + +-export_type([ + namespace/0, + id/0, + args/0, + call/0, + response/0, + event/0, + history/0, + machine/0, + signal/0, + result/0, + range/0, + processor_opts/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(event_body()) -> {undefined | pos_integer(), binary()}. + +-callback unmarshal_event_body(undefined | pos_integer(), binary()) -> event_body(). + +-callback marshal_aux_state(term()) -> binary(). + +-callback unmarshal_aux_state(binary()) -> term(). + +%% Optional: collapse passes event_id and timestamp (HG invoice). Default: apply_event/2. +-callback apply_event(event_id(), timestamp(), event_body(), term()) -> term(). + +-optional_callbacks([ + process_notification/2, + marshal_event_body/1, + unmarshal_event_body/2, + marshal_aux_state/1, + unmarshal_aux_state/1, + apply_event/4 +]). + +%% 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([remove/2]). +-export([trace/2]). + +%% Progressor processor + +-export([process/3]). + +%% Registry (namespace -> handler module) + +-export([get_child_spec/1]). +-export([start_link/1]). +-export([init/1]). + +%% Event-sourcing helpers (replaces ff_machine) + +-export([collapse/2]). +-export([emit_event/1]). +-export([emit_events/1]). +-export([timestamp/0]). + +%% + +-spec start(namespace(), id(), args()) -> {ok, ok} | {error, exists | 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, {exception, _, _} = Exception} -> + raise_exception(Exception) + end. + +-spec call(namespace(), id(), call()) -> {ok, response()} | {error, notfound | 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 | 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, _, _} = Exception} -> + raise_exception(Exception); + {error, _} = Error -> + Error + end. + +-spec repair(namespace(), id(), args()) -> + {ok, term()} | {error, notfound | working | failed | {repair, {failed, binary()}}}. +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, _, _} = Exception} -> + raise_exception(Exception); + {error, Reason} -> + {error, {repair, {failed, Reason}}} + end. + +-spec get(namespace(), id(), range()) -> {ok, machine()} | {error, notfound}. +get(NS, ID, Range) -> + Req = request(NS, ID, undefined, range_map(Range)), + case progressor:get(Req) of + {ok, Process} -> + Handler = get_handler_module(NS), + {ok, unmarshal_machine(Handler, NS, Process)}; + {error, <<"process not found">>} -> + {error, notfound}; + {error, {exception, _, _} = Exception} -> + raise_exception(Exception) + end. + +-spec get(namespace(), id()) -> {ok, machine()} | {error, notfound}. +get(NS, ID) -> + get(NS, ID, {undefined, undefined, forward}). + +-spec get_history(namespace(), id()) -> {ok, history()} | {error, notfound}. +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, notfound}. +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, notfound}. +get_history(NS, ID, After, Limit, Direction) -> + case get(NS, ID, {After, Limit, Direction}) of + {ok, #{history := History}} -> + {ok, History}; + Error -> + Error + end. + +-spec notify(namespace(), id(), args()) -> ok | {error, notfound}. +notify(NS, ID, Args) -> + case call(NS, ID, {notify, Args}) of + {ok, _} -> ok; + {error, notfound} = Error -> + Error + end. + +-spec remove(namespace(), id()) -> ok | {error, notfound}. +remove(NS, ID) -> + case call(NS, ID, remove) of + {ok, _} -> ok; + {error, notfound} = Error -> + Error + end. + +-spec trace(namespace(), id()) -> {ok, [map()]} | {error, term()}. +trace(NS, ID) -> + progressor:trace(#{ns => NS, id => ID}). + +%% Progressor processor callback. +%% progressor config: #{client => prg_machine, options => #{ns => invoice, ...}} + +-spec process({init | call | repair | notify | timeout, binary(), map()}, processor_opts(), binary()) -> + {ok, map()} | {error, term()}. +process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> + Enter = maps:get(env_enter, Opts, fun(_) -> ok end), + Leave = maps:get(env_leave, Opts, fun() -> ok end), + try + {WoodyCtx, OtelCtx} = decode_rpc_context(BinCtx), + ok = woody_rpc_helper:attach_otel_context(OtelCtx), + ok = run_env_enter(Enter, WoodyCtx), + Handler = get_handler_module(NS), + 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) + catch + Class:Reason:_Stacktrace -> + {error, ?PROCESSOR_EXCEPTION(Class, Reason, _Stacktrace)} + after + Leave() + end. + +%% Registry + +-spec get_child_spec([module()]) -> supervisor:child_spec(). +get_child_spec(Handlers) -> + #{ + id => prg_machine_dispatch, + start => {?MODULE, start_link, [Handlers]}, + type => supervisor + }. + +-spec start_link([module()]) -> {ok, pid()}. +start_link(Handlers) -> + supervisor:start_link(?MODULE, Handlers). + +-spec init([module()]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init(Handlers) -> + _ = ets:new(?TABLE, [named_table, {read_concurrency, true}]), + true = ets:insert_new(?TABLE, [{H:namespace(), H} || H <- Handlers]), + {ok, {#{}, []}}. + +%% Event-sourcing (replaces ff_machine collapse/emit) + +-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_event(term()) -> [{ev, timestamp(), term()}]. +emit_event(Event) -> + emit_events([Event]). + +-spec emit_events([term()]) -> [{ev, timestamp(), term()}]. +emit_events(Events) -> + Ts = timestamp(), + [{ev, Ts, Body} || Body <- Events]. + +-spec timestamp() -> timestamp(). +timestamp() -> + calendar:universal_time(). + +%% 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), + dispatch_notification(Handler, Args, Machine); +dispatch(Handler, call, BinArgs, Machine) -> + case decode_term(BinArgs) of + {notify, Args} -> + dispatch_notification(Handler, 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. + +dispatch_notification(Handler, Args, Machine) -> + case erlang:function_exported(Handler, process_notification, 2) of + true -> + Handler:process_notification(Args, Machine); + false -> + #{} + 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, #{events := Events, action := Action, auxst := AuxSt}) -> + genlib_map:compact(#{ + events => marshal_new_events(Handler, LastEventID, Events), + action => prg_machine_action:to_progressor(Action), + aux_state => marshal_aux_state(Handler, AuxSt) + }); +marshal_intent(Handler, LastEventID, Result) -> + marshal_intent(Handler, LastEventID, maps:merge(#{events => [], auxst => undefined}, Result)). + +%% 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 := TsSec, + metadata := Meta, + payload := Payload +}) -> + Format = maps:get(<<"format">>, Meta, maps:get(format, Meta, undefined)), + Body = unmarshal_event_body(Handler, Format, Payload), + {EventID, event_timestamp_to_datetime(TsSec), Body}; +unmarshal_event(Handler, #{event_id := _EventID} = Ev) -> + Meta = maps:get(metadata, Ev, #{}), + unmarshal_event(Handler, Ev#{metadata => Meta, timestamp => maps:get(timestamp, Ev, 0)}). + +marshal_new_events(Handler, LastEventID, Bodies) -> + Ts = erlang:system_time(microsecond), + lists:zipwith( + fun(EventID, Body) -> + {Format, Bin} = marshal_event_body(Handler, Body), + #{ + event_id => EventID, + timestamp => Ts div 1000000, + metadata => event_metadata(Format), + payload => Bin + } + end, + lists:seq(LastEventID + 1, LastEventID + length(Bodies)), + Bodies + ). + +marshal_event_body(Handler, Body) -> + case erlang:function_exported(Handler, marshal_event_body, 1) of + true -> + Handler:marshal_event_body(Body); + false -> + {undefined, term_to_binary(Body)} + end. + +unmarshal_event_body(Handler, Format, Payload) -> + case erlang:function_exported(Handler, unmarshal_event_body, 2) of + true -> + Handler:unmarshal_event_body(Format, Payload); + false -> + binary_to_term(Payload) + end. + +marshal_aux_state(Handler, AuxSt) -> + case erlang:function_exported(Handler, marshal_aux_state, 1) of + true -> + Handler:marshal_aux_state(AuxSt); + false -> + term_to_binary(AuxSt) + end. + +unmarshal_aux_state(_Handler, undefined) -> + undefined; +unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> + case erlang:function_exported(Handler, unmarshal_aux_state, 1) of + true -> + Handler:unmarshal_aux_state(Bin); + false -> + binary_to_term(Bin) + end. + +event_metadata(undefined) -> + #{<<"format">> => 0}; +event_metadata(Format) when is_integer(Format) -> + #{<<"format">> => Format}. + +event_timestamp_to_datetime({{_, _, _}, {_, _, _}} = Dt) -> + Dt; +event_timestamp_to_datetime(Ts) when is_integer(Ts) -> + TsSeconds = prg_utils:to_seconds(Ts), + calendar:system_time_to_universal_time(TsSeconds, second). + +dispatch_apply_event(Handler, EventID, Ts, Body, Model) -> + case erlang:function_exported(Handler, apply_event, 4) of + true -> + Handler:apply_event(EventID, Ts, Body, Model); + false -> + case erlang:function_exported(Handler, apply_event, 2) of + true -> + Handler:apply_event(Body, Model); + false -> + erlang:error({apply_event_not_defined, Handler}) + end + end. + +initial_model(_Handler, AuxState) -> + maps:get(model, AuxState, undefined). + +get_handler_module(NS) -> + ets:lookup_element(?TABLE, NS, 2). + +%% 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 = + try application:get_env(prg_machine, woody_context_loader, undefined) of + {M, F} when is_atom(M), is_atom(F) -> + M:F(); + Loader when is_function(Loader, 0) -> + Loader(); + undefined -> + woody_context:new() + catch + _:_ -> + woody_context:new() + end, + 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_env_enter(Enter, WoodyCtx) when is_function(Enter, 1) -> + Enter(WoodyCtx); +run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> + Enter(). + +encode_term(Term) -> + term_to_binary(Term). + +decode_term(Term) when is_binary(Term) -> + binary_to_term(Term); +decode_term(Term) -> + Term. + +encode_range(After, Limit, Direction) -> + genlib_map:compact(#{ + offset => After, + limit => Limit, + direction => Direction + }). + +range_map({After, Limit, Direction}) -> + encode_range(After, Limit, Direction); +range_map(#{offset := _} = Range) -> + Range. + +range_from_process(#{range := #{offset := Offset, limit := Limit, direction := Direction}}) -> + {Offset, Limit, Direction}; +range_from_process(_) -> + {undefined, undefined, forward}. + +raise_exception({exception, Class, Reason}) -> + erlang:raise(Class, Reason, []). diff --git a/apps/prg_machine/src/prg_machine_action.erl b/apps/prg_machine/src/prg_machine_action.erl new file mode 100644 index 00000000..ccdaa7fc --- /dev/null +++ b/apps/prg_machine/src/prg_machine_action.erl @@ -0,0 +1,98 @@ +-module(prg_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]). +-export([to_progressor/1]). + +-type seconds() :: non_neg_integer(). +-type datetime() :: calendar:datetime() | binary(). +-type timer() :: {timeout, seconds()} | {deadline, datetime()}. +-type t() :: + undefined + | unset_timer + | remove + | #{set_timer := timer(), remove => boolean()}. + +-export_type([t/0, timer/0, seconds/0]). + +-spec new() -> t(). +new() -> + #{}. + +-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), 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, Deadline}, Action). + +-spec set_timer(timer()) -> t(). +set_timer(Timer) -> + set_timer(Timer, new()). + +-spec set_timer(timer(), t()) -> t(). +set_timer(Timer, Action) -> + Action#{set_timer => Timer}. + +-spec unset_timer() -> t(). +unset_timer() -> + unset_timer(new()). + +-spec unset_timer(t()) -> t(). +unset_timer(Action) when is_map(Action) -> + maps:without([set_timer], Action); +unset_timer(unset_timer) -> + unset_timer. + +-spec mark_removal(t()) -> t(). +mark_removal(Action) -> + Action#{remove => true}. + +-spec to_progressor(t()) -> progressor_action() | undefined. +to_progressor(undefined) -> + undefined; +to_progressor(unset_timer) -> + unset_timer; +to_progressor(remove) -> + #{remove => true}; +to_progressor(#{set_timer := Timer, remove := true}) -> + #{set_timer => marshal_timer(Timer), remove => true}; +to_progressor(#{set_timer := Timer}) -> + #{set_timer => marshal_timer(Timer)}; +to_progressor(#{remove := true}) -> + #{remove => true}; +to_progressor(#{}) -> + undefined. + +%% + +-type progressor_action() :: #{set_timer := non_neg_integer(), remove => true} | unset_timer. + +marshal_timer({timeout, Seconds}) when is_integer(Seconds) -> + erlang:system_time(microsecond) div 1000000 + Seconds; +marshal_timer({deadline, {_, _} = Dt}) -> + genlib_time:daytime_to_unixtime(Dt); +marshal_timer({deadline, Bin}) when is_binary(Bin) -> + calendar:rfc3339_to_system_time(unicode:characters_to_list(Bin), [{unit, second}]). diff --git a/config/sys.config b/config/sys.config index 15013612..44c88a4e 100644 --- a/config/sys.config +++ b/config/sys.config @@ -361,11 +361,16 @@ {namespaces, #{ 'invoice' => #{ processor => #{ - client => hg_progressor, + client => prg_machine, options => #{ - party_client => #{}, - ns => <<"invoice">>, - handler => hg_machine + ns => invoice, + env_enter => fun(WoodyCtx) -> + ok = hg_context:save(hg_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> hg_context:cleanup() end } }, storage => #{ @@ -380,111 +385,92 @@ }, 'invoice_template' => #{ processor => #{ - client => hg_progressor, + client => prg_machine, options => #{ - party_client => #{}, - ns => <<"invoice_template">>, - handler => hg_machine + ns => invoice_template, + env_enter => fun(WoodyCtx) -> + ok = hg_context:save(hg_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> hg_context:cleanup() end } }, 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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } }, '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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } }, '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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } }, '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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } }, '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', + env_enter => fun(WoodyCtx) -> + ok = ff_context:save(ff_context:create(#{ + woody_context => WoodyCtx, + party_client => party_client:create_client() + })) + end, + env_leave => fun() -> ff_context:cleanup() end } } } diff --git a/docs/prg-machine-migration-context.md b/docs/prg-machine-migration-context.md new file mode 100644 index 00000000..0305873b --- /dev/null +++ b/docs/prg-machine-migration-context.md @@ -0,0 +1,315 @@ +# Миграция на `prg_machine`: контекст для следующих доработок + +Документ фиксирует **цель**, **целевую архитектуру**, **фактическое состояние** ветки `epic/monorepo` (hellgate, ~59 файлов, **не закоммичено** на момент написания) и **открытые хвосты**. + +Оркестрация Ralph: `/Users/artemfedorenko/Documents/paymentsols/ralph-2` (goal в `.ralph/goal.md`, задачи 1–36 verified, **37 — CT — не завершена**). + +--- + +## 1. Цель и направление + +**Единый runtime** для всех progressor namespace в Hellgate и Fistful: + +``` +woody handler (hg_*_handler, ff_*_handler) + → prg_machine:start | call | get | repair | notify | remove | trace + → progressor (storage + worker pool) + → prg_machine:process/3 + → domain module (-behaviour(prg_machine)) +``` + +**Убрать из prod path:** + +| Было | Статус | +|------|--------| +| `hg_machine` + `hg_progressor_handler` | **удалены** | +| `ff_machine` + `fistful` как machinery processor | **удалены** | +| `*_machinery_schema` в `ff_server` | **удалены** | +| `machinery_prg_backend` в `config/sys.config` для prod NS | **убран** | +| `hg_machine_action` | **удалён** → `prg_machine_action` | + +**Не в scope этой миграции (отдельные goals):** + +- Trace API на Thrift (`docs/trace-api-thrift.md`) +- Hybrid MG↔progressor (`hg_hybrid`, machinegun) +- Полное удаление зависимости `machinery` из `rebar.config` +- Дедупликация HG/FF утилит (`payproc_common`, `ff_core`) — другой Ralph goal + +**Принципы:** + +- Один путь данных, без dual-write и адаптеров «старый API → новый» в проде +- Доменные границы HG/FF сохраняются; общее — только в `apps/prg_machine` +- `hg_proto` / Thrift encode **не** тащить в `prg_machine` (тонкий `hg_invoicing_machine_client` в hellgate) + +--- + +## 2. Как должно работать: `prg_machine` + +### 2.1. OTP app `apps/prg_machine` + +| Модуль | Роль | +|--------|------| +| `prg_machine` | behaviour, client API, `process/3`, registry (ETS), `collapse`/`emit_events` | +| `prg_machine_action` | таймеры / remove → формат progressor | + +**Registry:** при старте `prg_machine:get_child_spec([Module, …])` в ETS `prg_machine_dispatch` кладётся `{Namespace, HandlerModule}` по `Handler:namespace/0`. Паттерн перенесён из старого `hg_machine` (без woody MG routes). + +**Client API** (`start`, `call`, `get`, `repair`, `notify`, `remove`, `trace`) — обёртки над `progressor:*` с encode/decode term и woody/otel context. + +**`process/3`** — callback progressor: + +1. `env_enter(WoodyCtx)` — поднять `hg_context` / `ff_context` +2. `unmarshal_machine` — history + aux_state из storage +3. `dispatch` → `Handler:init | process_call | process_signal | process_repair | process_notification` +4. `marshal_process_result` — events, action, aux_state обратно в progressor +5. `env_leave()` в `after` + +**Контекст RPC:** `application:set_env(prg_machine, woody_context_loader, Loader)` в `hellgate:start/2` и `ff_server:start/2` (fun или `{M,F}`), fallback на `woody_context:new()`. + +### 2.2. Behaviour (доменный модуль) + +Обязательные callbacks: + +- `namespace/0`, `init/2`, `process_signal/2`, `process_call/2`, `process_repair/2` +- `marshal_event_body/1`, `unmarshal_event_body/2`, `marshal_aux_state/1`, `unmarshal_aux_state/1` + +Опционально: `process_notification/2`. + +**Результат домена** (`result()`): + +```erlang +#{ + events => [EventBody, ...], + action => prg_machine_action:t(), % таймер / remove / undefined + auxst => term() % обычно #{ctx => ...} для FF, #{} для HG +} +``` + +**Actions:** `prg_machine_action` заменяет `hg_machine_action` и FF `continue`/`sleep`/`unset_timer`. Маппинг в progressor — `prg_machine_action:to_progressor/1`. + +### 2.3. Event-sourcing helpers + +| Функция | Назначение | +|---------|------------| +| `collapse/2` | fold по history: `apply_event/4` (EventID, Ts, Body, Model) если экспортирован, иначе `apply_event/2` (FF) | +| `emit_event/1`, `emit_events/1` | обёртка с timestamp для новых событий | +| `initial_model/2` | старт fold: `maps:get(model, AuxState, undefined)` — на практике почти всегда `undefined` | + +**FF:** домены вызывают `prg_machine:collapse(Mod, Machine)` в `*_machine` и внутри домена. + +**HG invoice (хвост):** пока `collapse_st/1` / `collapse_history/1`; целевой паттерн — `prg_machine:collapse` + `apply_event/4` (см. `.ralph/goal-hg-collapse.md` в ralph-2). + +### 2.4. Конфиг progressor (`config/sys.config`) + +Единый шаблон для каждого NS: + +```erlang +processor => #{ + client => prg_machine, + options => #{ + ns => , + env_enter => fun(WoodyCtx) -> ... context:save(...) end, + env_leave => fun() -> ... context:cleanup() end + } +} +``` + +Без `handler`, `schema`, `machinery_prg_backend`. + +### 2.5. Тонкие обёртки + +| Слой | Паттерн | +|------|---------| +| `*_machine.erl` | только `prg_machine:*` client API | +| `ff_*_handler.erl` | woody → `*_machine` / домен, без `machinery:` | +| `hg_invoicing_machine_client` | Thrift RPC к invoice machines через `prg_machine:call/6` + `hg_proto_utils` | + +--- + +## 3. Что сделано (этапы P0–P5) + +| Этап | Состояние | Содержание | +|------|-----------|------------| +| **P0** | ✅ | `prg_machine` в `rebar.config` + `{applications}`; `woody_context_loader`; `rebar3 compile` | +| **P1** | ✅ | `ff/deposit_v1` end-to-end | +| **P2** | ✅ | 5 FF NS: deposit, source, destination, withdrawal, session | +| **P2b** | ⏸ вырезано | `ff/identity`, `ff/wallet_v2` — NS убраны из config (модулей в репо не было) | +| **P3** | ✅ | `invoice` на `prg_machine` | +| **P4** | ✅ частично | `invoice_template` на `prg_machine`; `customer`, `recurrent_paytools` — NS убраны из config | +| **P5** | ✅ частично | удалены `hg_machine`, `ff_machine`, `fistful.erl`, processor glue; **не** полный grep-gate по всему репо | + +### 3.1. Prod namespaces на `prg_machine` (7 шт.) + +| NS | Домен | Registry child spec | +|----|-------|---------------------| +| `invoice` | `hg_invoice` | `hellgate.erl` | +| `invoice_template` | `hg_invoice_template` | `hellgate.erl` | +| `ff/deposit_v1` | `ff_deposit` | `ff_server.erl` | +| `ff/source_v1` | `ff_source` | `ff_server.erl` | +| `ff/destination_v2` | `ff_destination` | `ff_server.erl` | +| `ff/withdrawal_v2` | `ff_withdrawal` | `ff_server.erl` | +| `ff/withdrawal/session_v2` | `ff_withdrawal_session` | `ff_server.erl` | + +### 3.2. Удалённые / ключевые изменения + +**Удалено:** + +- `apps/hellgate/src/hg_machine.erl`, `hg_machine_action.erl` +- `apps/fistful/src/ff_machine.erl`, `fistful.erl` +- `apps/hg_progressor/src/hg_progressor_handler.erl` +- `apps/ff_server/src/ff_*_machinery_schema.erl` (×5) + +**Добавлено:** + +- `apps/prg_machine/` (`prg_machine.erl`, `prg_machine_action.erl`) +- `apps/ff_transfer/src/ff_machine_codec.erl` — marshal/unmarshal aux и общий codec +- `apps/hellgate/src/hg_invoicing_machine_client.erl` + +**Переписано:** + +- FF домены: `-behaviour(prg_machine)` (`ff_deposit`, `ff_source`, `ff_destination`, `ff_withdrawal`, `ff_withdrawal_session`) +- HG: `hg_invoice`, `hg_invoice_template` — behaviour + `prg_machine` client +- `ff_repair` — на `prg_machine:collapse` / `emit_events` +- `ff_machine_handler` — trace через `prg_machine:trace/2` (JSON HTTP) +- CT helper: `hg_ct_helper.erl` — progressor processor `client => prg_machine` + +### 3.3. Ralph verification + +- Задачи **1–34, 36** — verified +- Задача **37** (полный CT) — **не завершена** (прерывание ~37 мин, resource_exhausted) +- Integration gate: `rebar3 compile` OK, CT deferred +- Ветка: `epic/monorepo`, diff **+1684 / −3224**, commit/PR нет + +--- + +## 4. Диаграмма потока (call) + +```mermaid +sequenceDiagram + participant WH as woody handler + participant PM as prg_machine client + participant PR as progressor + participant P3 as prg_machine:process/3 + participant DM as domain module + + WH->>PM: call(NS, ID, Args) + PM->>PR: progressor:call(Req) + PR->>P3: process({call, BinArgs, Process}, Opts, Ctx) + P3->>P3: env_enter, unmarshal_machine + P3->>DM: process_call(Args, Machine) + DM->>DM: collapse_st / collapse (state) + DM-->>P3: {Response, #{events, action, auxst}} + P3->>P3: marshal_process_result + P3->>P3: env_leave + P3-->>PR: {ok, Intent} + PR-->>PM: {ok, Response} + PM-->>WH: decode_term(Response) +``` + +--- + +## 5. Известные хвосты (следующие доработки) + +### 5.1. Блокеры перед merge + +| # | Хвост | Действие | +|---|-------|----------| +| 1 | **CT не прогонялись** | Запустить suites вручную (docker: postgres, party, dmt) | +| 2 | **Нет commit/PR** | Code review + коммит на `epic/monorepo` | + +**CT suites (минимум):** + +```bash +cd /Users/artemfedorenko/Documents/work/hellgate +rebar3 ct --suite apps/ff_server/test/ff_deposit_handler_SUITE +rebar3 ct --suite apps/ff_server/test/ff_withdrawal_handler_SUITE +rebar3 ct --suite apps/ff_server/test/ff_withdrawal_session_repair_SUITE +rebar3 ct --suite apps/hellgate/test/hg_invoice_lite_tests_SUITE +rebar3 ct --suite apps/hellgate/test/hg_invoice_tests_SUITE +rebar3 ct --suite apps/hellgate/test/hg_invoice_template_tests_SUITE +rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE +``` + +### 5.2. Legacy machinery (вне prod NS, но в репо) + +| Модуль / config | Проблема | +|-----------------|----------| +| `apps/fistful/src/ff_limit.erl` | `-behaviour(machinery)`, вызовы `machinery:get/call/start` | +| `test/bender/sys.config`, `test/party-management/sys.config` | `client => machinery_prg_backend` | +| `apps/ff_cth/src/ct_payment_system.erl` | мёртвый `{machinery_backend, progressor}` | +| `apps/machinery_extra/` | остаётся для `ff_limit` и тестов | + +### 5.3. HG hybrid / progressor glue + +| Модуль | Роль | +|--------|------| +| `hg_progressor.erl` | **остался** — `call_automaton/2` для machinegun thrift (hybrid, CT cleanup) | +| `hg_hybrid.erl` | маршрутизация MG vs progressor | + +Это **не** prod path для invoice NS, но нужно понимать при удалении `hg_progressor` app целиком. + +### 5.4. Trace API + +- Сейчас: FF internal HTTP JSON (`ff_machine_handler` → `prg_machine:trace/2`) +- Цель (отдельно): Thrift по `progressor_trace.thrift`, см. `docs/trace-api-thrift.md` +- В git status были черновики `hg_progressor_trace*` — **не** в финальном дереве `apps/hg_progressor` + +### 5.5. P2b — orphan NS + +`ff/identity`, `ff/wallet_v2`, HG `customer`, `recurrent_paytools` — убраны из `sys.config`. Если понадобятся в проде — отдельный PR с доменными модулями + `prg_machine`. + +### 5.6. Технический долг в runtime + +- `initial_model/2` — `_Handler` не используется; `model` в aux на практике не пишется +- `binary_to_term` в decode без `[safe]` в fallback path `prg_machine` — стоит проверить +- `hg_invoice` vs FF: унификация через `apply_event/4` в runtime (вариант C); HG migration — goal `goal-hg-collapse.md` + +### 5.7. Grep-инварианты (целевые после полного P5) + +```bash +rg 'hg_machine:' apps/hellgate apps/hg_progressor --glob '*.erl' # 0 prod +rg 'machinery_prg_backend|ff_machine:' apps/fistful apps/ff_transfer apps/ff_server --glob '*.erl' # 0 кроме ff_limit +rg "client => machinery_prg_backend" config/sys.config # 0 +``` + +--- + +## 6. Чеклист для нового домена / NS + +1. `config/sys.config` — `processor => #{client => prg_machine, options => #{ns => ..., env_enter, env_leave}}` +2. Доменный модуль: `-behaviour(prg_machine)` + callbacks +3. `apply_event/2` (FF) или `apply_event/4` (HG: event_id + timestamp + body) для `prg_machine:collapse/2` +4. Codec событий в `marshal_event_body` / `unmarshal_event_body` (или `ff_machine_codec`) +5. `*_machine.erl` — только `prg_machine:*` +6. Woody handler — без `machinery` / `hg_machine` +7. Добавить модуль в `get_child_spec([...])` в `hellgate.erl` или `ff_server.erl` +8. CT suite для NS +9. `rebar3 compile` + grep gate + +--- + +## 7. Связанные файлы (точки входа) + +| Путь | Зачем смотреть | +|------|----------------| +| `apps/prg_machine/src/prg_machine.erl` | behaviour, process/3, collapse | +| `apps/prg_machine/src/prg_machine_action.erl` | таймеры | +| `config/sys.config` | все prod NS | +| `apps/hellgate/src/hellgate.erl` | HG registry | +| `apps/ff_server/src/ff_server.erl` | FF registry + woody | +| `apps/hellgate/src/hg_invoice.erl` | образец HG behaviour | +| `apps/ff_transfer/src/ff_deposit.erl` | образец FF behaviour + collapse | +| `apps/hellgate/src/hg_invoicing_machine_client.erl` | Thrift → prg_machine | +| `apps/fistful/src/ff_repair.erl` | repair + collapse | +| `docs/trace-api-thrift.md` | следующий этап trace | + +--- + +## 8. История Ralph (кратко) + +- **Goal:** `.ralph/goal.md` в ralph-2 +- **Completed:** tasks 1–36 (infra, FF×5, HG invoice+template, cleanup, CT helper fix) +- **Open:** task 37 — full CT; review round 1 не закрыт (`review_phase_completed: false`) +- **Устаревший артефакт:** `.ralph/summary.md` в ralph-2 — обновлён ссылкой на этот документ + +*Дата отчёта: 2026-06-04* diff --git a/docs/trace-api-thrift.md b/docs/trace-api-thrift.md new file mode 100644 index 00000000..7ff44e5f --- /dev/null +++ b/docs/trace-api-thrift.md @@ -0,0 +1,262 @@ +# 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:trace/2` → `machinery:trace/3` → `machinery_prg_backend:trace/3` | +| Декодирование | `ff_*_machinery_schema` + codec (`ff_deposit_codec`, …) | +| Ответ | `json:encode/1` после `ff_machine:json_compatible_value/1` | + +Маршруты (`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..bef15c89 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 ] }}, @@ -52,13 +51,8 @@ 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 + ff_wallet_machinery_schema ] }}, {elvis_style, max_function_arity, #{max_arity => 10}}, diff --git a/rebar.config b/rebar.config index bacf7d6f..664bec26 100644 --- a/rebar.config +++ b/rebar.config @@ -36,6 +36,7 @@ {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.2"}}}, {scoper, {git, "https://github.com/valitydev/scoper.git", {tag, "v1.1.0"}}}, {thrift, {git, "https://github.com/valitydev/thrift_erlang.git", {tag, "v1.0.0"}}}, + %% TODO: bump tag after progressor_trace.thrift is released in damsel {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.33"}}}, {payproc_errors, {git, "https://github.com/valitydev/payproc-errors-erlang.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, @@ -47,6 +48,7 @@ {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"}}}, + {prg_machine, {path, "apps/prg_machine"}}, {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"}}}, From a7acb6cba2bbb556f752d639fdea56bdad1c7ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 5 Jun 2026 17:25:41 +0300 Subject: [PATCH 02/62] Refactors invoice handling and template management to utilize prg_machine for state retrieval and history management. Updates function signatures and error handling for improved robustness, while removing obsolete collapse_history functions. --- .cursor/agents/generic-worker-composer.md | 15 ++++++ apps/hellgate/src/hg_invoice.erl | 63 +++++++++-------------- apps/hellgate/src/hg_invoice_handler.erl | 14 ++--- apps/hellgate/src/hg_invoice_template.erl | 41 +++++++-------- 4 files changed, 66 insertions(+), 67 deletions(-) create mode 100644 .cursor/agents/generic-worker-composer.md diff --git a/.cursor/agents/generic-worker-composer.md b/.cursor/agents/generic-worker-composer.md new file mode 100644 index 00000000..f9f632b3 --- /dev/null +++ b/.cursor/agents/generic-worker-composer.md @@ -0,0 +1,15 @@ +--- +name: generic-worker-composer +model: composer-2.5[fast=false] +description: Универсальный исполнитель (Composer 2.5). Выполняет любое задание от оркестратора без привязки к жёсткому сценарию. +--- + +Ты — универсальный исполнитель. Оркестратор передаёт тебе задание. + +## Правила +- Выполни задание полностью. +- Не добавляй лишних рассуждений — только ответ на задание. +- Если задание требует структурированного формата — следуй ему. +- В конце ответа добавь блок: `---МОДЕЛЬ: generic-worker-composer` + +Твоё поведение определяется **только** заданием оркестратора. Никаких дополнительных ограничений или предустановок. diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 0592e6f7..336b9827 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -44,7 +44,6 @@ -export([marshal_invoice/1]). -export([unmarshal_invoice/1]). -export([unmarshal_history/1]). --export([collapse_history/1]). %% Machine callbacks @@ -60,7 +59,7 @@ -export([unmarshal_event_body/2]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). --export([apply_event/2]). +-export([apply_event/4]). %% Internal @@ -103,9 +102,9 @@ -spec get(prg_machine:id()) -> {ok, st()} | {error, notfound}. get(ID) -> - case prg_machine:get_history(?NS, ID) of - {ok, History} -> - {ok, collapse_history(History)}; + case prg_machine:get(?NS, ID) of + {ok, Machine} -> + {ok, prg_machine:collapse(?MODULE, Machine)}; Error -> Error end. @@ -307,7 +306,7 @@ init(Invoice, _Machine) -> -spec process_repair(prg_machine:args(), machine()) -> prg_result() | no_return(). process_repair(Args, Machine) -> - St = collapse_st(Machine), + St = prg_machine:collapse(?MODULE, Machine), to_prg_result(handle_repair(Args, St)). handle_repair({changes, Changes, RepairAction, Params}, St) -> @@ -339,7 +338,7 @@ handle_repair({scenario, Scenario}, #st{activity = {payment, PaymentID}} = St) - -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(Signal, Machine) -> - St = collapse_st(Machine), + St = prg_machine:collapse(?MODULE, Machine), to_prg_result(handle_signal(Signal, St)). handle_signal(timeout, #st{activity = {payment, PaymentID}} = St) -> @@ -394,7 +393,7 @@ handle_expiration(St) -> -spec process_call(call(), machine()) -> {prg_machine:response(), prg_result()}. process_call(Call, Machine) -> - St = collapse_st(Machine), + St = prg_machine:collapse(?MODULE, Machine), try CallResult = handle_call(Call, St), _ = log_changes(maps:get(changes, CallResult, []), validate_changes(CallResult)), @@ -862,33 +861,6 @@ repair_scenario(Scenario, #st{activity = {payment, PaymentID}} = St) -> %% --spec collapse_st(machine()) -> st(). -collapse_st(#{history := History}) -> - lists:foldl( - fun({ID, Dt, Changes}, St0) -> - St1 = apply_event(Changes, St0, event_timestamp_to_binary(Dt)), - St1#st{latest_event_id = ID} - end, - #st{}, - History - ). - -event_timestamp_to_binary(Bin) when is_binary(Bin) -> - Bin; -event_timestamp_to_binary(Dt) -> - hg_datetime:format_dt(Dt). - --spec collapse_history([prg_machine:event()]) -> st(). -collapse_history(History) -> - lists:foldl( - fun({ID, Dt, Changes}, St0) -> - St1 = collapse_changes(Changes, St0, #{timestamp => event_timestamp_to_binary(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). @@ -1023,12 +995,23 @@ get_message(invoice_status_changed) -> %% prg_machine codec --spec apply_event([invoice_change()], st() | undefined) -> st(). -apply_event(Changes, St) -> - apply_event(Changes, St, undefined). +-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) -> + hg_datetime:format_dt(Dt). --spec apply_event([invoice_change()], st() | undefined, event_timestamp() | undefined) -> st(). -apply_event(Changes, St0, Dt) -> +-spec apply_event_changes([invoice_change()], st() | undefined, event_timestamp() | binary() | undefined) -> st(). +apply_event_changes(Changes, St0, Dt) -> St = case St0 of undefined -> #st{}; _ -> St0 end, Opts = case Dt of diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index 48652984..fd0e7e2d 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -221,14 +221,16 @@ 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({exception, #payproc_InvoiceNotFound{}}) + end. get_state(ID, AfterID, Limit) -> - hg_invoice:collapse_history(get_history(ID, AfterID, Limit)). - -get_history(ID) -> - History = prg_machine:get_history(hg_invoice:namespace(), ID), - hg_invoice:unmarshal_history(map_history_error(History)). + History = get_history(ID, AfterID, Limit), + prg_machine:collapse(hg_invoice, #{history => History, aux_state => #{}}). get_history(ID, AfterID, Limit) -> History = prg_machine:get_history(hg_invoice:namespace(), ID, AfterID, Limit), diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index 5dbce709..3383241c 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -27,6 +27,7 @@ -export([unmarshal_event_body/2]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% API @@ -49,9 +50,15 @@ 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{}); + {error, Reason} -> + error(Reason) + end. %% Woody handler @@ -190,9 +197,6 @@ call(ID, Function, Args) -> map_error(Error) end. -get_history(TplID) -> - map_history_error(prg_machine:get_history(?NS, TplID)). - -spec map_error(notfound | any()) -> no_return(). map_error(notfound) -> throw(#payproc_InvoiceTemplateNotFound{}); @@ -204,13 +208,6 @@ 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{}); -map_history_error({error, Reason}) -> - error(Reason). - %% Machine -type create_params() :: dmsl_payproc_thrift:'InvoiceTemplateCreateParams'(). @@ -269,8 +266,8 @@ process_signal({repair, _}, _Machine) -> #{}. -spec process_call(call(), machine()) -> {prg_machine:response(), prg_result()}. -process_call(Call, #{history := History}) -> - St = collapse_history(History), +process_call(Call, Machine) -> + St = prg_machine:collapse(?MODULE, Machine), try handle_call(Call, St) of {ok, Changes} -> {ok, #{events => [Changes]}}; @@ -287,12 +284,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; From 96c95383ca4b7433e69289639818830878d027d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 5 Jun 2026 19:43:54 +0300 Subject: [PATCH 03/62] Consolidate HG/FF process context into operation_context. Remove hg_context and ff_context wrappers; call sites use operation_context scoped helpers. prg_machine resolves env hooks from context_binding instead of context_module. Co-authored-by: Cursor --- apps/ff_cth/src/ct_helper.erl | 6 +- apps/ff_cth/src/ct_payment_system.erl | 40 +--- apps/ff_server/src/ff_server.erl | 4 +- apps/ff_server/src/ff_woody_wrapper.erl | 6 +- apps/fistful/src/ff_context.erl | 93 --------- apps/fistful/src/ff_machine_tag.erl | 4 +- apps/fistful/src/ff_party.erl | 6 +- apps/fistful/src/ff_woody_client.erl | 2 +- apps/fistful/src/fistful.app.src | 1 + apps/hellgate/src/hellgate.app.src | 1 + apps/hellgate/src/hellgate.erl | 4 +- apps/hellgate/src/hg_context.erl | 108 ---------- apps/hellgate/src/hg_customer_client.erl | 2 +- apps/hellgate/src/hg_invoice_payment.erl | 6 +- apps/hellgate/src/hg_machine_tag.erl | 4 +- apps/hellgate/src/hg_party.erl | 6 +- apps/hellgate/src/hg_payment_institution.erl | 6 +- apps/hellgate/test/hg_ct_fixture.erl | 12 +- apps/hellgate/test/hg_ct_helper.erl | 16 +- .../test/hg_direct_recurrent_tests_SUITE.erl | 8 +- .../test/hg_invoice_lite_tests_SUITE.erl | 10 +- .../test/hg_invoice_template_tests_SUITE.erl | 8 +- apps/hellgate/test/hg_invoice_tests_SUITE.erl | 16 +- .../test/hg_route_rules_tests_SUITE.erl | 8 +- apps/hg_progressor/src/hg_progressor.erl | 4 +- .../hg_proto/src/hg_woody_service_wrapper.erl | 10 +- apps/hg_proto/src/hg_woody_wrapper.erl | 2 +- apps/operation_context/rebar.config | 13 ++ .../src/operation_context.app.src | 16 ++ .../src/operation_context.erl | 187 ++++++++++++++++++ apps/prg_machine/src/prg_machine.erl | 35 +++- .../test/operation_context_tests.erl | 73 +++++++ .../test/prg_machine_env_mock_context.erl | 28 +++ .../test/prg_machine_env_mock_handler.erl | 25 +++ .../test/prg_machine_env_tests.erl | 107 ++++++++++ apps/routing/src/hg_route_collector.erl | 26 +-- config/sys.config | 77 +++----- docs/prg-machine-migration-context.md | 23 ++- 38 files changed, 616 insertions(+), 387 deletions(-) delete mode 100644 apps/fistful/src/ff_context.erl delete mode 100644 apps/hellgate/src/hg_context.erl create mode 100644 apps/operation_context/rebar.config create mode 100644 apps/operation_context/src/operation_context.app.src create mode 100644 apps/operation_context/src/operation_context.erl create mode 100644 apps/prg_machine/test/operation_context_tests.erl create mode 100644 apps/prg_machine/test/prg_machine_env_mock_context.erl create mode 100644 apps/prg_machine/test/prg_machine_env_mock_handler.erl create mode 100644 apps/prg_machine/test/prg_machine_env_tests.erl diff --git a/apps/ff_cth/src/ct_helper.erl b/apps/ff_cth/src/ct_helper.erl index 2b1ca745..81fd0b2c 100644 --- a/apps/ff_cth/src/ct_helper.erl +++ b/apps/ff_cth/src/ct_helper.erl @@ -197,8 +197,8 @@ stop_app(AppName) -> -spec set_context(config()) -> ok. set_context(C) -> - ok = ff_context:save( - ff_context:create(#{ + ok = operation_context:save_fistful( + operation_context:create(#{ party_client => party_client:create_client(), woody_context => cfg('$woody_ctx', C) }) @@ -206,7 +206,7 @@ set_context(C) -> -spec unset_context() -> ok. unset_context() -> - ok = ff_context:cleanup(). + ok = operation_context:cleanup_fistful(). %% diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index fa0b52e4..7edbfcd3 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -261,13 +261,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/source_v1', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => operation_context:fistful_binding() } } }, @@ -276,13 +270,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/destination_v2', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => operation_context:fistful_binding() } } }, @@ -291,13 +279,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/deposit_v1', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => operation_context:fistful_binding() } } }, @@ -306,13 +288,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/withdrawal_v2', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => operation_context:fistful_binding() } } }, @@ -321,13 +297,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/withdrawal/session_v2', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => operation_context:fistful_binding() } } } diff --git a/apps/ff_server/src/ff_server.erl b/apps/ff_server/src/ff_server.erl index e85407f9..feae5755 100644 --- a/apps/ff_server/src/ff_server.erl +++ b/apps/ff_server/src/ff_server.erl @@ -136,9 +136,9 @@ setup_metrics() -> -spec woody_rpc_context() -> woody_context:ctx(). woody_rpc_context() -> - try ff_context:load() of + try operation_context:load_fistful() of Ctx -> - ff_context:get_woody_context(Ctx) + operation_context:get_woody_context(Ctx) catch Class:Reason -> _ = logger:warning("Failed to load context with error class '~s' and reason: ~p", [Class, Reason]), diff --git a/apps/ff_server/src/ff_woody_wrapper.erl b/apps/ff_server/src/ff_woody_wrapper.erl index bf7830aa..f0ca6c1d 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 = operation_context:save_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() + operation_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). + operation_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/fistful/src/ff_context.erl b/apps/fistful/src/ff_context.erl deleted file mode 100644 index b22fa4a8..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() -> - _ = catch 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_machine_tag.erl b/apps/fistful/src/ff_machine_tag.erl index 52e18b9f..f898eca1 100644 --- a/apps/fistful/src/ff_machine_tag.erl +++ b/apps/fistful/src/ff_machine_tag.erl @@ -11,7 +11,7 @@ -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 = operation_context:get_woody_context(operation_context:load_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 = operation_context:get_woody_context(operation_context:load_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..a19b8ff0 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 = operation_context:load_fistful(), + Client = operation_context:get_party_client(Context), + ClientContext = operation_context:get_party_client_context(Context), {Client, ClientContext}. %% Terms stuff diff --git a/apps/fistful/src/ff_woody_client.erl b/apps/fistful/src/ff_woody_client.erl index 9ca7b554..47332234 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, operation_context:get_woody_context(operation_context:load_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 92771480..7e64521c 100644 --- a/apps/fistful/src/fistful.app.src +++ b/apps/fistful/src/fistful.app.src @@ -19,6 +19,7 @@ damsel, dmt_client, party_client, + operation_context, binbase_proto, bender_client, opentelemetry_api, diff --git a/apps/hellgate/src/hellgate.app.src b/apps/hellgate/src/hellgate.app.src index 49515373..06075541 100644 --- a/apps/hellgate/src/hellgate.app.src +++ b/apps/hellgate/src/hellgate.app.src @@ -24,6 +24,7 @@ dmt_client, party_client, bender_client, + operation_context, payproc_errors, erl_health, limiter_proto, diff --git a/apps/hellgate/src/hellgate.erl b/apps/hellgate/src/hellgate.erl index 7fc09c67..269499fa 100644 --- a/apps/hellgate/src/hellgate.erl +++ b/apps/hellgate/src/hellgate.erl @@ -116,9 +116,9 @@ setup_metrics() -> -spec woody_rpc_context() -> woody_context:ctx(). woody_rpc_context() -> - try hg_context:load() of + try operation_context:load_hellgate() of Ctx -> - hg_context:get_woody_context(Ctx) + operation_context:get_woody_context(Ctx) catch Class:Reason -> _ = logger:warning("Failed to load context with error class '~s' and reason: ~p", [Class, Reason]), 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..4594e490 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()) + operation_context:get_woody_context(operation_context:load_hellgate()) catch error:badarg -> woody_context:new() end, diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index a9a27c8d..d703a286 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -4098,9 +4098,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 = operation_context:load_hellgate(), + Client = operation_context:get_party_client(HgContext), + Context = operation_context:get_party_client_context(HgContext), {Client, Context}. is_route_cascade_available( diff --git a/apps/hellgate/src/hg_machine_tag.erl b/apps/hellgate/src/hg_machine_tag.erl index 8dd632e5..6d85acb0 100644 --- a/apps/hellgate/src/hg_machine_tag.erl +++ b/apps/hellgate/src/hg_machine_tag.erl @@ -13,7 +13,7 @@ -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 = operation_context:get_woody_context(operation_context:load_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 = operation_context:get_woody_context(operation_context:load_hellgate()), case bender_client:gen_constant(tag_to_external_id(NS, Tag), EntityID, WoodyContext, Context) of {ok, EntityID} -> ok; diff --git a/apps/hellgate/src/hg_party.erl b/apps/hellgate/src/hg_party.erl index a6b967f8..1830ade2 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 = operation_context:load_hellgate(), + Client = operation_context:get_party_client(HgContext), + Context = operation_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..e43f83f2 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 = operation_context:load_hellgate(), + Client = operation_context:get_party_client(HgContext), + Context = operation_context:get_party_client_context(HgContext), {Client, Context}. diff --git a/apps/hellgate/test/hg_ct_fixture.erl b/apps/hellgate/test/hg_ct_fixture.erl index 16321519..3be78a65 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 = operation_context:save_hellgate(operation_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(), + _ = operation_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 = operation_context:save_hellgate(operation_context:create()), SettlementAccountID = hg_accounting:create_account(CurrencyCode), SubagentAccountID = hg_accounting:create_account(CurrencyCode), - hg_context:cleanup(), + operation_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 = operation_context:save_hellgate(operation_context:create()), AccountID1 = hg_accounting:create_account(CurrencyCode), AccountID2 = hg_accounting:create_account(CurrencyCode), - hg_context:cleanup(), + operation_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 0397de08..98f0a680 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -322,13 +322,7 @@ start_app(progressor = AppName) -> client => prg_machine, options => #{ ns => invoice, - env_enter => fun(WoodyCtx) -> - ok = hg_context:save(hg_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> hg_context:cleanup() end + context_binding => operation_context:hellgate_binding() } }, worker_pool_size => 150 @@ -338,13 +332,7 @@ start_app(progressor = AppName) -> client => prg_machine, options => #{ ns => invoice_template, - env_enter => fun(WoodyCtx) -> - ok = hg_context:save(hg_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> hg_context:cleanup() end + context_binding => operation_context:hellgate_binding() } } } diff --git a/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl b/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl index 546dba6d..333e9be0 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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), C1 = [ @@ -164,9 +164,9 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> config(). end_per_suite(C) -> - _ = hg_domain:cleanup(), + _ = hg_domain:cleanup_hellgate(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_progressor:cleanup_hellgate(), [application:stop(App) || App <- cfg(apps, C)]. -spec init_per_group(group_name(), config()) -> config(). diff --git a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl index 362f2d41..301fd9d0 100644 --- a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl @@ -98,11 +98,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 = operation_context:save_hellgate(operation_context:create()), ShopConfigRef = hg_ct_helper:create_party_and_shop( PartyConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient ), - ok = hg_context:cleanup(), + ok = operation_context:cleanup_hellgate(), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), ok = hg_invoice_helper:start_kv_store(SupPid), @@ -119,9 +119,9 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> _. end_per_suite(C) -> - _ = hg_domain:cleanup(), + _ = hg_domain:cleanup_hellgate(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_progressor:cleanup_hellgate(), _ = [application:stop(App) || App <- cfg(apps, C)], hg_invoice_helper:stop_kv_store(cfg(test_sup, C)), exit(cfg(test_sup, C), shutdown). @@ -145,7 +145,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 = operation_context:save_hellgate(operation_context:create()), [ {client, Client} | C diff --git a/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl index 31e0347d..92294c7a 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 = operation_context:save_hellgate(operation_context:create()), ShopConfigRef = hg_ct_helper:create_party_and_shop(PartyConfigRef, ?cat(1), <<"RUB">>, ?trms(1), ?pinst(1), Client), - ok = hg_context:cleanup(), + ok = operation_context:cleanup_hellgate(), [ {party_config_ref, PartyConfigRef}, {party_client, Client}, @@ -110,9 +110,9 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> _. end_per_suite(C) -> - _ = hg_domain:cleanup(), + _ = hg_domain:cleanup_hellgate(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_progressor:cleanup_hellgate(), [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..33ff6d02 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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), @@ -573,9 +573,9 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> _. end_per_suite(C) -> - _ = hg_domain:cleanup(), + _ = hg_domain:cleanup_hellgate(), _ = application:stop(progressor), - _ = hg_progressor:cleanup(), + _ = hg_progressor:cleanup_hellgate(), _ = [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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), _ = case cfg(original_domain_revision, C) of Revision when is_integer(Revision) -> @@ -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 = operation_context:save_hellgate(operation_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 = operation_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..26e66b1c 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_progressor:cleanup_hellgate(), 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 = operation_context:set_party_client(cfg(party_client, C), operation_context:create()), + ok = operation_context:save_hellgate(Ctx), C. -spec end_per_testcase(test_case_name(), config()) -> ok. end_per_testcase(_Name, _C) -> - ok = hg_context:cleanup(), + ok = operation_context:cleanup_hellgate(), ok. cfg(Key, C) -> diff --git a/apps/hg_progressor/src/hg_progressor.erl b/apps/hg_progressor/src/hg_progressor.erl index 267f6a81..5baf733f 100644 --- a/apps/hg_progressor/src/hg_progressor.erl +++ b/apps/hg_progressor/src/hg_progressor.erl @@ -119,9 +119,9 @@ handle_exception({exception, Class, Reason}) -> get_context() -> WoodyContext = - try hg_context:load() of + try operation_context:load_hellgate() of Ctx -> - hg_context:get_woody_context(Ctx) + operation_context:get_woody_context(Ctx) catch Class:Reason -> _ = logger:warning("Failed to load context with error class '~s' and reason: ~p", [Class, Reason]), diff --git a/apps/hg_proto/src/hg_woody_service_wrapper.erl b/apps/hg_proto/src/hg_woody_service_wrapper.erl index 911871fa..d82d3896 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 = operation_context:save_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() + operation_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()) -> operation_context:context(). create_context(WoodyContext, Opts) -> ContextOptions = #{ woody_context => WoodyContext }, - Context = hg_context:create(ContextOptions), + Context = operation_context:create(ContextOptions), configure_party_client(Context, Opts). configure_party_client(Context0, #{party_client := PartyClient}) -> - hg_context:set_party_client(PartyClient, Context0); + operation_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..c1008812 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 = operation_context:get_woody_context(operation_context:load_hellgate()), Request = {Service, Function, Args}, woody_client:call( Request, diff --git a/apps/operation_context/rebar.config b/apps/operation_context/rebar.config new file mode 100644 index 00000000..1031f8fc --- /dev/null +++ b/apps/operation_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/operation_context/src/operation_context.app.src b/apps/operation_context/src/operation_context.app.src new file mode 100644 index 00000000..12b34b67 --- /dev/null +++ b/apps/operation_context/src/operation_context.app.src @@ -0,0 +1,16 @@ +{application, operation_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/operation_context/src/operation_context.erl b/apps/operation_context/src/operation_context.erl new file mode 100644 index 00000000..33ff2664 --- /dev/null +++ b/apps/operation_context/src/operation_context.erl @@ -0,0 +1,187 @@ +-module(operation_context). + +-export([create/0]). +-export([create/1]). +-export([save/2]). +-export([load/1]). +-export([cleanup/2]). + +-export([save_hellgate/1]). +-export([load_hellgate/0]). +-export([cleanup_hellgate/0]). + +-export([save_fistful/1]). +-export([load_fistful/0]). +-export([cleanup_fistful/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]). + +-export([hellgate_binding/0]). +-export([fistful_binding/0]). +-export([env_enter/2]). +-export([env_leave/1]). + +-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([ + 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(). + +-define(HG_REGISTRY_KEY, {p, l, stored_hg_context}). +-define(FF_REGISTRY_KEY, {p, l, {ff_context, 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(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(registry_key(), cleanup_mode()) -> ok. +cleanup(RegistryKey, strict) -> + true = gproc:unreg(RegistryKey), + ok; +cleanup(RegistryKey, lenient) -> + _ = catch gproc:unreg(RegistryKey), + ok. + +-spec save_hellgate(context()) -> ok. +save_hellgate(Context) -> + save(?HG_REGISTRY_KEY, Context). + +-spec load_hellgate() -> context() | no_return(). +load_hellgate() -> + load(?HG_REGISTRY_KEY). + +-spec cleanup_hellgate() -> ok. +cleanup_hellgate() -> + cleanup(?HG_REGISTRY_KEY, strict). + +-spec save_fistful(context()) -> ok. +save_fistful(Context) -> + save(?FF_REGISTRY_KEY, Context). + +-spec load_fistful() -> context() | no_return(). +load_fistful() -> + load(?FF_REGISTRY_KEY). + +-spec cleanup_fistful() -> ok. +cleanup_fistful() -> + cleanup(?FF_REGISTRY_KEY, lenient). + +-spec hellgate_binding() -> binding(). +hellgate_binding() -> + #{ + registry_key => ?HG_REGISTRY_KEY, + cleanup_mode => strict + }. + +-spec fistful_binding() -> binding(). +fistful_binding() -> + #{ + registry_key => ?FF_REGISTRY_KEY, + cleanup_mode => lenient + }. + +-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). + +-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()) -> 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). diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 74b95a02..73ff9e1f 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -39,10 +39,13 @@ -type env_enter_fun() :: fun(() -> ok) | fun((woody_context:ctx()) -> ok). +-type context_binding() :: operation_context:binding(). + -type processor_opts() :: #{ ns := namespace(), env_enter => env_enter_fun(), - env_leave => fun(() -> ok) + env_leave => fun(() -> ok), + context_binding => context_binding() }. -export_type([ @@ -256,8 +259,8 @@ trace(NS, ID) -> -spec process({init | call | repair | notify | timeout, binary(), map()}, processor_opts(), binary()) -> {ok, map()} | {error, term()}. process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> - Enter = maps:get(env_enter, Opts, fun(_) -> ok end), - Leave = maps:get(env_leave, Opts, fun() -> ok end), + Enter = resolve_env_enter(Opts), + Leave = resolve_env_leave(Opts), try {WoodyCtx, OtelCtx} = decode_rpc_context(BinCtx), ok = woody_rpc_helper:attach_otel_context(OtelCtx), @@ -510,6 +513,32 @@ decode_rpc_context(<<>>) -> decode_rpc_context(Bin) -> woody_rpc_helper:decode_rpc_context(decode_term(Bin)). +resolve_env_enter(Opts) -> + case maps:is_key(env_enter, Opts) of + true -> + maps:get(env_enter, Opts); + false -> + case maps:get(context_binding, Opts, undefined) of + Binding when is_map(Binding) -> + fun(WoodyCtx) -> operation_context:env_enter(WoodyCtx, Binding) end; + _ -> + fun(_) -> ok end + end + end. + +resolve_env_leave(Opts) -> + case maps:is_key(env_leave, Opts) of + true -> + maps:get(env_leave, Opts); + false -> + case maps:get(context_binding, Opts, undefined) of + Binding when is_map(Binding) -> + fun() -> operation_context:env_leave(Binding) end; + _ -> + fun() -> ok end + end + end. + run_env_enter(Enter, WoodyCtx) when is_function(Enter, 1) -> Enter(WoodyCtx); run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> diff --git a/apps/prg_machine/test/operation_context_tests.erl b/apps/prg_machine/test/operation_context_tests.erl new file mode 100644 index 00000000..f12e6e6f --- /dev/null +++ b/apps/prg_machine/test/operation_context_tests.erl @@ -0,0 +1,73 @@ +-module(operation_context_tests). + +-compile(nowarn_unused_function). + +-include_lib("eunit/include/eunit.hrl"). + +-define(HG_KEY, {p, l, stored_hg_context}). +-define(FF_KEY, {p, l, {ff_context, stored_context}}). + +-spec test() -> _. + +test() -> + operation_context_test_(). + +-spec operation_context_test_() -> _. + +operation_context_test_() -> + {setup, fun setup/0, fun cleanup/1, [ + fun colocated_keys_isolated/0, + fun scoped_helpers/0 + ]}. + +-spec setup() -> ok. + +setup() -> + {ok, _} = application:ensure_all_started(gproc), + {ok, _} = application:ensure_all_started(woody), + ok. + +-spec cleanup(_) -> ok. + +cleanup(_) -> + ok. + +-spec colocated_keys_isolated() -> _. + +colocated_keys_isolated() -> + WoodyHg = woody_context:add_meta(woody_context:new(), #{<<"app">> => <<"hg">>}), + WoodyFf = woody_context:add_meta(woody_context:new(), #{<<"app">> => <<"ff">>}), + try + CtxHg = operation_context:create(#{woody_context => WoodyHg}), + CtxFf = operation_context:create(#{woody_context => WoodyFf}), + ok = operation_context:save(?HG_KEY, CtxHg), + ok = operation_context:save(?FF_KEY, CtxFf), + CtxHgLoaded = operation_context:load(?HG_KEY), + CtxFfLoaded = operation_context:load(?FF_KEY), + ?assertEqual(WoodyHg, operation_context:get_woody_context(CtxHgLoaded)), + ?assertEqual(WoodyFf, operation_context:get_woody_context(CtxFfLoaded)), + ?assertNotEqual( + operation_context:get_party_client_context(CtxHgLoaded), + operation_context:get_party_client_context(CtxFfLoaded) + ), + ok = operation_context:cleanup(?HG_KEY, strict), + CtxFfAfterHgCleanup = operation_context:load(?FF_KEY), + ?assertEqual(WoodyFf, operation_context:get_woody_context(CtxFfAfterHgCleanup)), + ok = operation_context:cleanup(?FF_KEY, lenient) + after + _ = catch operation_context:cleanup(?HG_KEY, lenient), + _ = catch operation_context:cleanup(?FF_KEY, lenient) + end. + +-spec scoped_helpers() -> _. + +scoped_helpers() -> + WoodyCtx = woody_context:new(), + try + ok = operation_context:save_fistful(operation_context:create(#{woody_context => WoodyCtx})), + ?assertEqual(WoodyCtx, operation_context:get_woody_context(operation_context:load_fistful())), + ok = operation_context:cleanup_fistful(), + ok = operation_context:cleanup_fistful() + after + _ = catch operation_context:cleanup_fistful() + end. 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..f3841c94 --- /dev/null +++ b/apps/prg_machine/test/prg_machine_env_mock_context.erl @@ -0,0 +1,28 @@ +-module(prg_machine_env_mock_context). + +-export([env_enter/1, env_leave/0]). +-export([reset/0, events/0, record/1]). + +-spec env_enter(woody_context:ctx()) -> ok. +env_enter(WoodyCtx) -> + record({enter, WoodyCtx}), + ok. + +-spec env_leave() -> ok. +env_leave() -> + record(leave), + ok. + +-spec reset() -> ok. +reset() -> + persistent_term:put({?MODULE, events}, []), + ok. + +-spec events() -> [enter | leave | {enter, woody_context:ctx()} | explicit_enter | explicit_leave]. +events() -> + persistent_term:get({?MODULE, events}, []). + +-spec record(enter | leave | {enter, woody_context:ctx()} | explicit_enter | explicit_leave) -> 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..dba2a4e6 --- /dev/null +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -0,0 +1,25 @@ +-module(prg_machine_env_mock_handler). + +-behaviour(prg_machine). + +-export([namespace/0, init/2, process_signal/2, process_call/2, process_repair/2]). + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + env_test_ns. + +-spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). +init(_Args, _Machine) -> + #{events => [], action => prg_machine_action:new(), auxst => undefined}. + +-spec process_signal(prg_machine:signal(), prg_machine:machine()) -> prg_machine:result(). +process_signal(_Signal, _Machine) -> + #{events => [], action => prg_machine_action:new(), auxst => undefined}. + +-spec process_call(prg_machine:call(), prg_machine:machine()) -> {prg_machine:response(), prg_machine:result()}. +process_call(_Call, _Machine) -> + {ok, #{events => [], action => prg_machine_action:new(), auxst => undefined}}. + +-spec process_repair(prg_machine:args(), prg_machine:machine()) -> prg_machine:result() | {error, term()}. +process_repair(_Args, _Machine) -> + #{events => [], action => prg_machine_action:new(), auxst => undefined}. diff --git a/apps/prg_machine/test/prg_machine_env_tests.erl b/apps/prg_machine/test/prg_machine_env_tests.erl new file mode 100644 index 00000000..5465a586 --- /dev/null +++ b/apps/prg_machine/test/prg_machine_env_tests.erl @@ -0,0 +1,107 @@ +-module(prg_machine_env_tests). + +-compile(nowarn_unused_function). + +-include_lib("eunit/include/eunit.hrl"). + +-define(TABLE, prg_machine_dispatch). +-define(NS, env_test_ns). +-define(TEST_REGISTRY_KEY, {p, l, prg_machine_env_test_context}). +-define(TEST_BINDING, #{ + registry_key => ?TEST_REGISTRY_KEY, + cleanup_mode => lenient +}). + +-spec test() -> _. + +test() -> + {setup, fun setup/0, fun cleanup/1, [ + ?_test(noop_when_hooks_absent_test()), + ?_test(explicit_fun_overrides_context_binding_test()) + ]}. + +-spec noop_when_hooks_absent_test() -> _. + +noop_when_hooks_absent_test() -> + ok = ensure_woody_available(), + ok = prg_machine_env_mock_context:reset(), + _ = run_process(#{ns => ?NS}), + ?assertEqual([], prg_machine_env_mock_context:events()). + +-spec explicit_fun_overrides_context_binding_test() -> _. + +explicit_fun_overrides_context_binding_test() -> + ok = ensure_woody_available(), + ok = prg_machine_env_mock_context:reset(), + Enter = fun(_) -> + prg_machine_env_mock_context:record(explicit_enter), + ok + end, + Leave = fun() -> + prg_machine_env_mock_context:record(explicit_leave), + ok + end, + Opts = #{ + ns => ?NS, + env_enter => Enter, + env_leave => Leave, + context_binding => ?TEST_BINDING + }, + _ = run_process(Opts), + ?assertEqual([explicit_enter, explicit_leave], prg_machine_env_mock_context:events()). + +-spec setup() -> ok. + +setup() -> + _ = 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(operation_context), + _ = ensure_dispatch_table(), + true = ets:insert(?TABLE, {?NS, prg_machine_env_mock_handler}), + ok = prg_machine_env_mock_context:reset(), + ok. + +-spec cleanup(_) -> ok. + +cleanup(_) -> + _ = ets:delete(?TABLE, ?NS), + _ = catch operation_context:cleanup(?TEST_REGISTRY_KEY, lenient), + ok. + +-spec ensure_woody_available() -> ok. + +ensure_woody_available() -> + {ok, _} = application:ensure_all_started(snowflake), + _ = woody_context:new(), + ok. + +-spec ensure_dispatch_table() -> atom(). + +ensure_dispatch_table() -> + case ets:info(?TABLE) of + undefined -> + ets:new(?TABLE, [named_table, {read_concurrency, true}]); + _ -> + ?TABLE + end. + +-spec run_process(prg_machine:processor_opts()) -> _. +run_process(Opts) -> + run_process(Opts, <<>>). + +-spec run_process(prg_machine:processor_opts(), binary()) -> _. + +run_process(Opts, BinCtx) -> + Process = #{ + process_id => <<"env-hook-test">>, + last_event_id => 0, + history => [], + aux_state => undefined + }, + prg_machine:process({init, term_to_binary(#{}), Process}, Opts, BinCtx). diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index 0ab435a7..34a5b41c 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 = operation_context:load_hellgate(), try genlib_pmap:map( fun(Route) -> - ok = hg_context:save(HgContext), + ok = operation_context:save_hellgate(HgContext), hg_inspector:fill_blacklist(Route, BlCtx) end, Routes, @@ -268,9 +268,9 @@ acceptable_terminal(Predestination, ProviderRef, TerminalRef, 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 = operation_context:load_hellgate(), + Client = operation_context:get_party_client(HgContext), + Context = operation_context:get_party_client_context(HgContext), {Client, Context}. check_terms_acceptability(payment, Terms, VS) -> @@ -455,7 +455,7 @@ setup_fill_blacklist_test() -> -spec cleanup_fill_blacklist_test(_) -> ok. cleanup_fill_blacklist_test(_Ok) -> try - hg_context:cleanup() + operation_context:cleanup_hellgate() catch _:_ -> ok end, @@ -468,15 +468,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 = operation_context:create(#{woody_context => woody_context:new()}), + ok = operation_context:save_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, operation_context:load_hellgate()), Parent ! {worker_done, self(), Ref}, Route end), @@ -487,15 +487,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 = operation_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 = operation_context:create(#{woody_context => woody_context:new()}), + ok = operation_context:save_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), @@ -514,7 +514,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 = operation_context:cleanup_hellgate() end. -spec collect_worker_pids(reference(), non_neg_integer()) -> [pid()]. diff --git a/config/sys.config b/config/sys.config index 44c88a4e..82a83eea 100644 --- a/config/sys.config +++ b/config/sys.config @@ -364,13 +364,10 @@ client => prg_machine, options => #{ ns => invoice, - env_enter => fun(WoodyCtx) -> - ok = hg_context:save(hg_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> hg_context:cleanup() end + context_binding => #{ + registry_key => {p, l, stored_hg_context}, + cleanup_mode => strict + } } }, storage => #{ @@ -388,13 +385,10 @@ client => prg_machine, options => #{ ns => invoice_template, - env_enter => fun(WoodyCtx) -> - ok = hg_context:save(hg_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> hg_context:cleanup() end + context_binding => #{ + registry_key => {p, l, stored_hg_context}, + cleanup_mode => strict + } } }, worker_pool_size => 5 @@ -404,13 +398,10 @@ client => prg_machine, options => #{ ns => 'ff/source_v1', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => #{ + registry_key => {p, l, {ff_context, stored_context}}, + cleanup_mode => lenient + } } } }, @@ -419,13 +410,10 @@ client => prg_machine, options => #{ ns => 'ff/destination_v2', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => #{ + registry_key => {p, l, {ff_context, stored_context}}, + cleanup_mode => lenient + } } } }, @@ -434,13 +422,10 @@ client => prg_machine, options => #{ ns => 'ff/deposit_v1', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => #{ + registry_key => {p, l, {ff_context, stored_context}}, + cleanup_mode => lenient + } } } }, @@ -449,13 +434,10 @@ client => prg_machine, options => #{ ns => 'ff/withdrawal_v2', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => #{ + registry_key => {p, l, {ff_context, stored_context}}, + cleanup_mode => lenient + } } } }, @@ -464,13 +446,10 @@ client => prg_machine, options => #{ ns => 'ff/withdrawal/session_v2', - env_enter => fun(WoodyCtx) -> - ok = ff_context:save(ff_context:create(#{ - woody_context => WoodyCtx, - party_client => party_client:create_client() - })) - end, - env_leave => fun() -> ff_context:cleanup() end + context_binding => #{ + registry_key => {p, l, {ff_context, stored_context}}, + cleanup_mode => lenient + } } } } diff --git a/docs/prg-machine-migration-context.md b/docs/prg-machine-migration-context.md index 0305873b..09235e7c 100644 --- a/docs/prg-machine-migration-context.md +++ b/docs/prg-machine-migration-context.md @@ -33,7 +33,7 @@ woody handler (hg_*_handler, ff_*_handler) - Trace API на Thrift (`docs/trace-api-thrift.md`) - Hybrid MG↔progressor (`hg_hybrid`, machinegun) - Полное удаление зависимости `machinery` из `rebar.config` -- Дедупликация HG/FF утилит (`payproc_common`, `ff_core`) — другой Ralph goal +- Дедупликация HG/FF утилит (`operation_context`, `ff_core`) — другой Ralph goal **Принципы:** @@ -58,7 +58,7 @@ woody handler (hg_*_handler, ff_*_handler) **`process/3`** — callback progressor: -1. `env_enter(WoodyCtx)` — поднять `hg_context` / `ff_context` +1. `env_enter(WoodyCtx)` — поднять `operation_context` (HG или FF binding) 2. `unmarshal_machine` — history + aux_state из storage 3. `dispatch` → `Handler:init | process_call | process_signal | process_repair | process_notification` 4. `marshal_process_result` — events, action, aux_state обратно в progressor @@ -101,19 +101,32 @@ woody handler (hg_*_handler, ff_*_handler) ### 2.4. Конфиг progressor (`config/sys.config`) -Единый шаблон для каждого NS: +Единый шаблон для каждого NS (prod — 7 namespace: 2× HG + 5× FF): ```erlang processor => #{ client => prg_machine, options => #{ ns => , - env_enter => fun(WoodyCtx) -> ... context:save(...) end, - env_leave => fun() -> ... context:cleanup() end + %% HG (strict) / FF (lenient) — см. operation_context:hellgate_binding/0, fistful_binding/0 + context_binding => #{ + registry_key => {p, l, stored_hg_context}, + cleanup_mode => strict + } } } ``` +`prg_machine:process/3` поднимает RPC-контекст через `operation_context:env_enter/2` и снимает через `operation_context:env_leave/1` по `context_binding`, если в `options` не заданы явные хуки `env_enter` / `env_leave`. + +**Приоритет резолва** (`resolve_env_enter/1`, `resolve_env_leave/1` в `prg_machine.erl`): + +1. Явный fun в `env_enter` / `env_leave` — перекрывает всё (CT с кастомным `party_client` и т.п.) +2. `context_binding => Binding` — `operation_context:env_enter(WoodyCtx, Binding)` / `env_leave(Binding)` +3. noop — `fun(_) -> ok end` / `fun() -> ok end`, если ни fun, ни binding не заданы + +Стандартный enter: `woody_context` + `party_client:create_client()` в gproc по `registry_key` из binding. + Без `handler`, `schema`, `machinery_prg_backend`. ### 2.5. Тонкие обёртки From a060cd706cac12ddc49e1a30b2241686e39efc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 10:24:02 +0300 Subject: [PATCH 04/62] Remove hg_progressor application and related references. Update test suites to utilize cleanup_progressor_namespaces function from hg_ct_helper for namespace cleanup. Adjust rebar.config and hellgate.app.src to reflect the removal of hg_progressor dependencies. --- apps/hellgate/src/hellgate.app.src | 1 - apps/hellgate/test/hg_ct_helper.erl | 9 + .../test/hg_direct_recurrent_tests_SUITE.erl | 2 +- .../test/hg_invoice_lite_tests_SUITE.erl | 2 +- .../test/hg_invoice_template_tests_SUITE.erl | 2 +- apps/hellgate/test/hg_invoice_tests_SUITE.erl | 2 +- .../test/hg_route_rules_tests_SUITE.erl | 2 +- apps/hg_progressor/rebar.config | 2 - apps/hg_progressor/src/hg_hybrid.erl | 229 ------------------ apps/hg_progressor/src/hg_progressor.app.src | 18 -- apps/hg_progressor/src/hg_progressor.erl | 212 ---------------- .../machinery_extra/src/machinery_msgpack.erl | 98 ++++++++ docs/prg-machine-migration-context.md | 25 +- rebar.config | 2 +- 14 files changed, 121 insertions(+), 485 deletions(-) delete mode 100644 apps/hg_progressor/rebar.config delete mode 100644 apps/hg_progressor/src/hg_hybrid.erl delete mode 100644 apps/hg_progressor/src/hg_progressor.app.src delete mode 100644 apps/hg_progressor/src/hg_progressor.erl create mode 100644 apps/machinery_extra/src/machinery_msgpack.erl diff --git a/apps/hellgate/src/hellgate.app.src b/apps/hellgate/src/hellgate.app.src index 06075541..df93d919 100644 --- a/apps/hellgate/src/hellgate.app.src +++ b/apps/hellgate/src/hellgate.app.src @@ -11,7 +11,6 @@ herd, progressor, prg_machine, - hg_progressor, hg_proto, routing, cowboy, diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index 98f0a680..5933bcd0 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"). @@ -1027,3 +1029,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 333e9be0..44647285 100644 --- a/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl +++ b/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl @@ -166,7 +166,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup_hellgate(), _ = application:stop(progressor), - _ = hg_progressor:cleanup_hellgate(), + _ = 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_invoice_lite_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl index 301fd9d0..c1e129cb 100644 --- a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl @@ -121,7 +121,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup_hellgate(), _ = application:stop(progressor), - _ = hg_progressor:cleanup_hellgate(), + _ = 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). diff --git a/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl index 92294c7a..79e79941 100644 --- a/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl @@ -112,7 +112,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup_hellgate(), _ = application:stop(progressor), - _ = hg_progressor:cleanup_hellgate(), + _ = 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 33ff6d02..cfee2339 100644 --- a/apps/hellgate/test/hg_invoice_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_tests_SUITE.erl @@ -575,7 +575,7 @@ init_per_suite(C) -> end_per_suite(C) -> _ = hg_domain:cleanup_hellgate(), _ = application:stop(progressor), - _ = hg_progressor:cleanup_hellgate(), + _ = 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). diff --git a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl index 26e66b1c..00f90da3 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_hellgate(), + _ = hg_ct_helper:cleanup_progressor_namespaces(), hg_mock_helper:stop_sup(SupPid). -spec init_per_group(group_name(), config()) -> config(). 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 a710241a..00000000 --- a/apps/hg_progressor/src/hg_hybrid.erl +++ /dev/null @@ -1,229 +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 call_machinegun('GetMachine', {MachineDesc}) 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 call_machinegun('GetMachine', {MachineDesc}) 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. - -call_machinegun(Function, Args) -> - case hg_woody_wrapper:call(automaton, Function, Args) of - {ok, _} = Result -> - Result; - {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. - --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 69109259..00000000 --- a/apps/hg_progressor/src/hg_progressor.app.src +++ /dev/null @@ -1,18 +0,0 @@ -{application, hg_progressor, [ - {description, "An OTP library"}, - {vsn, "0.1.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib, - damsel, - progressor, - prg_machine, - hg_proto - ]}, - {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 5baf733f..00000000 --- a/apps/hg_progressor/src/hg_progressor.erl +++ /dev/null @@ -1,212 +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]). - -%-ifdef(TEST). --export([cleanup/0]). -%-endif. - --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 - ], - lists:foreach(fun(NsID) -> prg_test_utils:cleanup(#{ns => NsID}) end, Namespaces). - -%-endif. - -%% Internal functions - --spec handle_exception(_) -> no_return(). -handle_exception({exception, Class, Reason}) -> - erlang:raise(Class, Reason, []). - -get_context() -> - WoodyContext = - try operation_context:load_hellgate() of - Ctx -> - operation_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). - -maybe_unmarshal(_, undefined) -> - undefined; -maybe_unmarshal(Type, Value) -> - unmarshal(Type, Value). - -unmarshal(term, Term) -> - erlang:term_to_binary(Term); -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/machinery_extra/src/machinery_msgpack.erl b/apps/machinery_extra/src/machinery_msgpack.erl new file mode 100644 index 00000000..261315c7 --- /dev/null +++ b/apps/machinery_extra/src/machinery_msgpack.erl @@ -0,0 +1,98 @@ +%%% +%%% Msgpack manipulation employed by machinegun interfaces. +%%% Extended in hellgate with pack/1 for thrift Value wire encoding. + +-module(machinery_msgpack). + +-include_lib("mg_proto/include/mg_proto_msgpack_thrift.hrl"). + +%% API + +-export([wrap/1]). +-export([unwrap/1]). +-export([nil/0]). +-export([pack/1]). + +-type t() :: mg_proto_msgpack_thrift:'Value'(). + +-export_type([t/0]). + +%% + +-spec wrap + (nil) -> t(); + (boolean()) -> t(); + (integer()) -> t(); + (float()) -> t(); + %% string + (binary()) -> t(); + %% binary + ({binary, binary()}) -> t(); + ([t()]) -> t(); + (#{t() => t()}) -> t(). +wrap(nil) -> + {nl, #mg_msgpack_Nil{}}; +wrap(V) when is_boolean(V) -> + {b, V}; +wrap(V) when is_integer(V) -> + {i, V}; +wrap(V) when is_float(V) -> + V; +wrap(V) when is_binary(V) -> + % Assuming well-formed UTF-8 bytestring. + {str, V}; +wrap({binary, V}) when is_binary(V) -> + {bin, V}; +wrap(V) when is_list(V) -> + {arr, V}; +wrap(V) when is_map(V) -> + {obj, V}. + +-spec unwrap(t()) -> + nil + | boolean() + | integer() + | float() + %% string + | binary() + %% binary + | {binary, binary()} + | [t()] + | #{t() => t()}. +unwrap({nl, #mg_msgpack_Nil{}}) -> + nil; +unwrap({b, V}) when is_boolean(V) -> + V; +unwrap({i, V}) when is_integer(V) -> + V; +unwrap({flt, V}) when is_float(V) -> + V; +unwrap({str, V}) when is_binary(V) -> + % Assuming well-formed UTF-8 bytestring. + V; +unwrap({bin, V}) when is_binary(V) -> + {binary, V}; +unwrap({arr, V}) when is_list(V) -> + V; +unwrap({obj, V}) when is_map(V) -> + V. + +-spec nil() -> t(). +nil() -> + wrap(nil). + +-spec pack(t()) -> {ok, binary()}. +pack(Value) -> + Type = {struct, union, {mg_proto_msgpack_thrift, 'Value'}}, + {ok, serialize(Type, Value)}. + +serialize(Type, Data) -> + {ok, Trans} = thrift_membuffer_transport:new(), + {ok, Proto} = thrift_binary_protocol:new(Trans, [{strict_read, true}, {strict_write, true}]), + case thrift_protocol:write(Proto, {Type, Data}) of + {NewProto, ok} -> + {_, {ok, Result}} = thrift_protocol:close_transport(NewProto), + Result; + {_NewProto, {error, Reason}} -> + erlang:error({thrift, {protocol, Reason}}) + end. diff --git a/docs/prg-machine-migration-context.md b/docs/prg-machine-migration-context.md index 09235e7c..14225bde 100644 --- a/docs/prg-machine-migration-context.md +++ b/docs/prg-machine-migration-context.md @@ -31,7 +31,7 @@ woody handler (hg_*_handler, ff_*_handler) **Не в scope этой миграции (отдельные goals):** - Trace API на Thrift (`docs/trace-api-thrift.md`) -- Hybrid MG↔progressor (`hg_hybrid`, machinegun) +- ~~Hybrid MG↔progressor (`hg_hybrid`, machinegun)~~ — **удалено** - Полное удаление зависимости `machinery` из `rebar.config` - Дедупликация HG/FF утилит (`operation_context`, `ff_core`) — другой Ralph goal @@ -169,7 +169,7 @@ processor => #{ - `apps/hellgate/src/hg_machine.erl`, `hg_machine_action.erl` - `apps/fistful/src/ff_machine.erl`, `fistful.erl` -- `apps/hg_progressor/src/hg_progressor_handler.erl` +- `apps/hg_progressor/` (`hg_progressor_handler`, `hg_hybrid`, `call_automaton` glue) - `apps/ff_server/src/ff_*_machinery_schema.erl` (×5) **Добавлено:** @@ -252,35 +252,26 @@ rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE | `apps/ff_cth/src/ct_payment_system.erl` | мёртвый `{machinery_backend, progressor}` | | `apps/machinery_extra/` | остаётся для `ff_limit` и тестов | -### 5.3. HG hybrid / progressor glue - -| Модуль | Роль | -|--------|------| -| `hg_progressor.erl` | **остался** — `call_automaton/2` для machinegun thrift (hybrid, CT cleanup) | -| `hg_hybrid.erl` | маршрутизация MG vs progressor | - -Это **не** prod path для invoice NS, но нужно понимать при удалении `hg_progressor` app целиком. - -### 5.4. Trace API +### 5.3. Trace API - Сейчас: FF internal HTTP JSON (`ff_machine_handler` → `prg_machine:trace/2`) - Цель (отдельно): Thrift по `progressor_trace.thrift`, см. `docs/trace-api-thrift.md` -- В git status были черновики `hg_progressor_trace*` — **не** в финальном дереве `apps/hg_progressor` +- В git status были черновики `hg_progressor_trace*` — **не** в финальном дереве (app `hg_progressor` удалён) -### 5.5. P2b — orphan NS +### 5.4. P2b — orphan NS `ff/identity`, `ff/wallet_v2`, HG `customer`, `recurrent_paytools` — убраны из `sys.config`. Если понадобятся в проде — отдельный PR с доменными модулями + `prg_machine`. -### 5.6. Технический долг в runtime +### 5.5. Технический долг в runtime - `initial_model/2` — `_Handler` не используется; `model` в aux на практике не пишется - `binary_to_term` в decode без `[safe]` в fallback path `prg_machine` — стоит проверить - `hg_invoice` vs FF: унификация через `apply_event/4` в runtime (вариант C); HG migration — goal `goal-hg-collapse.md` -### 5.7. Grep-инварианты (целевые после полного P5) +### 5.6. Grep-инварианты (целевые после полного P5) ```bash -rg 'hg_machine:' apps/hellgate apps/hg_progressor --glob '*.erl' # 0 prod +rg 'hg_machine:' apps/hellgate --glob '*.erl' # 0 prod rg 'machinery_prg_backend|ff_machine:' apps/fistful apps/ff_transfer apps/ff_server --glob '*.erl' # 0 кроме ff_limit rg "client => machinery_prg_backend" config/sys.config # 0 ``` diff --git a/rebar.config b/rebar.config index 664bec26..b6b74fe8 100644 --- a/rebar.config +++ b/rebar.config @@ -170,5 +170,5 @@ {shell, [ {config, "config/sys.config"}, - {apps, [hellgate, hg_client, hg_progressor, hg_proto, routing, recon]} + {apps, [hellgate, hg_client, hg_proto, routing, recon]} ]}. From 3fdb979773d05e8f41178548828ed5013bdae35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 12:26:08 +0300 Subject: [PATCH 05/62] Refactor action types from prg_machine_action to progressor_action across multiple modules. Update type definitions and function calls to ensure consistency in handling actions. Remove obsolete prg_machine_action module. --- .gitignore | 1 - _checkouts/progressor | 1 + .../ff_transfer/src/ff_adapter_withdrawal.erl | 2 +- apps/ff_transfer/src/ff_adjustment.erl | 2 +- apps/ff_transfer/src/ff_adjustment_utils.erl | 2 +- apps/ff_transfer/src/ff_deposit.erl | 12 +-- apps/ff_transfer/src/ff_deposit_machine.erl | 10 +- apps/ff_transfer/src/ff_destination.erl | 2 +- apps/ff_transfer/src/ff_source.erl | 2 +- apps/ff_transfer/src/ff_withdrawal.erl | 10 +- .../ff_transfer/src/ff_withdrawal_session.erl | 18 ++-- apps/hellgate/src/hg_invoice.erl | 28 +++--- apps/hellgate/src/hg_invoice_payment.erl | 62 ++++++------ .../src/hg_invoice_payment_chargeback.erl | 16 +-- .../src/hg_invoice_payment_refund.erl | 16 +-- .../src/hg_invoice_registered_payment.erl | 6 +- apps/hellgate/src/hg_invoice_repair.erl | 2 +- apps/hellgate/src/hg_session.erl | 18 ++-- apps/prg_machine/src/prg_machine.erl | 4 +- apps/prg_machine/src/prg_machine_action.erl | 98 ------------------- .../test/prg_machine_env_mock_handler.erl | 8 +- rebar.lock | 4 - 22 files changed, 111 insertions(+), 213 deletions(-) create mode 160000 _checkouts/progressor delete mode 100644 apps/prg_machine/src/prg_machine_action.erl diff --git a/.gitignore b/.gitignore index 7b464896..03af9e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # general log /_build/ -/_checkouts/ *~ erl_crash.dump rebar3.crashdump diff --git a/_checkouts/progressor b/_checkouts/progressor new file mode 160000 index 00000000..90f46570 --- /dev/null +++ b/_checkouts/progressor @@ -0,0 +1 @@ +Subproject commit 90f46570007a1cbf7cac960059fe6c8018702489 diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal.erl b/apps/ff_transfer/src/ff_adapter_withdrawal.erl index 7857f46e..4cc6333f 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() :: prg_machine_action:timer(). +-type timer() :: progressor_action:timer(). -type transaction_info() :: ff_adapter:transaction_info(). -type failure() :: ff_adapter:failure(). diff --git a/apps/ff_transfer/src/ff_adjustment.erl b/apps/ff_transfer/src/ff_adjustment.erl index 9434a53f..d82b38b2 100644 --- a/apps/ff_transfer/src/ff_adjustment.erl +++ b/apps/ff_transfer/src/ff_adjustment.erl @@ -91,7 +91,7 @@ -type target_status() :: term(). -type final_cash_flow() :: ff_cash_flow:final_cash_flow(). -type p_transfer() :: ff_postings_transfer:transfer(). --type action() :: prg_machine_action:t() | undefined. +-type action() :: progressor_action:t() | undefined. -type process_result() :: {action(), [event()]}. -type legacy_event() :: any(). -type external_id() :: id(). diff --git a/apps/ff_transfer/src/ff_adjustment_utils.erl b/apps/ff_transfer/src/ff_adjustment_utils.erl index 7f05fee7..299636e6 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() :: prg_machine_action:t() | undefined. +-type action() :: progressor_action:t() | undefined. -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 05705d24..bdd6f468 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -151,7 +151,7 @@ -type is_negative() :: boolean(). -type cash() :: ff_cash:cash(). -type cash_range() :: ff_range:range(cash()). --type action() :: prg_machine_action:t() | undefined. +-type action() :: progressor_action:t() | undefined. -type ctx() :: ff_entity_context:context(). -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). @@ -330,7 +330,7 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => prg_machine_action:instant(), + action => progressor_action:instant(), auxst => #{ctx => Ctx} }. @@ -682,15 +682,15 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> prg_machine_action:t() | undefined. +-spec map_action(action()) -> progressor_action:t() | undefined. map_action(undefined) -> undefined; map_action(continue) -> - prg_machine_action:instant(); + progressor_action:instant(); map_action(sleep) -> - prg_machine_action:instant(); + progressor_action:instant(); map_action({setup_timer, Timer}) -> - prg_machine_action:set_timer(Timer). + progressor_action:set_timer(Timer). -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(undefined) -> diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index f4a9b0b3..c07ae0ee 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -34,7 +34,7 @@ -type action() :: continue | sleep - | {setup_timer, prg_machine_action:timer()} + | {setup_timer, progressor_action:timer()} | undefined. -export_type([id/0]). @@ -171,15 +171,15 @@ history_times(History) -> History ). --spec map_action(action()) -> prg_machine_action:t() | undefined. +-spec map_action(action()) -> progressor_action:t() | undefined. map_action(undefined) -> undefined; map_action(continue) -> - prg_machine_action:instant(); + progressor_action:instant(); map_action(sleep) -> - prg_machine_action:instant(); + progressor_action:instant(); map_action({setup_timer, Timer}) -> - prg_machine_action:set_timer(Timer). + progressor_action:set_timer(Timer). codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> {DateTime, USec} = Timestamp; diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index 21cac757..b92891ce 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -255,7 +255,7 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => prg_machine_action:instant(), + action => progressor_action:instant(), auxst => #{ctx => Ctx} }. diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index 268b4712..fc838580 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -231,7 +231,7 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => prg_machine_action:instant(), + action => progressor_action:instant(), auxst => #{ctx => Ctx} }. diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 375032c6..bfd96498 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -1845,7 +1845,7 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => prg_machine_action:instant(), + action => progressor_action:instant(), auxst => #{ctx => Ctx} }. @@ -1931,15 +1931,15 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> prg_machine_action:t() | undefined. +-spec map_action(action()) -> progressor_action:t() | undefined. map_action(undefined) -> undefined; map_action(continue) -> - prg_machine_action:instant(); + progressor_action:instant(); map_action(sleep) -> - prg_machine_action:unset_timer(); + progressor_action:unset_timer(); map_action({setup_timer, Timer}) -> - prg_machine_action:set_timer(Timer). + progressor_action:set_timer(Timer). -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(undefined) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index 1ab3c0ee..5ae61ac3 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -121,8 +121,8 @@ -type action() :: undefined | continue - | {setup_callback, ff_withdrawal_callback:tag(), prg_machine_action:timer()} - | {setup_timer, prg_machine_action:timer()} + | {setup_callback, ff_withdrawal_callback:tag(), progressor_action:timer()} + | {setup_timer, progressor_action:timer()} | retry | finish. @@ -400,7 +400,7 @@ namespace() -> init(Events, _Machine) -> #{ events => Events, - action => prg_machine_action:instant(), + action => progressor_action:instant(), auxst => #{ctx => ff_entity_context:new()} }. @@ -489,20 +489,20 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action(), session_state()) -> prg_machine_action:t() | undefined. +-spec map_action(action(), session_state()) -> progressor_action:t() | undefined. map_action(undefined, _Session) -> undefined; map_action(continue, _Session) -> - prg_machine_action:instant(); + progressor_action:instant(); map_action({setup_callback, Tag, Timer}, Session) -> ok = ff_machine_tag:create_binding(?NS, Tag, id(Session)), - prg_machine_action:set_timer(Timer); + progressor_action:set_timer(Timer); map_action({setup_timer, Timer}, _Session) -> - prg_machine_action:set_timer(Timer); + progressor_action:set_timer(Timer); map_action(finish, _Session) -> - prg_machine_action:unset_timer(); + progressor_action:unset_timer(); map_action(retry, _Session) -> - prg_machine_action:instant(). + progressor_action:instant(). -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(undefined) -> diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 336b9827..516da587 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -96,7 +96,7 @@ -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). --type action() :: prg_machine_action:t(). +-type action() :: progressor_action:t(). %% API @@ -298,7 +298,7 @@ init(Invoice, _Machine) -> Changes = [?invoice_created(UnmarshalledInvoice)], #{ events => [Changes], - action => set_invoice_timer(prg_machine_action:new(), #st{invoice = UnmarshalledInvoice}), + action => set_invoice_timer(progressor_action:new(), #st{invoice = UnmarshalledInvoice}), auxst => #{} }. @@ -352,18 +352,18 @@ handle_signal(timeout, #st{activity = invoice} = St) -> construct_repair_action(CA) when CA /= undefined -> lists:foldl( fun merge_repair_action/2, - prg_machine_action:new(), + progressor_action:new(), [{timer, CA#repair_ComplexAction.timer}, {remove, CA#repair_ComplexAction.remove}] ); construct_repair_action(undefined) -> - prg_machine_action:new(). + progressor_action:new(). merge_repair_action({timer, {set_timer, #repair_SetTimerAction{timer = Timer}}}, Action) -> - prg_machine_action:set_timer(Timer, Action); + progressor_action:set_timer(Timer, Action); merge_repair_action({timer, {unset_timer, #repair_UnsetTimerAction{}}}, Action) -> - prg_machine_action:unset_timer(Action); + progressor_action:unset_timer(Action); merge_repair_action({remove, #repair_RemoveAction{}}, Action) -> - prg_machine_action:mark_removal(Action); + progressor_action:mark_removal(Action); merge_repair_action({_, undefined}, Action) -> Action. @@ -456,7 +456,7 @@ handle_call({{'Invoicing', 'Rescind'}, {_InvoiceID, Reason}}, St0) -> #{ response => ok, changes => [?invoice_status_changed(?invoice_cancelled(hg_utils:format_reason(Reason)))], - action => prg_machine_action:unset_timer(), + action => progressor_action:unset_timer(), state => St }; handle_call({{'Invoicing', 'RefundPayment'}, {_InvoiceID, PaymentID, Params}}, St0) -> @@ -526,7 +526,7 @@ 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}}) -> - prg_machine_action:set_deadline(Due, Action); + progressor_action:set_deadline(Due, Action); set_invoice_timer(_Status, Action, _St) -> Action. @@ -1074,23 +1074,23 @@ changes_from_msgpack_data(Data) -> -type event_timestamp() :: calendar:datetime(). --spec action_to_prg(prg_machine_action:t() | undefined) -> action(). +-spec action_to_prg(progressor_action:t() | undefined) -> action(). action_to_prg(#mg_stateproc_ComplexAction{timer = Timer, remove = Remove}) -> - Action0 = prg_machine_action:new(), + Action0 = progressor_action:new(), Action1 = case Timer of undefined -> Action0; {set_timer, #mg_stateproc_SetTimerAction{timer = T}} -> - prg_machine_action:set_timer(T, Action0); + progressor_action:set_timer(T, Action0); {unset_timer, #mg_stateproc_UnsetTimerAction{}} -> - prg_machine_action:unset_timer(Action0) + progressor_action:unset_timer(Action0) end, case Remove of undefined -> Action1; #mg_stateproc_RemoveAction{} -> - prg_machine_action:mark_removal(Action1) + progressor_action:mark_removal(Action1) end; action_to_prg(Action) -> Action. diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index d703a286..f8d22a0e 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -388,7 +388,7 @@ get_chargeback_opts(#st{opts = Opts} = St) -> %% -type event() :: dmsl_payproc_thrift:'InvoicePaymentChangePayload'(). --type action() :: prg_machine_action:t(). +-type action() :: progressor_action:t(). -type events() :: [event()]. -type result() :: {events(), action()}. -type machine_result() :: {next | done, result()}. @@ -462,7 +462,7 @@ init_(PaymentID, Params, #{timestamp := CreatedAt} = Opts) -> [] end, Events = [?payment_started(Payment2)] ++ CascadeTokenEvents, - {collapse_changes(Events, undefined, #{}), {Events, prg_machine_action:instant()}}. + {collapse_changes(Events, undefined, #{}), {Events, progressor_action:instant()}}. seed_bank_card_from_parent(PartyConfigRef, BCT, #{parent_payment := ParentPayment}) -> case get_recurrent_token(ParentPayment) of @@ -985,7 +985,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, prg_machine_action:instant()}}. + {ok, {Changes, progressor_action:instant()}}. partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocation) -> Payment = get_payment(St0), @@ -1009,7 +1009,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, prg_machine_action:instant()}}. + {ok, {Changes, progressor_action:instant()}}. -spec cancel(st(), binary()) -> {ok, result()}. cancel(St, Reason) -> @@ -1017,7 +1017,7 @@ cancel(St, Reason) -> _ = assert_activity({payment, flow_waiting}, St), _ = assert_payment_flow(hold, Payment), Changes = start_session(?cancelled_with_reason(Reason)), - {ok, {Changes, prg_machine_action:instant()}}. + {ok, {Changes, progressor_action:instant()}}. assert_capture_cost_currency(undefined, _) -> ok; @@ -1148,7 +1148,7 @@ refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> refund => Refund, cash_flow => FinalCashflow }), - {Refund, {Changes, prg_machine_action:instant()}}. + {Refund, {Changes, progressor_action:instant()}}. -spec manual_refund(refund_params(), st(), opts()) -> {domain_refund(), result()}. manual_refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> @@ -1165,7 +1165,7 @@ manual_refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> cash_flow => FinalCashflow, transaction_info => TransactionInfo }), - {Refund, {Changes, prg_machine_action:instant()}}. + {Refund, {Changes, progressor_action:instant()}}. make_refund(Params, Payment, Revision, CreatedAt, St, Opts) -> _ = assert_no_pending_chargebacks(St), @@ -1625,7 +1625,7 @@ construct_adjustment( state = State }, Events = [?adjustment_ev(ID, ?adjustment_created(Adjustment)) | AdditionalEvents], - {Adjustment, {Events, prg_machine_action:instant()}}. + {Adjustment, {Events, progressor_action:instant()}}. construct_adjustment_id(#st{adjustments = As}) -> erlang:integer_to_binary(length(As) + 1). @@ -1679,7 +1679,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], prg_machine_action:new()}}. + {done, {[Event], progressor_action:new()}}. prepare_adjustment_cashflow(Adjustment, St, Options) -> PlanID = construct_adjustment_plan_id(Adjustment, St, Options), @@ -1755,7 +1755,7 @@ process_signal(timeout, St, Options) -> ). process_timeout(St) -> - Action = prg_machine_action:new(), + Action = progressor_action:new(), repair_process_timeout(get_activity(St), Action, St). -spec process_timeout(activity(), action(), st()) -> machine_result(). @@ -1909,14 +1909,14 @@ process_shop_limit_initialization(Action, St) -> _ = hold_shop_limits(Opts, St), case check_shop_limits(Opts, St) of ok -> - {next, {[?shop_limit_initiated()], prg_machine_action:set_timeout(0, Action)}}; + {next, {[?shop_limit_initiated()], progressor_action:set_timeout(0, Action)}}; {error, {limit_overflow = Error, IDs}} -> Failure = construct_shop_limit_failure(Error, IDs), Events = [ ?shop_limit_initiated(), ?payment_rollback_started(Failure) ], - {next, {Events, prg_machine_action:set_timeout(0, Action)}} + {next, {Events, progressor_action:set_timeout(0, Action)}} end. construct_shop_limit_failure(limit_overflow, IDs) -> @@ -1927,13 +1927,13 @@ construct_shop_limit_failure(limit_overflow, IDs) -> 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))], prg_machine_action:set_timeout(0, Action)}}. + {done, {[?payment_status_changed(?failed(Failure))], progressor_action:set_timeout(0, Action)}}. -spec process_shop_limit_finalization(action(), st()) -> machine_result(). process_shop_limit_finalization(Action, St) -> Opts = get_opts(St), _ = commit_shop_limits(Opts, St), - {next, {[?shop_limit_applied()], prg_machine_action:set_timeout(0, Action)}}. + {next, {[?shop_limit_applied()], progressor_action:set_timeout(0, Action)}}. -spec process_risk_score(action(), st()) -> machine_result(). process_risk_score(Action, St) -> @@ -1947,7 +1947,7 @@ process_risk_score(Action, St) -> Events = [?risk_score_changed(RiskScore)], case check_risk_score(RiskScore) of ok -> - {next, {Events, prg_machine_action:set_timeout(0, Action)}}; + {next, {Events, progressor_action:set_timeout(0, Action)}}; {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) @@ -1984,7 +1984,7 @@ process_routing(Action, St) -> Revision, St ), - {next, {Events, prg_machine_action:set_timeout(0, Action)}} + {next, {Events, progressor_action:set_timeout(0, Action)}} end end. @@ -2109,7 +2109,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, prg_machine_action:set_timeout(0, Action)}} + {next, {Events, progressor_action:set_timeout(0, Action)}} end. log_rejected_route_groups(Result, VS) -> @@ -2182,7 +2182,7 @@ process_cash_flow_building(Action, St) -> {1, FinalCashflow} ), Events = [?cash_flow_changed(FinalCashflow)], - {next, {Events, prg_machine_action:set_timeout(0, Action)}}. + {next, {Events, progressor_action:set_timeout(0, Action)}}. %% @@ -2239,7 +2239,7 @@ 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, prg_machine_action:instant()}}. + {next, {Events, progressor_action:instant()}}. process_accounter_update(Action, #st{partial_cash_flow = FinalCashflow, capture_data = CaptureData} = St) -> #payproc_InvoicePaymentCaptureData{ @@ -2256,7 +2256,7 @@ process_accounter_update(Action, #st{partial_cash_flow = FinalCashflow, capture_ ] ), Events = start_session(?captured(Reason, Cost, Cart, Allocation)), - {next, {Events, prg_machine_action:set_timeout(0, Action)}}. + {next, {Events, progressor_action:set_timeout(0, Action)}}. %% @@ -2281,11 +2281,11 @@ process_session(St) -> process_session(undefined, St0) -> Target = get_target(St0), TargetType = get_target_type(Target), - Action = prg_machine_action:new(), + Action = progressor_action:new(), case validate_processing_deadline(get_payment(St0), TargetType) of ok -> Events = start_session(Target), - Result = {Events, prg_machine_action:set_timeout(0, Action)}, + Result = {Events, progressor_action:set_timeout(0, Action)}, {next, Result}; Failure -> process_failure(get_activity(St0), [], Action, Failure, St0) @@ -2310,7 +2310,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 = prg_machine_action:set_timeout(0, Action), + NewAction = progressor_action:set_timeout(0, Action), InvoiceID = get_invoice_id(get_invoice(get_opts(St0))), St1 = collapse_changes(Events1, St0, #{invoice_id => InvoiceID}), _ = @@ -2357,7 +2357,7 @@ finalize_payment(Action, St) -> _ -> start_session(Target) end, - {done, {StartEvents, prg_machine_action:set_timeout(0, Action)}}. + {done, {StartEvents, progressor_action:set_timeout(0, Action)}}. -spec process_result(action(), st()) -> machine_result(). process_result(Action, St) -> @@ -2397,18 +2397,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)], prg_machine_action:set_timeout(0, Action)}}; + {next, {[?cash_flow_changed(FinalCashflow)], progressor_action:set_timeout(0, Action)}}; 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 = prg_machine_action:set_timeout(0, Action), + NewAction = progressor_action:set_timeout(0, Action), 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 = prg_machine_action:set_timeout(0, Action), + NewAction = progressor_action:set_timeout(0, Action), %% We need to rollback only current route. %% Previously used routes are supposed to have their limits already rolled back. Route = get_route(St), @@ -2590,7 +2590,7 @@ process_fatal_payment_failure(?captured(), _Events, _Action, Failure, _St) -> error({invalid_capture_failure, Failure}); process_fatal_payment_failure(?processed(), Events, Action, Failure, _St) -> RollbackStarted = [?payment_rollback_started(Failure)], - {next, {Events ++ RollbackStarted, prg_machine_action:set_timeout(0, Action)}}. + {next, {Events ++ RollbackStarted, progressor_action:set_timeout(0, Action)}}. retry_session(Action, Target, Timeout) -> NewEvents = start_session(Target), @@ -2645,15 +2645,15 @@ do_check_failure_type(_Failure) -> get_action(?processed(), Action, St) -> case get_payment_flow(get_payment(St)) of ?invoice_payment_flow_instant() -> - prg_machine_action:set_timeout(0, Action); + progressor_action:set_timeout(0, Action); ?invoice_payment_flow_hold(_, HeldUntil) -> - prg_machine_action:set_deadline(HeldUntil, Action) + progressor_action:set_deadline(HeldUntil, Action) end; get_action(_Target, Action, _St) -> Action. set_timer(Timer, Action) -> - prg_machine_action:set_timer(Timer, Action). + progressor_action:set_timer(Timer, Action). get_provider_payment_terms(St, Revision) -> Opts = get_opts(St), diff --git a/apps/hellgate/src/hg_invoice_payment_chargeback.erl b/apps/hellgate/src/hg_invoice_payment_chargeback.erl index ed4b9487..15185501 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() :: - prg_machine_action:t(). + progressor_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, prg_machine_action:new(), Opts); + update_cash_flow(State, progressor_action:new(), Opts); process_timeout(updating_cash_flow, State, _Action, Opts) -> - update_cash_flow(State, prg_machine_action:instant(), Opts); + update_cash_flow(State, progressor_action:instant(), 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 = prg_machine_action:instant(), + Action = progressor_action:instant(), 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 = prg_machine_action:instant(), + Action = progressor_action:instant(), 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 = prg_machine_action:instant(), + Action = progressor_action:instant(), 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 = prg_machine_action:instant(), + Action = progressor_action:instant(), 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 = prg_machine_action:instant(), + Action = progressor_action:instant(), 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 9f5908c4..e6709c49 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -103,7 +103,7 @@ -type event() :: dmsl_payproc_thrift:'InvoicePaymentChangePayload'(). -type event_payload() :: dmsl_payproc_thrift:'InvoicePaymentRefundChangePayload'(). -type events() :: [event()]. --type action() :: prg_machine_action:t(). +-type action() :: progressor_action:t(). -type result() :: {events(), action()}. -type machine_result() :: {next | done, result()}. @@ -271,10 +271,10 @@ do_process(accounter, Refund) -> do_process(failure, Refund) -> process_failure(Refund); do_process(finished, _Refund) -> - {done, {[], prg_machine_action:new()}}. + {done, {[], progressor_action:new()}}. process_refund_cashflow(Refund) -> - Action = prg_machine_action:set_timeout(0, prg_machine_action:new()), + Action = progressor_action:set_timeout(0, progressor_action:new()), PartyConfigRef = get_injected_party_config_ref(Refund), ShopConfigRef = get_injected_shop_config_ref(Refund), Shop = get_injected_shop(Refund), @@ -316,7 +316,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 = prg_machine_action:set_timeout(0, Action), + NewAction = progressor_action:set_timeout(0, Action), {next, {Events1, NewAction}}; {finished, ?session_failed(Failure)} -> case check_retry_possibility(Failure, Refund) of @@ -326,7 +326,7 @@ finish_session_processing({Events0, Action}, Session, Refund) -> {next, {Events1 ++ SessionEvents, SessionAction}}; fatal -> RollbackStarted = [?refund_rollback_started(Failure)], - {next, {Events1 ++ RollbackStarted, prg_machine_action:set_timeout(0, Action)}} + {next, {Events1 ++ RollbackStarted, progressor_action:set_timeout(0, Action)}} end; _ -> {next, {Events1, Action}} @@ -335,14 +335,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())], prg_machine_action:new()}}. + {done, {[?refund_status_changed(?refund_succeeded())], progressor_action:new()}}. process_failure(Refund) -> Failure = failure(Refund), _ = rollback_refund_limits(Refund), _PostingPlanLog = rollback_refund_cashflow(Refund), Events = [?refund_status_changed(?refund_failed(Failure))], - {done, {Events, prg_machine_action:new()}}. + {done, {Events, progressor_action:new()}}. hold_refund_limits(Refund) -> DomainRefund = refund(Refund), @@ -453,7 +453,7 @@ get_manual_refund_events(_) -> retry_session(Action, Timeout) -> NewEvents = [hg_session:wrap_event(?refunded(), hg_session:create())], - NewAction = prg_machine_action:set_timer({timeout, Timeout}, Action), + NewAction = progressor_action:set_timer({timeout, Timeout}, Action), {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 ba173500..c80edae7 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, prg_machine_action:instant()}}. + {collapse_changes(Events, undefined, ChangeOpts), {Events, progressor_action:instant()}}. -spec merge_change( hg_invoice_payment:change(), @@ -147,7 +147,7 @@ process_signal(timeout, St, Options) -> ). process_timeout(St) -> - Action = prg_machine_action:new(), + Action = progressor_action:new(), process_timeout(hg_invoice_payment:get_activity(St), Action, St). process_timeout({payment, processing_capture}, Action, St) -> @@ -172,7 +172,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, prg_machine_action:set_timeout(0, Action)}}. + {next, {Events, progressor_action:set_timeout(0, Action)}}. 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 55b4ad41..baf7d837 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}))], prg_machine_action:instant()}}}; + {result, {done, {[?payment_status_changed(?failed({failure, Failure}))], progressor_action:instant()}}}; 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_session.erl b/apps/hellgate/src/hg_session.erl index f3f6d482..7e0e35cb 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() :: prg_machine_action:t(). +-type action() :: progressor_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, prg_machine_action:instant()}, + Result = {SessionEvents, progressor_action:instant()}, 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) -> - {{[], prg_machine_action:new()}, Session}. + {{[], progressor_action:new()}, 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, prg_machine_action:new()}, + Result = {SessionEvents, progressor_action:new()}, 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, prg_machine_action:new(), Session), + {Events4, Action} = handle_proxy_intent(Intent, progressor_action:new(), 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, prg_machine_action:unset_timer(prg_machine_action:new()), Session), + {Events4, Action} = handle_proxy_intent(Intent, progressor_action:unset_timer(progressor_action:new()), 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, prg_machine_action:new()}. + {Events1 ++ Events2, progressor_action:new()}. apply_result({Events, _Action} = Result, T) -> {Result, update_state_with(Events, T)}. @@ -367,7 +367,7 @@ handle_proxy_intent(#proxy_provider_FinishIntent{status = {failure, Failure}}, A Events = [?session_finished(?session_failed({failure, Failure}))], {Events, Action}; handle_proxy_intent(#proxy_provider_SleepIntent{timer = Timer}, Action0, _Session) -> - Action1 = prg_machine_action:set_timer(Timer, Action0), + Action1 = progressor_action:set_timer(Timer, Action0), {[], Action1}; handle_proxy_intent( #proxy_provider_SuspendIntent{tag = Tag, timeout = Timer, timeout_behaviour = TimeoutBehaviour}, @@ -376,7 +376,7 @@ handle_proxy_intent( ) -> #{payment_id := PaymentID, invoice_id := InvoiceID} = tag_context(Session), ok = hg_machine_tag:create_binding(hg_invoice:namespace(), Tag, PaymentID, InvoiceID), - Action1 = prg_machine_action:set_timer(Timer, Action0), + Action1 = progressor_action:set_timer(Timer, Action0), Events = [?session_suspended(Tag, TimeoutBehaviour)], {Events, Action1}. diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 73ff9e1f..37015ea0 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -33,7 +33,7 @@ -type signal() :: timeout | {repair, args()}. -type result() :: #{ events => [event_body()], - action => prg_machine_action:t(), + action => progressor_action:t(), auxst => term() }. @@ -369,7 +369,7 @@ marshal_process_result(_Handler, _LastEventID, {error, Reason}) -> marshal_intent(Handler, LastEventID, #{events := Events, action := Action, auxst := AuxSt}) -> genlib_map:compact(#{ events => marshal_new_events(Handler, LastEventID, Events), - action => prg_machine_action:to_progressor(Action), + action => Action, aux_state => marshal_aux_state(Handler, AuxSt) }); marshal_intent(Handler, LastEventID, Result) -> diff --git a/apps/prg_machine/src/prg_machine_action.erl b/apps/prg_machine/src/prg_machine_action.erl deleted file mode 100644 index ccdaa7fc..00000000 --- a/apps/prg_machine/src/prg_machine_action.erl +++ /dev/null @@ -1,98 +0,0 @@ --module(prg_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]). --export([to_progressor/1]). - --type seconds() :: non_neg_integer(). --type datetime() :: calendar:datetime() | binary(). --type timer() :: {timeout, seconds()} | {deadline, datetime()}. --type t() :: - undefined - | unset_timer - | remove - | #{set_timer := timer(), remove => boolean()}. - --export_type([t/0, timer/0, seconds/0]). - --spec new() -> t(). -new() -> - #{}. - --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), 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, Deadline}, Action). - --spec set_timer(timer()) -> t(). -set_timer(Timer) -> - set_timer(Timer, new()). - --spec set_timer(timer(), t()) -> t(). -set_timer(Timer, Action) -> - Action#{set_timer => Timer}. - --spec unset_timer() -> t(). -unset_timer() -> - unset_timer(new()). - --spec unset_timer(t()) -> t(). -unset_timer(Action) when is_map(Action) -> - maps:without([set_timer], Action); -unset_timer(unset_timer) -> - unset_timer. - --spec mark_removal(t()) -> t(). -mark_removal(Action) -> - Action#{remove => true}. - --spec to_progressor(t()) -> progressor_action() | undefined. -to_progressor(undefined) -> - undefined; -to_progressor(unset_timer) -> - unset_timer; -to_progressor(remove) -> - #{remove => true}; -to_progressor(#{set_timer := Timer, remove := true}) -> - #{set_timer => marshal_timer(Timer), remove => true}; -to_progressor(#{set_timer := Timer}) -> - #{set_timer => marshal_timer(Timer)}; -to_progressor(#{remove := true}) -> - #{remove => true}; -to_progressor(#{}) -> - undefined. - -%% - --type progressor_action() :: #{set_timer := non_neg_integer(), remove => true} | unset_timer. - -marshal_timer({timeout, Seconds}) when is_integer(Seconds) -> - erlang:system_time(microsecond) div 1000000 + Seconds; -marshal_timer({deadline, {_, _} = Dt}) -> - genlib_time:daytime_to_unixtime(Dt); -marshal_timer({deadline, Bin}) when is_binary(Bin) -> - calendar:rfc3339_to_system_time(unicode:characters_to_list(Bin), [{unit, second}]). diff --git a/apps/prg_machine/test/prg_machine_env_mock_handler.erl b/apps/prg_machine/test/prg_machine_env_mock_handler.erl index dba2a4e6..6df73ce6 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_handler.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -10,16 +10,16 @@ namespace() -> -spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). init(_Args, _Machine) -> - #{events => [], action => prg_machine_action:new(), auxst => undefined}. + #{events => [], action => progressor_action:new(), auxst => undefined}. -spec process_signal(prg_machine:signal(), prg_machine:machine()) -> prg_machine:result(). process_signal(_Signal, _Machine) -> - #{events => [], action => prg_machine_action:new(), auxst => undefined}. + #{events => [], action => progressor_action:new(), auxst => undefined}. -spec process_call(prg_machine:call(), prg_machine:machine()) -> {prg_machine:response(), prg_machine:result()}. process_call(_Call, _Machine) -> - {ok, #{events => [], action => prg_machine_action:new(), auxst => undefined}}. + {ok, #{events => [], action => progressor_action:new(), auxst => undefined}}. -spec process_repair(prg_machine:args(), prg_machine:machine()) -> prg_machine:result() | {error, term()}. process_repair(_Args, _Machine) -> - #{events => [], action => prg_machine_action:new(), auxst => undefined}. + #{events => [], action => progressor_action:new(), auxst => undefined}. diff --git a/rebar.lock b/rebar.lock index 21ff1a68..0187242b 100644 --- a/rebar.lock +++ b/rebar.lock @@ -115,10 +115,6 @@ {git,"https://github.com/valitydev/payproc-errors-erlang.git", {ref,"8ae8586239ef68098398acf7eb8363d9ec3b3234"}}, 0}, - {<<"progressor">>, - {git,"https://github.com/valitydev/progressor.git", - {ref,"90f46570007a1cbf7cac960059fe6c8018702489"}}, - 0}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.11.0">>},0}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.9">>},0}, {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.15">>},1}, From 9cedcd353df2ea563d014d5586ba4955b28e745e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 12:33:53 +0300 Subject: [PATCH 06/62] Update progressor subproject reference and refactor prg_machine to utilize history_range for event retrieval across multiple modules. Adjust function signatures and type definitions for consistency in handling machine events. --- apps/ff_server/src/ff_machine_handler.erl | 2 +- apps/ff_transfer/src/ff_deposit_machine.erl | 2 +- .../src/ff_destination_machine.erl | 2 +- apps/ff_transfer/src/ff_source_machine.erl | 2 +- .../ff_transfer/src/ff_withdrawal_machine.erl | 2 +- .../src/ff_withdrawal_session_machine.erl | 2 +- apps/hellgate/src/hg_invoice.erl | 6 +- apps/prg_machine/src/prg_machine.erl | 56 +++++++++---------- 8 files changed, 36 insertions(+), 38 deletions(-) diff --git a/apps/ff_server/src/ff_machine_handler.erl b/apps/ff_server/src/ff_machine_handler.erl index d305f97c..9e11730d 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} ?= prg_machine:trace(NS, ProcessID), + {ok, Trace} ?= progressor:trace(#{ns => NS, id => ProcessID}), Body = unicode:characters_to_binary(json:encode(Trace)), Req = cowboy_req:reply(200, #{}, Body, Request), {ok, Req, undefined} diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index c07ae0ee..846935d6 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -94,7 +94,7 @@ get(ID) -> {ok, st()} | {error, unknown_deposit_error()}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, {After, Limit, forward}) of + case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index 5886bc9c..1fff667f 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -74,7 +74,7 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, {After, Limit, forward}) of + case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index f974198f..a2c09c72 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -74,7 +74,7 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, {After, Limit, forward}) of + case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 9d1007d9..b189ca9b 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -108,7 +108,7 @@ get(ID) -> {ok, st()} | {error, unknown_withdrawal_error()}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, {After, Limit, forward}) of + case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 61c06759..ca0bc060 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -84,7 +84,7 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, {After, Limit, forward}) of + case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 516da587..5ba11583 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -1109,11 +1109,13 @@ marshal_invoice(Invoice) -> data := {bin, binary()} | term() }. --spec unmarshal_history([prg_machine:event()]) -> [prg_machine:event([invoice_change()])]. +-spec unmarshal_history([prg_machine:machine_event()]) -> + [{prg_machine:event_id(), event_timestamp(), [invoice_change()]}]. unmarshal_history(Events) -> [unmarshal_event(Event) || Event <- Events]. --spec unmarshal_event(prg_machine:event()) -> prg_machine:event([invoice_change()]). +-spec unmarshal_event(prg_machine:machine_event()) -> + {prg_machine:event_id(), event_timestamp(), [invoice_change()]}. unmarshal_event({ID, Dt, Payload}) when is_list(Payload) -> {ID, Dt, Payload}; unmarshal_event({ID, Dt, Payload}) -> diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 37015ea0..197cf38e 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -3,31 +3,30 @@ %%% 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"). + -define(TABLE, prg_machine_dispatch). -define(PROCESSOR_EXCEPTION(Class, Reason, _Stacktrace), {exception, Class, Reason}). %% Types --type namespace() :: atom(). --type id() :: binary(). +-type namespace() :: namespace_id(). -type args() :: term(). -type call() :: term(). -type response() :: ok | {ok, term()} | {exception, term()}. --type event_id() :: pos_integer(). -type timestamp() :: calendar:datetime(). -type event_body() :: term(). --type event() :: {event_id(), timestamp(), event_body()}. --type history() :: [event()]. - --type range() :: {undefined | event_id(), undefined | non_neg_integer(), forward | backward}. +%% Domain history tuple (not progressor storage event() map). +-type machine_event() :: {event_id(), timestamp(), event_body()}. +-type history() :: [machine_event()]. -type machine() :: #{ namespace := namespace(), id := id(), history := history(), aux_state := term(), - range => range() + range => history_range() }. -type signal() :: timeout | {repair, args()}. @@ -41,7 +40,7 @@ -type context_binding() :: operation_context:binding(). --type processor_opts() :: #{ +-type process_options() :: #{ ns := namespace(), env_enter => env_enter_fun(), env_leave => fun(() -> ok), @@ -51,16 +50,17 @@ -export_type([ namespace/0, id/0, + event_id/0, + history_range/0, args/0, call/0, response/0, - event/0, + machine_event/0, history/0, machine/0, signal/0, result/0, - range/0, - processor_opts/0 + process_options/0 ]). %% Domain behaviour @@ -110,7 +110,7 @@ -export([get_history/5]). -export([notify/3]). -export([remove/2]). --export([trace/2]). +-export([history_range/3]). %% Progressor processor @@ -197,9 +197,9 @@ repair(NS, ID, Args) -> {error, {repair, {failed, Reason}}} end. --spec get(namespace(), id(), range()) -> {ok, machine()} | {error, notfound}. +-spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, notfound}. get(NS, ID, Range) -> - Req = request(NS, ID, undefined, range_map(Range)), + Req = request(NS, ID, undefined, Range), case progressor:get(Req) of {ok, Process} -> Handler = get_handler_module(NS), @@ -212,7 +212,7 @@ get(NS, ID, Range) -> -spec get(namespace(), id()) -> {ok, machine()} | {error, notfound}. get(NS, ID) -> - get(NS, ID, {undefined, undefined, forward}). + get(NS, ID, #{direction => forward}). -spec get_history(namespace(), id()) -> {ok, history()} | {error, notfound}. get_history(NS, ID) -> @@ -226,7 +226,7 @@ get_history(NS, ID, After, Limit) -> -spec get_history(namespace(), id(), event_id() | undefined, non_neg_integer() | undefined, forward | backward) -> {ok, history()} | {error, notfound}. get_history(NS, ID, After, Limit, Direction) -> - case get(NS, ID, {After, Limit, Direction}) of + case get(NS, ID, history_range(After, Limit, Direction)) of {ok, #{history := History}} -> {ok, History}; Error -> @@ -249,14 +249,15 @@ remove(NS, ID) -> Error end. --spec trace(namespace(), id()) -> {ok, [map()]} | {error, term()}. -trace(NS, ID) -> - progressor:trace(#{ns => NS, id => ID}). +-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()}, processor_opts(), binary()) -> +-spec process({init | call | repair | notify | timeout, binary(), map()}, process_options(), binary()) -> {ok, map()} | {error, term()}. process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> Enter = resolve_env_enter(Opts), @@ -337,7 +338,7 @@ dispatch(Handler, call, BinArgs, Machine) -> {notify, Args} -> dispatch_notification(Handler, Args, Machine); remove -> - #{events => [], action => remove, auxst => maps:get(aux_state, Machine)}; + #{events => [], action => progressor_action:remove(), auxst => maps:get(aux_state, Machine)}; Call -> Handler:process_call(Call, Machine) end; @@ -559,15 +560,10 @@ encode_range(After, Limit, Direction) -> direction => Direction }). -range_map({After, Limit, Direction}) -> - encode_range(After, Limit, Direction); -range_map(#{offset := _} = Range) -> - Range. - -range_from_process(#{range := #{offset := Offset, limit := Limit, direction := Direction}}) -> - {Offset, Limit, Direction}; +range_from_process(#{range := Range = #{}}) -> + Range; range_from_process(_) -> - {undefined, undefined, forward}. + #{direction => forward}. raise_exception({exception, Class, Reason}) -> erlang:raise(Class, Reason, []). From 59c6a0e3ff7565f3a5defc04f34393a576cd2bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 14:12:26 +0300 Subject: [PATCH 07/62] Update progressor subproject reference and refactor type definitions across multiple modules. Change action type definitions to use 'continue' instead of 'progressor_action:t', and update function signatures to align with new machine structure. Remove obsolete type definitions and improve consistency in handling machine events. --- apps/ff_cth/src/ct_limiter.erl | 2 +- apps/ff_server/src/ff_codec.erl | 5 ++- apps/ff_transfer/src/ff_adjustment.erl | 2 +- apps/ff_transfer/src/ff_deposit.erl | 21 +++------- apps/ff_transfer/src/ff_destination.erl | 13 ++---- apps/ff_transfer/src/ff_machine_codec.erl | 10 +---- apps/ff_transfer/src/ff_source.erl | 13 ++---- apps/ff_transfer/src/ff_withdrawal.erl | 17 +++----- .../ff_transfer/src/ff_withdrawal_session.erl | 15 +++---- apps/fistful/src/ff_repair.erl | 26 ++++++++++-- apps/hellgate/src/hg_invoice.erl | 42 ++++++++++--------- apps/hellgate/src/hg_invoice_handler.erl | 15 ++++--- apps/hellgate/src/hg_invoice_template.erl | 12 +++--- apps/hellgate/src/hg_machine_tag.erl | 7 ++-- apps/prg_machine/src/prg_machine.erl | 5 ++- 15 files changed, 99 insertions(+), 106 deletions(-) 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_server/src/ff_codec.erl b/apps/ff_server/src/ff_codec.erl index 6efbb94d..465ba10b 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 @@ -586,7 +589,7 @@ maybe_marshal(_Type, undefined) -> maybe_marshal(Type, Value) -> marshal(Type, Value). --spec parse_timestamp(binary()) -> prg_machine:timestamp(). +-spec parse_timestamp(binary()) -> timestamp(). parse_timestamp(Bin) -> try MicroSeconds = genlib_rfc3339:parse(Bin, microsecond), diff --git a/apps/ff_transfer/src/ff_adjustment.erl b/apps/ff_transfer/src/ff_adjustment.erl index d82b38b2..d02d3701 100644 --- a/apps/ff_transfer/src/ff_adjustment.erl +++ b/apps/ff_transfer/src/ff_adjustment.erl @@ -91,7 +91,7 @@ -type target_status() :: term(). -type final_cash_flow() :: ff_cash_flow:final_cash_flow(). -type p_transfer() :: ff_postings_transfer:transfer(). --type action() :: progressor_action:t() | undefined. +-type action() :: continue | undefined. -type process_result() :: {action(), [event()]}. -type legacy_event() :: any(). -type external_id() :: id(). diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index bdd6f468..82d99077 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -151,7 +151,7 @@ -type is_negative() :: boolean(). -type cash() :: ff_cash:cash(). -type cash_range() :: ff_range:range(cash()). --type action() :: progressor_action:t() | undefined. +-type action() :: continue | undefined. -type ctx() :: ff_entity_context:context(). -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). @@ -686,15 +686,9 @@ from_repair_result(#{events := Events} = Result, Machine) -> map_action(undefined) -> undefined; map_action(continue) -> - progressor_action:instant(); -map_action(sleep) -> - progressor_action:instant(); -map_action({setup_timer, Timer}) -> - progressor_action:set_timer(Timer). + progressor_action:instant(). -spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(undefined) -> - []; repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. @@ -704,14 +698,11 @@ event_body_from_timestamped({ev, _Timestamp, Change}) -> event_body_from_timestamped(Change) -> Change. --type repair_machine() :: #{ - history := [{pos_integer(), {ev, non_neg_integer(), event()}}], - aux_state := term() -}. - --spec to_repair_machine(machine()) -> repair_machine(). -to_repair_machine(#{history := History, aux_state := AuxState}) -> +-spec to_repair_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 }. diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index b92891ce..ae12441e 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -312,8 +312,6 @@ from_repair_result(#{events := Events} = Result, Machine) -> }. -spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(undefined) -> - []; repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. @@ -323,14 +321,11 @@ event_body_from_timestamped({ev, _Timestamp, Change}) -> event_body_from_timestamped(Change) -> Change. --type repair_machine() :: #{ - history := [{pos_integer(), {ev, non_neg_integer(), event()}}], - aux_state := term() -}. - --spec to_repair_machine(machine()) -> repair_machine(). -to_repair_machine(#{history := History, aux_state := AuxState}) -> +-spec to_repair_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 }. diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl index 8430e354..fe15ab83 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -48,17 +48,11 @@ unmarshal_event(Domain, Format, _Payload) -> -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - {Encoded, _} = machinery_mg_schema_generic:marshal({aux_state, undefined}, AuxSt, #{}), - payload_to_binary(Encoded). + payload_to_binary(machinery_mg_schema_generic:marshal(AuxSt)). -spec unmarshal_aux_state(binary()) -> term(). unmarshal_aux_state(Payload) when is_binary(Payload) -> - {AuxSt, _} = machinery_mg_schema_generic:unmarshal( - {aux_state, undefined}, - {bin, Payload}, - #{} - ), - AuxSt. + machinery_mg_schema_generic:unmarshal({bin, Payload}). -spec payload_to_binary(machinery_msgpack:t()) -> binary(). payload_to_binary({bin, Bin}) when is_binary(Bin) -> diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index fc838580..58a34a75 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -288,8 +288,6 @@ from_repair_result(#{events := Events} = Result, Machine) -> }. -spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(undefined) -> - []; repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. @@ -299,14 +297,11 @@ event_body_from_timestamped({ev, _Timestamp, Change}) -> event_body_from_timestamped(Change) -> Change. --type repair_machine() :: #{ - history := [{pos_integer(), {ev, non_neg_integer(), event()}}], - aux_state := term() -}. - --spec to_repair_machine(machine()) -> repair_machine(). -to_repair_machine(#{history := History, aux_state := AuxState}) -> +-spec to_repair_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 }. diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index bfd96498..431a8527 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -1020,9 +1020,7 @@ handle_child_result({undefined, Events} = Result, Withdrawal) -> {ok, Wallet} = fetch_wallet(wallet_id(Withdrawal), party_id(Withdrawal), DomainRevision), ok = ff_party:wallet_log_balance(wallet_id(Withdrawal), Wallet), Result - end; -handle_child_result({_OtherAction, _Events} = Result, _Withdrawal) -> - Result. + end. -spec is_childs_active(withdrawal_state()) -> boolean(). is_childs_active(Withdrawal) -> @@ -1942,8 +1940,6 @@ map_action({setup_timer, Timer}) -> progressor_action:set_timer(Timer). -spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(undefined) -> - []; repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. @@ -1953,14 +1949,11 @@ event_body_from_timestamped({ev, _Timestamp, Change}) -> event_body_from_timestamped(Change) -> Change. --type repair_machine() :: #{ - history := [{pos_integer(), {ev, non_neg_integer(), event()}}], - aux_state := term() -}. - --spec to_repair_machine(machine()) -> repair_machine(). -to_repair_machine(#{history := History, aux_state := AuxState}) -> +-spec to_repair_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 }. diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index 5ae61ac3..c79f0f97 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -428,7 +428,7 @@ process_call(CallArgs, _Machine) -> process_repair(Scenario, Machine) -> ScenarioProcessors = #{ set_session_result => fun(Args, RMachine) -> - Session = prg_machine:collapse(?MODULE, RMachine), + Session = prg_machine:collapse(?MODULE, ff_repair:to_prg_machine(RMachine)), {Action, Events} = set_session_result(Args, Session), {ok, {ok, #{action => Action, events => Events}}} end @@ -505,8 +505,6 @@ map_action(retry, _Session) -> progressor_action:instant(). -spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(undefined) -> - []; repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. @@ -516,14 +514,11 @@ event_body_from_timestamped({ev, _Timestamp, Change}) -> event_body_from_timestamped(Change) -> Change. --type repair_machine() :: #{ - history := [{pos_integer(), {ev, non_neg_integer(), event()}}], - aux_state := term() -}. - --spec to_repair_machine(machine()) -> repair_machine(). -to_repair_machine(#{history := History, aux_state := AuxState}) -> +-spec to_repair_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 }. diff --git a/apps/fistful/src/ff_repair.erl b/apps/fistful/src/ff_repair.erl index 80641b77..33ac8842 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 @@ -59,6 +60,7 @@ -export_type([repair_error/0]). -export_type([repair_response/0]). -export_type([invalid_result_error/0]). +-export_type([machine/0]). %% Internal types @@ -66,6 +68,8 @@ -type model_aux_state() :: any(). -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() }. @@ -124,18 +128,32 @@ apply_processor(Processor, Args, Machine) -> {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 := RepairHistory, aux_state := AuxSt}, #{events := 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), PrgNewHistory = [ - {ID, Ts, Body} - || {ID, {ev, Ts, Body}} <- lists:zip(IDs, NewEvents) + {EventID, Ts, Body} + || {EventID, {ev, Ts, Body}} <- lists:zip(IDs, NewEvents) ], + Machine = (to_prg_machine(RepairMachine))#{ + history => PrgHistory0 ++ PrgNewHistory, + aux_state => AuxSt + }, try - _ = prg_machine:collapse(Mod, #{history => PrgHistory0 ++ PrgNewHistory, aux_state => AuxSt}), + _ = prg_machine:collapse(Mod, Machine), {ok, valid} catch error:Error:Stack -> diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 5ba11583..fef7166b 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -239,9 +239,13 @@ get_payment_state(PaymentSession) -> process_callback(Tag, Callback) -> process_with_tag(Tag, fun(MachineID) -> case prg_machine:call(?NS, MachineID, {callback, Tag, Callback}) of - {ok, _} = Ok -> + {ok, {ok, _} = Ok} -> Ok; - {exception, invalid_callback} -> + {ok, ok} -> + ok; + {ok, {exception, invalid_callback}} -> + {error, invalid_callback}; + {ok, {error, invalid_callback}} -> {error, invalid_callback}; {error, _} = Error -> Error @@ -253,10 +257,14 @@ process_callback(Tag, Callback) -> process_session_change_by_tag(Tag, SessionChange) -> process_with_tag(Tag, fun(MachineID) -> case prg_machine:call(?NS, MachineID, {session_change, Tag, SessionChange}) of - ok -> + {ok, ok} -> + ok; + {ok, {ok, _}} -> ok; - {exception, invalid_callback} -> + {ok, {exception, invalid_callback}} -> {error, notfound}; + {ok, {error, _}} -> + {error, failed}; {error, _} = Error -> Error end @@ -1010,16 +1018,10 @@ event_timestamp_to_binary(Bin) when is_binary(Bin) -> event_timestamp_to_binary(Dt) -> hg_datetime:format_dt(Dt). --spec apply_event_changes([invoice_change()], st() | undefined, event_timestamp() | binary() | undefined) -> st(). +-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, - Opts = - case Dt of - undefined -> #{}; - Bin when is_binary(Bin) -> #{timestamp => Bin}; - CalDt -> #{timestamp => hg_datetime:format_dt(CalDt)} - end, - collapse_changes(Changes, St, Opts). + collapse_changes(Changes, St, #{timestamp => Dt}). -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Changes) when is_list(Changes) -> @@ -1069,10 +1071,10 @@ try_unmarshal_msgpack_payload(Payload) -> 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(Data) -> - unmarshal_event_payload(#{format_version => ?EVENT_FORMAT_VERSION, data => Data}). - --type event_timestamp() :: calendar:datetime(). +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. -spec action_to_prg(progressor_action:t() | undefined) -> action(). action_to_prg(#mg_stateproc_ComplexAction{timer = Timer, remove = Remove}) -> @@ -1110,16 +1112,16 @@ marshal_invoice(Invoice) -> }. -spec unmarshal_history([prg_machine:machine_event()]) -> - [{prg_machine:event_id(), event_timestamp(), [invoice_change()]}]. + [{prg_machine:event_id(), hg_datetime:timestamp(), [invoice_change()]}]. unmarshal_history(Events) -> [unmarshal_event(Event) || Event <- Events]. -spec unmarshal_event(prg_machine:machine_event()) -> - {prg_machine:event_id(), event_timestamp(), [invoice_change()]}. + {prg_machine:event_id(), hg_datetime:timestamp(), [invoice_change()]}. unmarshal_event({ID, Dt, Payload}) when is_list(Payload) -> - {ID, Dt, 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(legacy_event_payload()) -> [invoice_change()]. unmarshal_event_payload(#{format_version := 1, data := {bin, Changes}}) -> diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index fd0e7e2d..d3ea0a8b 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -150,8 +150,7 @@ ensure_started(ID, TemplateID, Params, Allocation, Mutations, DomainRevision) -> Invoice = hg_invoice:create(ID, TemplateID, Params, Allocation, Mutations, DomainRevision), case prg_machine:start(hg_invoice:namespace(), ID, hg_invoice:marshal_invoice(Invoice)) of {ok, _} -> ok; - {error, exists} -> ok; - {error, Reason} -> erlang:error(Reason) + {error, exists} -> ok end. call(ID, Function, Args) -> @@ -230,7 +229,13 @@ get_state(ID) -> get_state(ID, AfterID, Limit) -> History = get_history(ID, AfterID, Limit), - prg_machine:collapse(hg_invoice, #{history => History, aux_state => #{}}). + Machine = #{ + namespace => hg_invoice:namespace(), + id => ID, + history => History, + aux_state => #{} + }, + prg_machine:collapse(hg_invoice, Machine). get_history(ID, AfterID, Limit) -> History = prg_machine:get_history(hg_invoice:namespace(), ID, AfterID, Limit), @@ -247,10 +252,8 @@ publish_invoice_event(InvoiceID, {ID, Dt, Event}) -> payload = ?invoice_ev(Event) }. -format_event_timestamp(Dt) when is_binary(Dt) -> - Dt; format_event_timestamp(Dt) -> - hg_datetime:format_dt(Dt). + Dt. map_history_error({ok, Result}) -> Result; diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index 3383241c..7fc11e35 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -55,9 +55,7 @@ get_invoice_template(ID) -> _ = assert_invoice_template_not_deleted(lists:last(History)), prg_machine:collapse(?MODULE, Machine); {error, notfound} -> - throw(#payproc_InvoiceTemplateNotFound{}); - {error, Reason} -> - error(Reason) + throw(#payproc_InvoiceTemplateNotFound{}) end. %% Woody handler @@ -287,7 +285,7 @@ handle_call({{'InvoiceTemplating', 'Delete'}, {_TplID}}, _Tpl) -> -spec apply_event( prg_machine:event_id(), prg_machine:timestamp(), - invoice_template_change(), + [invoice_template_change()], tpl() | undefined ) -> tpl(). apply_event(_EventID, _Ts, Changes, Tpl) -> @@ -392,8 +390,10 @@ try_unmarshal_msgpack_payload(Payload) -> 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(Data) -> - unmarshal_event_payload(#{format_version => ?EVENT_FORMAT_VERSION, data => Data}). +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'}}, diff --git a/apps/hellgate/src/hg_machine_tag.erl b/apps/hellgate/src/hg_machine_tag.erl index 6d85acb0..289cef93 100644 --- a/apps/hellgate/src/hg_machine_tag.erl +++ b/apps/hellgate/src/hg_machine_tag.erl @@ -7,9 +7,9 @@ -export([create_binding/4]). -type tag() :: dmsl_base_thrift:'Tag'(). --type ns() :: hg_stateproc_types:ns(). +-type ns() :: prg_machine:namespace(). -type entity_id() :: dmsl_base_thrift:'ID'(). --type machine_id() :: hg_stateproc_types:id(). +-type machine_id() :: prg_machine:id(). -spec get_binding(ns(), tag()) -> {ok, entity_id(), machine_id()} | {error, notfound}. get_binding(NS, Tag) -> @@ -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/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 197cf38e..670b5090 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -13,7 +13,7 @@ -type namespace() :: namespace_id(). -type args() :: term(). -type call() :: term(). --type response() :: ok | {ok, term()} | {exception, term()}. +-type response() :: ok | {ok, term()} | {error, term()} | {exception, term()}. -type timestamp() :: calendar:datetime(). -type event_body() :: term(). @@ -55,6 +55,8 @@ args/0, call/0, response/0, + timestamp/0, + event_body/0, machine_event/0, history/0, machine/0, @@ -565,5 +567,6 @@ range_from_process(#{range := Range = #{}}) -> range_from_process(_) -> #{direction => forward}. +-spec raise_exception({exception, atom(), term()}) -> no_return(). raise_exception({exception, Class, Reason}) -> erlang:raise(Class, Reason, []). From 9cf670b2177eca2e0cf0fedd29fa75ec22db0587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 21:19:07 +0300 Subject: [PATCH 08/62] Remove progressor subproject reference from the repository. --- _checkouts/progressor | 1 - 1 file changed, 1 deletion(-) delete mode 160000 _checkouts/progressor diff --git a/_checkouts/progressor b/_checkouts/progressor deleted file mode 160000 index 90f46570..00000000 --- a/_checkouts/progressor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 90f46570007a1cbf7cac960059fe6c8018702489 From a417e2f9029c2ef3ae7ba9988e9066b68638c491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 21:19:11 +0300 Subject: [PATCH 09/62] Update .gitignore to exclude _checkouts directory and remove obsolete generic-worker-composer documentation. Refactor rebar.config to include local overrides for progressor. Clean up unused action type definitions in ff_deposit_machine. Enhance hg_invoicing_machine_client with improved response normalization and update test suite with new payment success trace functionality. Remove outdated operation_context tests and prg_machine_env_tests. --- .cursor/agents/generic-worker-composer.md | 15 -- .gitignore | 1 + apps/ff_transfer/src/ff_deposit_machine.erl | 19 --- apps/fistful/src/fistful.app.src | 2 +- apps/hellgate/src/hellgate.app.src | 2 +- .../src/hg_invoicing_machine_client.erl | 47 ++---- .../test/hg_invoice_lite_tests_SUITE.erl | 18 +++ .../src/operation_context.erl | 52 +++++++ apps/prg_machine/src/prg_machine.erl | 121 ++++++++++++++-- .../test/operation_context_tests.erl | 73 ---------- .../test/prg_machine_env_tests.erl | 107 -------------- docs/prg-machine-review-plan.md | 135 ++++++++++++++++++ rebar.config | 1 + 13 files changed, 333 insertions(+), 260 deletions(-) delete mode 100644 .cursor/agents/generic-worker-composer.md delete mode 100644 apps/prg_machine/test/operation_context_tests.erl delete mode 100644 apps/prg_machine/test/prg_machine_env_tests.erl create mode 100644 docs/prg-machine-review-plan.md diff --git a/.cursor/agents/generic-worker-composer.md b/.cursor/agents/generic-worker-composer.md deleted file mode 100644 index f9f632b3..00000000 --- a/.cursor/agents/generic-worker-composer.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: generic-worker-composer -model: composer-2.5[fast=false] -description: Универсальный исполнитель (Composer 2.5). Выполняет любое задание от оркестратора без привязки к жёсткому сценарию. ---- - -Ты — универсальный исполнитель. Оркестратор передаёт тебе задание. - -## Правила -- Выполни задание полностью. -- Не добавляй лишних рассуждений — только ответ на задание. -- Если задание требует структурированного формата — следуй ему. -- В конце ответа добавь блок: `---МОДЕЛЬ: generic-worker-composer` - -Твоё поведение определяется **только** заданием оркестратора. Никаких дополнительных ограничений или предустановок. diff --git a/.gitignore b/.gitignore index 03af9e8d..7b464896 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # general log /_build/ +/_checkouts/ *~ erl_crash.dump rebar3.crashdump diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index 846935d6..3d81676b 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -31,12 +31,6 @@ -type unknown_deposit_error() :: {unknown_deposit, id()}. --type action() :: - continue - | sleep - | {setup_timer, progressor_action:timer()} - | undefined. - -export_type([id/0]). -export_type([st/0]). -export_type([change/0]). @@ -60,7 +54,6 @@ -export([deposit/1]). -export([ctx/1]). --export([map_action/1]). %% Pipeline @@ -140,8 +133,6 @@ ctx(#{ctx := Ctx}) -> %% Internals --compile({nowarn_unused_function, [map_action/1]}). - -spec machine_to_st(prg_machine:machine()) -> st(). machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> Model = prg_machine:collapse(ff_deposit, Machine), @@ -171,16 +162,6 @@ history_times(History) -> History ). --spec map_action(action()) -> progressor_action:t() | undefined. -map_action(undefined) -> - undefined; -map_action(continue) -> - progressor_action:instant(); -map_action(sleep) -> - progressor_action:instant(); -map_action({setup_timer, Timer}) -> - progressor_action:set_timer(Timer). - codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> {DateTime, USec} = Timestamp; codec_timestamp(DateTime) -> diff --git a/apps/fistful/src/fistful.app.src b/apps/fistful/src/fistful.app.src index 7e64521c..a9e3a6a5 100644 --- a/apps/fistful/src/fistful.app.src +++ b/apps/fistful/src/fistful.app.src @@ -11,6 +11,7 @@ ff_core, snowflake, progressor, + operation_context, prg_machine, machinery, machinery_extra, @@ -19,7 +20,6 @@ damsel, dmt_client, party_client, - operation_context, binbase_proto, bender_client, opentelemetry_api, diff --git a/apps/hellgate/src/hellgate.app.src b/apps/hellgate/src/hellgate.app.src index df93d919..cee3bbc3 100644 --- a/apps/hellgate/src/hellgate.app.src +++ b/apps/hellgate/src/hellgate.app.src @@ -10,6 +10,7 @@ fault_detector_proto, herd, progressor, + operation_context, prg_machine, hg_proto, routing, @@ -23,7 +24,6 @@ dmt_client, party_client, bender_client, - operation_context, payproc_errors, erl_health, limiter_proto, diff --git a/apps/hellgate/src/hg_invoicing_machine_client.erl b/apps/hellgate/src/hg_invoicing_machine_client.erl index 9d20d976..2b0a1154 100644 --- a/apps/hellgate/src/hg_invoicing_machine_client.erl +++ b/apps/hellgate/src/hg_invoicing_machine_client.erl @@ -1,7 +1,7 @@ -module(hg_invoicing_machine_client). %%% Thrift RPC to invoicing machines via progressor. -%%% Encode/decode with hg_proto_utils; transport via prg_machine:call/6. +%%% 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]). @@ -30,12 +30,10 @@ thrift_call(NS, ID, Service, FunRef, Args) -> non_neg_integer() | undefined, forward | backward ) -> response() | {error, notfound | failed}. -thrift_call(NS, ID, ServiceName, FunRef, Args, After, Limit, Direction) -> - EncodedArgs = marshal_thrift_args(ServiceName, FunRef, Args), - MachineCall = {FunRef, unmarshal_thrift_args(ServiceName, FunRef, EncodedArgs)}, - case prg_machine:call(NS, ID, MachineCall, After, Limit, Direction) of +thrift_call(NS, ID, _ServiceName, FunRef, Args, After, Limit, Direction) -> + case prg_machine:call(NS, ID, {FunRef, Args}, After, Limit, Direction) of {ok, Response} -> - unmarshal_thrift_response(ServiceName, FunRef, Response); + normalize_response(Response); {error, notfound} -> {error, notfound}; {error, failed} -> @@ -44,33 +42,10 @@ thrift_call(NS, ID, ServiceName, FunRef, Args, After, Limit, Direction) -> Error end. -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). - -unmarshal_thrift_args(ServiceName, FunctionRef, EncodedArgs) -> - {Service, _Function} = FunctionRef, - {Module, Service} = hg_proto:get_service(ServiceName), - FullFunctionRef = {Module, FunctionRef}, - hg_proto_utils:deserialize_function_args(FullFunctionRef, EncodedArgs). - -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} when is_binary(EncodedReply) -> - Reply = hg_proto_utils:deserialize_function_reply(FullFunctionRef, EncodedReply), - {ok, Reply}; - {ok, Reply} -> - {ok, Reply}; - {exception, EncodedException} when is_binary(EncodedException) -> - Exception = hg_proto_utils:deserialize_function_exception(FullFunctionRef, EncodedException), - {exception, Exception}; - {exception, Exception} -> - {exception, Exception} - 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/test/hg_invoice_lite_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl index c1e129cb..dda26217 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]). @@ -63,6 +64,7 @@ groups() -> {payments, [parallel], [ payment_start_idempotency, payment_success, + payment_success_trace, payment_w_first_blacklisted_success, payment_w_all_blacklisted, register_payment_success, @@ -251,6 +253,22 @@ payment_success(C) -> Trx ). +-spec payment_success_trace(config()) -> test_return(). +payment_success_trace(C) -> + Client = cfg(client, C), + InvoiceID = start_invoice(<<"rubberduck">>, make_due_date(10), 42000, C), + PaymentParams = make_payment_params(?pmt_sys(<<"visa-ref">>)), + PaymentID = process_payment(InvoiceID, PaymentParams, Client), + PaymentID = await_payment_capture(InvoiceID, PaymentID, Client), + {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(). payment_w_first_blacklisted_success(C) -> Client = cfg(client, C), diff --git a/apps/operation_context/src/operation_context.erl b/apps/operation_context/src/operation_context.erl index 33ff2664..8445d2fc 100644 --- a/apps/operation_context/src/operation_context.erl +++ b/apps/operation_context/src/operation_context.erl @@ -185,3 +185,55 @@ 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"). + +-define(HG_TEST_KEY, {p, l, stored_hg_context}). +-define(FF_TEST_KEY, {p, l, {ff_context, stored_context}}). + +-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(?HG_TEST_KEY, CtxHg), + ok = save(?FF_TEST_KEY, CtxFf), + CtxHgLoaded = load(?HG_TEST_KEY), + CtxFfLoaded = load(?FF_TEST_KEY), + ?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(?HG_TEST_KEY, strict), + CtxFfAfterHgCleanup = load(?FF_TEST_KEY), + ?assertEqual(WoodyFf, get_woody_context(CtxFfAfterHgCleanup)), + ok = cleanup(?FF_TEST_KEY, lenient) + after + _ = catch cleanup(?HG_TEST_KEY, lenient), + _ = catch cleanup(?FF_TEST_KEY, lenient) + 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_fistful(create(#{woody_context => WoodyCtx})), + ?assertEqual(WoodyCtx, get_woody_context(load_fistful())), + ok = cleanup_fistful(), + ok = cleanup_fistful() + after + _ = catch cleanup_fistful() + end. + +-endif. diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 670b5090..79abb2c5 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -274,8 +274,9 @@ process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> Result = dispatch(Handler, CallType, BinArgs, Machine), marshal_process_result(Handler, LastEventID, Result) catch - Class:Reason:_Stacktrace -> - {error, ?PROCESSOR_EXCEPTION(Class, Reason, _Stacktrace)} + Class:Reason:Stacktrace -> + logger:error("prg_machine process failed: ~p:~p~n~p", [Class, Reason, Stacktrace]), + {error, ?PROCESSOR_EXCEPTION(Class, Reason, Stacktrace)} after Leave() end. @@ -401,9 +402,8 @@ unmarshal_event(Handler, #{ Format = maps:get(<<"format">>, Meta, maps:get(format, Meta, undefined)), Body = unmarshal_event_body(Handler, Format, Payload), {EventID, event_timestamp_to_datetime(TsSec), Body}; -unmarshal_event(Handler, #{event_id := _EventID} = Ev) -> - Meta = maps:get(metadata, Ev, #{}), - unmarshal_event(Handler, Ev#{metadata => Meta, timestamp => maps:get(timestamp, Ev, 0)}). +unmarshal_event(_Handler, #{event_id := EventID} = Ev) -> + erlang:error({missing_event_payload, EventID, maps:keys(Ev)}). marshal_new_events(Handler, LastEventID, Bodies) -> Ts = erlang:system_time(microsecond), @@ -434,7 +434,7 @@ unmarshal_event_body(Handler, Format, Payload) -> true -> Handler:unmarshal_event_body(Format, Payload); false -> - binary_to_term(Payload) + binary_to_term(Payload, [safe]) end. marshal_aux_state(Handler, AuxSt) -> @@ -452,7 +452,7 @@ unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> true -> Handler:unmarshal_aux_state(Bin); false -> - binary_to_term(Bin) + binary_to_term(Bin, [safe]) end. event_metadata(undefined) -> @@ -551,7 +551,7 @@ encode_term(Term) -> term_to_binary(Term). decode_term(Term) when is_binary(Term) -> - binary_to_term(Term); + binary_to_term(Term, [safe]); decode_term(Term) -> Term. @@ -570,3 +570,108 @@ range_from_process(_) -> -spec raise_exception({exception, atom(), term()}) -> no_return(). raise_exception({exception, Class, Reason}) -> erlang:raise(Class, Reason, []). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-define(TEST_NS, env_test_ns). +-define(TEST_REGISTRY_KEY, {p, l, prg_machine_env_test_context}). +-define(TEST_BINDING, #{ + registry_key => ?TEST_REGISTRY_KEY, + cleanup_mode => lenient +}). + +-spec test() -> _. + +-spec noop_when_hooks_absent_test_() -> _. +noop_when_hooks_absent_test_() -> + {setup, fun setup_env_hook_test/0, fun cleanup_env_hook_test/1, [ + ?_test(noop_when_hooks_absent()) + ]}. + +-spec explicit_fun_overrides_context_binding_test_() -> _. +explicit_fun_overrides_context_binding_test_() -> + {setup, fun setup_env_hook_test/0, fun cleanup_env_hook_test/1, [ + ?_test(explicit_fun_overrides_context_binding()) + ]}. + +-spec noop_when_hooks_absent() -> _. +noop_when_hooks_absent() -> + ok = ensure_woody_available(), + ok = prg_machine_env_mock_context:reset(), + _ = run_env_hook_process(#{ns => ?TEST_NS}), + ?assertEqual([], prg_machine_env_mock_context:events()). + +-spec explicit_fun_overrides_context_binding() -> _. +explicit_fun_overrides_context_binding() -> + ok = ensure_woody_available(), + ok = prg_machine_env_mock_context:reset(), + Enter = fun(_) -> + prg_machine_env_mock_context:record(explicit_enter), + ok + end, + Leave = fun() -> + prg_machine_env_mock_context:record(explicit_leave), + ok + end, + Opts = #{ + ns => ?TEST_NS, + env_enter => Enter, + env_leave => Leave, + context_binding => ?TEST_BINDING + }, + _ = run_env_hook_process(Opts), + ?assertEqual([explicit_enter, explicit_leave], 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(operation_context), + _ = ensure_env_hook_dispatch_table(), + true = ets:insert(?TABLE, {?TEST_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), + _ = catch operation_context:cleanup(?TEST_REGISTRY_KEY, lenient), + 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() -> atom(). +ensure_env_hook_dispatch_table() -> + case ets:info(?TABLE) of + undefined -> + ets:new(?TABLE, [named_table, {read_concurrency, true}]); + _ -> + ?TABLE + end. + +-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). + +-endif. diff --git a/apps/prg_machine/test/operation_context_tests.erl b/apps/prg_machine/test/operation_context_tests.erl deleted file mode 100644 index f12e6e6f..00000000 --- a/apps/prg_machine/test/operation_context_tests.erl +++ /dev/null @@ -1,73 +0,0 @@ --module(operation_context_tests). - --compile(nowarn_unused_function). - --include_lib("eunit/include/eunit.hrl"). - --define(HG_KEY, {p, l, stored_hg_context}). --define(FF_KEY, {p, l, {ff_context, stored_context}}). - --spec test() -> _. - -test() -> - operation_context_test_(). - --spec operation_context_test_() -> _. - -operation_context_test_() -> - {setup, fun setup/0, fun cleanup/1, [ - fun colocated_keys_isolated/0, - fun scoped_helpers/0 - ]}. - --spec setup() -> ok. - -setup() -> - {ok, _} = application:ensure_all_started(gproc), - {ok, _} = application:ensure_all_started(woody), - ok. - --spec cleanup(_) -> ok. - -cleanup(_) -> - ok. - --spec colocated_keys_isolated() -> _. - -colocated_keys_isolated() -> - WoodyHg = woody_context:add_meta(woody_context:new(), #{<<"app">> => <<"hg">>}), - WoodyFf = woody_context:add_meta(woody_context:new(), #{<<"app">> => <<"ff">>}), - try - CtxHg = operation_context:create(#{woody_context => WoodyHg}), - CtxFf = operation_context:create(#{woody_context => WoodyFf}), - ok = operation_context:save(?HG_KEY, CtxHg), - ok = operation_context:save(?FF_KEY, CtxFf), - CtxHgLoaded = operation_context:load(?HG_KEY), - CtxFfLoaded = operation_context:load(?FF_KEY), - ?assertEqual(WoodyHg, operation_context:get_woody_context(CtxHgLoaded)), - ?assertEqual(WoodyFf, operation_context:get_woody_context(CtxFfLoaded)), - ?assertNotEqual( - operation_context:get_party_client_context(CtxHgLoaded), - operation_context:get_party_client_context(CtxFfLoaded) - ), - ok = operation_context:cleanup(?HG_KEY, strict), - CtxFfAfterHgCleanup = operation_context:load(?FF_KEY), - ?assertEqual(WoodyFf, operation_context:get_woody_context(CtxFfAfterHgCleanup)), - ok = operation_context:cleanup(?FF_KEY, lenient) - after - _ = catch operation_context:cleanup(?HG_KEY, lenient), - _ = catch operation_context:cleanup(?FF_KEY, lenient) - end. - --spec scoped_helpers() -> _. - -scoped_helpers() -> - WoodyCtx = woody_context:new(), - try - ok = operation_context:save_fistful(operation_context:create(#{woody_context => WoodyCtx})), - ?assertEqual(WoodyCtx, operation_context:get_woody_context(operation_context:load_fistful())), - ok = operation_context:cleanup_fistful(), - ok = operation_context:cleanup_fistful() - after - _ = catch operation_context:cleanup_fistful() - end. diff --git a/apps/prg_machine/test/prg_machine_env_tests.erl b/apps/prg_machine/test/prg_machine_env_tests.erl deleted file mode 100644 index 5465a586..00000000 --- a/apps/prg_machine/test/prg_machine_env_tests.erl +++ /dev/null @@ -1,107 +0,0 @@ --module(prg_machine_env_tests). - --compile(nowarn_unused_function). - --include_lib("eunit/include/eunit.hrl"). - --define(TABLE, prg_machine_dispatch). --define(NS, env_test_ns). --define(TEST_REGISTRY_KEY, {p, l, prg_machine_env_test_context}). --define(TEST_BINDING, #{ - registry_key => ?TEST_REGISTRY_KEY, - cleanup_mode => lenient -}). - --spec test() -> _. - -test() -> - {setup, fun setup/0, fun cleanup/1, [ - ?_test(noop_when_hooks_absent_test()), - ?_test(explicit_fun_overrides_context_binding_test()) - ]}. - --spec noop_when_hooks_absent_test() -> _. - -noop_when_hooks_absent_test() -> - ok = ensure_woody_available(), - ok = prg_machine_env_mock_context:reset(), - _ = run_process(#{ns => ?NS}), - ?assertEqual([], prg_machine_env_mock_context:events()). - --spec explicit_fun_overrides_context_binding_test() -> _. - -explicit_fun_overrides_context_binding_test() -> - ok = ensure_woody_available(), - ok = prg_machine_env_mock_context:reset(), - Enter = fun(_) -> - prg_machine_env_mock_context:record(explicit_enter), - ok - end, - Leave = fun() -> - prg_machine_env_mock_context:record(explicit_leave), - ok - end, - Opts = #{ - ns => ?NS, - env_enter => Enter, - env_leave => Leave, - context_binding => ?TEST_BINDING - }, - _ = run_process(Opts), - ?assertEqual([explicit_enter, explicit_leave], prg_machine_env_mock_context:events()). - --spec setup() -> ok. - -setup() -> - _ = 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(operation_context), - _ = ensure_dispatch_table(), - true = ets:insert(?TABLE, {?NS, prg_machine_env_mock_handler}), - ok = prg_machine_env_mock_context:reset(), - ok. - --spec cleanup(_) -> ok. - -cleanup(_) -> - _ = ets:delete(?TABLE, ?NS), - _ = catch operation_context:cleanup(?TEST_REGISTRY_KEY, lenient), - ok. - --spec ensure_woody_available() -> ok. - -ensure_woody_available() -> - {ok, _} = application:ensure_all_started(snowflake), - _ = woody_context:new(), - ok. - --spec ensure_dispatch_table() -> atom(). - -ensure_dispatch_table() -> - case ets:info(?TABLE) of - undefined -> - ets:new(?TABLE, [named_table, {read_concurrency, true}]); - _ -> - ?TABLE - end. - --spec run_process(prg_machine:processor_opts()) -> _. -run_process(Opts) -> - run_process(Opts, <<>>). - --spec run_process(prg_machine:processor_opts(), binary()) -> _. - -run_process(Opts, BinCtx) -> - Process = #{ - process_id => <<"env-hook-test">>, - last_event_id => 0, - history => [], - aux_state => undefined - }, - prg_machine:process({init, term_to_binary(#{}), Process}, Opts, BinCtx). diff --git a/docs/prg-machine-review-plan.md b/docs/prg-machine-review-plan.md new file mode 100644 index 00000000..239101d7 --- /dev/null +++ b/docs/prg-machine-review-plan.md @@ -0,0 +1,135 @@ +# Строгое ревью ветки `add_prg_layer` vs `epic/monorepo` — план доработки + +Ревью кода (независимое, без CT-прогона — только `rebar3 compile`). Сборка проходит. +Diff: 95 файлов, +3604 / −3987. Суть ветки — перевод HG/FF машин с `machinery`/`hg_machine`/`ff_machine` +на единый слой `prg_machine` (progressor): 7 prod namespace, новые app'ы `prg_machine` и `operation_context`, +удаление старого machinery/hg_progressor glue. + +> **Контекст подхода (уточнено автором).** Ветка — про *смену способа интеграции* с progressor, поэтому +> изменения **намеренно** заходят и в сам progressor (модуль `progressor_action`, правки `progressor.hrl`), +> а не размазываются обёртками по прикладному коду. Это осознанное правило, а не «срез угла». Единственное +> требование к такому подходу — **воспроизводимость сборки** (см. P0-1). + +Легенда приоритетов: **P0** — блокер merge, **P1** — корректность/риск регрессии, **P2** — качество/техдолг. + +--- + +## Итог по «срезанным углам» + +- **elvis** — НЕ ослаблен: из `elvis.config` только удалены исключения для удалённых `*_machinery_schema`, новых нет. +- **dialyzer (`rebar.config`)** — НЕ ослаблен: `warnings` (`unmatched_returns`, `error_handling`, `unknown`) + `plt_apps => all_deps` без изменений. +- **`erl_opts`** (`warnings_as_errors`, `warn_missing_spec`, …) — без изменений, сборка чистая. +- Подавления, добавленные веткой, — 3: `nowarn` на мёртвую `map_action/1` (P2-1) и `nowarn_unused_function` на два тест-модуля (P2-6). `-dialyzer(nowarn_function,…)` в `ff_ct_*`/`hg_invoice_tests_SUITE` — доветочные. + +**Вывод:** линтер/диалайзер как механизм не обойдён. Реальный риск — воспроизводимость сборки (P0-1) и точечный техдолг (P2). + +--- + +## P0 — блокеры перед merge + +### P0-1. Воспроизводимость сборки: правки `progressor` не закоммичены/не выпущены +Подход (дорабатывать сам progressor) — ок. Проблема в том, что **на текущий момент сборка собирается только на этой машине**: +- `_checkouts/progressor` — git-link `160000` на ref `90f4657`, **без записи в `.gitmodules`** → при клоне контент не подтянется. +- Рабочее дерево чекаута **грязное**: незакоммиченный `include/progressor.hrl` + **untracked `src/progressor_action.erl`**. +- Прод-код ветки (`prg_machine.erl:35,343`, `hg_invoice.erl`, `ff_*_machine.erl`) уже зависит от `progressor_action` — модуля, которого **нет ни в одном теге/коммите** upstream. +- `rebar.config` всё ещё указывает `{progressor, {git, …, {tag, "v1.0.24"}}}` (там нет `progressor_action`), а из `rebar.lock` пиннинг `progressor` удалён → источник версии противоречив. + +Шаги: +1. Закоммитить и запушить правки в `valitydev/progressor` (`progressor_action`, `progressor.hrl`), смержить, **выпустить тег**. +2. `rebar.config` → `{progressor, {git, …, {tag, ""}}}`; убрать дубль (`progressor` git + `prg_machine` path + `_checkouts`), оставить один источник истины. +3. Удалить `_checkouts/progressor` из индекса; вернуть `progressor` в `rebar.lock` (`rebar3 lock`). +4. Проверка: на чистом клоне `rm -rf _checkouts && rebar3 compile` — собирается без локального чекаута. + +### P0-2. Нет коммита/PR +- Содержательно готово, но не оформлено PR на `epic/monorepo` (история — 7 коммитов с авто-сообщениями). +- Шаги: после P0-1 закоммитить, оформить PR, прогнать CI. + +### P0-3. CT не прогонялись +- Ни один CT-suite не запускался. Для платёжного процессинга — обязательный гейт. +- Минимум suites (docker: postgres, party-management, dmt): + - `apps/ff_server/test/{ff_deposit_handler_SUITE, ff_withdrawal_handler_SUITE, ff_withdrawal_session_repair_SUITE}` + - `apps/hellgate/test/{hg_invoice_lite_tests_SUITE, hg_invoice_tests_SUITE, hg_invoice_template_tests_SUITE, hg_direct_recurrent_tests_SUITE}` +- Шаги: поднять окружение, прогнать suites, зафиксировать в PR. + +--- + +## P1 — корректность и риск регрессий + +### P1-1. `hg_invoicing_machine_client:thrift_call` — двойная сериализация + мёртвые ветки (подтверждено, фиксить) +`apps/hellgate/src/hg_invoicing_machine_client.erl:33-45`, `:59-76`. +- Args сериализуются (`marshal_thrift_args`) и тут же десериализуются (`unmarshal_thrift_args`), а в `prg_machine:call` уходит **десериализованный** терм (далее ещё раз `term_to_binary`). Сериализация — лишняя работа. +- `unmarshal_thrift_response`: ветки `is_binary(...)` недостижимы (транспорт — `term_to_binary`, не thrift-байты). +- Шаги: выбрать единый контракт транспорта call-args (скорее всего — слать готовый терм без thrift round-trip) и убрать мёртвые ветки декода. Сверить с `hg_invoice:handle_call/2` (ожидает `{FunRef, Args}`). + +### P1-2. Удалён CT-тест `payment_success_trace` без замены (подтверждено, ошибка) +`apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl` — убран `payment_success_trace/1` (проверка trace-API), trace переехал на FF internal HTTP JSON, но нового теста нет. +- Шаги: восстановить покрытие trace (тест на актуальный эндпоинт) либо завести явную задачу под отдельный goal (`docs/trace-api-thrift.md`) и сослаться на неё в PR. + +### ~~P1-x. Паритет prod по выпавшим NS~~ — снято +`customer`, `recurrent_paytools`, `ff/identity`, `ff/wallet_v2` выпали корректно (подтверждено автором): доменных модулей нет, на `epic/monorepo` в `hellgate.erl` регистрировались только `hg_invoice`/`hg_invoice_template`. **Не регрессия, действий не требуется.** + +--- + +## P1→P2 (понижено после расследования). `prg_machine:process/3` ловит все исключения + +`apps/prg_machine/src/prg_machine.erl:264-281`, `:570-572`. + +**Вывод расследования: «оно работает», это не блокер.** Цепочка проверена: +- `hg_invoice:process_call/2` (`:402-413`) сам оборачивает `handle_call` в `try ... catch throw:Exception -> {{exception, Exception}, #{}}`. То есть бизнес-`throw` превращается в **успешный** ответ-исключение (`{Response, Result}`), а не в `{error,…}`. Машина при бизнес-ошибке **не ломается**. +- В progressor (`prg_worker:do_process_task/4` → `handle_result_error/5`): `{error,…}` для `call`/`init`/`repair` → `error_and_stop` (машина в `error`), для `timeout`/`remove` → `error_and_retry`. До catch-all в `process/3` доходят только **непредвиденные** падения — а они и в старом MG ломали/фейлили машину. Поведение эквивалентно. + +**Остаточные (P2) хвосты этого места:** +1. **Теряется stacktrace.** `?PROCESSOR_EXCEPTION` кладёт только `{exception, Class, Reason}`, а `raise_exception/1` делает `erlang:raise(Class, Reason, [])`. Для диагностики непредвиденных падений стектрейс нужно логировать (хотя бы `logger:error` со `Stacktrace`). +2. **Транзиентные woody-ошибки на `init`/`call`.** Для этих task progressor делает `error_and_stop` (нет retry). Транзиентная недоступность зависимости во время `call`/`init` → машина в перманентном `error` (нужен repair). Проверить, ожидаемо ли это (в т.ч. retry-policy неймспейса), и при необходимости классифицировать `?WOODY_ERROR(resource_unavailable|result_unknown)`. +3. **`process_signal/2` без `try/catch`** в `hg_invoice` (`:347-358`) — `throw` из `handle_signal` уйдёт в catch-all и для `timeout` уйдёт в retry. Подтвердить, что это намеренно (а не «проглатывание» бизнес-ошибки). + +--- + +## P2 — техдолг (кандидаты на детальное расследование) + +### P2-1. Мёртвый код `map_action/1` в `ff_deposit_machine` +`apps/ff_transfer/src/ff_deposit_machine.erl:63 (export), :143 (nowarn), :174-182`. +- Функция не используется; есть только в `ff_deposit_machine` (в остальных `ff_*_machine` её нет — реальный маппинг идёт через `action_to_prg`/`progressor_action` в домене). +- Двойная избыточность: функция **экспортирована** (`-export([map_action/1])`) → `nowarn_unused_function` для неё бессмысленен (экспортируемые не считаются unused). +- Маппинг `sleep -> progressor_action:instant()` семантически неверен (`instant() = set_timeout(0)` — немедленный таймаут, а не «сон»). +- Шаги: удалить `map_action/1`, его `-export` и `-compile(nowarn…)`; убедиться, что во всех `ff_*_machine` action-маппинг единообразен. + +### P2-2. `binary_to_term` без `[safe]` в fallback-путях `prg_machine` +`apps/prg_machine/src/prg_machine.erl:437,455,554` (`decode_term`, fallback `unmarshal_event_body`/`unmarshal_aux_state`). +- HG-домены уже используют `binary_to_term(Payload, [safe])` (`hg_invoice.erl:1047,1050,1066`, `hg_invoice_template.erl`) — хорошо. +- Generic-fallback и `decode_term` (call-args/response/repair-args/rpc-context) — без `[safe]`. Риск низкий (данные формируем сами через `term_to_binary`), но как defense-in-depth: добавить `[safe]`, а fallback-кодеки сделать явной ошибкой вместо тихого `term_to_binary`/`binary_to_term`. + +### P2-3. `prg_machine.app.src` не объявляет рантайм-зависимость `operation_context` +`apps/prg_machine/src/prg_machine.app.src` — в `applications` есть `progressor`, но нет `operation_context`, хотя `prg_machine.erl:526,539` вызывает `operation_context:env_enter/leave` (а `:41` использует тип `operation_context:binding()`). +- Шаги: добавить `operation_context` в `applications` (порядок старта приложений в релизе). Проверить, что `prg_utils` (используется в `prg_machine.erl:466`) приходит из `progressor`. + +### P2-4. Артефакт тулинга в прод-репозитории +`.cursor/agents/generic-worker-composer.md` (+15 строк) закоммичен в hellgate. +- Шаги: удалить из ветки; при необходимости — в `.gitignore`. + +### P2-5. Дубль/временное в `rebar.config` +- TODO «bump tag after progressor_trace.thrift is released in damsel» + тройное определение progressor (git tag + `prg_machine` path + `_checkouts`). +- Шаги: закрыть вместе с P0-1 (один источник истины), снять/затрекать TODO. + +### P2-6. `nowarn_unused_function` на весь модуль в тестах +`apps/prg_machine/test/{prg_machine_env_tests,operation_context_tests}.erl`. +- Шаги: точечно `nowarn`/удалить неиспользуемые хелперы вместо глушения всего модуля. + +### P2-7. Робастность `unmarshal_event/2` +`apps/prg_machine/src/prg_machine.erl:404-406` — событие без `payload`: вторая клауза рекурсивно вызывает себя, не добавляя `payload` → потенциальный бесконечный цикл (edge-case, на практике payload всегда есть). +- Шаги: явная клауза/гард для события без payload. + +### P2-8. Прогнать статанализ на ветке +- `rebar3 dialyzer` и `rebar3 lint` локально на ветке не гонялись; конфиг не ослаблялся → «бесплатная» проверка. +- Шаги: прогнать оба до PR. + +--- + +## Чек-лист готовности к merge + +- [ ] P0-1: правки `progressor` закоммичены/затегированы; `rebar.config`/`rebar.lock` без `_checkouts`; чистый клон компилится +- [ ] P0-2: коммит + PR на `epic/monorepo`, зелёный CI +- [ ] P0-3: ключевые CT-suites зелёные +- [ ] P1-1: упрощён транспорт call-args в `hg_invoicing_machine_client`, убраны мёртвые ветки +- [ ] P1-2: trace покрыт тестом или заведена задача +- [ ] P2-1…P2-8: техдолг закрыт или вынесен в backlog (с акцентом на stacktrace в catch-all и semantics signal/transient-error) diff --git a/rebar.config b/rebar.config index b6b74fe8..d0b69c51 100644 --- a/rebar.config +++ b/rebar.config @@ -47,6 +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"}}}, + %% Local overrides: _checkouts/progressor (progressor_action, action types) — see .gitignore {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.24"}}}, {prg_machine, {path, "apps/prg_machine"}}, {machinery, {git, "https://github.com/valitydev/machinery-erlang.git", {tag, "v1.1.22"}}}, From ab6b896fd23b26e01f835644ddbd5db8deb46132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 21:19:27 +0300 Subject: [PATCH 10/62] Refactor type specifications and formatting across multiple modules for consistency. Update history_times spec to improve readability and adjust function calls in ff_machine_codec for better alignment with coding standards. --- apps/ff_transfer/src/ff_deposit_machine.erl | 3 ++- apps/ff_transfer/src/ff_destination_machine.erl | 3 ++- apps/ff_transfer/src/ff_machine_codec.erl | 8 ++++++-- apps/ff_transfer/src/ff_source_machine.erl | 3 ++- apps/ff_transfer/src/ff_withdrawal_machine.erl | 3 ++- .../ff_transfer/src/ff_withdrawal_session_machine.erl | 3 ++- apps/fistful/src/ff_repair.erl | 2 +- apps/hellgate/src/hg_invoice.erl | 6 +++++- apps/hellgate/src/hg_invoice_handler.erl | 8 +++++--- apps/hellgate/src/hg_invoice_template.erl | 8 +++++--- apps/operation_context/src/operation_context.erl | 11 +++++++---- apps/prg_machine/src/prg_machine.erl | 6 ++---- .../prg_machine/test/prg_machine_env_mock_handler.erl | 2 +- 13 files changed, 42 insertions(+), 24 deletions(-) diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index 3d81676b..fc277a02 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -147,7 +147,8 @@ machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> history_to_events(History) -> [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. --spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +-spec history_times(prg_machine:history()) -> + {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. history_times([]) -> {undefined, undefined}; history_times(History) -> diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index 1fff667f..29dacc94 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -118,7 +118,8 @@ machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> history_to_events(History) -> [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. --spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +-spec history_times(prg_machine:history()) -> + {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. history_times([]) -> {undefined, undefined}; history_times(History) -> diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl index fe15ab83..aa9b3724 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -12,7 +12,9 @@ -spec marshal_event(domain(), format_version(), timestamped_event()) -> machinery_msgpack:t(). marshal_event(deposit, 1, Timestamped) -> - marshal_thrift_event(Timestamped, ff_deposit_codec, timestamped_change, fistful_deposit_thrift, 'TimestampedChange'); + marshal_thrift_event( + Timestamped, ff_deposit_codec, timestamped_change, fistful_deposit_thrift, 'TimestampedChange' + ); marshal_event(source, 1, Timestamped) -> marshal_thrift_event(Timestamped, ff_source_codec, timestamped_change, fistful_source_thrift, 'TimestampedChange'); marshal_event(destination, 1, Timestamped) -> @@ -20,7 +22,9 @@ marshal_event(destination, 1, Timestamped) -> Timestamped, ff_destination_codec, timestamped_change, fistful_destination_thrift, 'TimestampedChange' ); marshal_event(withdrawal, 1, Timestamped) -> - marshal_thrift_event(Timestamped, ff_withdrawal_codec, timestamped_change, fistful_wthd_thrift, 'TimestampedChange'); + marshal_thrift_event( + Timestamped, ff_withdrawal_codec, timestamped_change, fistful_wthd_thrift, 'TimestampedChange' + ); marshal_event(withdrawal_session, 1, Timestamped) -> marshal_thrift_event( Timestamped, ff_withdrawal_session_codec, timestamped_change, fistful_wthd_session_thrift, 'TimestampedChange' diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index a2c09c72..288848c7 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -118,7 +118,8 @@ machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> history_to_events(History) -> [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. --spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +-spec history_times(prg_machine:history()) -> + {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. history_times([]) -> {undefined, undefined}; history_times(History) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index b189ca9b..51766750 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -179,7 +179,8 @@ machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> history_to_events(History) -> [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. --spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +-spec history_times(prg_machine:history()) -> + {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. history_times([]) -> {undefined, undefined}; history_times(History) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index ca0bc060..9813829d 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -147,7 +147,8 @@ machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> history_to_events(History) -> [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. --spec history_times(prg_machine:history()) -> {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. +-spec history_times(prg_machine:history()) -> + {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. history_times([]) -> {undefined, undefined}; history_times(History) -> diff --git a/apps/fistful/src/ff_repair.erl b/apps/fistful/src/ff_repair.erl index 33ac8842..3f4f02e5 100644 --- a/apps/fistful/src/ff_repair.erl +++ b/apps/fistful/src/ff_repair.erl @@ -146,7 +146,7 @@ validate_result(Mod, RepairMachine, #{events := NewEvents}) -> IDs = lists:seq(HistoryLen + 1, HistoryLen + NewEventsLen), PrgNewHistory = [ {EventID, Ts, Body} - || {EventID, {ev, Ts, Body}} <- lists:zip(IDs, NewEvents) + || {EventID, {ev, Ts, Body}} <- lists:zip(IDs, NewEvents) ], Machine = (to_prg_machine(RepairMachine))#{ history => PrgHistory0 ++ PrgNewHistory, diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index fef7166b..d13be868 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -1020,7 +1020,11 @@ event_timestamp_to_binary(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, + St = + case St0 of + undefined -> #st{}; + _ -> St0 + end, collapse_changes(Changes, St, #{timestamp => Dt}). -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index d3ea0a8b..a97213e7 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -154,9 +154,11 @@ ensure_started(ID, TemplateID, Params, Allocation, Mutations, DomainRevision) -> end. call(ID, Function, Args) -> - case hg_invoicing_machine_client: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); diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index 7fc11e35..5ad78cd3 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -182,9 +182,11 @@ start(ID, Params) -> map_start_error(prg_machine:start(?NS, ID, EncodedParams)). call(ID, Function, Args) -> - case hg_invoicing_machine_client: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} -> diff --git a/apps/operation_context/src/operation_context.erl b/apps/operation_context/src/operation_context.erl index 8445d2fc..d338aaee 100644 --- a/apps/operation_context/src/operation_context.erl +++ b/apps/operation_context/src/operation_context.erl @@ -137,10 +137,13 @@ fistful_binding() -> -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() - })). + 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}) -> diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 79abb2c5..16d4e69a 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -239,16 +239,14 @@ get_history(NS, ID, After, Limit, Direction) -> notify(NS, ID, Args) -> case call(NS, ID, {notify, Args}) of {ok, _} -> ok; - {error, notfound} = Error -> - Error + {error, notfound} = Error -> Error end. -spec remove(namespace(), id()) -> ok | {error, notfound}. remove(NS, ID) -> case call(NS, ID, remove) of {ok, _} -> ok; - {error, notfound} = Error -> - Error + {error, notfound} = Error -> Error end. -spec history_range(undefined | event_id(), undefined | non_neg_integer(), forward | backward) -> diff --git a/apps/prg_machine/test/prg_machine_env_mock_handler.erl b/apps/prg_machine/test/prg_machine_env_mock_handler.erl index 6df73ce6..8e3803e2 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_handler.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -10,7 +10,7 @@ namespace() -> -spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). init(_Args, _Machine) -> - #{events => [], action => progressor_action:new(), auxst => undefined}. + #{events => [], action => progressor_action:new(), auxst => undefined}. -spec process_signal(prg_machine:signal(), prg_machine:machine()) -> prg_machine:result(). process_signal(_Signal, _Machine) -> From 2e125ca4548a92c14820528eb8160da0bc194669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 21:36:08 +0300 Subject: [PATCH 11/62] Refactor event marshaling and unmarshaling in ff_machine_codec to use function references for codec operations. Update cleanup logic in operation_context and prg_machine to remove catch statements for better error handling. Remove prg_machine subproject reference from rebar.config. --- apps/ff_transfer/src/ff_machine_codec.erl | 72 ++++++++++++++----- .../src/operation_context.erl | 12 ++-- apps/prg_machine/src/prg_machine.erl | 2 +- rebar.config | 1 - 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl index aa9b3724..3b59c699 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -13,39 +13,77 @@ -spec marshal_event(domain(), format_version(), timestamped_event()) -> machinery_msgpack:t(). marshal_event(deposit, 1, Timestamped) -> marshal_thrift_event( - Timestamped, ff_deposit_codec, timestamped_change, fistful_deposit_thrift, 'TimestampedChange' + Timestamped, + fun(T) -> ff_deposit_codec:marshal(timestamped_change, T) end, + fistful_deposit_thrift, + 'TimestampedChange' ); marshal_event(source, 1, Timestamped) -> - marshal_thrift_event(Timestamped, ff_source_codec, timestamped_change, fistful_source_thrift, 'TimestampedChange'); + 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, ff_destination_codec, timestamped_change, fistful_destination_thrift, 'TimestampedChange' + Timestamped, + fun(T) -> ff_destination_codec:marshal(timestamped_change, T) end, + fistful_destination_thrift, + 'TimestampedChange' ); marshal_event(withdrawal, 1, Timestamped) -> marshal_thrift_event( - Timestamped, ff_withdrawal_codec, timestamped_change, fistful_wthd_thrift, 'TimestampedChange' + 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, ff_withdrawal_session_codec, timestamped_change, fistful_wthd_session_thrift, 'TimestampedChange' + 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(), format_version(), binary()) -> timestamped_event(). unmarshal_event(deposit, 1, Payload) -> - unmarshal_thrift_event(Payload, ff_deposit_codec, timestamped_change, fistful_deposit_thrift, 'TimestampedChange'); + unmarshal_thrift_event( + Payload, + fun(T) -> ff_deposit_codec:unmarshal(timestamped_change, T) end, + fistful_deposit_thrift, + 'TimestampedChange' + ); unmarshal_event(source, 1, Payload) -> - unmarshal_thrift_event(Payload, ff_source_codec, timestamped_change, fistful_source_thrift, 'TimestampedChange'); + unmarshal_thrift_event( + Payload, + fun(T) -> ff_source_codec:unmarshal(timestamped_change, T) end, + fistful_source_thrift, + 'TimestampedChange' + ); unmarshal_event(destination, 1, Payload) -> unmarshal_thrift_event( - Payload, ff_destination_codec, timestamped_change, fistful_destination_thrift, 'TimestampedChange' + Payload, + fun(T) -> ff_destination_codec:unmarshal(timestamped_change, T) end, + fistful_destination_thrift, + 'TimestampedChange' ); unmarshal_event(withdrawal, 1, Payload) -> - unmarshal_thrift_event(Payload, ff_withdrawal_codec, timestamped_change, fistful_wthd_thrift, 'TimestampedChange'); + unmarshal_thrift_event( + Payload, + fun(T) -> ff_withdrawal_codec:unmarshal(timestamped_change, T) end, + fistful_wthd_thrift, + 'TimestampedChange' + ); unmarshal_event(withdrawal_session, 1, Payload) -> unmarshal_thrift_event( - Payload, ff_withdrawal_session_codec, timestamped_change, fistful_wthd_session_thrift, 'TimestampedChange' + Payload, + fun(T) -> ff_withdrawal_session_codec:unmarshal(timestamped_change, T) end, + fistful_wthd_session_thrift, + 'TimestampedChange' ); unmarshal_event(Domain, Format, _Payload) -> erlang:error({unknown_event_format, Domain, Format}). @@ -67,24 +105,22 @@ payload_to_binary(Payload) -> -spec marshal_thrift_event( timestamped_event(), - module(), - atom(), + fun((timestamped_event()) -> term()), atom(), atom() ) -> machinery_msgpack:t(). -marshal_thrift_event(Timestamped, Codec, Tag, ThriftModule, ThriftStruct) -> - ThriftChange = Codec:marshal(Tag, Timestamped), +marshal_thrift_event(Timestamped, MarshalFun, ThriftModule, ThriftStruct) -> + ThriftChange = MarshalFun(Timestamped), Type = {struct, struct, {ThriftModule, ThriftStruct}}, {bin, ff_proto_utils:serialize(Type, ThriftChange)}. -spec unmarshal_thrift_event( binary(), - module(), - atom(), + fun((term()) -> timestamped_event()), atom(), atom() ) -> timestamped_event(). -unmarshal_thrift_event(Payload, Codec, Tag, ThriftModule, ThriftStruct) -> +unmarshal_thrift_event(Payload, UnmarshalFun, ThriftModule, ThriftStruct) -> Type = {struct, struct, {ThriftModule, ThriftStruct}}, ThriftChange = ff_proto_utils:deserialize(Type, Payload), - Codec:unmarshal(Tag, ThriftChange). + UnmarshalFun(ThriftChange). diff --git a/apps/operation_context/src/operation_context.erl b/apps/operation_context/src/operation_context.erl index d338aaee..f8155746 100644 --- a/apps/operation_context/src/operation_context.erl +++ b/apps/operation_context/src/operation_context.erl @@ -94,7 +94,11 @@ cleanup(RegistryKey, strict) -> true = gproc:unreg(RegistryKey), ok; cleanup(RegistryKey, lenient) -> - _ = catch gproc:unreg(RegistryKey), + try + true = gproc:unreg(RegistryKey) + catch + _:_ -> ok + end, ok. -spec save_hellgate(context()) -> ok. @@ -221,8 +225,8 @@ colocated_keys_isolated_test() -> ?assertEqual(WoodyFf, get_woody_context(CtxFfAfterHgCleanup)), ok = cleanup(?FF_TEST_KEY, lenient) after - _ = catch cleanup(?HG_TEST_KEY, lenient), - _ = catch cleanup(?FF_TEST_KEY, lenient) + cleanup(?HG_TEST_KEY, lenient), + cleanup(?FF_TEST_KEY, lenient) end. -spec scoped_helpers_test() -> _. @@ -236,7 +240,7 @@ scoped_helpers_test() -> ok = cleanup_fistful(), ok = cleanup_fistful() after - _ = catch cleanup_fistful() + cleanup_fistful() end. -endif. diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 16d4e69a..fa25ff1e 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -640,7 +640,7 @@ setup_env_hook_test() -> -spec cleanup_env_hook_test(_) -> ok. cleanup_env_hook_test(_) -> _ = ets:delete(?TABLE, ?TEST_NS), - _ = catch operation_context:cleanup(?TEST_REGISTRY_KEY, lenient), + operation_context:cleanup(?TEST_REGISTRY_KEY, lenient), ok. -spec ensure_woody_available() -> ok. diff --git a/rebar.config b/rebar.config index d0b69c51..d280de77 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,6 @@ {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, %% Local overrides: _checkouts/progressor (progressor_action, action types) — see .gitignore {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.24"}}}, - {prg_machine, {path, "apps/prg_machine"}}, {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"}}}, From df3e0aa67066dc15c13d707517f187086c1051dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 6 Jun 2026 23:57:46 +0300 Subject: [PATCH 12/62] Remove ff_limit module and associated test suite, along with references to machinery in fistful.app.src. This cleanup is part of the transition away from the machinery framework, streamlining the codebase for future development. --- apps/fistful/src/ff_limit.erl | 303 -------------------------- apps/fistful/src/fistful.app.src | 2 - apps/fistful/test/ff_limit_SUITE.erl | 123 ----------- docs/prg-machine-migration-context.md | 5 +- docs/prg-machine-review-plan.md | 135 ------------ 5 files changed, 2 insertions(+), 566 deletions(-) delete mode 100644 apps/fistful/src/ff_limit.erl delete mode 100644 apps/fistful/test/ff_limit_SUITE.erl delete mode 100644 docs/prg-machine-review-plan.md 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/fistful.app.src b/apps/fistful/src/fistful.app.src index a9e3a6a5..5d381d92 100644 --- a/apps/fistful/src/fistful.app.src +++ b/apps/fistful/src/fistful.app.src @@ -13,8 +13,6 @@ progressor, operation_context, prg_machine, - machinery, - machinery_extra, woody, uuid, damsel, 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/docs/prg-machine-migration-context.md b/docs/prg-machine-migration-context.md index 14225bde..ea917969 100644 --- a/docs/prg-machine-migration-context.md +++ b/docs/prg-machine-migration-context.md @@ -247,10 +247,9 @@ rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE | Модуль / config | Проблема | |-----------------|----------| -| `apps/fistful/src/ff_limit.erl` | `-behaviour(machinery)`, вызовы `machinery:get/call/start` | | `test/bender/sys.config`, `test/party-management/sys.config` | `client => machinery_prg_backend` | | `apps/ff_cth/src/ct_payment_system.erl` | мёртвый `{machinery_backend, progressor}` | -| `apps/machinery_extra/` | остаётся для `ff_limit` и тестов | +| `apps/machinery_extra/` | остаётся для `machinery_msgpack` в FF transfer и тестах | ### 5.3. Trace API @@ -272,7 +271,7 @@ rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE ```bash rg 'hg_machine:' apps/hellgate --glob '*.erl' # 0 prod -rg 'machinery_prg_backend|ff_machine:' apps/fistful apps/ff_transfer apps/ff_server --glob '*.erl' # 0 кроме ff_limit +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 ``` diff --git a/docs/prg-machine-review-plan.md b/docs/prg-machine-review-plan.md deleted file mode 100644 index 239101d7..00000000 --- a/docs/prg-machine-review-plan.md +++ /dev/null @@ -1,135 +0,0 @@ -# Строгое ревью ветки `add_prg_layer` vs `epic/monorepo` — план доработки - -Ревью кода (независимое, без CT-прогона — только `rebar3 compile`). Сборка проходит. -Diff: 95 файлов, +3604 / −3987. Суть ветки — перевод HG/FF машин с `machinery`/`hg_machine`/`ff_machine` -на единый слой `prg_machine` (progressor): 7 prod namespace, новые app'ы `prg_machine` и `operation_context`, -удаление старого machinery/hg_progressor glue. - -> **Контекст подхода (уточнено автором).** Ветка — про *смену способа интеграции* с progressor, поэтому -> изменения **намеренно** заходят и в сам progressor (модуль `progressor_action`, правки `progressor.hrl`), -> а не размазываются обёртками по прикладному коду. Это осознанное правило, а не «срез угла». Единственное -> требование к такому подходу — **воспроизводимость сборки** (см. P0-1). - -Легенда приоритетов: **P0** — блокер merge, **P1** — корректность/риск регрессии, **P2** — качество/техдолг. - ---- - -## Итог по «срезанным углам» - -- **elvis** — НЕ ослаблен: из `elvis.config` только удалены исключения для удалённых `*_machinery_schema`, новых нет. -- **dialyzer (`rebar.config`)** — НЕ ослаблен: `warnings` (`unmatched_returns`, `error_handling`, `unknown`) + `plt_apps => all_deps` без изменений. -- **`erl_opts`** (`warnings_as_errors`, `warn_missing_spec`, …) — без изменений, сборка чистая. -- Подавления, добавленные веткой, — 3: `nowarn` на мёртвую `map_action/1` (P2-1) и `nowarn_unused_function` на два тест-модуля (P2-6). `-dialyzer(nowarn_function,…)` в `ff_ct_*`/`hg_invoice_tests_SUITE` — доветочные. - -**Вывод:** линтер/диалайзер как механизм не обойдён. Реальный риск — воспроизводимость сборки (P0-1) и точечный техдолг (P2). - ---- - -## P0 — блокеры перед merge - -### P0-1. Воспроизводимость сборки: правки `progressor` не закоммичены/не выпущены -Подход (дорабатывать сам progressor) — ок. Проблема в том, что **на текущий момент сборка собирается только на этой машине**: -- `_checkouts/progressor` — git-link `160000` на ref `90f4657`, **без записи в `.gitmodules`** → при клоне контент не подтянется. -- Рабочее дерево чекаута **грязное**: незакоммиченный `include/progressor.hrl` + **untracked `src/progressor_action.erl`**. -- Прод-код ветки (`prg_machine.erl:35,343`, `hg_invoice.erl`, `ff_*_machine.erl`) уже зависит от `progressor_action` — модуля, которого **нет ни в одном теге/коммите** upstream. -- `rebar.config` всё ещё указывает `{progressor, {git, …, {tag, "v1.0.24"}}}` (там нет `progressor_action`), а из `rebar.lock` пиннинг `progressor` удалён → источник версии противоречив. - -Шаги: -1. Закоммитить и запушить правки в `valitydev/progressor` (`progressor_action`, `progressor.hrl`), смержить, **выпустить тег**. -2. `rebar.config` → `{progressor, {git, …, {tag, ""}}}`; убрать дубль (`progressor` git + `prg_machine` path + `_checkouts`), оставить один источник истины. -3. Удалить `_checkouts/progressor` из индекса; вернуть `progressor` в `rebar.lock` (`rebar3 lock`). -4. Проверка: на чистом клоне `rm -rf _checkouts && rebar3 compile` — собирается без локального чекаута. - -### P0-2. Нет коммита/PR -- Содержательно готово, но не оформлено PR на `epic/monorepo` (история — 7 коммитов с авто-сообщениями). -- Шаги: после P0-1 закоммитить, оформить PR, прогнать CI. - -### P0-3. CT не прогонялись -- Ни один CT-suite не запускался. Для платёжного процессинга — обязательный гейт. -- Минимум suites (docker: postgres, party-management, dmt): - - `apps/ff_server/test/{ff_deposit_handler_SUITE, ff_withdrawal_handler_SUITE, ff_withdrawal_session_repair_SUITE}` - - `apps/hellgate/test/{hg_invoice_lite_tests_SUITE, hg_invoice_tests_SUITE, hg_invoice_template_tests_SUITE, hg_direct_recurrent_tests_SUITE}` -- Шаги: поднять окружение, прогнать suites, зафиксировать в PR. - ---- - -## P1 — корректность и риск регрессий - -### P1-1. `hg_invoicing_machine_client:thrift_call` — двойная сериализация + мёртвые ветки (подтверждено, фиксить) -`apps/hellgate/src/hg_invoicing_machine_client.erl:33-45`, `:59-76`. -- Args сериализуются (`marshal_thrift_args`) и тут же десериализуются (`unmarshal_thrift_args`), а в `prg_machine:call` уходит **десериализованный** терм (далее ещё раз `term_to_binary`). Сериализация — лишняя работа. -- `unmarshal_thrift_response`: ветки `is_binary(...)` недостижимы (транспорт — `term_to_binary`, не thrift-байты). -- Шаги: выбрать единый контракт транспорта call-args (скорее всего — слать готовый терм без thrift round-trip) и убрать мёртвые ветки декода. Сверить с `hg_invoice:handle_call/2` (ожидает `{FunRef, Args}`). - -### P1-2. Удалён CT-тест `payment_success_trace` без замены (подтверждено, ошибка) -`apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl` — убран `payment_success_trace/1` (проверка trace-API), trace переехал на FF internal HTTP JSON, но нового теста нет. -- Шаги: восстановить покрытие trace (тест на актуальный эндпоинт) либо завести явную задачу под отдельный goal (`docs/trace-api-thrift.md`) и сослаться на неё в PR. - -### ~~P1-x. Паритет prod по выпавшим NS~~ — снято -`customer`, `recurrent_paytools`, `ff/identity`, `ff/wallet_v2` выпали корректно (подтверждено автором): доменных модулей нет, на `epic/monorepo` в `hellgate.erl` регистрировались только `hg_invoice`/`hg_invoice_template`. **Не регрессия, действий не требуется.** - ---- - -## P1→P2 (понижено после расследования). `prg_machine:process/3` ловит все исключения - -`apps/prg_machine/src/prg_machine.erl:264-281`, `:570-572`. - -**Вывод расследования: «оно работает», это не блокер.** Цепочка проверена: -- `hg_invoice:process_call/2` (`:402-413`) сам оборачивает `handle_call` в `try ... catch throw:Exception -> {{exception, Exception}, #{}}`. То есть бизнес-`throw` превращается в **успешный** ответ-исключение (`{Response, Result}`), а не в `{error,…}`. Машина при бизнес-ошибке **не ломается**. -- В progressor (`prg_worker:do_process_task/4` → `handle_result_error/5`): `{error,…}` для `call`/`init`/`repair` → `error_and_stop` (машина в `error`), для `timeout`/`remove` → `error_and_retry`. До catch-all в `process/3` доходят только **непредвиденные** падения — а они и в старом MG ломали/фейлили машину. Поведение эквивалентно. - -**Остаточные (P2) хвосты этого места:** -1. **Теряется stacktrace.** `?PROCESSOR_EXCEPTION` кладёт только `{exception, Class, Reason}`, а `raise_exception/1` делает `erlang:raise(Class, Reason, [])`. Для диагностики непредвиденных падений стектрейс нужно логировать (хотя бы `logger:error` со `Stacktrace`). -2. **Транзиентные woody-ошибки на `init`/`call`.** Для этих task progressor делает `error_and_stop` (нет retry). Транзиентная недоступность зависимости во время `call`/`init` → машина в перманентном `error` (нужен repair). Проверить, ожидаемо ли это (в т.ч. retry-policy неймспейса), и при необходимости классифицировать `?WOODY_ERROR(resource_unavailable|result_unknown)`. -3. **`process_signal/2` без `try/catch`** в `hg_invoice` (`:347-358`) — `throw` из `handle_signal` уйдёт в catch-all и для `timeout` уйдёт в retry. Подтвердить, что это намеренно (а не «проглатывание» бизнес-ошибки). - ---- - -## P2 — техдолг (кандидаты на детальное расследование) - -### P2-1. Мёртвый код `map_action/1` в `ff_deposit_machine` -`apps/ff_transfer/src/ff_deposit_machine.erl:63 (export), :143 (nowarn), :174-182`. -- Функция не используется; есть только в `ff_deposit_machine` (в остальных `ff_*_machine` её нет — реальный маппинг идёт через `action_to_prg`/`progressor_action` в домене). -- Двойная избыточность: функция **экспортирована** (`-export([map_action/1])`) → `nowarn_unused_function` для неё бессмысленен (экспортируемые не считаются unused). -- Маппинг `sleep -> progressor_action:instant()` семантически неверен (`instant() = set_timeout(0)` — немедленный таймаут, а не «сон»). -- Шаги: удалить `map_action/1`, его `-export` и `-compile(nowarn…)`; убедиться, что во всех `ff_*_machine` action-маппинг единообразен. - -### P2-2. `binary_to_term` без `[safe]` в fallback-путях `prg_machine` -`apps/prg_machine/src/prg_machine.erl:437,455,554` (`decode_term`, fallback `unmarshal_event_body`/`unmarshal_aux_state`). -- HG-домены уже используют `binary_to_term(Payload, [safe])` (`hg_invoice.erl:1047,1050,1066`, `hg_invoice_template.erl`) — хорошо. -- Generic-fallback и `decode_term` (call-args/response/repair-args/rpc-context) — без `[safe]`. Риск низкий (данные формируем сами через `term_to_binary`), но как defense-in-depth: добавить `[safe]`, а fallback-кодеки сделать явной ошибкой вместо тихого `term_to_binary`/`binary_to_term`. - -### P2-3. `prg_machine.app.src` не объявляет рантайм-зависимость `operation_context` -`apps/prg_machine/src/prg_machine.app.src` — в `applications` есть `progressor`, но нет `operation_context`, хотя `prg_machine.erl:526,539` вызывает `operation_context:env_enter/leave` (а `:41` использует тип `operation_context:binding()`). -- Шаги: добавить `operation_context` в `applications` (порядок старта приложений в релизе). Проверить, что `prg_utils` (используется в `prg_machine.erl:466`) приходит из `progressor`. - -### P2-4. Артефакт тулинга в прод-репозитории -`.cursor/agents/generic-worker-composer.md` (+15 строк) закоммичен в hellgate. -- Шаги: удалить из ветки; при необходимости — в `.gitignore`. - -### P2-5. Дубль/временное в `rebar.config` -- TODO «bump tag after progressor_trace.thrift is released in damsel» + тройное определение progressor (git tag + `prg_machine` path + `_checkouts`). -- Шаги: закрыть вместе с P0-1 (один источник истины), снять/затрекать TODO. - -### P2-6. `nowarn_unused_function` на весь модуль в тестах -`apps/prg_machine/test/{prg_machine_env_tests,operation_context_tests}.erl`. -- Шаги: точечно `nowarn`/удалить неиспользуемые хелперы вместо глушения всего модуля. - -### P2-7. Робастность `unmarshal_event/2` -`apps/prg_machine/src/prg_machine.erl:404-406` — событие без `payload`: вторая клауза рекурсивно вызывает себя, не добавляя `payload` → потенциальный бесконечный цикл (edge-case, на практике payload всегда есть). -- Шаги: явная клауза/гард для события без payload. - -### P2-8. Прогнать статанализ на ветке -- `rebar3 dialyzer` и `rebar3 lint` локально на ветке не гонялись; конфиг не ослаблялся → «бесплатная» проверка. -- Шаги: прогнать оба до PR. - ---- - -## Чек-лист готовности к merge - -- [ ] P0-1: правки `progressor` закоммичены/затегированы; `rebar.config`/`rebar.lock` без `_checkouts`; чистый клон компилится -- [ ] P0-2: коммит + PR на `epic/monorepo`, зелёный CI -- [ ] P0-3: ключевые CT-suites зелёные -- [ ] P1-1: упрощён транспорт call-args в `hg_invoicing_machine_client`, убраны мёртвые ветки -- [ ] P1-2: trace покрыт тестом или заведена задача -- [ ] P2-1…P2-8: техдолг закрыт или вынесен в backlog (с акцентом на stacktrace в catch-all и semantics signal/transient-error) From 3fe991e5e06e6de0ff4274e66db5b9b616300b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sun, 7 Jun 2026 17:10:22 +0300 Subject: [PATCH 13/62] Refactor prg_machine to enhance error handling and streamline functionality. Remove dead code related to machinery backend in ct_payment_system. Update rebar.config to reflect changes in dependencies and clarify trace API goals. Improve type specifications and handling of auxiliary state in various modules, ensuring consistency and better logging of exceptions. --- apps/ff_cth/src/ct_payment_system.erl | 1 - apps/hellgate/src/hg_invoice.erl | 2 +- apps/prg_machine/src/prg_machine.app.src | 2 +- apps/prg_machine/src/prg_machine.erl | 252 ++++++++++++++---- apps/prg_machine/src/prg_machine_registry.erl | 72 +++++ .../prg_machine_aux_state_test_handler.erl | 61 +++++ .../test/prg_machine_env_mock_handler.erl | 8 +- docs/prg-machine-migration-context.md | 41 +-- docs/prg-machine-review-plan.md | 80 ++++++ docs/trace-api-thrift.md | 8 +- rebar.config | 2 +- test/bender/sys.config | 2 +- test/party-management/sys.config | 2 +- 13 files changed, 455 insertions(+), 78 deletions(-) create mode 100644 apps/prg_machine/src/prg_machine_registry.erl create mode 100644 apps/prg_machine/test/prg_machine_aux_state_test_handler.erl create mode 100644 docs/prg-machine-review-plan.md diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index 7edbfcd3..b25d7f8a 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, diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index d13be868..d10b2c13 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -409,7 +409,7 @@ process_call(Call, Machine) -> {call_response(Response), to_prg_result(CallResult)} catch throw:Exception -> - {{exception, Exception}, #{}} + {{exception, Exception}, to_prg_result(#{})} end. -spec handle_call(call(), st()) -> call_result(). diff --git a/apps/prg_machine/src/prg_machine.app.src b/apps/prg_machine/src/prg_machine.app.src index 59d9ae33..55fc0485 100644 --- a/apps/prg_machine/src/prg_machine.app.src +++ b/apps/prg_machine/src/prg_machine.app.src @@ -1,7 +1,7 @@ {application, prg_machine, [ {description, "Unified progressor machine runtime for HG and FF"}, {vsn, "0.1.0"}, - {registered, []}, + {registered, [prg_machine_registry]}, {applications, [ kernel, stdlib, diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index fa25ff1e..35f550a3 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -6,7 +6,7 @@ -include_lib("progressor/include/progressor.hrl"). -define(TABLE, prg_machine_dispatch). --define(PROCESSOR_EXCEPTION(Class, Reason, _Stacktrace), {exception, Class, Reason}). +-define(PROCESSOR_EXCEPTION(Class, Reason, Stacktrace), {exception, Class, Reason, Stacktrace}). %% Types @@ -121,8 +121,6 @@ %% Registry (namespace -> handler module) -export([get_child_spec/1]). --export([start_link/1]). --export([init/1]). %% Event-sourcing helpers (replaces ff_machine) @@ -147,6 +145,8 @@ start(NS, ID, Args) -> {error, <<"process already exists">>} -> {error, exists}; {error, {exception, _, _} = Exception} -> + raise_exception(Exception); + {error, {exception, _, _, _} = Exception} -> raise_exception(Exception) end. @@ -169,6 +169,8 @@ call(NS, ID, CallArgs, After, Limit, Direction) -> {error, failed}; {error, {exception, _, _} = Exception} -> raise_exception(Exception); + {error, {exception, _, _, _} = Exception} -> + raise_exception(Exception); {error, _} = Error -> Error end. @@ -195,20 +197,28 @@ repair(NS, ID, Args) -> {error, failed}; {error, {exception, _, _} = Exception} -> raise_exception(Exception); + {error, {exception, _, _, _} = Exception} -> + raise_exception(Exception); {error, Reason} -> {error, {repair, {failed, Reason}}} end. --spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, notfound}. +-spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, notfound | {unknown_namespace, namespace()}}. get(NS, ID, Range) -> Req = request(NS, ID, undefined, Range), case progressor:get(Req) of {ok, Process} -> - Handler = get_handler_module(NS), - {ok, unmarshal_machine(Handler, NS, Process)}; + case get_handler_module(NS) of + {ok, Handler} -> + {ok, unmarshal_machine(Handler, NS, Process)}; + {error, _} = Error -> + Error + end; {error, <<"process not found">>} -> {error, notfound}; {error, {exception, _, _} = Exception} -> + raise_exception(Exception); + {error, {exception, _, _, _} = Exception} -> raise_exception(Exception) end. @@ -263,18 +273,27 @@ process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> Enter = resolve_env_enter(Opts), Leave = resolve_env_leave(Opts), try - {WoodyCtx, OtelCtx} = decode_rpc_context(BinCtx), - ok = woody_rpc_helper:attach_otel_context(OtelCtx), - ok = run_env_enter(Enter, WoodyCtx), - Handler = get_handler_module(NS), - 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) + case get_handler_module(NS) of + {error, _} = Error -> + Error; + {ok, Handler} -> + {WoodyCtx, OtelCtx} = decode_rpc_context(BinCtx), + ok = woody_rpc_helper:attach_otel_context(OtelCtx), + ok = run_env_enter(Enter, WoodyCtx), + 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 catch Class:Reason:Stacktrace -> - logger:error("prg_machine process failed: ~p:~p~n~p", [Class, Reason, Stacktrace]), - {error, ?PROCESSOR_EXCEPTION(Class, Reason, Stacktrace)} + Exception = ?PROCESSOR_EXCEPTION(Class, Reason, Stacktrace), + logger:error( + "prg_machine process failed: ~p:~p", + [Class, Reason], + #{stacktrace => Stacktrace, exception => Exception} + ), + {error, Exception} after Leave() end. @@ -283,21 +302,7 @@ process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> -spec get_child_spec([module()]) -> supervisor:child_spec(). get_child_spec(Handlers) -> - #{ - id => prg_machine_dispatch, - start => {?MODULE, start_link, [Handlers]}, - type => supervisor - }. - --spec start_link([module()]) -> {ok, pid()}. -start_link(Handlers) -> - supervisor:start_link(?MODULE, Handlers). - --spec init([module()]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. -init(Handlers) -> - _ = ets:new(?TABLE, [named_table, {read_concurrency, true}]), - true = ets:insert_new(?TABLE, [{H:namespace(), H} || H <- Handlers]), - {ok, {#{}, []}}. + prg_machine_registry:get_child_spec(Handlers). %% Event-sourcing (replaces ff_machine collapse/emit) @@ -368,14 +373,17 @@ marshal_process_result(Handler, LastEventID, Result) when is_map(Result) -> marshal_process_result(_Handler, _LastEventID, {error, Reason}) -> {error, encode_term(Reason)}. -marshal_intent(Handler, LastEventID, #{events := Events, action := Action, auxst := AuxSt}) -> - genlib_map:compact(#{ - events => marshal_new_events(Handler, LastEventID, Events), - action => Action, - aux_state => marshal_aux_state(Handler, AuxSt) - }); -marshal_intent(Handler, LastEventID, Result) -> - marshal_intent(Handler, LastEventID, maps:merge(#{events => [], auxst => undefined}, Result)). +marshal_intent(Handler, LastEventID, Result) when is_map(Result) -> + Base = genlib_map:compact(#{ + events => marshal_new_events(Handler, LastEventID, maps:get(events, Result, [])), + action => maps:get(action, Result, progressor_action:new()) + }), + case maps:is_key(auxst, Result) of + true -> + Base#{aux_state => marshal_aux_state(Handler, maps:get(auxst, Result))}; + false -> + Base + end. %% Internals — progressor <-> machine @@ -477,11 +485,13 @@ dispatch_apply_event(Handler, EventID, Ts, Body, Model) -> end end. -initial_model(_Handler, AuxState) -> - maps:get(model, AuxState, undefined). +initial_model(_Handler, AuxState) when is_map(AuxState) -> + maps:get(model, AuxState, undefined); +initial_model(_Handler, _AuxState) -> + undefined. get_handler_module(NS) -> - ets:lookup_element(?TABLE, NS, 2). + prg_machine_registry:lookup(NS). %% RPC / terms @@ -565,7 +575,9 @@ range_from_process(#{range := Range = #{}}) -> range_from_process(_) -> #{direction => forward}. --spec raise_exception({exception, atom(), term()}) -> no_return(). +-spec raise_exception({exception, atom(), term()} | {exception, atom(), term(), list()}) -> no_return(). +raise_exception({exception, Class, Reason, Stacktrace}) when is_list(Stacktrace) -> + erlang:raise(Class, Reason, Stacktrace); raise_exception({exception, Class, Reason}) -> erlang:raise(Class, Reason, []). @@ -593,6 +605,28 @@ explicit_fun_overrides_context_binding_test_() -> ?_test(explicit_fun_overrides_context_binding()) ]}. +-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_without_handler_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_stacktrace_test_() -> _. +process_stacktrace_test_() -> + {setup, fun setup_aux_state_test/0, fun cleanup_aux_state_test/1, [ + ?_test(process_crash_includes_stacktrace()) + ]}. + -spec noop_when_hooks_absent() -> _. noop_when_hooks_absent() -> ok = ensure_woody_available(), @@ -649,14 +683,9 @@ ensure_woody_available() -> _ = woody_context:new(), ok. --spec ensure_env_hook_dispatch_table() -> atom(). +-spec ensure_env_hook_dispatch_table() -> ok. ensure_env_hook_dispatch_table() -> - case ets:info(?TABLE) of - undefined -> - ets:new(?TABLE, [named_table, {read_concurrency, true}]); - _ -> - ?TABLE - end. + prg_machine_registry:ensure_table(). -spec run_env_hook_process(process_options()) -> _. run_env_hook_process(Opts) -> @@ -672,4 +701,127 @@ run_env_hook_process(Opts, BinCtx) -> }, 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_without_handler_omits_aux_state() -> _. +notify_without_handler_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_includes_stacktrace() -> _. +process_crash_includes_stacktrace() -> + 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, <<>>), + {error, {exception, error, deliberate_crash, Stacktrace}} = + process({call, term_to_binary(crash), Process}, Opts, <<>>), + ?assert(is_list(Stacktrace)), + ?assert(lists:keymember(prg_machine_aux_state_test_handler, 1, Stacktrace)). + -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..1a5d1094 --- /dev/null +++ b/apps/prg_machine/src/prg_machine_registry.erl @@ -0,0 +1,72 @@ +-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]). + +-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:info(?TABLE) of + undefined -> + {error, {unknown_namespace, NS}}; + _ -> + case ets:lookup(?TABLE, NS) of + [{NS, Handler}] -> + {ok, Handler}; + [] -> + {error, {unknown_namespace, NS}} + end + 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, #{handlers := [module()]}}. +init(Handlers) -> + ok = ensure_table(), + true = ets:insert(?TABLE, [{H:namespace(), H} || H <- Handlers]), + {ok, #{handlers => Handlers}}. + +-spec handle_call(term(), {pid(), term()}, map()) -> {reply, term(), map()}. +handle_call(_Request, _From, State) -> + {reply, {error, unsupported}, State}. + +-spec handle_cast(term(), map()) -> {noreply, map()}. +handle_cast(_Msg, State) -> + {noreply, State}. + +-spec handle_info(term(), map()) -> {noreply, map()}. +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..a0ec9aba --- /dev/null +++ b/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl @@ -0,0 +1,61 @@ +-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, + marshal_aux_state/1, + unmarshal_aux_state/1 +]). + +-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 => progressor_action:new(), + 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 => progressor_action:new()}. + +-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 => progressor_action:new(), auxst => #{model => Model}}}. + +-spec process_repair(prg_machine:args(), prg_machine:machine()) -> prg_machine:result() | {error, term()}. +process_repair(_Args, _Machine) -> + #{events => [], action => progressor_action:new()}. + +-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]). diff --git a/apps/prg_machine/test/prg_machine_env_mock_handler.erl b/apps/prg_machine/test/prg_machine_env_mock_handler.erl index 8e3803e2..22b98b11 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_handler.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -10,16 +10,16 @@ namespace() -> -spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). init(_Args, _Machine) -> - #{events => [], action => progressor_action:new(), auxst => undefined}. + #{events => [], action => progressor_action:new()}. -spec process_signal(prg_machine:signal(), prg_machine:machine()) -> prg_machine:result(). process_signal(_Signal, _Machine) -> - #{events => [], action => progressor_action:new(), auxst => undefined}. + #{events => [], action => progressor_action:new()}. -spec process_call(prg_machine:call(), prg_machine:machine()) -> {prg_machine:response(), prg_machine:result()}. process_call(_Call, _Machine) -> - {ok, #{events => [], action => progressor_action:new(), auxst => undefined}}. + {ok, #{events => [], action => progressor_action:new()}}. -spec process_repair(prg_machine:args(), prg_machine:machine()) -> prg_machine:result() | {error, term()}. process_repair(_Args, _Machine) -> - #{events => [], action => progressor_action:new(), auxst => undefined}. + #{events => [], action => progressor_action:new()}. diff --git a/docs/prg-machine-migration-context.md b/docs/prg-machine-migration-context.md index ea917969..d5b0eb2f 100644 --- a/docs/prg-machine-migration-context.md +++ b/docs/prg-machine-migration-context.md @@ -1,6 +1,6 @@ # Миграция на `prg_machine`: контекст для следующих доработок -Документ фиксирует **цель**, **целевую архитектуру**, **фактическое состояние** ветки `epic/monorepo` (hellgate, ~59 файлов, **не закоммичено** на момент написания) и **открытые хвосты**. +Документ фиксирует **цель**, **целевую архитектуру**, **фактическое состояние** ветки `add_prg_layer` (hellgate) и **открытые хвосты**. Merge target: `epic/monorepo`. Оркестрация Ralph: `/Users/artemfedorenko/Documents/paymentsols/ralph-2` (goal в `.ralph/goal.md`, задачи 1–36 verified, **37 — CT — не завершена**). @@ -49,10 +49,11 @@ woody handler (hg_*_handler, ff_*_handler) | Модуль | Роль | |--------|------| -| `prg_machine` | behaviour, client API, `process/3`, registry (ETS), `collapse`/`emit_events` | +| `prg_machine` | behaviour, client API, `process/3`, `collapse`/`emit_events` | +| `prg_machine_registry` | gen_server — владелец ETS `prg_machine_dispatch`, `lookup/1` → `{unknown_namespace, NS}` | | `prg_machine_action` | таймеры / remove → формат progressor | -**Registry:** при старте `prg_machine:get_child_spec([Module, …])` в ETS `prg_machine_dispatch` кладётся `{Namespace, HandlerModule}` по `Handler:namespace/0`. Паттерн перенесён из старого `hg_machine` (без woody MG routes). +**Registry:** `prg_machine:get_child_spec([Module, …])` поднимает `prg_machine_registry`; при старте в ETS кладётся `{Namespace, HandlerModule}` по `Handler:namespace/0`. При рестарте реестра таблица пересоздаётся из того же списка handlers. **Client API** (`start`, `call`, `get`, `repair`, `notify`, `remove`, `trace`) — обёртки над `progressor:*` с encode/decode term и woody/otel context. @@ -61,9 +62,11 @@ woody handler (hg_*_handler, ff_*_handler) 1. `env_enter(WoodyCtx)` — поднять `operation_context` (HG или FF binding) 2. `unmarshal_machine` — history + aux_state из storage 3. `dispatch` → `Handler:init | process_call | process_signal | process_repair | process_notification` -4. `marshal_process_result` — events, action, aux_state обратно в progressor +4. `marshal_process_result` — events, action, aux_state (только при явном `auxst` от домена) обратно в progressor 5. `env_leave()` в `after` +При необработанном исключении в домене: `{error, {exception, Class, Reason, Stacktrace}}` + structured log (`stacktrace`, `exception` в metadata). + **Контекст RPC:** `application:set_env(prg_machine, woody_context_loader, Loader)` в `hellgate:start/2` и `ff_server:start/2` (fun или `{M,F}`), fallback на `woody_context:new()`. ### 2.2. Behaviour (доменный модуль) @@ -93,7 +96,7 @@ woody handler (hg_*_handler, ff_*_handler) |---------|------------| | `collapse/2` | fold по history: `apply_event/4` (EventID, Ts, Body, Model) если экспортирован, иначе `apply_event/2` (FF) | | `emit_event/1`, `emit_events/1` | обёртка с timestamp для новых событий | -| `initial_model/2` | старт fold: `maps:get(model, AuxState, undefined)` — на практике почти всегда `undefined` | +| `initial_model/2` | старт fold: `maps:get(model, AuxState, undefined)` при `is_map(AuxState)`, иначе `undefined` | **FF:** домены вызывают `prg_machine:collapse(Mod, Machine)` в `*_machine` и внутри домена. @@ -183,7 +186,7 @@ processor => #{ - FF домены: `-behaviour(prg_machine)` (`ff_deposit`, `ff_source`, `ff_destination`, `ff_withdrawal`, `ff_withdrawal_session`) - HG: `hg_invoice`, `hg_invoice_template` — behaviour + `prg_machine` client - `ff_repair` — на `prg_machine:collapse` / `emit_events` -- `ff_machine_handler` — trace через `prg_machine:trace/2` (JSON HTTP) +- `ff_machine_handler` — trace через `progressor:trace/1` (JSON HTTP; Thrift — отдельный goal, `docs/trace-api-thrift.md`) - CT helper: `hg_ct_helper.erl` — progressor processor `client => prg_machine` ### 3.3. Ralph verification @@ -191,7 +194,8 @@ processor => #{ - Задачи **1–34, 36** — verified - Задача **37** (полный CT) — **не завершена** (прерывание ~37 мин, resource_exhausted) - Integration gate: `rebar3 compile` OK, CT deferred -- Ветка: `epic/monorepo`, diff **+1684 / −3224**, commit/PR нет +- Ветка: `add_prg_layer` → merge в `epic/monorepo`; PR ещё не открыт +- Runtime fixes (этапы 1, 3 review-plan): aux_state, registry, stacktrace — **в коде** --- @@ -228,7 +232,7 @@ sequenceDiagram | # | Хвост | Действие | |---|-------|----------| | 1 | **CT не прогонялись** | Запустить suites вручную (docker: postgres, party, dmt) | -| 2 | **Нет commit/PR** | Code review + коммит на `epic/monorepo` | +| 2 | **Нет PR** | PR `add_prg_layer → epic/monorepo` после зелёного CT | **CT suites (минимум):** @@ -245,15 +249,16 @@ rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE ### 5.2. Legacy machinery (вне prod NS, но в репо) -| Модуль / config | Проблема | -|-----------------|----------| -| `test/bender/sys.config`, `test/party-management/sys.config` | `client => machinery_prg_backend` | -| `apps/ff_cth/src/ct_payment_system.erl` | мёртвый `{machinery_backend, progressor}` | +| Модуль / config | Статус | +|-----------------|--------| +| `test/bender/sys.config`, `test/party-management/sys.config` | **намеренно** `machinery_prg_backend` — docker-sidecar сервисы bender / party-management (вне scope HG/FF) | +| `apps/ff_cth/src/ct_payment_system.erl` | **очищено** — убран мёртвый `{machinery_backend, progressor}` | | `apps/machinery_extra/` | остаётся для `machinery_msgpack` в FF transfer и тестах | +| `rebar.config` damsel pin | ждёт `progressor_trace.thrift` в damsel — см. `docs/trace-api-thrift.md` | ### 5.3. Trace API -- Сейчас: FF internal HTTP JSON (`ff_machine_handler` → `prg_machine:trace/2`) +- Сейчас: FF internal HTTP JSON (`ff_machine_handler` → `progressor:trace/1`) - Цель (отдельно): Thrift по `progressor_trace.thrift`, см. `docs/trace-api-thrift.md` - В git status были черновики `hg_progressor_trace*` — **не** в финальном дереве (app `hg_progressor` удалён) @@ -263,9 +268,13 @@ rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE ### 5.5. Технический долг в runtime -- `initial_model/2` — `_Handler` не используется; `model` в aux на практике не пишется +- ~~`marshal_intent` портит aux_state при отсутствии `auxst`~~ — **исправлено** (этап 1) +- ~~`initial_model/2` badmap на не-map aux_state~~ — **исправлено** (этап 1) +- ~~registry на пустом supervisor, `ets:lookup_element` badarg~~ — **исправлено** (`prg_machine_registry`, этап 3) +- ~~stacktrace теряется в `process/3`~~ — **исправлено** (4-tuple + log metadata, этап 3) - `binary_to_term` в decode без `[safe]` в fallback path `prg_machine` — стоит проверить - `hg_invoice` vs FF: унификация через `apply_event/4` в runtime (вариант C); HG migration — goal `goal-hg-collapse.md` +- L1: FF `marshal_event_body` оборачивает тело в фиктивный `{ev, {ts,0}, Body}` — косметика, не блокер ### 5.6. Grep-инварианты (целевые после полного P5) @@ -296,6 +305,8 @@ rg "client => machinery_prg_backend" config/sys.config # 0 | Путь | Зачем смотреть | |------|----------------| | `apps/prg_machine/src/prg_machine.erl` | behaviour, process/3, collapse | +| `apps/prg_machine/src/prg_machine_registry.erl` | ETS registry owner | +| `docs/prg-machine-review-plan.md` | поэтапный план ревью и доработок | | `apps/prg_machine/src/prg_machine_action.erl` | таймеры | | `config/sys.config` | все prod NS | | `apps/hellgate/src/hellgate.erl` | HG registry | @@ -315,4 +326,4 @@ rg "client => machinery_prg_backend" config/sys.config # 0 - **Open:** task 37 — full CT; review round 1 не закрыт (`review_phase_completed: false`) - **Устаревший артефакт:** `.ralph/summary.md` в ralph-2 — обновлён ссылкой на этот документ -*Дата отчёта: 2026-06-04* +*Дата отчёта: 2026-06-07* diff --git a/docs/prg-machine-review-plan.md b/docs/prg-machine-review-plan.md new file mode 100644 index 00000000..58596da8 --- /dev/null +++ b/docs/prg-machine-review-plan.md @@ -0,0 +1,80 @@ +# Ревью ветки `add_prg_layer` (vs `epic/monorepo`) и план доработки + +Строгое ревью миграции HG/FF на единый `prg_machine`-runtime поверх `progressor`. +Diff `+3725 / −3979`, 91 файл, 11 коммитов. `rebar3 compile` проходит, grep-инварианты по prod-путям чистые. + +Дата ревью: 2026-06-07. + +--- + +## Осознанные решения (не блокеры) + +- **`progressor_action` собирается из локального `_checkouts/progressor`** (`v1.0.24` + 1 локальный коммит), а `rebar.config` пинит `{tag, "v1.0.24"}`; `_checkouts/` в `.gitignore`, `rebar.lock` не лочит progressor. На чистом клоне сборка/xref упадут (`undefined module progressor_action`). **Решено оставить как есть на текущем этапе** — будет закрыто отдельно (апстрим-тег в `valitydev/progressor` либо вендоринг `progressor_action` в `apps/prg_machine`). +- **`ff_limit` остаётся на `-behaviour(machinery)`** + `machinery:get/call/start` → `machinery`/`machinery_extra` пока не удаляются. Отдельный goal. +- **Trace API** — сейчас FF internal HTTP JSON; перевод на Thrift (`docs/trace-api-thrift.md`) — отдельный goal. +- **Orphan NS** (`ff/identity`, `ff/wallet_v2`, HG `customer`, `recurrent_paytools`) — убраны из `sys.config`, вернутся отдельным PR при необходимости. + +--- + +## Блокеры перед merge + +| # | Проблема | Действие | +|---|----------|----------| +| B2 | CT не прогонялись (full-CT не завершена) | Поднять инфру и прогнать suites (см. Этап 2) | +| B3 | Нет PR, ветка не влита в `epic/monorepo` | Открыть PR после зелёных gate+CT (Этап 5) | + +--- + +## Высокий приоритет (корректность рантайма) + +### H1. Порча `aux_state` на exception-пути HG invoice +Подтверждено: `progressor` (`prg_worker.erl:624-630`) персистит `aux_state` из результата, если ключ присутствует. +Exception-ветка call в `hg_invoice.erl:412` возвращает голый `#{}` мимо `to_prg_result`; `prg_machine:marshal_intent/3` подставляет `auxst => undefined` → `marshal_aux_state(undefined)` даёт непустой бинарь → progressor перезаписывает `aux_state`. На следующем `collapse` `initial_model/2` делает `maps:get(model, undefined, _)` → **badmap**, машина уходит в error. +Бизнес-exception в invoice-call — штатная частая ситуация → ломает invoice после первого отклонённого вызова. Та же дыра: `dispatch_notification` без `process_notification/2` возвращает `#{}`. + +### H2. `initial_model/2` не защищён от не-map `aux_state` +Любой путь, дающий `aux_state` ≠ map, валит `collapse`. Нужен guard `when is_map(...)` с fallback в `undefined`. + +--- + +## Средний приоритет + +- **M1.** `prg_machine:marshal_intent` всегда эмитит `aux_state`, даже когда домен его не возвращал. Эмитить ключ только при явном `auxst` от домена (тогда progressor сохранит прежнее значение) — системное исправление H1/H2. +- **M2.** Реестр namespace на ETS, owner — «пустой» супервизор без детей. Падение процесса = потеря таблицы = краш `ets:lookup_element` во всех `get_handler_module/1`. Сделать owner устойчивым (`heir`/gen_server) + понятная ошибка `{unknown_namespace, NS}`. +- **M3.** `process/3` заворачивает доменные ошибки в `{error, {exception, Class, Reason}}`, теряя stacktrace. Сохранять stacktrace в лог/мету. +- **M4.** Мёртвый/legacy machinery-конфиг в тест-фикстурах: `ct_payment_system.erl:86` `{machinery_backend, progressor}`; `test/bender/sys.config`, `test/party-management/sys.config` на `machinery_prg_backend`. Убрать/задокументировать. + +--- + +## Низкий приоритет + +- **L1.** FF `marshal_event_body` оборачивает тело в фиктивный `{ev, {ts,0}, Body}` (двойной timestamp, реальный — из storage). Косметика. +- **L2.** Доки: `prg-machine-migration-context.md` местами устарел (пишет про `epic/monorepo`, хотя сейчас `add_prg_layer`). Синхронизировать. +- **L3.** `rebar.config:39` TODO про bump damsel-тега после релиза `progressor_trace.thrift` — связать с Trace-API goal. + +--- + +## Пошаговый план + +### Этап 1 — корректность рантайма (H1, H2, M1) — можно делать в коде сразу +1. `prg_machine:marshal_intent/3`: эмитить ключ `aux_state` **только** когда домен вернул `auxst`; при отсутствии — не трогать сохранённый aux_state. +2. `prg_machine:initial_model/2`: guard `when is_map(AuxState)`, иначе `undefined`. +3. `hg_invoice.erl:412`: exception-ветку вернуть через `to_prg_result(#{})` (или явный `#{auxst => #{}}`). +4. Тест: invoice-call с бизнес-exception, затем `timeout`/повторный call — машина не должна уходить в error. Аналогично `notify` без `process_notification/2`. + +### Этап 2 — CT (B2) +5. Поднять инфру (postgres/progressor, party-management, dmt, bender), прогнать минимум: + - `ff_deposit_handler_SUITE`, `ff_withdrawal_handler_SUITE`, `ff_withdrawal_session_repair_SUITE` + - `hg_invoice_lite_tests_SUITE`, `hg_invoice_tests_SUITE`, `hg_invoice_template_tests_SUITE`, `hg_direct_recurrent_tests_SUITE` +6. Зафиксировать результаты. До зелёного CT merge не делать. + +### Этап 3 — устойчивость рантайма (M2, M3) +7. Упрочнить реестр namespace (устойчивый owner таблицы + понятная ошибка при отсутствии NS). +8. Сохранять stacktrace в `process/3` структурированно. + +### Этап 4 — чистка хвостов (M4, L*) +9. Убрать мёртвый `{machinery_backend, progressor}` из `ct_payment_system.erl`; решить судьбу `machinery_prg_backend` в `test/bender`, `test/party-management`. +10. Обновить доки (`prg-machine-migration-context.md`), связать L3 с Trace-API goal. + +### Этап 5 — PR (B3) +11. Открыть PR `add_prg_layer → epic/monorepo` после зелёных `compile`/`xref`/`lint`/`dialyzer`/CT, с описанием миграции и списком осознанных хвостов. diff --git a/docs/trace-api-thrift.md b/docs/trace-api-thrift.md index 7ff44e5f..12af6604 100644 --- a/docs/trace-api-thrift.md +++ b/docs/trace-api-thrift.md @@ -36,9 +36,11 @@ |------|----------------| | HTTP | Cowboy (не Woody): `GET /traces/internal/{entity}/{process_id}` | | Handler | `apps/ff_server/src/ff_machine_handler.erl` | -| Домен | `ff_machine:trace/2` → `machinery:trace/3` → `machinery_prg_backend:trace/3` | -| Декодирование | `ff_*_machinery_schema` + codec (`ff_deposit_codec`, …) | -| Ответ | `json:encode/1` после `ff_machine:json_compatible_value/1` | +| Домен | `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`): diff --git a/rebar.config b/rebar.config index d280de77..b4baffdb 100644 --- a/rebar.config +++ b/rebar.config @@ -36,7 +36,7 @@ {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.2"}}}, {scoper, {git, "https://github.com/valitydev/scoper.git", {tag, "v1.1.0"}}}, {thrift, {git, "https://github.com/valitydev/thrift_erlang.git", {tag, "v1.0.0"}}}, - %% TODO: bump tag after progressor_trace.thrift is released in damsel + %% Trace API goal: bump damsel after progressor_trace.thrift release — docs/trace-api-thrift.md {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.33"}}}, {payproc_errors, {git, "https://github.com/valitydev/payproc-errors-erlang.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, diff --git a/test/bender/sys.config b/test/bender/sys.config index 5993022f..96516ede 100644 --- a/test/bender/sys.config +++ b/test/bender/sys.config @@ -1,6 +1,5 @@ [ {bender, [ - {machinery_backend, progressor}, {services, #{ bender => #{path => <<"/v1/bender">>}, generator => #{path => <<"/v1/generator">>} @@ -119,6 +118,7 @@ worker_pool_size => 100, process_step_timeout => 30 }}, + %% Docker-sidecar bender (вне HG/FF): machinery_prg_backend — намеренно, не prg_machine. {namespaces, #{ 'bender_generator' => #{ processor => #{ diff --git a/test/party-management/sys.config b/test/party-management/sys.config index 732034a0..13310aa5 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 }}, + %% Docker-sidecar party-management (вне HG/FF): machinery_prg_backend — намеренно, не prg_machine. {namespaces, #{ 'party' => #{ processor => #{ From fd7dacf09eb4103800647f7a61de69d7bf55e3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sun, 7 Jun 2026 20:07:50 +0300 Subject: [PATCH 14/62] Refactor event handling in ff_transfer modules to unify timestamping and improve notification processing. Remove machinery_extra application and related code, streamlining the codebase. Update dependencies in rebar.config to reflect the removal of obsolete machinery references and enhance overall consistency in event marshaling. --- apps/ff_core/src/ff_core.app.src | 4 +- apps/ff_core/src/ff_machine_schema.erl | 69 ++++ .../src/ff_msgpack.erl} | 15 +- apps/ff_server/src/ff_deposit_codec.erl | 2 +- apps/ff_transfer/src/ff_destination.erl | 4 +- apps/ff_transfer/src/ff_machine_codec.erl | 12 +- apps/ff_transfer/src/ff_source.erl | 4 +- apps/ff_transfer/src/ff_transfer.app.src | 2 - apps/ff_transfer/src/ff_withdrawal.erl | 2 +- .../ff_transfer/src/ff_withdrawal_session.erl | 4 +- .../src/machinery_extra.app.src | 17 - .../src/machinery_gensrv_backend.erl | 346 ------------------ .../src/machinery_gensrv_backend_sup.erl | 43 --- apps/machinery_extra/src/machinery_time.erl | 33 -- docs/prg-machine-migration-context.md | 8 +- docs/prg-machine-remaining-debt.md | 48 +++ docs/prg-machine-review-plan.md | 24 +- elvis.config | 7 +- 18 files changed, 154 insertions(+), 490 deletions(-) create mode 100644 apps/ff_core/src/ff_machine_schema.erl rename apps/{machinery_extra/src/machinery_msgpack.erl => ff_core/src/ff_msgpack.erl} (85%) delete mode 100644 apps/machinery_extra/src/machinery_extra.app.src delete mode 100644 apps/machinery_extra/src/machinery_gensrv_backend.erl delete mode 100644 apps/machinery_extra/src/machinery_gensrv_backend_sup.erl delete mode 100644 apps/machinery_extra/src/machinery_time.erl create mode 100644 docs/prg-machine-remaining-debt.md diff --git a/apps/ff_core/src/ff_core.app.src b/apps/ff_core/src/ff_core.app.src index 6bf479a1..4c3921d4 100644 --- a/apps/ff_core/src/ff_core.app.src +++ b/apps/ff_core/src/ff_core.app.src @@ -5,7 +5,9 @@ {applications, [ kernel, stdlib, - genlib + genlib, + mg_proto, + thrift ]}, {env, []}, {modules, []}, diff --git a/apps/ff_core/src/ff_machine_schema.erl b/apps/ff_core/src/ff_machine_schema.erl new file mode 100644 index 00000000..bb526532 --- /dev/null +++ b/apps/ff_core/src/ff_machine_schema.erl @@ -0,0 +1,69 @@ +%%% +%%% Storage schema for arbitrary persistent Erlang terms (aux_state wire format). + +-module(ff_machine_schema). + +-import(ff_msgpack, [ + nil/0, + wrap/1, + unwrap/1 +]). + +-export([marshal/1]). +-export([unmarshal/1]). + +-type eterm() :: + atom() + | number() + | tuple() + | binary() + | list() + | map(). + +-spec marshal(eterm()) -> ff_msgpack:t(). +marshal(undefined) -> + nil(); +marshal(V) when is_boolean(V) -> + wrap(V); +marshal(V) when is_atom(V) -> + wrap(atom_to_binary(V, utf8)); +marshal(V) when is_number(V) -> + wrap(V); +marshal(V) when is_binary(V) -> + wrap({binary, V}); +marshal([]) -> + wrap([]); +marshal(V) when is_list(V) -> + wrap([marshal(lst) | lists:map(fun marshal/1, V)]); +marshal(V) when is_tuple(V) -> + wrap([marshal(tup) | lists:map(fun marshal/1, tuple_to_list(V))]); +marshal(V) when is_map(V) -> + wrap([marshal(map), wrap(genlib_map:truemap(fun(Ke, Ve) -> {marshal(Ke), marshal(Ve)} end, V))]); +marshal(V) -> + erlang:error(badarg, [V]). + +-spec unmarshal(ff_msgpack:t()) -> eterm(). +unmarshal(M) -> + unmarshal_v(unwrap(M)). + +unmarshal_v(nil) -> + undefined; +unmarshal_v(V) when is_boolean(V) -> + V; +unmarshal_v(V) when is_binary(V) -> + binary_to_existing_atom(V, utf8); +unmarshal_v(V) when is_number(V) -> + V; +unmarshal_v({binary, V}) -> + V; +unmarshal_v([]) -> + []; +unmarshal_v([Ty | Vs]) -> + unmarshal_v(unmarshal(Ty), Vs). + +unmarshal_v(lst, Vs) -> + lists:map(fun unmarshal/1, Vs); +unmarshal_v(tup, Es) -> + list_to_tuple(unmarshal_v(lst, Es)); +unmarshal_v(map, [V]) -> + genlib_map:truemap(fun(Ke, Ve) -> {unmarshal(Ke), unmarshal(Ve)} end, unwrap(V)). diff --git a/apps/machinery_extra/src/machinery_msgpack.erl b/apps/ff_core/src/ff_msgpack.erl similarity index 85% rename from apps/machinery_extra/src/machinery_msgpack.erl rename to apps/ff_core/src/ff_msgpack.erl index 261315c7..2f6233c4 100644 --- a/apps/machinery_extra/src/machinery_msgpack.erl +++ b/apps/ff_core/src/ff_msgpack.erl @@ -1,13 +1,10 @@ %%% -%%% Msgpack manipulation employed by machinegun interfaces. -%%% Extended in hellgate with pack/1 for thrift Value wire encoding. +%%% Msgpack wire encoding for machine aux-state and legacy machinegun payloads. --module(machinery_msgpack). +-module(ff_msgpack). -include_lib("mg_proto/include/mg_proto_msgpack_thrift.hrl"). -%% API - -export([wrap/1]). -export([unwrap/1]). -export([nil/0]). @@ -17,16 +14,12 @@ -export_type([t/0]). -%% - -spec wrap (nil) -> t(); (boolean()) -> t(); (integer()) -> t(); (float()) -> t(); - %% string (binary()) -> t(); - %% binary ({binary, binary()}) -> t(); ([t()]) -> t(); (#{t() => t()}) -> t(). @@ -39,7 +32,6 @@ wrap(V) when is_integer(V) -> wrap(V) when is_float(V) -> V; wrap(V) when is_binary(V) -> - % Assuming well-formed UTF-8 bytestring. {str, V}; wrap({binary, V}) when is_binary(V) -> {bin, V}; @@ -53,9 +45,7 @@ wrap(V) when is_map(V) -> | boolean() | integer() | float() - %% string | binary() - %% binary | {binary, binary()} | [t()] | #{t() => t()}. @@ -68,7 +58,6 @@ unwrap({i, V}) when is_integer(V) -> unwrap({flt, V}) when is_float(V) -> V; unwrap({str, V}) when is_binary(V) -> - % Assuming well-formed UTF-8 bytestring. V; unwrap({bin, V}) when is_binary(V) -> {binary, V}; 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_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index ae12441e..47b8b5a7 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -280,11 +280,11 @@ process_repair(Scenario, Machine) -> -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{}. + #{events => [], action => progressor_action:instant()}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, ff_time:now(), Body}, + Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, Encoded = ff_machine_codec:marshal_event(destination, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl index 3b59c699..23f7c0d4 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -10,7 +10,7 @@ -type format_version() :: pos_integer(). -type timestamped_event() :: {ev, term(), term()}. --spec marshal_event(domain(), format_version(), timestamped_event()) -> machinery_msgpack:t(). +-spec marshal_event(domain(), format_version(), timestamped_event()) -> ff_msgpack:t(). marshal_event(deposit, 1, Timestamped) -> marshal_thrift_event( Timestamped, @@ -90,17 +90,17 @@ unmarshal_event(Domain, Format, _Payload) -> -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - payload_to_binary(machinery_mg_schema_generic:marshal(AuxSt)). + payload_to_binary(ff_machine_schema:marshal(AuxSt)). -spec unmarshal_aux_state(binary()) -> term(). unmarshal_aux_state(Payload) when is_binary(Payload) -> - machinery_mg_schema_generic:unmarshal({bin, Payload}). + ff_machine_schema:unmarshal({bin, Payload}). --spec payload_to_binary(machinery_msgpack:t()) -> binary(). +-spec payload_to_binary(ff_msgpack:t()) -> binary(). payload_to_binary({bin, Bin}) when is_binary(Bin) -> Bin; payload_to_binary(Payload) -> - {ok, Bin} = machinery_msgpack:pack(Payload), + {ok, Bin} = ff_msgpack:pack(Payload), Bin. -spec marshal_thrift_event( @@ -108,7 +108,7 @@ payload_to_binary(Payload) -> fun((timestamped_event()) -> term()), atom(), atom() -) -> machinery_msgpack:t(). +) -> ff_msgpack:t(). marshal_thrift_event(Timestamped, MarshalFun, ThriftModule, ThriftStruct) -> ThriftChange = MarshalFun(Timestamped), Type = {struct, struct, {ThriftModule, ThriftStruct}}, diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index 58a34a75..1b20faa2 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -256,11 +256,11 @@ process_repair(Scenario, Machine) -> -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{}. + #{events => [], action => progressor_action:instant()}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, ff_time:now(), Body}, + Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, Encoded = ff_machine_codec:marshal_event(source, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. diff --git a/apps/ff_transfer/src/ff_transfer.app.src b/apps/ff_transfer/src/ff_transfer.app.src index ca8dcbdd..76583367 100644 --- a/apps/ff_transfer/src/ff_transfer.app.src +++ b/apps/ff_transfer/src/ff_transfer.app.src @@ -9,8 +9,6 @@ ff_core, progressor, prg_machine, - machinery, - machinery_extra, damsel, fistful, limiter_proto diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 431a8527..98505643 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -1888,7 +1888,7 @@ process_notification({session_finished, SessionID, SessionResult}, Machine) -> -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, ff_time:now(), Body}, + Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, Encoded = ff_machine_codec:marshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index c79f0f97..031c5d33 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -442,11 +442,11 @@ process_repair(Scenario, Machine) -> -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{}. + #{events => [], action => progressor_action:instant()}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, ff_time:now(), Body}, + Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, Encoded = ff_machine_codec:marshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. 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/docs/prg-machine-migration-context.md b/docs/prg-machine-migration-context.md index d5b0eb2f..5e1174f2 100644 --- a/docs/prg-machine-migration-context.md +++ b/docs/prg-machine-migration-context.md @@ -253,7 +253,7 @@ rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE |-----------------|--------| | `test/bender/sys.config`, `test/party-management/sys.config` | **намеренно** `machinery_prg_backend` — docker-sidecar сервисы bender / party-management (вне scope HG/FF) | | `apps/ff_cth/src/ct_payment_system.erl` | **очищено** — убран мёртвый `{machinery_backend, progressor}` | -| `apps/machinery_extra/` | остаётся для `machinery_msgpack` в FF transfer и тестах | +| `apps/machinery_extra/` | **удалён** — codec-слой в `ff_core` (`ff_msgpack`, `ff_machine_schema`) | | `rebar.config` damsel pin | ждёт `progressor_trace.thrift` в damsel — см. `docs/trace-api-thrift.md` | ### 5.3. Trace API @@ -272,9 +272,9 @@ rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE - ~~`initial_model/2` badmap на не-map aux_state~~ — **исправлено** (этап 1) - ~~registry на пустом supervisor, `ets:lookup_element` badarg~~ — **исправлено** (`prg_machine_registry`, этап 3) - ~~stacktrace теряется в `process/3`~~ — **исправлено** (4-tuple + log metadata, этап 3) -- `binary_to_term` в decode без `[safe]` в fallback path `prg_machine` — стоит проверить -- `hg_invoice` vs FF: унификация через `apply_event/4` в runtime (вариант C); HG migration — goal `goal-hg-collapse.md` -- L1: FF `marshal_event_body` оборачивает тело в фиктивный `{ev, {ts,0}, Body}` — косметика, не блокер +- ~~`binary_to_term` без `[safe]`~~ — **закрыто** +- `hg_invoice` двойной collapse — отложено (см. `prg-machine-remaining-debt.md` §5) +- ~~L1: разные timestamp в `marshal_event_body`~~ — **закрыто** (единый `{prg_machine:timestamp(), 0}`) ### 5.6. Grep-инварианты (целевые после полного P5) diff --git a/docs/prg-machine-remaining-debt.md b/docs/prg-machine-remaining-debt.md new file mode 100644 index 00000000..0744b180 --- /dev/null +++ b/docs/prg-machine-remaining-debt.md @@ -0,0 +1,48 @@ +# `add_prg_layer`: что осталось и техдолг + +Актуально для ветки `add_prg_layer` → merge target `epic/monorepo`. + +## Закрыто + +| # | Пункт | Что сделано | +|---|-------|-------------| +| 1 | Зависимость `machinery` в FF prod | `ff_msgpack` + `ff_machine_schema` в `ff_core`; `machinery`/`machinery_extra` убраны из `ff_transfer.app.src`; app `machinery_extra` удалён | +| 2 | Мёртвый код в `machinery_extra` | `machinery_gensrv_backend*` удалены вместе с app | +| 3 | FF `marshal_event_body` — разные timestamp | Все FF-домены: `{prg_machine:timestamp(), 0}` | +| 4 | FF `process_notification` — пустые `#{}` | Явный noop: `#{events => [], action => progressor_action:instant()}` | +| 7 | `binary_to_term` без `[safe]` | Было закрыто ранее | +| — | Elvis / docs мусор | Убраны ignore для несуществующих модулей; review-plan и migration-context синхронизированы | + +`{machinery, …}` в корневом `rebar.config` **остаётся** — только для docker-sidecar тестов (`test/bender`, `test/party-management` → `machinery_prg_backend`). + +--- + +## Открытый техдолг (низкий приоритет) + +### 5. HG invoice — двойной collapse + +- `prg_machine:collapse` → `apply_event/4` → `collapse_changes` +- `handle_call` → `validate_changes` → снова `collapse_changes` по in-memory state + +Работает, но дублирование. Целевой паттерн — один путь через `prg_machine:collapse` (отдельный goal). + +### 6. Registry без ETS `heir` + +`prg_machine_registry` при падении пересоздаёт таблицу и перерегистрирует handlers из child_spec. Краткое окно без таблицы теоретически возможно. + +**Действие:** `heir` на supervisor — только если понадобится zero-downtime. + +### L1 (косметика). Фиктивная обёртка `{ev, Ts, Body}` в payload + +Progressor ставит timestamp в storage; в thrift-payload ts по-прежнему фиктивный, но единообразный. Полный отказ от обёртки потребует смены wire-формата `TimestampedChange`. + +--- + +## Grep-инварианты (prod FF/HG) + +```bash +rg 'machinery_msgpack|machinery_extra|machinery_time' apps/fistful apps/ff_transfer apps/ff_server --glob '*.erl' # 0 +rg 'machinery' apps/ff_transfer/src/ff_transfer.app.src # 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 +``` diff --git a/docs/prg-machine-review-plan.md b/docs/prg-machine-review-plan.md index 58596da8..1f7b0185 100644 --- a/docs/prg-machine-review-plan.md +++ b/docs/prg-machine-review-plan.md @@ -10,7 +10,7 @@ Diff `+3725 / −3979`, 91 файл, 11 коммитов. `rebar3 compile` пр ## Осознанные решения (не блокеры) - **`progressor_action` собирается из локального `_checkouts/progressor`** (`v1.0.24` + 1 локальный коммит), а `rebar.config` пинит `{tag, "v1.0.24"}`; `_checkouts/` в `.gitignore`, `rebar.lock` не лочит progressor. На чистом клоне сборка/xref упадут (`undefined module progressor_action`). **Решено оставить как есть на текущем этапе** — будет закрыто отдельно (апстрим-тег в `valitydev/progressor` либо вендоринг `progressor_action` в `apps/prg_machine`). -- **`ff_limit` остаётся на `-behaviour(machinery)`** + `machinery:get/call/start` → `machinery`/`machinery_extra` пока не удаляются. Отдельный goal. +- **`machinery` в корневом `rebar.config`** — только docker-sidecar тесты (`machinery_prg_backend` в `test/bender`, `test/party-management`). FF prod-путь на `ff_core` (`ff_msgpack`, `ff_machine_schema`). - **Trace API** — сейчас FF internal HTTP JSON; перевод на Thrift (`docs/trace-api-thrift.md`) — отдельный goal. - **Orphan NS** (`ff/identity`, `ff/wallet_v2`, HG `customer`, `recurrent_paytools`) — убраны из `sys.config`, вернутся отдельным PR при необходимости. @@ -25,31 +25,31 @@ Diff `+3725 / −3979`, 91 файл, 11 коммитов. `rebar3 compile` пр --- -## Высокий приоритет (корректность рантайма) +## Высокий приоритет (корректность рантайма) — **закрыто** -### H1. Порча `aux_state` на exception-пути HG invoice +### H1. Порча `aux_state` на exception-пути HG invoice — **исправлено** Подтверждено: `progressor` (`prg_worker.erl:624-630`) персистит `aux_state` из результата, если ключ присутствует. Exception-ветка call в `hg_invoice.erl:412` возвращает голый `#{}` мимо `to_prg_result`; `prg_machine:marshal_intent/3` подставляет `auxst => undefined` → `marshal_aux_state(undefined)` даёт непустой бинарь → progressor перезаписывает `aux_state`. На следующем `collapse` `initial_model/2` делает `maps:get(model, undefined, _)` → **badmap**, машина уходит в error. Бизнес-exception в invoice-call — штатная частая ситуация → ломает invoice после первого отклонённого вызова. Та же дыра: `dispatch_notification` без `process_notification/2` возвращает `#{}`. -### H2. `initial_model/2` не защищён от не-map `aux_state` -Любой путь, дающий `aux_state` ≠ map, валит `collapse`. Нужен guard `when is_map(...)` с fallback в `undefined`. +### H2. `initial_model/2` не защищён от не-map `aux_state` — **исправлено** +Guard `when is_map(...)` с fallback в `undefined`. --- -## Средний приоритет +## Средний приоритет — **закрыто** -- **M1.** `prg_machine:marshal_intent` всегда эмитит `aux_state`, даже когда домен его не возвращал. Эмитить ключ только при явном `auxst` от домена (тогда progressor сохранит прежнее значение) — системное исправление H1/H2. -- **M2.** Реестр namespace на ETS, owner — «пустой» супервизор без детей. Падение процесса = потеря таблицы = краш `ets:lookup_element` во всех `get_handler_module/1`. Сделать owner устойчивым (`heir`/gen_server) + понятная ошибка `{unknown_namespace, NS}`. -- **M3.** `process/3` заворачивает доменные ошибки в `{error, {exception, Class, Reason}}`, теряя stacktrace. Сохранять stacktrace в лог/мету. -- **M4.** Мёртвый/legacy machinery-конфиг в тест-фикстурах: `ct_payment_system.erl:86` `{machinery_backend, progressor}`; `test/bender/sys.config`, `test/party-management/sys.config` на `machinery_prg_backend`. Убрать/задокументировать. +- **M1.** `marshal_intent` эмитит `aux_state` только при явном `auxst` — **исправлено**. +- **M2.** Реестр namespace — `prg_machine_registry` (gen_server owner) + `{unknown_namespace, NS}` — **исправлено**. ETS `heir` — отложено (см. `prg-machine-remaining-debt.md` §6). +- **M3.** Stacktrace в `process/3` (4-tuple + log metadata) — **исправлено**. +- **M4.** `ct_payment_system` очищен; `test/bender` / `test/party-management` на `machinery_prg_backend` — **намеренно** (docker-sidecar, вне HG/FF). --- ## Низкий приоритет -- **L1.** FF `marshal_event_body` оборачивает тело в фиктивный `{ev, {ts,0}, Body}` (двойной timestamp, реальный — из storage). Косметика. -- **L2.** Доки: `prg-machine-migration-context.md` местами устарел (пишет про `epic/monorepo`, хотя сейчас `add_prg_layer`). Синхронизировать. +- **L1.** FF `marshal_event_body` — timestamp унифицирован (`{prg_machine:timestamp(), 0}`); фиктивная обёртка в wire остаётся — косметика. +- **L2.** Доки синхронизированы (`prg-machine-migration-context.md`, `prg-machine-remaining-debt.md`). - **L3.** `rebar.config:39` TODO про bump damsel-тега после релиза `progressor_trace.thrift` — связать с Trace-API goal. --- diff --git a/elvis.config b/elvis.config index bef15c89..bff14eb7 100644 --- a/elvis.config +++ b/elvis.config @@ -43,16 +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_identity_machinery_schema, - ff_wallet_machinery_schema + hg_routing ] }}, {elvis_style, max_function_arity, #{max_arity => 10}}, From 51675e4ae29e3b33783d136cf6bca8892d367325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 8 Jun 2026 14:44:11 +0300 Subject: [PATCH 15/62] Enhance hg_invoice module by introducing a new handler_result type for improved result handling in process_call. Update to_prg_result function to ensure proper handling of changes and action without auxiliary state. Refactor cleanup calls in test suites to use hg_domain:cleanup for consistency across tests. --- apps/hellgate/src/hg_invoice.erl | 40 ++++++++++++------- .../test/hg_direct_recurrent_tests_SUITE.erl | 2 +- .../test/hg_invoice_lite_tests_SUITE.erl | 2 +- .../test/hg_invoice_template_tests_SUITE.erl | 2 +- apps/hellgate/test/hg_invoice_tests_SUITE.erl | 2 +- docs/prg-machine-review-plan.md | 2 +- test/bender/sys.config | 1 + 7 files changed, 31 insertions(+), 20 deletions(-) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index d10b2c13..3a016a4f 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -398,6 +398,14 @@ handle_expiration(St) -> 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(), machine()) -> {prg_machine:response(), prg_result()}. process_call(Call, Machine) -> @@ -409,7 +417,7 @@ process_call(Call, Machine) -> {call_response(Response), to_prg_result(CallResult)} catch throw:Exception -> - {{exception, Exception}, to_prg_result(#{})} + {{exception, Exception}, #{}} end. -spec handle_call(call(), st()) -> call_result(). @@ -697,24 +705,26 @@ wrap_payment_impact(PaymentID, {Response, {Changes, Action}}, St, OccurredAt) -> state => St }. --spec to_prg_result(map()) -> prg_result(). +-spec to_prg_result(handler_result()) -> prg_result(). to_prg_result(Result) -> _ = validate_changes(Result), to_prg_result_(Result). -to_prg_result_(#{changes := Changes = [_ | _]} = Result) -> - genlib_map:compact(#{ - events => [Changes], - action => maps:get(action, Result, undefined), - auxst => maps:get(auxst, Result, #{}) - }); -to_prg_result_(#{action := Action} = Result) -> - genlib_map:compact(#{ - action => Action, - auxst => maps:get(auxst, Result, #{}) - }); -to_prg_result_(#{}) -> - #{auxst => #{}}. +%% 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. -spec call_response(ok | term()) -> prg_machine:response(). call_response(ok) -> diff --git a/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl b/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl index 44647285..6361178a 100644 --- a/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl +++ b/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl @@ -164,7 +164,7 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> config(). end_per_suite(C) -> - _ = hg_domain:cleanup_hellgate(), + _ = hg_domain:cleanup(), _ = application:stop(progressor), _ = hg_ct_helper:cleanup_progressor_namespaces(), [application:stop(App) || App <- cfg(apps, C)]. diff --git a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl index dda26217..ba7612c5 100644 --- a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl @@ -121,7 +121,7 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> _. end_per_suite(C) -> - _ = hg_domain:cleanup_hellgate(), + _ = hg_domain:cleanup(), _ = application:stop(progressor), _ = hg_ct_helper:cleanup_progressor_namespaces(), _ = [application:stop(App) || App <- cfg(apps, C)], diff --git a/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl index 79e79941..b1de5225 100644 --- a/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl @@ -110,7 +110,7 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> _. end_per_suite(C) -> - _ = hg_domain:cleanup_hellgate(), + _ = hg_domain:cleanup(), _ = application:stop(progressor), _ = hg_ct_helper:cleanup_progressor_namespaces(), [application:stop(App) || App <- cfg(apps, C)]. diff --git a/apps/hellgate/test/hg_invoice_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_tests_SUITE.erl index cfee2339..c04a3191 100644 --- a/apps/hellgate/test/hg_invoice_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_tests_SUITE.erl @@ -573,7 +573,7 @@ init_per_suite(C) -> -spec end_per_suite(config()) -> _. end_per_suite(C) -> - _ = hg_domain:cleanup_hellgate(), + _ = hg_domain:cleanup(), _ = application:stop(progressor), _ = hg_ct_helper:cleanup_progressor_namespaces(), _ = [application:stop(App) || App <- cfg(apps, C)], diff --git a/docs/prg-machine-review-plan.md b/docs/prg-machine-review-plan.md index 1f7b0185..59bb9942 100644 --- a/docs/prg-machine-review-plan.md +++ b/docs/prg-machine-review-plan.md @@ -29,7 +29,7 @@ Diff `+3725 / −3979`, 91 файл, 11 коммитов. `rebar3 compile` пр ### H1. Порча `aux_state` на exception-пути HG invoice — **исправлено** Подтверждено: `progressor` (`prg_worker.erl:624-630`) персистит `aux_state` из результата, если ключ присутствует. -Exception-ветка call в `hg_invoice.erl:412` возвращает голый `#{}` мимо `to_prg_result`; `prg_machine:marshal_intent/3` подставляет `auxst => undefined` → `marshal_aux_state(undefined)` даёт непустой бинарь → progressor перезаписывает `aux_state`. На следующем `collapse` `initial_model/2` делает `maps:get(model, undefined, _)` → **badmap**, машина уходит в error. +Exception-ветка call в `hg_invoice.erl` возвращает голый `#{}` (без `auxst`); `marshal_intent/3` не эмитит `aux_state` → progressor сохраняет прежний aux_state. Раньше `to_prg_result(#{})` падал в `validate_changes` и/или насильно ставил `auxst => #{}`. Бизнес-exception в invoice-call — штатная частая ситуация → ломает invoice после первого отклонённого вызова. Та же дыра: `dispatch_notification` без `process_notification/2` возвращает `#{}`. ### H2. `initial_model/2` не защищён от не-map `aux_state` — **исправлено** diff --git a/test/bender/sys.config b/test/bender/sys.config index 96516ede..6c2d1c15 100644 --- a/test/bender/sys.config +++ b/test/bender/sys.config @@ -1,5 +1,6 @@ [ {bender, [ + {machinery_backend, progressor}, {services, #{ bender => #{path => <<"/v1/bender">>}, generator => #{path => <<"/v1/generator">>} From b4014441135694399beb712dc95cd791b85b0662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 8 Jun 2026 15:23:06 +0300 Subject: [PATCH 16/62] Enhance payment processing logic in hg_dummy_provider by introducing a dynamic maximum pending polls limit based on payment scenarios. Update transaction state handling to improve failure conditions. Refactor exception handling in prg_machine to conform to new progressor standards, removing stacktrace from exception tuples for cleaner error reporting. --- apps/hellgate/test/hg_dummy_provider.erl | 8 +++++++- apps/prg_machine/src/prg_machine.erl | 21 +++++++++++---------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/hellgate/test/hg_dummy_provider.erl b/apps/hellgate/test/hg_dummy_provider.erl index 5725d5d9..4035e0fc 100644 --- a/apps/hellgate/test/hg_dummy_provider.erl +++ b/apps/hellgate/test/hg_dummy_provider.erl @@ -268,10 +268,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 Count > MaxPending -> finish(failure(authorization_failed), undefined); {pending, Count} -> set_transaction_state(Key, {pending, Count + 1}), @@ -862,6 +863,11 @@ set_transaction_state(Key, Value) -> get_transaction_state(Key) -> hg_kv_store:get(Key). +offsite_preauth_max_pending_polls(preauth_3ds_offsite) -> + 5; +offsite_preauth_max_pending_polls(_) -> + 2. + maybe_sleep(#{<<"sleep_ms">> := TimeMs}) when is_binary(TimeMs) -> timer:sleep(binary_to_integer(TimeMs)); maybe_sleep(_Opts) -> diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 35f550a3..22096a60 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -6,7 +6,8 @@ -include_lib("progressor/include/progressor.hrl"). -define(TABLE, prg_machine_dispatch). --define(PROCESSOR_EXCEPTION(Class, Reason, Stacktrace), {exception, Class, Reason, Stacktrace}). +%% progressor is_retryable/5 and machinery_prg_backend expect a 3-tuple; stacktrace stays in logs only. +-define(PROCESSOR_EXCEPTION(Class, Reason, _Stacktrace), {exception, Class, Reason}). %% Types @@ -621,10 +622,10 @@ registry_runtime_test_() -> ?_test(process_unknown_namespace_returns_error()) ]}. --spec process_stacktrace_test_() -> _. -process_stacktrace_test_() -> +-spec process_exception_test_() -> _. +process_exception_test_() -> {setup, fun setup_aux_state_test/0, fun cleanup_aux_state_test/1, [ - ?_test(process_crash_includes_stacktrace()) + ?_test(process_crash_conforms_progressor_exception()) ]}. -spec noop_when_hooks_absent() -> _. @@ -809,8 +810,8 @@ process_unknown_namespace_returns_error() -> process({init, term_to_binary(#{}), Process}, Opts, <<>>) ). --spec process_crash_includes_stacktrace() -> _. -process_crash_includes_stacktrace() -> +-spec process_crash_conforms_progressor_exception() -> _. +process_crash_conforms_progressor_exception() -> Opts = #{ns => ?AUX_STATE_TEST_NS}, Process = #{ process_id => <<"crash-test">>, @@ -819,9 +820,9 @@ process_crash_includes_stacktrace() -> aux_state => undefined }, {ok, _} = process({init, term_to_binary(#{}), Process}, Opts, <<>>), - {error, {exception, error, deliberate_crash, Stacktrace}} = - process({call, term_to_binary(crash), Process}, Opts, <<>>), - ?assert(is_list(Stacktrace)), - ?assert(lists:keymember(prg_machine_aux_state_test_handler, 1, Stacktrace)). + ?assertEqual( + {error, {exception, error, deliberate_crash}}, + process({call, term_to_binary(crash), Process}, Opts, <<>>) + ). -endif. From 50bf84a76873a2c90caf23a543feaf100890a65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 8 Jun 2026 15:56:39 +0300 Subject: [PATCH 17/62] Enhance hg_dummy_provider to support offsite preauthorization failure scenarios by adding handling for preauth_3ds_offsite_fail. Update transaction state management and polling limits for offsite preauth flows. Refactor payment processing logic to reset state upon failure, improving overall robustness of payment handling. --- apps/hellgate/test/hg_dummy_provider.erl | 15 ++++++++++++--- apps/hellgate/test/hg_invoice_tests_SUITE.erl | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/hellgate/test/hg_dummy_provider.erl b/apps/hellgate/test/hg_dummy_provider.erl index 4035e0fc..ce3ecdb3 100644 --- a/apps/hellgate/test/hg_dummy_provider.erl +++ b/apps/hellgate/test/hg_dummy_provider.erl @@ -201,8 +201,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, @@ -272,7 +273,7 @@ process_payment(?processed(), <<"sleeping_with_user_interaction">>, PaymentInfo, case get_transaction_state(Key) of processed -> finish(success(PaymentInfo), Trx); - {pending, Count} when Count > MaxPending -> + {pending, Count} when is_integer(MaxPending), Count > MaxPending -> finish(failure(authorization_failed), undefined); {pending, Count} -> set_transaction_state(Key, {pending, Count + 1}), @@ -583,6 +584,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">>}}) -> @@ -638,6 +641,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 | forbidden @@ -664,6 +668,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 @@ -864,10 +869,14 @@ get_transaction_state(Key) -> hg_kv_store:get(Key). offsite_preauth_max_pending_polls(preauth_3ds_offsite) -> - 5; + 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_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_tests_SUITE.erl index c04a3191..5bc70aca 100644 --- a/apps/hellgate/test/hg_invoice_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_tests_SUITE.erl @@ -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), From c0f6bf760179739bed18e01848159a929a24a753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 9 Jun 2026 19:04:16 +0300 Subject: [PATCH 18/62] Add unpack function to ff_msgpack for binary deserialization; enhance ff_machine_codec with binary_to_payload function for improved payload handling; update ff_withdrawal to handle additional child result cases. --- apps/ff_core/src/ff_msgpack.erl | 21 +++++++++- apps/ff_cth/src/ct_payment_system.erl | 1 + apps/ff_transfer/src/ff_machine_codec.erl | 10 ++++- apps/ff_transfer/src/ff_transfer.app.src | 1 + apps/ff_transfer/src/ff_withdrawal.erl | 4 +- docs/prg-invoice-single-collapse-goal.md | 48 +++++++++++++++++++++++ 6 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 docs/prg-invoice-single-collapse-goal.md diff --git a/apps/ff_core/src/ff_msgpack.erl b/apps/ff_core/src/ff_msgpack.erl index 2f6233c4..51048907 100644 --- a/apps/ff_core/src/ff_msgpack.erl +++ b/apps/ff_core/src/ff_msgpack.erl @@ -9,6 +9,7 @@ -export([unwrap/1]). -export([nil/0]). -export([pack/1]). +-export([unpack/1]). -type t() :: mg_proto_msgpack_thrift:'Value'(). @@ -72,9 +73,17 @@ nil() -> -spec pack(t()) -> {ok, binary()}. pack(Value) -> - Type = {struct, union, {mg_proto_msgpack_thrift, 'Value'}}, + Type = value_type(), {ok, serialize(Type, Value)}. +-spec unpack(binary()) -> {ok, t()}. +unpack(Bin) when is_binary(Bin) -> + Type = value_type(), + {ok, deserialize(Type, Bin)}. + +value_type() -> + {struct, union, {mg_proto_msgpack_thrift, 'Value'}}. + serialize(Type, Data) -> {ok, Trans} = thrift_membuffer_transport:new(), {ok, Proto} = thrift_binary_protocol:new(Trans, [{strict_read, true}, {strict_write, true}]), @@ -85,3 +94,13 @@ serialize(Type, Data) -> {_NewProto, {error, Reason}} -> erlang:error({thrift, {protocol, Reason}}) end. + +deserialize(Type, Data) -> + {ok, Trans} = thrift_membuffer_transport:new(Data), + {ok, Proto} = thrift_binary_protocol:new(Trans, [{strict_read, true}, {strict_write, true}]), + case thrift_protocol:read(Proto, Type) of + {_NewProto, {ok, Result}} -> + Result; + {_NewProto, {error, Reason}} -> + erlang:error({thrift, {protocol, Reason}}) + end. diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index b25d7f8a..d5cd0fc3 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -149,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), diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl index 23f7c0d4..fb731f0f 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -5,6 +5,7 @@ -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -export([payload_to_binary/1]). +-export([binary_to_payload/1]). -type domain() :: deposit | source | destination | withdrawal | withdrawal_session. -type format_version() :: pos_integer(). @@ -93,8 +94,10 @@ marshal_aux_state(AuxSt) -> payload_to_binary(ff_machine_schema:marshal(AuxSt)). -spec unmarshal_aux_state(binary()) -> term(). +unmarshal_aux_state(<<>>) -> + #{}; unmarshal_aux_state(Payload) when is_binary(Payload) -> - ff_machine_schema:unmarshal({bin, Payload}). + ff_machine_schema:unmarshal(binary_to_payload(Payload)). -spec payload_to_binary(ff_msgpack:t()) -> binary(). payload_to_binary({bin, Bin}) when is_binary(Bin) -> @@ -103,6 +106,11 @@ payload_to_binary(Payload) -> {ok, Bin} = ff_msgpack:pack(Payload), Bin. +-spec binary_to_payload(binary()) -> ff_msgpack:t(). +binary_to_payload(Bin) when is_binary(Bin) -> + {ok, Payload} = ff_msgpack:unpack(Bin), + Payload. + -spec marshal_thrift_event( timestamped_event(), fun((timestamped_event()) -> term()), diff --git a/apps/ff_transfer/src/ff_transfer.app.src b/apps/ff_transfer/src/ff_transfer.app.src index 76583367..6ab8798c 100644 --- a/apps/ff_transfer/src/ff_transfer.app.src +++ b/apps/ff_transfer/src/ff_transfer.app.src @@ -8,6 +8,7 @@ genlib, ff_core, progressor, + operation_context, prg_machine, damsel, fistful, diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 98505643..770be58e 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -1020,7 +1020,9 @@ handle_child_result({undefined, Events} = Result, Withdrawal) -> {ok, Wallet} = fetch_wallet(wallet_id(Withdrawal), party_id(Withdrawal), DomainRevision), ok = ff_party:wallet_log_balance(wallet_id(Withdrawal), Wallet), Result - end. + end; +handle_child_result({_OtherAction, _Events} = Result, _Withdrawal) -> + Result. -spec is_childs_active(withdrawal_state()) -> boolean(). is_childs_active(Withdrawal) -> diff --git a/docs/prg-invoice-single-collapse-goal.md b/docs/prg-invoice-single-collapse-goal.md new file mode 100644 index 00000000..a98ae1a9 --- /dev/null +++ b/docs/prg-invoice-single-collapse-goal.md @@ -0,0 +1,48 @@ +Контекст собрал. Ключевые факты, на которых строится план: + +- Реплей: `prg_machine:collapse/2` → `dispatch_apply_event` → `hg_invoice:apply_event/4` → `apply_event_changes/3` → `collapse_changes/3` с `#{timestamp => Dt}` (**нестрого**: невалидный переход только warning). +- Пост-call валидация: `validate_changes/1` → `collapse_changes/3` напрямую с `#{validation => strict}` (**строго**: `validate_transition` бросает `{invalid_transition,...}`). Мимо `prg_machine` и мимо `apply_event/4`. +- В `process_call` свежие changes сворачиваются дважды: на 415 (`validate_changes` → нужен пост-state для `log_changes`) и на 710 (`to_prg_result` → результат выбрасывается, только guard). В `process_signal`/`process_repair` — один раз (только 710). +- Новые changes на момент валидации ещё не имеют `event_id`/`timestamp` — их назначает `marshal_new_events` в самом конце (`LastEventID+1...`, `system_time`). + +Вот план. + +## План варианта Б — единый путь сворачивания через `prg_machine` + +### Цель +Свежие (ещё не persisted) изменения после `handle_call/handle_signal/handle_repair` должны накатываться на состояние тем же механизмом `apply_event`, что и реплей истории, а не отдельным прямым вызовом `collapse_changes`. Один код-путь, одинаковые опции/таймстампы, явный режим валидации. + +### Этап 0. Зафиксировать инварианты (до правок) +- Записать, что реплей обязан остаться нестрогим (история доверенная, иначе старые машины перестанут подниматься), а валидация новых changes — строгой. Значит «единый путь» = одна функция фолда с параметром режима, а не одинаковое поведение. +- Подтвердить, что пост-state из валидации реально нужен только `log_changes` (в `to_prg_result` он выбрасывается). Это разрешает слить два прохода в один. + +### Этап 1. Ввести в `prg_machine` API применения новых изменений +- Добавить публичную функцию (рабочее имя `apply_new_events/3` или `fold_events/4`), которая принимает handler, текущую `Model` (collapsed `St`) и список новых event-bodies, и фолдит их через тот же `dispatch_apply_event`, что и `collapse/2`. +- Назначать провизорные `event_id`/`timestamp` так же, как `marshal_new_events`: `event_id` = `LastEventID + n`, `timestamp` = текущее время. Источник `LastEventID` — из `Model`/machine, чтобы `apply_event/4` корректно проставил `latest_event_id`. +- Прокинуть в функцию режим (`strict | lenient`), чтобы handler мог переключать строгость. `collapse/2` остаётся как есть (lenient). +- Решить, как режим доходит до `apply_event/4`: либо доп. аргумент колбэка (расширение опционального `apply_event/4` → `apply_event/5`, с сохранением обратной совместимости через `function_exported`), либо через временное поле в `Model`. Предпочтительно — явный режим в сигнатуре, чтобы не прятать состояние в модели. + +### Этап 2. Согласовать `apply_event` в `hg_invoice` с режимом +- `apply_event_changes/3` и `collapse_changes/3` уже принимают `Opts`; нужно, чтобы режим из Этапа 1 транслировался в `#{validation => strict}` (для новых changes) либо отсутствовал (для реплея). +- Убедиться, что `merge_change`/`merge_payment_change`/`validate_transition` получают `Opts` единообразно вне зависимости от того, пришёл ли change из истории или из свежего результата. +- Аккуратно с `timestamp`: для новых changes сейчас он берётся из обёртки `?payment_ev(... OccurredAt)` и из `Opts`. Проверить, что провизорный `Ts` из Этапа 1 не конфликтует с уже зашитым `OccurredAt` в changes (двойной источник времени). + +### Этап 3. Переписать `validate_changes` на новый API +- Заменить прямые `collapse_changes(Changes, St, #{validation => strict})` / `#{}` внутри `validate_changes/1` на вызов нового `prg_machine`-API с соответствующим режимом (`strict` по умолчанию, `lenient` для ветки `validate := false`, которой пользуется repair через `should_validate_transitions`). +- Сохранить контракт: функция по-прежнему возвращает пост-state (для `log_changes`) и бросает на невалидном переходе. + +### Этап 4. Убрать двойной фолд в `process_call` +- Свернуть changes один раз: вычислить пост-state через новый API однократно, передать его и в `log_changes`, и в построение результата. +- `to_prg_result/1` перестаёт вызывать `validate_changes` внутри (строка 710); валидация делается явно один раз в `process_call`. Для `process_signal`/`process_repair`, которые валидируют только через `to_prg_result`, ввести тот же единый вызов на их уровне, чтобы поведение (строгость, исключения) не поменялось. +- Следствие: `to_prg_result_/1` остаётся чистым маршалингом результата без побочной валидации. + +### Этап 5. Тесты и проверка эквивалентности +- Прогнать `hg_invoice_tests_SUITE` целиком — это основной регресс по сценариям call/signal/repair. +- Точечно проверить: (а) невалидная последовательность changes по-прежнему бросает `{invalid_transition,...}` на call; (б) repair с `validate_transitions = false` остаётся нестрогим; (в) реплей старых машин не начал бросать (lenient сохранён); (г) логи `log_changes` не изменились (тот же пост-state). +- Сверить, что `latest_event_id` после применения новых changes совпадает с тем, что назначит `marshal_new_events` (иначе разъедутся id событий). +- Проверить, что аналогичный паттерн не используется в FF-хендлерах (debt #5 — только HG invoice); если новый API в `prg_machine` общий, убедиться, что он не ломает контракт FF-доменов (они на `apply_event/2`). + +### Риски и развилки +- **Двойной источник timestamp** (провизорный из движка vs `OccurredAt` в обёртке change) — главный источник тонких багов; нужно выбрать единственный источник истины. +- **Расширение контракта `prg_machine`** (`apply_event/5` или новый колбэк) затрагивает всех хендлеров — держать строго опциональным/обратносовместимым. +- **Объём.** Этапы 1–2 — ядро (контракт движка + транзит режима), 3–4 — собственно дедупликация в invoice, 5 — верификация. Минимально полезный срез: можно сделать только Этап 4 (убрать внутрикольцевой двойной фолд) как промежуточный коммит, не трогая контракт движка, а Этапы 1–3 — отдельным шагом. From 139530ef97fcacd92d378de6ec71cf904d1bcfc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 11 Jun 2026 00:44:01 +0300 Subject: [PATCH 19/62] Enhance error handling across multiple modules by adding support for 'failed' error cases in ff_withdrawal_adapter_host, ff_withdrawal_machine, and ff_withdrawal_session_machine. Update process_callback specifications to include 'failed' as a possible error type. Refactor test cases to improve error assertions and ensure consistent handling of exceptions. Additionally, streamline ff_ct_machine test setup and teardown processes for better clarity and reliability. --- .../src/ff_withdrawal_adapter_host.erl | 8 +- .../src/ff_adapter_withdrawal_codec.erl | 11 +- .../ff_transfer/src/ff_withdrawal_machine.erl | 6 +- .../src/ff_withdrawal_session_machine.erl | 8 +- apps/ff_transfer/test/ff_ct_machine.erl | 21 +- apps/ff_transfer/test/ff_withdrawal_SUITE.erl | 2 +- .../test/ff_withdrawal_limits_SUITE.erl | 90 +++--- apps/fistful/test/ff_ct_sleepy_provider.erl | 4 +- apps/hellgate/src/hg_invoice.erl | 9 +- apps/prg_machine/src/prg_machine.erl | 20 +- docs/prg-machine-error-semantics-hg-ff.md | 271 ++++++++++++++++++ docs/prg-machine-migration-context.md | 1 + 12 files changed, 374 insertions(+), 77 deletions(-) create mode 100644 docs/prg-machine-error-semantics-hg-ff.md diff --git a/apps/ff_server/src/ff_withdrawal_adapter_host.erl b/apps/ff_server/src/ff_withdrawal_adapter_host.erl index 7413f3f3..4d57a803 100644 --- a/apps/ff_server/src/ff_withdrawal_adapter_host.erl +++ b/apps/ff_server/src/ff_withdrawal_adapter_host.erl @@ -30,7 +30,13 @@ handle_function_('ProcessCallback', {Callback}, _Opts) -> {error, {session_already_finished, Context}} -> {ok, marshal(process_callback_result, {finished, Context})}; {error, {unknown_session, _Ref}} -> - woody_error:raise(business, #wthd_provider_SessionNotFound{}) + woody_error:raise(business, #wthd_provider_SessionNotFound{}); + {error, failed} -> + erlang:error(failed); + {error, {exception, _, _}} -> + erlang:error(failed); + {error, {exception, _, _, _}} -> + erlang:error(failed) end. %% diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl index 1da635d5..2c5ebc62 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl @@ -338,7 +338,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) -> @@ -419,3 +419,12 @@ 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). +%% progressor_action:timer() expects {deadline, calendar:datetime() | binary()}. +unmarshal_provider_timer({deadline, Deadline}) when is_binary(Deadline) -> + {deadline, Deadline}; +unmarshal_provider_timer({deadline, {DateTime, _USec}}) -> + {deadline, DateTime}; +unmarshal_provider_timer(Timer) -> + Timer. diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 51766750..8a904fe2 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -200,7 +200,11 @@ call(ID, Call) -> {ok, Reply} -> Reply; {error, notfound} -> - {error, {unknown_withdrawal, ID}} + {error, {unknown_withdrawal, ID}}; + {error, failed} -> + {error, failed}; + {error, _} = Error -> + Error end. codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 9813829d..c8a43b13 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -120,7 +120,7 @@ repair(ID, Scenario) -> -spec process_callback(callback_params()) -> {ok, process_callback_response()} - | {error, process_callback_error()}. + | {error, process_callback_error() | failed}. process_callback(#{tag := Tag} = Params) -> case ff_machine_tag:get_binding(?NS, Tag) of {ok, EntityID} -> @@ -168,7 +168,11 @@ call(Ref, Call) -> {ok, Reply} -> Reply; {error, notfound} -> - {error, {unknown_session, Ref}} + {error, {unknown_session, Ref}}; + {error, failed} -> + {error, failed}; + {error, _} = Error -> + Error end. codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> diff --git a/apps/ff_transfer/test/ff_ct_machine.erl b/apps/ff_transfer/test/ff_ct_machine.erl index b6be7ef0..a4026f87 100644 --- a/apps/ff_transfer/test/ff_ct_machine.erl +++ b/apps/ff_transfer/test/ff_ct_machine.erl @@ -11,15 +11,24 @@ -export([clear_hook/1]). -define(DISPATCH_TABLE, prg_machine_dispatch). +-define(ORIGINAL_MODULE, 'prg_machine_meck_original'). -spec load_per_suite() -> ok. load_per_suite() -> - meck:new(prg_machine, [no_link, passthrough]), - meck:expect(prg_machine, process, fun process/3). + 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(prg_machine). + case lists:member(prg_machine, meck:mocked()) of + true -> meck:unload(prg_machine); + false -> ok + end. -type hook() :: fun((prg_machine:machine(), module(), _) -> _). @@ -38,12 +47,12 @@ process({timeout, _BinArgs, #{process_id := ID} = _Process} = Call, #{ns := NS} Handler = handler_module(NS), {ok, Machine} = prg_machine:get(NS, ID), _ = Fun(Machine, Handler, undefined), - meck:passthrough([prg_machine, process, [Call, Opts, BinCtx]]); + ?ORIGINAL_MODULE:process(Call, Opts, BinCtx); undefined -> - meck:passthrough([prg_machine, process, [Call, Opts, BinCtx]]) + ?ORIGINAL_MODULE:process(Call, Opts, BinCtx) end; process(Call, Opts, BinCtx) -> - meck:passthrough([prg_machine, process, [Call, Opts, BinCtx]]). + ?ORIGINAL_MODULE:process(Call, Opts, BinCtx). handler_module(NS) -> case ets:lookup(?DISPATCH_TABLE, NS) of diff --git a/apps/ff_transfer/test/ff_withdrawal_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_SUITE.erl index ce5ad64d..f3b02590 100644 --- a/apps/ff_transfer/test/ff_withdrawal_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_SUITE.erl @@ -886,7 +886,7 @@ session_repair_test(C) -> ?assertEqual(pending, await_session_processing_status(WithdrawalID, pending)), SessionID = get_session_id(WithdrawalID), ?assertEqual(<<"callback_processing">>, await_session_adapter_state(SessionID, <<"callback_processing">>)), - ?assertError({failed, _, _}, call_process_callback(Callback)), + ?assertMatch({error, {exception, _, _}}, call_process_callback(Callback)), timer:sleep(3000), ?assertEqual(pending, await_session_processing_status(WithdrawalID, pending)), ok = repair_withdrawal_session(WithdrawalID), diff --git a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl index 4cbec99c..461837ab 100644 --- a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl @@ -72,18 +72,18 @@ groups() -> -spec init_per_suite(config()) -> config(). init_per_suite(C) -> - ff_ct_machine:load_per_suite(), - ct_helper:makeup_cfg( + C1 = ct_helper:makeup_cfg( [ ct_helper:test_case_name(init), ct_payment_system:setup() ], C - ). + ), + C1. -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 +543,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, _Args) -> - Withdrawal = prg_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, _Args) -> + Withdrawal = prg_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) -> + 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 +617,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 +664,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 +686,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 +698,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/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/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 3a016a4f..51465b07 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -282,16 +282,17 @@ process_with_tag(Tag, F) -> -spec fail(prg_machine:id()) -> ok. fail(ID) -> - try prg_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. %% diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 22096a60..a131132e 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -145,18 +145,16 @@ start(NS, ID, Args) -> Ok; {error, <<"process already exists">>} -> {error, exists}; - {error, {exception, _, _} = Exception} -> - raise_exception(Exception); - {error, {exception, _, _, _} = Exception} -> - raise_exception(Exception) + {error, _} = Error -> + Error end. --spec call(namespace(), id(), call()) -> {ok, response()} | {error, notfound | term()}. +-spec call(namespace(), id(), call()) -> {ok, response()} | {error, notfound | failed | 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 | term()}. + {ok, response()} | {error, notfound | failed | term()}. call(NS, ID, CallArgs, After, Limit, Direction) -> Req = request(NS, ID, CallArgs, encode_range(After, Limit, Direction)), case progressor:call(Req) of @@ -168,16 +166,12 @@ call(NS, ID, CallArgs, After, Limit, Direction) -> {error, notfound}; {error, <<"process is error">>} -> {error, failed}; - {error, {exception, _, _} = Exception} -> - raise_exception(Exception); - {error, {exception, _, _, _} = Exception} -> - raise_exception(Exception); {error, _} = Error -> Error end. -spec repair(namespace(), id(), args()) -> - {ok, term()} | {error, notfound | working | failed | {repair, {failed, binary()}}}. + {ok, term()} | {error, notfound | working | failed | {repair, {failed, term()}}}. repair(NS, ID, Args) -> Req = #{ ns => NS, @@ -196,10 +190,6 @@ repair(NS, ID, Args) -> {error, working}; {error, <<"process is error">>} -> {error, failed}; - {error, {exception, _, _} = Exception} -> - raise_exception(Exception); - {error, {exception, _, _, _} = Exception} -> - raise_exception(Exception); {error, Reason} -> {error, {repair, {failed, Reason}}} end. diff --git a/docs/prg-machine-error-semantics-hg-ff.md b/docs/prg-machine-error-semantics-hg-ff.md new file mode 100644 index 00000000..d047fc73 --- /dev/null +++ b/docs/prg-machine-error-semantics-hg-ff.md @@ -0,0 +1,271 @@ +# `prg_machine`: семантика ошибок, HG vs FF, где не сошлось + +Документ фиксирует **как было** (machinery), **что поменяли** в Hellgate и Fistful при унификации на `{error, failed}`, **где сломалось** и **какой контракт считать целевым**. Чтобы не разбирать заново при каждом CT-прогоне. + +См. также: `docs/prg-machine-migration-context.md` (общая архитектура), `docs/prg-machine-remaining-debt.md` (техдолг collapse). + +*Обновлено: 2026-06-10* + +--- + +## 1. Три слоя ошибок (не путать) + +| Слой | Откуда | Пример | Кто обрабатывает | +|------|--------|--------|------------------| +| **A. Progressor API** | `progressor:call/init/repair/get` вернул `{error, Reason}` | `<<"process not found">>`, `<<"process is waiting">>`, `{exception, ...}` | `prg_machine:start/call/repair/get` | +| **B. Processor response** | Вызов progressor **успешен**, в теле ответа `{error, ...}` или `{exception, ...}` | `{ok, {error, invalid_callback}}` из `hg_invoice:process_call` | Домен (`hg_invoice`, FF handlers) | +| **C. Доменный throw** | Woody handler / callback host | `erlang:throw(#payproc_InvoiceNotFound{})` | `hg_*_handler`, `ff_*_handler` | + +Путаница между **A** и **B** — главный источник регрессий после миграции. + +--- + +## 2. Как было: `machinery_prg_backend` + +Файл: `_build/default/lib/machinery/src/machinery_prg_backend.erl` (в prod HG/FF уже не используется, но контракт оттуда «въелся» в тесты и ожидания). + +### 2.1. `call/5` — маппинг ошибок progressor + +```erlang +{error, <<"process not found">>} -> {error, notfound}; +{error, <<"process is init">>} -> {error, notfound}; +{error, {exception, _, _}} -> erlang:error({failed, NS, ID}); % raise +{error, <<"process is error">>} -> erlang:error({failed, NS, ID}); % raise +{error, _Reason} = Error -> {ok, Error}; %% <-- важно +``` + +**Следствие:** любая «неизвестная» ошибка progressor (в т.ч. `<<"process is waiting">>`, `<<"process is running">>`) превращалась в **успешный** вызов machinery с телом `{error, Reason}`. + +Дальше HG Thrift-слой (`hg_invoice_handler:call/3`): + +```erlang +{ok, Reply} -> Reply; % Reply может быть {error, <<"process is waiting">>} +{error, Error} -> erlang:error(Error); +``` + +То есть при статусе процесса `waiting` (таймер, фоновая обработка) внешний `call` **не падал на уровне API**, а возвращал `{error, Reason}` как обычный ответ домена. + +### 2.2. `repair/5` + +Явные ветки: `notfound`, `working` (`<<"process is running">>`), `failed` (exception / `process is error`), остальное — `{error, {failed, DecodedReason}}`. + +### 2.3. Actions / таймеры + +Machinery маршалил actions из списка `mg_stateproc` в `#{set_timer => ...}` **в микросекундах** (`system_time(microsecond)`). + +Сейчас домены отдают `progressor_action` напрямую (`set_deadline`, `set_timer`, `set_timeout`) — progressor сам нормализует единицы через `prg_utils:to_microseconds/1`. Это **не** причина HG-fail (таймеры в progressor работают с секундами/deadline корректно). + +--- + +## 3. Как стало: `prg_machine` (прямой client) + +Файл: `apps/prg_machine/src/prg_machine.erl` + +### 3.1. Целевой контракт `prg_machine` (client API) + +**Не затирать** причину ошибки. Атом `failed` — только для «битого» процесса в storage progressor, не для exception из домена. + +| Progressor | `prg_machine:call` / `start` | +|------------|------------------------------| +| `<<"process not found">>` / `<<"process is init">>` | `{error, notfound}` | +| `<<"process is error">>` | `{error, failed}` | +| `{exception, Class, Reason}` (и 4-tuple со stacktrace) | **`{error, {exception, ...}}` pass-through** | +| `<<"process is waiting">>` и прочие guard-ошибки | **`{error, Reason}` pass-through** | +| `<<"process already exists">>` (`start`) | `{error, exists}` | + +`repair`: те же явные ветки + `working` для `<<"process is running">>`; прочие ошибки (включая `{exception, ...}`) → `{error, {repair, {failed, Reason}}}` — **Reason сохраняется**. + +На внешней границе (woody adapter) exception по-прежнему можно сворачивать в `failed` для контракта провайдера — см. `ff_withdrawal_adapter_host`. + +### 3.2. Регрессия (июнь 2026) + +Временно в `call/6` стояло: + +```erlang +{error, _} -> {error, failed} % catch-all — ПЛОХО +``` + +Вместо: + +```erlang +{error, _} = Error -> Error % pass-through прочих ошибок progressor +``` + +**Эффект:** `<<"process is waiting">>`, `<<"process is running">>` (на call) и прочие guard-ошибки progressor превращались в атом `failed`. Внешние вызовы и callback-пути вели себя иначе, чем при machinery `{ok, {error, Reason}}` или pass-through. + +**Симптомы в CT** (`lib.hellgate`, прогон 2026-06-09): 11 FAIL в `hg_invoice_tests_SUITE`, паттерн: + +- `{badmatch, timeout}` в `next_change` / `next_changes` (таймаут ожидания события 12s) +- в списке ожидаемых payment events появлялся атом `timeout` вместо `payment_rollback_started` и т.п. +- `consistent_account_balances` — побочный эффект незавершённых платежей + +Типичные кейсы: `payment_hold_cancellation`, `payment_hold_auto_cancellation`, каскады (`payment_cascade_*`), `deadline_doesnt_affect_payment_refund`. + +FF transfer после правок `ff_ct_machine`: **88 OK / 0 FAIL** (тот же прогон). + +### 3.3. Текущее состояние ветки + +`prg_machine:call`: + +```erlang +{error, <<"process is error">>} -> {error, failed}; +{error, _} = Error -> Error. +``` + +`start`: `exists` + pass-through. `repair`: `notfound` / `working` / `failed` + `{repair, {failed, Reason}}` с исходным `Reason`. + +**Антипаттерн** (убран): `{error, {exception, _, _}} -> {error, failed}` — теряет class/reason для интроспекции и логов. + +--- + +## 4. Что правили в Hellgate + +| Модуль | Изменение | Зачем | +|--------|-----------|-------| +| `hg_invoicing_machine_client` | `{error, failed}` + **`{error, _} = Error -> Error`** | Проброс pass-through с `prg_machine:call` в Thrift | +| `hg_invoice_handler:call/3` | `{error, Error} -> erlang:error(Error)` | Без изменений по смыслу; `failed` — один из возможных `Error` | +| `hg_invoice` | `fail/1`: `{error, failed}` и `{error, {exception,...}}` → `ok` (тестовый хелпер, намеренный crash) | Согласовано с pass-through exception | +| `hg_invoice` | `process_callback` / `process_session_change_by_tag`: ветка `{ok, {error,_}} -> {error, failed}` (processor response, слой B) | Ответ процессора с ошибкой в теле | +| `hg_invoice_handler:repair/2` | `{error, working}`, `{error, Reason}` | Без `failed` catch-all | + +**Не трогали (и не надо без отдельного goal):** + +- `handle_payment_result` → `set_invoice_timer` при `?cancelled()` / `?failed()` — логика таймера invoice due после платежа +- двойной collapse (`validate_changes` + `to_prg_result`) — см. `prg-machine-remaining-debt.md` + +### 4.1. HG: обработка `failed` на границах + +```erlang +%% hg_invoice_handler:call/3 +{error, Error} -> erlang:error(Error) % в т.ч. failed, <<"process is waiting">> + +%% hg_proxy_host_provider:handle_callback_result/1 +{error, Reason} -> error(Reason) % failed уходит наружу как exception +``` + +--- + +## 5. Что правили в Fistful (FF) + +| Модуль | Изменение | Зачем | +|--------|-----------|-------| +| `ff_withdrawal_machine:call/2` | `notfound`, `failed`, **`{error,_}=Error`** | Симметрия с HG client | +| `ff_withdrawal_session_machine:call/2` | то же | | +| `ff_withdrawal_session_machine:process_callback/1` | spec: `failed` в union | Явная ветка `{error, failed}` | +| `ff_withdrawal_adapter_host` | `{error, failed} -> erlang:error(failed)` | Как HG provider callback | +| `ff_deposit_machine:repair/2` | `{error, failed}` → domain error tuple | Repair-контракт | + +### 5.1. FF CT: `ff_ct_machine` (meck `prg_machine:process/3`) + +Отдельная история — **не влияет на HG suites**, но ломала FF transfer. + +Проблемы: + +1. `meck:passthrough([prg_machine, process, [...]])` — неверная сигнатура, `function_clause` на `init` +2. `meck:passthrough` из mock `process/3` — `{badmatch, undefined}` в `get_current_call()` +3. `?MODULE:process(...)` — не exported + +**Рабочее решение:** + +```erlang +meck:new(prg_machine, [no_link, passthrough]), +meck:expect(prg_machine, process, fun process/3). + +%% внутри mock: +'prg_machine_meck_original':process(Call, Opts, BinCtx). +``` + +Плюс: идемпотентный `load/unload`, hook до `create`, `maybe_unload` в `after`, `await_withdrawal_activity` с `linear(50, 200)`. + +### 5.2. FF тесты на processor exception + +`ff_withdrawal_SUITE:session_repair_test`: + +```erlang +?assertMatch({error, {exception, _, _}}, call_process_callback(Callback)) +``` + +`failed` (атом) — только если progressor вернул `<<"process is error">>` (битый процесс), не crash домена. + +--- + +## 6. Матрица «кто что ожидает» (где не сошлось) + +| Ситуация | Machinery | `prg_machine` (правильно) | `prg_machine` (catch-all bug) | +|----------|-----------|---------------------------|-------------------------------| +| Processor crash (exception) | `raise {failed,NS,ID}` | `{error, {exception,...}}` | `{error, failed}` ✗ (потеря деталей) | +| `process is error` | `raise {failed,NS,ID}` | `{error, failed}` | `{error, failed}` ✓ | +| `process is waiting` на **call** | `{ok, {error, <<"process is waiting">>}}` | `{error, <<"process is waiting">>}` | `{error, failed}` ✗ | +| `process is running` на **call** | `{ok, {error, <<"process is running">>}}` | `{error, <<"process is running">>}` | `{error, failed}` ✗ | +| `process is running` на **repair** | `{error, working}` | `{error, working}` | `{error, working}` ✓ | +| Ответ процессора `{error, X}` в теле | `{ok, {error, X}}` (через machinery decode) | `{ok, {error, X}}` через `decode_term` | без изменений ✓ | + +**Важно:** полная эмуляция machinery `{ok, Error}` для слоя A **не** реализована в `prg_machine` — вместо этого HG handler принимает `{error, Reason}` на client API. Поведение **близко**, но не идентично: при `{error, <<"process is waiting">>}` handler делает `erlang:error(Reason)`, а не возвращает tuple клиенту. Тесты проходили с pass-through; с `failed` — нет. + +Если понадобится **бит-в-бит** как machinery для call: + +```erlang +%% гипотетически в prg_machine:call, только для «мягких» guard-ошибок: +{error, <<"process is ", _/binary>> = Reason} -> + {ok, {error, Reason}}; +``` + +Пока **не делали** — достаточно pass-through + обработка в `hg_invoicing_machine_client`. + +--- + +## 7. Статус CT (на момент документа) + +| Suite | Результат | Примечание | +|-------|-----------|------------| +| `lib.ff_transfer` | 88 OK / 0 FAIL | после `ff_ct_machine` + codec/timer fixes | +| `lib.hellgate` | 231 OK / **11 FAIL** | до fix pass-through в `prg_machine:call` | +| `lib.ff_server` | 41 OK / **4 FAIL** | отдельно не разбирали | + +Прогон: `_build/test/logs/index.html`, hellgate run `2026-06-09_22.10.35`. + +**11 FAIL (hellgate):** + +`payment_hold_cancellation`, `payment_hold_auto_cancellation`, `payment_cascade_fail_wo_route_candidates`, `payment_cascade_limit_overflow`, `payment_cascade_fail_wo_available_attempt_limit`, `payment_cascade_failures`, `payment_cascade_deadline_failures`, `payment_cascade_fail_provider_error`, `deadline_doesnt_affect_payment_refund`, `accept_payment_chargeback_exceeded`, `consistent_account_balances`. + +Перепрогон после fix pass-through — **нужен вручную** (`make wdeps-common-test` / docker). + +Локально `rebar3` может падать с `corrupt atom table` — использовать docker testrunner из Makefile. + +--- + +## 8. Чеклист при правках `prg_machine:call` + +1. **`failed` только для `<<"process is error">>`** — не для `{exception, ...}` и не catch-all `{error, _}`. +2. Client-обёртки: `{error, _} = Error -> Error` (HG/FF `*_machine`, `hg_invoicing_machine_client`). +3. Woody-граница: при необходимости сворачивать `{error, {exception,...}}` в `erlang:error(failed)` локально (`ff_withdrawal_adapter_host`). +4. Processor response (слой B) — в домене (`{ok, {error,_}}`), не в `prg_machine`. +5. CT: `session_repair_test` ожидает `{error, {exception, _, _}}`, не `{error, failed}`. +6. FF meck: только `'prg_machine_meck_original':process/3`. + +--- + +## 9. Ключевые файлы (быстрые ссылки) + +| Путь | Содержание | +|------|------------| +| `apps/prg_machine/src/prg_machine.erl` | `start/call/repair`, маппинг ошибок | +| `_build/.../machinery_prg_backend.erl` | эталон старого поведения call | +| `apps/hellgate/src/hg_invoicing_machine_client.erl` | Thrift → prg_machine | +| `apps/hellgate/src/hg_invoice_handler.erl` | `call/3`, `repair/2`, `ensure_started` | +| `apps/hellgate/src/hg_invoice.erl` | callbacks, `set_invoice_timer`, session/callback | +| `apps/hellgate/src/hg_proxy_host_provider.erl` | provider → `process_session_change_by_tag` | +| `apps/hellgate/test/hg_invoice_helper.erl` | `next_change`, timeout 12s | +| `apps/ff_transfer/test/ff_ct_machine.erl` | meck timeout hooks | +| `apps/ff_transfer/src/ff_withdrawal_machine.erl` | FF call wrapper | +| `apps/ff_server/src/ff_withdrawal_adapter_host.erl` | adapter `failed` | +| `_checkouts/progressor/src/progressor.erl` | `check_process_status`, call требует `<<"running">>` | +| `_checkouts/progressor/src/progressor_action.erl` | таймеры/deadline | + +--- + +## 10. Открытые вопросы + +1. **Нужна ли эмуляция `{ok, {error, Reason}}`** для guard-ошибок progressor на call (как machinery) — или pass-through достаточен при текущих тестах. +2. **`lib.ff_server` 4 FAIL** — отдельное расследование. +3. **Полный зелёный `lib.hellgate`** после pass-through fix — подтвердить прогоном. diff --git a/docs/prg-machine-migration-context.md b/docs/prg-machine-migration-context.md index 5e1174f2..32d44347 100644 --- a/docs/prg-machine-migration-context.md +++ b/docs/prg-machine-migration-context.md @@ -316,6 +316,7 @@ rg "client => machinery_prg_backend" config/sys.config # 0 | `apps/hellgate/src/hg_invoicing_machine_client.erl` | Thrift → prg_machine | | `apps/fistful/src/ff_repair.erl` | repair + collapse | | `docs/trace-api-thrift.md` | следующий этап trace | +| `docs/prg-machine-error-semantics-hg-ff.md` | семантика ошибок, HG vs FF, CT-регрессии, meck | --- From 23117a3db47ef09b775a03da59ce144bbce7e267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 11 Jun 2026 01:58:49 +0300 Subject: [PATCH 20/62] Refactor ff_machine_handler to replace progressor:trace with ff_machine_trace for improved trace handling. Update test suite to include additional task type in JSON decoding. Enhance documentation to clarify changes in trace HTTP handling and address binary encoding issues. --- apps/ff_server/src/ff_machine_handler.erl | 2 +- .../test/ff_destination_handler_SUITE.erl | 3 +- apps/ff_transfer/src/ff_machine_trace.erl | 171 ++++++++++++++++++ docs/prg-machine-error-semantics-hg-ff.md | 11 +- 4 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 apps/ff_transfer/src/ff_machine_trace.erl diff --git a/apps/ff_server/src/ff_machine_handler.erl b/apps/ff_server/src/ff_machine_handler.erl index 9e11730d..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} ?= progressor:trace(#{ns => NS, id => 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/test/ff_destination_handler_SUITE.erl b/apps/ff_server/test/ff_destination_handler_SUITE.erl index c5a740ab..12f56794 100644 --- a/apps/ff_server/test/ff_destination_handler_SUITE.erl +++ b/apps/ff_server/test/ff_destination_handler_SUITE.erl @@ -173,7 +173,8 @@ trace_destination_test(C) -> ], <<"task_status">> := <<"finished">>, <<"task_type">> := <<"init">> - } + }, + #{<<"task_status">> := <<"finished">>, <<"task_type">> := <<"timeout">>} ] = json:decode(Body), ok. 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..fbc2e00d --- /dev/null +++ b/apps/ff_transfer/src/ff_machine_trace.erl @@ -0,0 +1,171 @@ +%%% 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), + Meta = maps:get(event_metadata, Event, #{}), + Format = maps:get(<<"format">>, Meta, maps:get(format, Meta, 1)), + EventID = maps:get(event_id, Event), + Ts = maps:get(event_timestamp, Event), + Body = Handler:unmarshal_event_body(Format, Payload), + #{ + event_id => EventID, + event_payload => json_compatible_value(Body), + event_timestamp => Ts + }. + +decode_context(undefined) -> + #{}; +decode_context(<<>>) -> + #{}; +decode_context(Bin) when is_binary(Bin) -> + woody_rpc_helper:decode_rpc_context(decode_term(Bin)); +decode_context(Ctx) when is_map(Ctx) -> + Ctx. + +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(undefined) -> + undefined; +decode_trace_value(Bin) when is_binary(Bin) -> + decode_term(Bin); +decode_trace_value(Value) -> + Value. + +decode_term(Bin) when is_binary(Bin) -> + binary_to_term(Bin, [safe]); +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/docs/prg-machine-error-semantics-hg-ff.md b/docs/prg-machine-error-semantics-hg-ff.md index d047fc73..90780339 100644 --- a/docs/prg-machine-error-semantics-hg-ff.md +++ b/docs/prg-machine-error-semantics-hg-ff.md @@ -264,8 +264,13 @@ meck:expect(prg_machine, process, fun process/3). --- -## 10. Открытые вопросы +## 10. Trace HTTP (`ff_machine_handler`) + +После миграции `progressor:trace/1` + `json:encode` ломался на сырых binary в `event_payload` (`invalid_byte, 131`). + +**Фикс:** `ff_machine_trace:trace/2` в `apps/ff_transfer` — порт `ff_machine:unmarshal_trace` (handler `unmarshal_event_body` + `json_compatible_value`). + +## 11. Открытые вопросы 1. **Нужна ли эмуляция `{ok, {error, Reason}}`** для guard-ошибок progressor на call (как machinery) — или pass-through достаточен при текущих тестах. -2. **`lib.ff_server` 4 FAIL** — отдельное расследование. -3. **Полный зелёный `lib.hellgate`** после pass-through fix — подтвердить прогоном. +2. **`repair_failed_session_with_failure`** — отдельно от trace: `{badmatch,{error,notfound}}` в session repair. From cef9d967e1af119191a2dee1e2de13043910d37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 11 Jun 2026 23:13:15 +0300 Subject: [PATCH 21/62] Refactor type specifications in ff_adjustment_utils, ff_adjustment, ff_withdrawal_machine, and ff_withdrawal to unify action types under ff_adjustment and ff_withdrawal modules. Enhance decode_context and decode_trace_value functions in ff_machine_trace for improved handling of binary and term values. Update process callback error handling in ff_withdrawal_session_machine to include processor_error type. Streamline test suite in ff_ct_machine for better clarity in process handling. --- apps/ff_transfer/src/ff_adjustment.erl | 1 + apps/ff_transfer/src/ff_adjustment_utils.erl | 2 +- apps/ff_transfer/src/ff_machine_trace.erl | 15 +++++++-------- apps/ff_transfer/src/ff_withdrawal.erl | 9 ++++++++- apps/ff_transfer/src/ff_withdrawal_machine.erl | 5 +---- .../src/ff_withdrawal_session_machine.erl | 6 +++++- apps/ff_transfer/test/ff_ct_machine.erl | 7 +++++-- 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/apps/ff_transfer/src/ff_adjustment.erl b/apps/ff_transfer/src/ff_adjustment.erl index d02d3701..e2e265a7 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]). diff --git a/apps/ff_transfer/src/ff_adjustment_utils.erl b/apps/ff_transfer/src/ff_adjustment_utils.erl index 299636e6..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() :: progressor_action:t() | 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_machine_trace.erl b/apps/ff_transfer/src/ff_machine_trace.erl index fbc2e00d..a948062c 100644 --- a/apps/ff_transfer/src/ff_machine_trace.erl +++ b/apps/ff_transfer/src/ff_machine_trace.erl @@ -59,10 +59,10 @@ decode_context(undefined) -> #{}; decode_context(<<>>) -> #{}; -decode_context(Bin) when is_binary(Bin) -> - woody_rpc_helper:decode_rpc_context(decode_term(Bin)); decode_context(Ctx) when is_map(Ctx) -> - Ctx. + Ctx; +decode_context(Value) -> + woody_rpc_helper:decode_rpc_context(decode_term(Value)). extract_trace_id(#{<<"otel">> := [OtelTraceID | _]}) -> OtelTraceID; @@ -81,13 +81,12 @@ extract_error_response({error, Reason}) -> extract_error_response(_) -> null. -decode_trace_value(undefined) -> - undefined; -decode_trace_value(Bin) when is_binary(Bin) -> - decode_term(Bin); decode_trace_value(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, [safe]); decode_term(Term) -> diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 770be58e..e4d878ec 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -187,7 +187,14 @@ -type invalid_withdrawal_status_error() :: {invalid_withdrawal_status, status()}. --type action() :: sleep | continue | undefined. +%% Transfer-layer action before map_action/1. Adapter timers normally live on the +%% session machine; {setup_timer, _} is supported at this boundary for repair and +%% symmetry with ff_withdrawal_session. +-type action() :: + sleep + | continue + | undefined + | {setup_timer, progressor_action:timer()}. -export_type([withdrawal/0]). -export_type([withdrawal_state/0]). diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 8a904fe2..14fc63ff 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -31,10 +31,7 @@ -type unknown_withdrawal_error() :: {unknown_withdrawal, id()}. --type action() :: - continue - | sleep - | undefined. +-type action() :: ff_withdrawal:action(). -type adjustment_params() :: ff_withdrawal:adjustment_params(). diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index c8a43b13..b8494e05 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -49,6 +49,10 @@ {unknown_session, {tag, id()}} | ff_withdrawal_session:process_callback_error(). +-type processor_error() :: + {exception, atom(), term()} + | {exception, atom(), term(), list()}. + -type ctx() :: ff_entity_context:context(). %% Pipeline @@ -120,7 +124,7 @@ repair(ID, Scenario) -> -spec process_callback(callback_params()) -> {ok, process_callback_response()} - | {error, process_callback_error() | failed}. + | {error, process_callback_error() | processor_error() | failed}. process_callback(#{tag := Tag} = Params) -> case ff_machine_tag:get_binding(?NS, Tag) of {ok, EntityID} -> diff --git a/apps/ff_transfer/test/ff_ct_machine.erl b/apps/ff_transfer/test/ff_ct_machine.erl index a4026f87..b278aee1 100644 --- a/apps/ff_transfer/test/ff_ct_machine.erl +++ b/apps/ff_transfer/test/ff_ct_machine.erl @@ -47,11 +47,14 @@ process({timeout, _BinArgs, #{process_id := ID} = _Process} = Call, #{ns := NS} Handler = handler_module(NS), {ok, Machine} = prg_machine:get(NS, ID), _ = Fun(Machine, Handler, undefined), - ?ORIGINAL_MODULE:process(Call, Opts, BinCtx); + call_original_process(Call, Opts, BinCtx); undefined -> - ?ORIGINAL_MODULE:process(Call, Opts, BinCtx) + call_original_process(Call, Opts, BinCtx) end; process(Call, Opts, BinCtx) -> + call_original_process(Call, Opts, BinCtx). + +call_original_process(Call, Opts, BinCtx) -> ?ORIGINAL_MODULE:process(Call, Opts, BinCtx). handler_module(NS) -> From 0b630290f077bb358e493f376322d88e79e002d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 11 Jun 2026 23:44:28 +0300 Subject: [PATCH 22/62] Refactor ff_machine_trace and prg_machine modules to improve event unmarshalling and type specifications. Update ff_ct_machine test suite to use the correct handler module reference. Introduce a new state record in prg_machine_registry for better state management. --- apps/ff_transfer/src/ff_machine_trace.erl | 2 +- apps/ff_transfer/test/ff_ct_machine.erl | 3 +-- apps/prg_machine/src/prg_machine.erl | 7 +++++++ apps/prg_machine/src/prg_machine_registry.erl | 18 ++++++++++++------ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/ff_transfer/src/ff_machine_trace.erl b/apps/ff_transfer/src/ff_machine_trace.erl index a948062c..489820ef 100644 --- a/apps/ff_transfer/src/ff_machine_trace.erl +++ b/apps/ff_transfer/src/ff_machine_trace.erl @@ -48,7 +48,7 @@ unmarshal_trace_event(Event, Handler) -> Format = maps:get(<<"format">>, Meta, maps:get(format, Meta, 1)), EventID = maps:get(event_id, Event), Ts = maps:get(event_timestamp, Event), - Body = Handler:unmarshal_event_body(Format, Payload), + Body = prg_machine:unmarshal_event_body(Handler, Format, Payload), #{ event_id => EventID, event_payload => json_compatible_value(Body), diff --git a/apps/ff_transfer/test/ff_ct_machine.erl b/apps/ff_transfer/test/ff_ct_machine.erl index b278aee1..c67b8dd2 100644 --- a/apps/ff_transfer/test/ff_ct_machine.erl +++ b/apps/ff_transfer/test/ff_ct_machine.erl @@ -11,7 +11,6 @@ -export([clear_hook/1]). -define(DISPATCH_TABLE, prg_machine_dispatch). --define(ORIGINAL_MODULE, 'prg_machine_meck_original'). -spec load_per_suite() -> ok. load_per_suite() -> @@ -55,7 +54,7 @@ process(Call, Opts, BinCtx) -> call_original_process(Call, Opts, BinCtx). call_original_process(Call, Opts, BinCtx) -> - ?ORIGINAL_MODULE:process(Call, Opts, BinCtx). + 'prg_machine_meck_original':process(Call, Opts, BinCtx). handler_module(NS) -> case ets:lookup(?DISPATCH_TABLE, NS) of diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index a131132e..b863c205 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -122,6 +122,8 @@ %% Registry (namespace -> handler module) -export([get_child_spec/1]). +-export([handler_namespace/1]). +-export([unmarshal_event_body/3]). %% Event-sourcing helpers (replaces ff_machine) @@ -295,6 +297,10 @@ process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> get_child_spec(Handlers) -> prg_machine_registry:get_child_spec(Handlers). +-spec handler_namespace(module()) -> namespace(). +handler_namespace(Handler) -> + Handler:namespace(). + %% Event-sourcing (replaces ff_machine collapse/emit) -spec collapse(module(), machine()) -> term(). @@ -426,6 +432,7 @@ marshal_event_body(Handler, Body) -> {undefined, term_to_binary(Body)} end. +-spec unmarshal_event_body(module(), undefined | pos_integer(), binary()) -> event_body(). unmarshal_event_body(Handler, Format, Payload) -> case erlang:function_exported(Handler, unmarshal_event_body, 2) of true -> diff --git a/apps/prg_machine/src/prg_machine_registry.erl b/apps/prg_machine/src/prg_machine_registry.erl index 1a5d1094..92f03659 100644 --- a/apps/prg_machine/src/prg_machine_registry.erl +++ b/apps/prg_machine/src/prg_machine_registry.erl @@ -14,6 +14,12 @@ -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) -> #{ @@ -53,20 +59,20 @@ ensure_table() -> ok end. --spec init([module()]) -> {ok, #{handlers := [module()]}}. +-spec init([module()]) -> {ok, state()}. init(Handlers) -> ok = ensure_table(), - true = ets:insert(?TABLE, [{H:namespace(), H} || H <- Handlers]), - {ok, #{handlers => Handlers}}. + true = ets:insert(?TABLE, [{prg_machine:handler_namespace(H), H} || H <- Handlers]), + {ok, #state{handlers = Handlers}}. --spec handle_call(term(), {pid(), term()}, map()) -> {reply, term(), map()}. +-spec handle_call(term(), {pid(), term()}, state()) -> {reply, term(), state()}. handle_call(_Request, _From, State) -> {reply, {error, unsupported}, State}. --spec handle_cast(term(), map()) -> {noreply, map()}. +-spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast(_Msg, State) -> {noreply, State}. --spec handle_info(term(), map()) -> {noreply, map()}. +-spec handle_info(term(), state()) -> {noreply, state()}. handle_info(_Info, State) -> {noreply, State}. From 4919fa987936a92ec785eef309d9641df0d43870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 12 Jun 2026 00:36:36 +0300 Subject: [PATCH 23/62] bumped progressor --- rebar.config | 3 +-- rebar.lock | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index b4baffdb..669d504b 100644 --- a/rebar.config +++ b/rebar.config @@ -47,8 +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"}}}, - %% Local overrides: _checkouts/progressor (progressor_action, action types) — see .gitignore - {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.24"}}}, + {progressor, {git, "https://github.com/valitydev/progressor.git", {branch, "add_action_module"}}}, {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"}}}, diff --git a/rebar.lock b/rebar.lock index 0187242b..e28cbaff 100644 --- a/rebar.lock +++ b/rebar.lock @@ -115,6 +115,10 @@ {git,"https://github.com/valitydev/payproc-errors-erlang.git", {ref,"8ae8586239ef68098398acf7eb8363d9ec3b3234"}}, 0}, + {<<"progressor">>, + {git,"https://github.com/valitydev/progressor.git", + {ref,"07e39efbd90e7916be866f84984d116911366d49"}}, + 0}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.11.0">>},0}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.9">>},0}, {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.15">>},1}, From e1160b98b0004b22ca249defe97a25eb1b1a6576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 12 Jun 2026 01:01:27 +0300 Subject: [PATCH 24/62] bumped cache --- .github/workflows/erlang-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 398f73312c69349fdc484988ac911c45a2a5a7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 12 Jun 2026 01:31:30 +0300 Subject: [PATCH 25/62] fixed --- apps/ff_transfer/test/ff_ct_machine.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/ff_transfer/test/ff_ct_machine.erl b/apps/ff_transfer/test/ff_ct_machine.erl index c67b8dd2..51e03841 100644 --- a/apps/ff_transfer/test/ff_ct_machine.erl +++ b/apps/ff_transfer/test/ff_ct_machine.erl @@ -56,6 +56,8 @@ 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}] -> From 4aa786e59be00a3fe0101a5959ddce1871a379cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 12 Jun 2026 13:46:24 +0300 Subject: [PATCH 26/62] Refactor action types across multiple modules to replace progressor_action with hg_machine_action for improved consistency. Update type specifications and action handling in ff_transfer, hellgate, and prg_machine modules. Adjust event processing logic to utilize timeout actions instead of progressor actions, enhancing clarity and maintainability. --- .../ff_transfer/src/ff_adapter_withdrawal.erl | 2 +- .../src/ff_adapter_withdrawal_codec.erl | 2 +- apps/ff_transfer/src/ff_deposit.erl | 8 +- apps/ff_transfer/src/ff_destination.erl | 22 +- apps/ff_transfer/src/ff_source.erl | 22 +- apps/ff_transfer/src/ff_withdrawal.erl | 14 +- .../ff_transfer/src/ff_withdrawal_session.erl | 22 +- apps/hellgate/src/hg_invoice.erl | 66 +++--- apps/hellgate/src/hg_invoice_payment.erl | 86 +++---- .../src/hg_invoice_payment_chargeback.erl | 16 +- .../src/hg_invoice_payment_refund.erl | 18 +- .../src/hg_invoice_registered_payment.erl | 15 +- apps/hellgate/src/hg_invoice_repair.erl | 2 +- apps/hellgate/src/hg_session.erl | 22 +- apps/prg_machine/src/hg_machine_action.erl | 42 ++++ apps/prg_machine/src/prg_machine.erl | 20 +- .../prg_machine_aux_state_test_handler.erl | 8 +- .../test/prg_machine_env_mock_handler.erl | 8 +- docs/step-effect-hg-migration.md | 221 ++++++++++++++++++ rebar.lock | 2 +- 20 files changed, 455 insertions(+), 163 deletions(-) create mode 100644 apps/prg_machine/src/hg_machine_action.erl create mode 100644 docs/step-effect-hg-migration.md diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal.erl b/apps/ff_transfer/src/ff_adapter_withdrawal.erl index 4cc6333f..3106129e 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() :: progressor_action:timer(). +-type timer() :: hg_machine_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 2c5ebc62..7b251950 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl @@ -421,7 +421,7 @@ 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). -%% progressor_action:timer() expects {deadline, calendar:datetime() | binary()}. +%% hg_machine_action:timer() expects {deadline, calendar:datetime() | binary()}. unmarshal_provider_timer({deadline, Deadline}) when is_binary(Deadline) -> {deadline, Deadline}; unmarshal_provider_timer({deadline, {DateTime, _USec}}) -> diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index 82d99077..a9f3804b 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -330,7 +330,7 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => progressor_action:instant(), + action => timeout, auxst => #{ctx => Ctx} }. @@ -682,11 +682,11 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> progressor_action:t() | undefined. +-spec map_action(action()) -> hg_machine_action:t(). map_action(undefined) -> - undefined; + idle; map_action(continue) -> - progressor_action:instant(). + timeout. -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index 47b8b5a7..da7da1cc 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -255,7 +255,7 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => progressor_action:instant(), + action => timeout, auxst => #{ctx => Ctx} }. @@ -280,7 +280,7 @@ process_repair(Scenario, Machine) -> -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{events => [], action => progressor_action:instant()}. + #{events => [], action => timeout}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> @@ -303,14 +303,28 @@ marshal_aux_state(AuxSt) -> unmarshal_aux_state(Payload) when is_binary(Payload) -> ff_machine_codec:unmarshal_aux_state(Payload). --spec from_repair_result(map(), machine()) -> prg_result(). +-type action() :: continue | undefined. + +-type repair_result() :: #{ + events := [term()], + action => action(), + aux_state => term() +}. + +-spec from_repair_result(repair_result(), machine()) -> prg_result(). from_repair_result(#{events := Events} = Result, Machine) -> #{ events => repair_events_to_domain(Events), - action => undefined, + action => map_action(maps:get(action, Result, undefined)), auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. +-spec map_action(action()) -> hg_machine_action:t(). +map_action(undefined) -> + idle; +map_action(continue) -> + timeout. + -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index 1b20faa2..e31c5a4a 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -231,7 +231,7 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => progressor_action:instant(), + action => timeout, auxst => #{ctx => Ctx} }. @@ -256,7 +256,7 @@ process_repair(Scenario, Machine) -> -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{events => [], action => progressor_action:instant()}. + #{events => [], action => timeout}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> @@ -279,14 +279,28 @@ marshal_aux_state(AuxSt) -> unmarshal_aux_state(Payload) when is_binary(Payload) -> ff_machine_codec:unmarshal_aux_state(Payload). --spec from_repair_result(map(), machine()) -> prg_result(). +-type action() :: continue | undefined. + +-type repair_result() :: #{ + events := [term()], + action => action(), + aux_state => term() +}. + +-spec from_repair_result(repair_result(), machine()) -> prg_result(). from_repair_result(#{events := Events} = Result, Machine) -> #{ events => repair_events_to_domain(Events), - action => undefined, + action => map_action(maps:get(action, Result, undefined)), auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. +-spec map_action(action()) -> hg_machine_action:t(). +map_action(undefined) -> + idle; +map_action(continue) -> + timeout. + -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index e4d878ec..bfda6aa5 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -194,7 +194,7 @@ sleep | continue | undefined - | {setup_timer, progressor_action:timer()}. + | {setup_timer, hg_machine_action:timer()}. -export_type([withdrawal/0]). -export_type([withdrawal_state/0]). @@ -1852,7 +1852,7 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => progressor_action:instant(), + action => timeout, auxst => #{ctx => Ctx} }. @@ -1938,15 +1938,15 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> progressor_action:t() | undefined. +-spec map_action(action()) -> hg_machine_action:t(). map_action(undefined) -> - undefined; + idle; map_action(continue) -> - progressor_action:instant(); + timeout; map_action(sleep) -> - progressor_action:unset_timer(); + suspend; map_action({setup_timer, Timer}) -> - progressor_action:set_timer(Timer). + hg_machine_action:schedule_timer(Timer). -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index 031c5d33..c0c195da 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -121,8 +121,8 @@ -type action() :: undefined | continue - | {setup_callback, ff_withdrawal_callback:tag(), progressor_action:timer()} - | {setup_timer, progressor_action:timer()} + | {setup_callback, ff_withdrawal_callback:tag(), hg_machine_action:timer()} + | {setup_timer, hg_machine_action:timer()} | retry | finish. @@ -400,7 +400,7 @@ namespace() -> init(Events, _Machine) -> #{ events => Events, - action => progressor_action:instant(), + action => timeout, auxst => #{ctx => ff_entity_context:new()} }. @@ -442,7 +442,7 @@ process_repair(Scenario, Machine) -> -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{events => [], action => progressor_action:instant()}. + #{events => [], action => timeout}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> @@ -489,20 +489,20 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action(), session_state()) -> progressor_action:t() | undefined. +-spec map_action(action(), session_state()) -> hg_machine_action:t(). map_action(undefined, _Session) -> - undefined; + idle; map_action(continue, _Session) -> - progressor_action:instant(); + timeout; map_action({setup_callback, Tag, Timer}, Session) -> ok = ff_machine_tag:create_binding(?NS, Tag, id(Session)), - progressor_action:set_timer(Timer); + hg_machine_action:schedule_timer(Timer); map_action({setup_timer, Timer}, _Session) -> - progressor_action:set_timer(Timer); + hg_machine_action:schedule_timer(Timer); map_action(finish, _Session) -> - progressor_action:unset_timer(); + suspend; map_action(retry, _Session) -> - progressor_action:instant(). + timeout. -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 51465b07..6b2e0e91 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -96,7 +96,7 @@ -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). --type action() :: progressor_action:t(). +-type action() :: hg_machine_action:t(). %% API @@ -307,7 +307,7 @@ init(Invoice, _Machine) -> Changes = [?invoice_created(UnmarshalledInvoice)], #{ events => [Changes], - action => set_invoice_timer(progressor_action:new(), #st{invoice = UnmarshalledInvoice}), + action => set_invoice_timer(idle, #st{invoice = UnmarshalledInvoice}), auxst => #{} }. @@ -359,22 +359,21 @@ handle_signal(timeout, #st{activity = invoice} = St) -> handle_expiration(St). construct_repair_action(CA) when CA /= undefined -> - lists:foldl( - fun merge_repair_action/2, - progressor_action:new(), - [{timer, CA#repair_ComplexAction.timer}, {remove, CA#repair_ComplexAction.remove}] - ); + case CA#repair_ComplexAction.remove of + #repair_RemoveAction{} -> + remove; + undefined -> + case CA#repair_ComplexAction.timer of + undefined -> + idle; + {set_timer, #repair_SetTimerAction{timer = Timer}} -> + hg_machine_action:schedule_timer(Timer); + {unset_timer, #repair_UnsetTimerAction{}} -> + suspend + end + end; construct_repair_action(undefined) -> - progressor_action:new(). - -merge_repair_action({timer, {set_timer, #repair_SetTimerAction{timer = Timer}}}, Action) -> - progressor_action:set_timer(Timer, Action); -merge_repair_action({timer, {unset_timer, #repair_UnsetTimerAction{}}}, Action) -> - progressor_action:unset_timer(Action); -merge_repair_action({remove, #repair_RemoveAction{}}, Action) -> - progressor_action:mark_removal(Action); -merge_repair_action({_, undefined}, Action) -> - Action. + idle. should_validate_transitions(#payproc_InvoiceRepairParams{validate_transitions = V}) when is_boolean(V) -> V; @@ -473,7 +472,7 @@ handle_call({{'Invoicing', 'Rescind'}, {_InvoiceID, Reason}}, St0) -> #{ response => ok, changes => [?invoice_status_changed(?invoice_cancelled(hg_utils:format_reason(Reason)))], - action => progressor_action:unset_timer(), + action => suspend, state => St }; handle_call({{'Invoicing', 'RefundPayment'}, {_InvoiceID, PaymentID, Params}}, St0) -> @@ -542,8 +541,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}}) -> - progressor_action:set_deadline(Due, Action); +set_invoice_timer(?invoice_unpaid(), _Action, #st{invoice = #domain_Invoice{due = Due}}) -> + hg_machine_action:schedule_deadline(Due); set_invoice_timer(_Status, Action, _St) -> Action. @@ -1091,24 +1090,23 @@ changes_from_msgpack_data(#{format_version := V, data := Data}) -> changes_from_msgpack_data(Changes) when is_list(Changes) -> Changes. --spec action_to_prg(progressor_action:t() | undefined) -> action(). +-spec action_to_prg(action() | undefined) -> action(). action_to_prg(#mg_stateproc_ComplexAction{timer = Timer, remove = Remove}) -> - Action0 = progressor_action:new(), - Action1 = - case Timer of - undefined -> - Action0; - {set_timer, #mg_stateproc_SetTimerAction{timer = T}} -> - progressor_action:set_timer(T, Action0); - {unset_timer, #mg_stateproc_UnsetTimerAction{}} -> - progressor_action:unset_timer(Action0) - end, case Remove of - undefined -> - Action1; #mg_stateproc_RemoveAction{} -> - progressor_action:mark_removal(Action1) + remove; + undefined -> + case Timer of + undefined -> + idle; + {set_timer, #mg_stateproc_SetTimerAction{timer = T}} -> + hg_machine_action:schedule_timer(T); + {unset_timer, #mg_stateproc_UnsetTimerAction{}} -> + suspend + end end; +action_to_prg(undefined) -> + idle; action_to_prg(Action) -> Action. diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index f8d22a0e..cf7c8741 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -388,7 +388,7 @@ get_chargeback_opts(#st{opts = Opts} = St) -> %% -type event() :: dmsl_payproc_thrift:'InvoicePaymentChangePayload'(). --type action() :: progressor_action:t(). +-type action() :: hg_machine_action:t(). -type events() :: [event()]. -type result() :: {events(), action()}. -type machine_result() :: {next | done, result()}. @@ -462,7 +462,7 @@ init_(PaymentID, Params, #{timestamp := CreatedAt} = Opts) -> [] end, Events = [?payment_started(Payment2)] ++ CascadeTokenEvents, - {collapse_changes(Events, undefined, #{}), {Events, progressor_action:instant()}}. + {collapse_changes(Events, undefined, #{}), {Events, timeout}}. seed_bank_card_from_parent(PartyConfigRef, BCT, #{parent_payment := ParentPayment}) -> case get_recurrent_token(ParentPayment) of @@ -985,7 +985,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, progressor_action:instant()}}. + {ok, {Changes, timeout}}. partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocation) -> Payment = get_payment(St0), @@ -1009,7 +1009,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, progressor_action:instant()}}. + {ok, {Changes, timeout}}. -spec cancel(st(), binary()) -> {ok, result()}. cancel(St, Reason) -> @@ -1017,7 +1017,7 @@ cancel(St, Reason) -> _ = assert_activity({payment, flow_waiting}, St), _ = assert_payment_flow(hold, Payment), Changes = start_session(?cancelled_with_reason(Reason)), - {ok, {Changes, progressor_action:instant()}}. + {ok, {Changes, timeout}}. assert_capture_cost_currency(undefined, _) -> ok; @@ -1148,7 +1148,7 @@ refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> refund => Refund, cash_flow => FinalCashflow }), - {Refund, {Changes, progressor_action:instant()}}. + {Refund, {Changes, timeout}}. -spec manual_refund(refund_params(), st(), opts()) -> {domain_refund(), result()}. manual_refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> @@ -1165,7 +1165,7 @@ manual_refund(Params, St0, #{timestamp := CreatedAt} = Opts) -> cash_flow => FinalCashflow, transaction_info => TransactionInfo }), - {Refund, {Changes, progressor_action:instant()}}. + {Refund, {Changes, timeout}}. make_refund(Params, Payment, Revision, CreatedAt, St, Opts) -> _ = assert_no_pending_chargebacks(St), @@ -1625,7 +1625,7 @@ construct_adjustment( state = State }, Events = [?adjustment_ev(ID, ?adjustment_created(Adjustment)) | AdditionalEvents], - {Adjustment, {Events, progressor_action:instant()}}. + {Adjustment, {Events, timeout}}. construct_adjustment_id(#st{adjustments = As}) -> erlang:integer_to_binary(length(As) + 1). @@ -1679,7 +1679,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], progressor_action:new()}}. + {done, {[Event], idle}}. prepare_adjustment_cashflow(Adjustment, St, Options) -> PlanID = construct_adjustment_plan_id(Adjustment, St, Options), @@ -1755,7 +1755,7 @@ process_signal(timeout, St, Options) -> ). process_timeout(St) -> - Action = progressor_action:new(), + Action = idle, repair_process_timeout(get_activity(St), Action, St). -spec process_timeout(activity(), action(), st()) -> machine_result(). @@ -1904,19 +1904,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()], progressor_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, progressor_action:set_timeout(0, Action)}} + {next, {Events, timeout}} end. construct_shop_limit_failure(limit_overflow, IDs) -> @@ -1924,16 +1924,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))], progressor_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()], progressor_action:set_timeout(0, Action)}}. + {next, {[?shop_limit_applied()], timeout}}. -spec process_risk_score(action(), st()) -> machine_result(). process_risk_score(Action, St) -> @@ -1947,7 +1947,7 @@ process_risk_score(Action, St) -> Events = [?risk_score_changed(RiskScore)], case check_risk_score(RiskScore) of ok -> - {next, {Events, progressor_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) @@ -1984,7 +1984,7 @@ process_routing(Action, St) -> Revision, St ), - {next, {Events, progressor_action:set_timeout(0, Action)}} + {next, {Events, timeout}} end end. @@ -2109,7 +2109,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, progressor_action:set_timeout(0, Action)}} + {next, {Events, timeout}} end. log_rejected_route_groups(Result, VS) -> @@ -2156,7 +2156,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), @@ -2182,7 +2182,7 @@ process_cash_flow_building(Action, St) -> {1, FinalCashflow} ), Events = [?cash_flow_changed(FinalCashflow)], - {next, {Events, progressor_action:set_timeout(0, Action)}}. + {next, {Events, timeout}}. %% @@ -2239,9 +2239,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, progressor_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, @@ -2256,7 +2256,7 @@ process_accounter_update(Action, #st{partial_cash_flow = FinalCashflow, capture_ ] ), Events = start_session(?captured(Reason, Cost, Cart, Allocation)), - {next, {Events, progressor_action:set_timeout(0, Action)}}. + {next, {Events, timeout}}. %% @@ -2281,11 +2281,11 @@ process_session(St) -> process_session(undefined, St0) -> Target = get_target(St0), TargetType = get_target_type(Target), - Action = progressor_action:new(), + Action = idle, case validate_processing_deadline(get_payment(St0), TargetType) of ok -> Events = start_session(Target), - Result = {Events, progressor_action:set_timeout(0, Action)}, + Result = {Events, timeout}, {next, Result}; Failure -> process_failure(get_activity(St0), [], Action, Failure, St0) @@ -2310,7 +2310,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 = progressor_action:set_timeout(0, Action), + NewAction = timeout, InvoiceID = get_invoice_id(get_invoice(get_opts(St0))), St1 = collapse_changes(Events1, St0, #{invoice_id => InvoiceID}), _ = @@ -2334,7 +2334,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() -> @@ -2357,13 +2357,13 @@ finalize_payment(Action, St) -> _ -> start_session(Target) end, - {done, {StartEvents, progressor_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 @@ -2397,18 +2397,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)], progressor_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 = progressor_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 = progressor_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), @@ -2588,9 +2588,9 @@ 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, progressor_action:set_timeout(0, Action)}}. + {next, {Events ++ RollbackStarted, timeout}}. retry_session(Action, Target, Timeout) -> NewEvents = start_session(Target), @@ -2642,18 +2642,18 @@ 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() -> - progressor_action:set_timeout(0, Action); + timeout; ?invoice_payment_flow_hold(_, HeldUntil) -> - progressor_action:set_deadline(HeldUntil, Action) + hg_machine_action:schedule_deadline(HeldUntil) end; get_action(_Target, Action, _St) -> Action. -set_timer(Timer, Action) -> - progressor_action:set_timer(Timer, Action). +set_timer(Timer, _Action) -> + hg_machine_action:schedule_timer(Timer). get_provider_payment_terms(St, Revision) -> Opts = get_opts(St), diff --git a/apps/hellgate/src/hg_invoice_payment_chargeback.erl b/apps/hellgate/src/hg_invoice_payment_chargeback.erl index 15185501..8bc7e2e3 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() :: - progressor_action:t(). + hg_machine_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, progressor_action:new(), Opts); + update_cash_flow(State, idle, Opts); process_timeout(updating_cash_flow, State, _Action, Opts) -> - update_cash_flow(State, progressor_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 = progressor_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 = progressor_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 = progressor_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 = progressor_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 = progressor_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 e6709c49..7ff22dff 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -103,7 +103,7 @@ -type event() :: dmsl_payproc_thrift:'InvoicePaymentChangePayload'(). -type event_payload() :: dmsl_payproc_thrift:'InvoicePaymentRefundChangePayload'(). -type events() :: [event()]. --type action() :: progressor_action:t(). +-type action() :: hg_machine_action:t(). -type result() :: {events(), action()}. -type machine_result() :: {next | done, result()}. @@ -271,10 +271,10 @@ do_process(accounter, Refund) -> do_process(failure, Refund) -> process_failure(Refund); do_process(finished, _Refund) -> - {done, {[], progressor_action:new()}}. + {done, {[], idle}}. process_refund_cashflow(Refund) -> - Action = progressor_action:set_timeout(0, progressor_action:new()), + Action = timeout, PartyConfigRef = get_injected_party_config_ref(Refund), ShopConfigRef = get_injected_shop_config_ref(Refund), Shop = get_injected_shop(Refund), @@ -316,7 +316,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 = progressor_action:set_timeout(0, Action), + NewAction = timeout, {next, {Events1, NewAction}}; {finished, ?session_failed(Failure)} -> case check_retry_possibility(Failure, Refund) of @@ -326,7 +326,7 @@ finish_session_processing({Events0, Action}, Session, Refund) -> {next, {Events1 ++ SessionEvents, SessionAction}}; fatal -> RollbackStarted = [?refund_rollback_started(Failure)], - {next, {Events1 ++ RollbackStarted, progressor_action:set_timeout(0, Action)}} + {next, {Events1 ++ RollbackStarted, timeout}} end; _ -> {next, {Events1, Action}} @@ -335,14 +335,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())], progressor_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, progressor_action:new()}}. + {done, {Events, idle}}. hold_refund_limits(Refund) -> DomainRefund = refund(Refund), @@ -451,9 +451,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 = progressor_action:set_timer({timeout, Timeout}, Action), + NewAction = hg_machine_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 c80edae7..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, progressor_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 = progressor_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, progressor_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 baf7d837..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}))], progressor_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_session.erl b/apps/hellgate/src/hg_session.erl index 7e0e35cb..abaea40a 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() :: progressor_action:t(). +-type action() :: hg_machine_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, progressor_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) -> - {{[], progressor_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, progressor_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, progressor_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, progressor_action:unset_timer(progressor_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, progressor_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 = progressor_action:set_timer(Timer, Action0), +handle_proxy_intent(#proxy_provider_SleepIntent{timer = Timer}, _Action0, _Session) -> + Action1 = hg_machine_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 = progressor_action:set_timer(Timer, Action0), + Action1 = hg_machine_action:schedule_timer(Timer), Events = [?session_suspended(Tag, TimeoutBehaviour)], {Events, Action1}. diff --git a/apps/prg_machine/src/hg_machine_action.erl b/apps/prg_machine/src/hg_machine_action.erl new file mode 100644 index 00000000..3ec269c8 --- /dev/null +++ b/apps/prg_machine/src/hg_machine_action.erl @@ -0,0 +1,42 @@ +-module(hg_machine_action). + +%%% Scheduling helpers for wire `action()` in processor intent. + +-include_lib("progressor/include/progressor.hrl"). + +-export([marshal_timer/1, schedule_timer/1, schedule_after/1, schedule_deadline/1]). + +-export_type([t/0, timer/0, seconds/0]). + +-type seconds() :: timeout_sec(). +-type datetime() :: calendar:datetime() | 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_after(seconds()) -> t(). +schedule_after(0) -> + timeout; +schedule_after(Seconds) when is_integer(Seconds), Seconds > 0 -> + {schedule, #{at => erlang:system_time(second) + Seconds, action => timeout}}. + +-spec schedule_deadline(datetime()) -> t(). +schedule_deadline(Deadline) -> + {schedule, #{at => marshal_timer({deadline, Deadline}), action => timeout}}. + +-spec marshal_timer(timer()) -> timestamp_sec(). +marshal_timer({timeout, 0}) -> + erlang:system_time(second); +marshal_timer({timeout, Seconds}) when is_integer(Seconds), Seconds >= 0 -> + erlang:system_time(second) + Seconds; +marshal_timer({deadline, {_, _} = Dt}) -> + calendar:datetime_to_gregorian_seconds(Dt) - ?EPOCH_DIFF; +marshal_timer({deadline, Bin}) when is_binary(Bin) -> + calendar:rfc3339_to_system_time(unicode:characters_to_list(Bin), [{unit, second}]); +marshal_timer(Other) -> + error({invalid_timer, Other}). diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index b863c205..7984901b 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -33,7 +33,7 @@ -type signal() :: timeout | {repair, args()}. -type result() :: #{ events => [event_body()], - action => progressor_action:t(), + action => action(), auxst => term() }. @@ -341,7 +341,7 @@ dispatch(Handler, call, BinArgs, Machine) -> {notify, Args} -> dispatch_notification(Handler, Args, Machine); remove -> - #{events => [], action => progressor_action:remove(), auxst => maps:get(aux_state, Machine)}; + #{events => [], action => remove, auxst => maps:get(aux_state, Machine)}; Call -> Handler:process_call(Call, Machine) end; @@ -371,15 +371,19 @@ marshal_process_result(_Handler, _LastEventID, {error, Reason}) -> {error, encode_term(Reason)}. marshal_intent(Handler, LastEventID, Result) when is_map(Result) -> - Base = genlib_map:compact(#{ - events => marshal_new_events(Handler, LastEventID, maps:get(events, Result, [])), - action => maps:get(action, Result, progressor_action:new()) - }), + 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 -> - Base#{aux_state => marshal_aux_state(Handler, maps:get(auxst, Result))}; + Base1#{aux_state => marshal_aux_state(Handler, maps:get(auxst, Result))}; false -> - Base + Base1 end. %% Internals — progressor <-> machine 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 index a0ec9aba..e8b586b9 100644 --- a/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl +++ b/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl @@ -24,14 +24,14 @@ namespace() -> init(_Args, _Machine) -> #{ events => [], - action => progressor_action:new(), + 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 => progressor_action:new()}. + #{events => [], action => idle}. -spec process_call(prg_machine:call(), prg_machine:machine()) -> {prg_machine:response(), prg_machine:result()}. @@ -41,11 +41,11 @@ process_call(crash, _Machine) -> erlang:error(deliberate_crash); process_call(recheck, Machine) -> Model = prg_machine:collapse(?MODULE, Machine), - {ok, #{events => [], action => progressor_action:new(), auxst => #{model => Model}}}. + {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 => progressor_action:new()}. + #{events => [], action => idle}. -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(undefined) -> diff --git a/apps/prg_machine/test/prg_machine_env_mock_handler.erl b/apps/prg_machine/test/prg_machine_env_mock_handler.erl index 22b98b11..46660b62 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_handler.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -10,16 +10,16 @@ namespace() -> -spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). init(_Args, _Machine) -> - #{events => [], action => progressor_action:new()}. + #{events => [], action => idle}. -spec process_signal(prg_machine:signal(), prg_machine:machine()) -> prg_machine:result(). process_signal(_Signal, _Machine) -> - #{events => [], action => progressor_action:new()}. + #{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 => progressor_action:new()}}. + {ok, #{events => [], action => idle}}. -spec process_repair(prg_machine:args(), prg_machine:machine()) -> prg_machine:result() | {error, term()}. process_repair(_Args, _Machine) -> - #{events => [], action => progressor_action:new()}. + #{events => [], action => idle}. diff --git a/docs/step-effect-hg-migration.md b/docs/step-effect-hg-migration.md new file mode 100644 index 00000000..e29b6818 --- /dev/null +++ b/docs/step-effect-hg-migration.md @@ -0,0 +1,221 @@ +# Миграция hellgate на action() (новый progressor) + +План переработки hellgate / ff / prg_machine после релиза progressor с явной +алгеброй `action()` в `processor_intent`. + +План доработки progressor: `progressor/docs/step-effect-migration.md`. + +**Предпосылка:** progressor tag `vX.Y.0` — в runtime **только** wire `action()`; +`progressor_action` удалён; `normalize` внутри progressor **нет**. + +--- + +## Затронутые области + +~17 модулей (были `progressor_action:*`, переведены на wire `action()`): + +| Область | Модули (основные) | +|---------|-------------------| +| prg_machine | `prg_machine.erl`, test handlers | +| HG invoice/payment | `hg_invoice.erl`, `hg_invoice_payment.erl`, `hg_session.erl`, … | +| FF transfer | `ff_withdrawal.erl`, `ff_withdrawal_session.erl`, `ff_source.erl`, … | +| Codecs | `ff_codec.erl`, `ff_withdrawal_codec.erl`, … | + +Текущая зависимость: `rebar.config` → `progressor` branch `add_action_module` (заменить на tag). + +--- + +## Принцип + +**Legacy MG/map живёт только в hellgate, до marshal в progressor.** + +``` +┌─────────────────────────────────────┐ +│ hellgate / ff (домен) │ оркестрация шага, MG repair +│ hg_machine_action (новый) │ timer/deadline → wire action() +└──────────────┬──────────────────────┘ + │ action() (progressor.hrl) +┌──────────────▼──────────────────────┐ +│ prg_machine │ marshal_intent → processor_intent +└──────────────┬──────────────────────┘ + │ +┌──────────────▼──────────────────────┐ +│ progressor │ dispatch_action — без legacy +└─────────────────────────────────────┘ +``` + +- Поле в intent по-прежнему **`action`**, не `effect`. +- Wire-значения — атомы и `{schedule, ...}`; см. `progressor/include/progressor.hrl`. +- **`progressor_action` не вернётся** — замена локальным `hg_machine_action` (или прямым wire в домене). + +### Таблица legacy → wire (для адаптера и доменов) + +| Было (`progressor_action` / map) | Стало (`action()`) | +|----------------------------------|---------------------| +| `new()` / `undefined` | поле не писать (= `idle`) | +| `instant()` / `set_timeout(0, _)` | `timeout` | +| `unset_timer` | `suspend` | +| `remove()` / `#{remove := true}` | `remove` | +| `set_timeout(N, _)` / `#{set_timer => Ts}` | `{schedule, #{at => Ts, action => timeout}}` | +| `set_deadline(Dt, _)` | `{schedule, #{at => unix(Dt), action => timeout}}` | +| `#{set_timer => Ts, remove := true}` | `{schedule, #{at => Ts, action => remove}}` | + +`at` — абсолютный unix sec; относительное время считает автор (`erlang:system_time(second) + N`). + +--- + +## Фаза 0. Bump зависимости + +1. `rebar.config`: `{progressor, {git, "...", {tag, "vX.Y.0"}}}`. +2. `rebar3 upgrade progressor`, обновить `rebar.lock`. + +**Сразу после bump compile не зелёный** — это ожидаемо: `progressor_action` исчез из зависимости. + +**Критерий:** lock обновлён; список compile errors = карта работ (grep `progressor_action`). + +--- + +## Фаза 1. `hg_machine_action` + `prg_machine` ✓ + +### `hg_machine_action` + +Только хелперы планирования (без coerce/legacy): + +- `t() :: action()` (`progressor.hrl`); +- `marshal_timer/1`, `schedule_timer/1`, `schedule_after/1`, `schedule_deadline/1`; +- wire-атомы (`idle`, `timeout`, `suspend`, `remove`) — напрямую в доменах. + +### `prg_machine` + +```erlang +-type result() :: #{ + events => [event_body()], + action => action(), + auxst => term() +}. +``` + +`marshal_intent/3`: `maps:get(action, Result, idle)` → в intent только wire; ключ `action` опускается для `idle`. + +Домены и FF переведены на wire; shim `progressor_action` и `from_legacy` удалены. + +**Критерий:** `rebar3 compile` green; `rebar3 eunit --module=prg_machine` green. + +--- + +## Фаза 2. Hellgate core + +Порядок по риску: + +| Модуль | ~вызовов | Заметки | +|--------|----------|---------| +| `hg_session.erl` | 10 | proxy/timer | +| `hg_invoice_payment_refund.erl` | 9 | | +| `hg_invoice_payment_chargeback.erl` | 8 | | +| `hg_invoice_payment.erl` | 31 | самый большой | +| `hg_invoice.erl` | 14+ | `action_to_prg`, repair | +| `hg_invoice_registered_payment.erl` | 3 | | +| `hg_invoice_repair.erl` | 1 | | + +Для каждого: + +- `progressor_action:*` → `hg_machine_action:*` или прямой wire (`timeout`, `suspend`, `{schedule, ...}`); +- `-type action()` в домене → `hg_machine_action:t()` или `action()`; +- аккумулятор `{Events, Action}` — перезапись `Action` на шаге, не `set_timeout(0, Old)`. + +### `hg_invoice` + +- `action_to_prg/1` → `hg_machine_action:from_mg/1`; +- `merge_repair_action/2` → `hg_machine_action:from_repair/2` (один исход, без затирания timer); +- `set_invoice_timer/2` → deadline → `{schedule, #{at => ..., action => timeout}}`. + +**Тест:** repair timer + remove → `{schedule, #{action => remove, at => ...}}`. + +**Критерий:** HG ct по invoice/payment green. + +--- + +## Фаза 3. FF transfer + +| Модуль | Паттерн | +|--------|---------| +| `ff_withdrawal.erl`, `ff_withdrawal_session.erl` | `map_action/1` → wire `action()` | +| `ff_source.erl`, `ff_destination.erl`, `ff_deposit.erl` | `instant` → `timeout` | + +```erlang +map_action(continue) -> timeout; +map_action(sleep) -> suspend; +map_action({setup_timer, T}) -> hg_machine_action:schedule_timeout(T). +``` + +**Критерий:** ff ct (withdrawal, destination suites) green. + +--- + +## Фаза 4. Codecs и repair API + +- `ff_codec.erl` — `repairer_ComplexAction` → `hg_machine_action:from_repair/2`; +- `ff_withdrawal_codec.erl`, `ff_*_codec.erl` — то же; +- `ff_repair.erl`, `hg_invoice_tests_SUITE` repair macros. + +**Критерий:** repair-тесты: timer only, remove only, timer + remove. + +--- + +## Фаза 5. Зачистка + +```bash +rg 'progressor_action' apps/ # → 0 ✓ +rg '#{set_timer' apps/ # → 0 ✓ +# unset_timer остаётся в thrift/repair unmarshal (MG), не в processor intent +``` + +1. ~~Удалить `from_legacy`~~ ✓ +2. Обновить `docs/prg-machine-migration-context.md`, `prg-machine-remaining-debt.md` при наличии. +3. Lock progressor на tag в `rebar.config` / `rebar.lock` (сейчас ref `4f6d78a`). + +**Критерий:** полный CI hellgate green. + +--- + +## Фаза 6. MG/thrift (опционально, отдельный PR) + +`#mg_stateproc_ComplexAction{}` в woody-путях — `hg_machine_action:from_mg/1`. + +Долгосрочно: thrift ComplexAction не протаскивается в progressor как map-ключи. + +--- + +## Порядок + +``` +progressor tag → hg_machine_action + prg_machine (фаза 1) + → HG domains (фаза 2) ∥ FF (фаза 3) + → codecs (фаза 4) → cleanup (фаза 5) +``` + +Фазы 2 и 3 частично параллелятся после готового `hg_machine_action`. + +--- + +## Не делать + +- `effect` / `prg_effect` / `normalize` **в progressor** — контракт уже другой. +- Возвращать `progressor_action` / legacy map в processor intent. +- Fold repair actions с затиранием timer — один `action()` на intent. +- Authoring `{timeout, N}` в intent progressor — только wire. + +--- + +## Чеклист приёмки + +- [x] progressor на ref `4f6d78a` (до tag) +- [x] `prg_machine:result()` — `action => action()` +- [x] `marshal_intent` — wire `action()` без coerce/legacy +- [x] нет `progressor_action` в apps/ +- [ ] repair timer + remove → `{schedule, #{action => remove, at => ...}}` (сейчас remove побеждает timer, как раньше) +- [ ] семантика `call_replace_timer` сохранена (новый schedule отменяет pending remove) +- [x] `from_legacy` / `coerce` удалены +- [x] `rebar3 compile` + `prg_machine` eunit green +- [ ] полный CI hellgate +- [ ] progressor tag в `rebar.config` diff --git a/rebar.lock b/rebar.lock index e28cbaff..e59db8b5 100644 --- a/rebar.lock +++ b/rebar.lock @@ -117,7 +117,7 @@ 0}, {<<"progressor">>, {git,"https://github.com/valitydev/progressor.git", - {ref,"07e39efbd90e7916be866f84984d116911366d49"}}, + {ref,"4f6d78a01854b8e03612719be4418dd8402a090e"}}, 0}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.11.0">>},0}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.9">>},0}, From 43a2ad6b57b63a2080b1626058378f69ac0bc38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 12 Jun 2026 14:05:34 +0300 Subject: [PATCH 27/62] Refactor action type specifications across multiple modules to replace hg_machine_action with prg_action for improved consistency. Update timer handling in ff_withdrawal, ff_deposit, ff_destination, and ff_source modules. Adjust event processing logic to utilize prg_action for scheduling and timer actions, enhancing clarity and maintainability. --- .../ff_transfer/src/ff_adapter_withdrawal.erl | 2 +- .../src/ff_adapter_withdrawal_codec.erl | 2 +- apps/ff_transfer/src/ff_deposit.erl | 2 +- apps/ff_transfer/src/ff_destination.erl | 2 +- apps/ff_transfer/src/ff_source.erl | 2 +- apps/ff_transfer/src/ff_withdrawal.erl | 6 +- .../ff_transfer/src/ff_withdrawal_session.erl | 10 +- apps/hellgate/src/hg_invoice.erl | 8 +- apps/hellgate/src/hg_invoice_payment.erl | 6 +- .../src/hg_invoice_payment_chargeback.erl | 2 +- .../src/hg_invoice_payment_refund.erl | 4 +- apps/hellgate/src/hg_session.erl | 6 +- .../{hg_machine_action.erl => prg_action.erl} | 4 +- docs/prg-invoice-single-collapse-goal.md | 48 --- docs/prg-machine-error-semantics-hg-ff.md | 276 --------------- docs/prg-machine-migration-context.md | 330 ------------------ docs/prg-machine-remaining-debt.md | 48 --- docs/prg-machine-review-plan.md | 80 ----- docs/prg-machine.md | 220 ++++++++++++ docs/step-effect-hg-migration.md | 221 ------------ 20 files changed, 248 insertions(+), 1031 deletions(-) rename apps/prg_machine/src/{hg_machine_action.erl => prg_action.erl} (92%) delete mode 100644 docs/prg-invoice-single-collapse-goal.md delete mode 100644 docs/prg-machine-error-semantics-hg-ff.md delete mode 100644 docs/prg-machine-migration-context.md delete mode 100644 docs/prg-machine-remaining-debt.md delete mode 100644 docs/prg-machine-review-plan.md create mode 100644 docs/prg-machine.md delete mode 100644 docs/step-effect-hg-migration.md diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal.erl b/apps/ff_transfer/src/ff_adapter_withdrawal.erl index 3106129e..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() :: hg_machine_action: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 7b251950..12271855 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl @@ -421,7 +421,7 @@ 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). -%% hg_machine_action:timer() expects {deadline, calendar:datetime() | binary()}. +%% prg_action:timer() expects {deadline, calendar:datetime() | binary()}. unmarshal_provider_timer({deadline, Deadline}) when is_binary(Deadline) -> {deadline, Deadline}; unmarshal_provider_timer({deadline, {DateTime, _USec}}) -> diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index a9f3804b..dae716f8 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -682,7 +682,7 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> hg_machine_action:t(). +-spec map_action(action()) -> prg_action:t(). map_action(undefined) -> idle; map_action(continue) -> diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index da7da1cc..d707b6d3 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -319,7 +319,7 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> hg_machine_action:t(). +-spec map_action(action()) -> prg_action:t(). map_action(undefined) -> idle; map_action(continue) -> diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index e31c5a4a..7c251446 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -295,7 +295,7 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> hg_machine_action:t(). +-spec map_action(action()) -> prg_action:t(). map_action(undefined) -> idle; map_action(continue) -> diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index bfda6aa5..9a36e045 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -194,7 +194,7 @@ sleep | continue | undefined - | {setup_timer, hg_machine_action:timer()}. + | {setup_timer, prg_action:timer()}. -export_type([withdrawal/0]). -export_type([withdrawal_state/0]). @@ -1938,7 +1938,7 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> hg_machine_action:t(). +-spec map_action(action()) -> prg_action:t(). map_action(undefined) -> idle; map_action(continue) -> @@ -1946,7 +1946,7 @@ map_action(continue) -> map_action(sleep) -> suspend; map_action({setup_timer, Timer}) -> - hg_machine_action:schedule_timer(Timer). + prg_action:schedule_timer(Timer). -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index c0c195da..b6c2c608 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -121,8 +121,8 @@ -type action() :: undefined | continue - | {setup_callback, ff_withdrawal_callback:tag(), hg_machine_action:timer()} - | {setup_timer, hg_machine_action:timer()} + | {setup_callback, ff_withdrawal_callback:tag(), prg_action:timer()} + | {setup_timer, prg_action:timer()} | retry | finish. @@ -489,16 +489,16 @@ from_repair_result(#{events := Events} = Result, Machine) -> auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action(), session_state()) -> hg_machine_action:t(). +-spec map_action(action(), session_state()) -> prg_action:t(). map_action(undefined, _Session) -> idle; map_action(continue, _Session) -> timeout; map_action({setup_callback, Tag, Timer}, Session) -> ok = ff_machine_tag:create_binding(?NS, Tag, id(Session)), - hg_machine_action:schedule_timer(Timer); + prg_action:schedule_timer(Timer); map_action({setup_timer, Timer}, _Session) -> - hg_machine_action:schedule_timer(Timer); + prg_action:schedule_timer(Timer); map_action(finish, _Session) -> suspend; map_action(retry, _Session) -> diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 6b2e0e91..a44d235d 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -96,7 +96,7 @@ -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). --type action() :: hg_machine_action:t(). +-type action() :: prg_action:t(). %% API @@ -367,7 +367,7 @@ construct_repair_action(CA) when CA /= undefined -> undefined -> idle; {set_timer, #repair_SetTimerAction{timer = Timer}} -> - hg_machine_action:schedule_timer(Timer); + prg_action:schedule_timer(Timer); {unset_timer, #repair_UnsetTimerAction{}} -> suspend end @@ -542,7 +542,7 @@ 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:schedule_deadline(Due); + prg_action:schedule_deadline(Due); set_invoice_timer(_Status, Action, _St) -> Action. @@ -1100,7 +1100,7 @@ action_to_prg(#mg_stateproc_ComplexAction{timer = Timer, remove = Remove}) -> undefined -> idle; {set_timer, #mg_stateproc_SetTimerAction{timer = T}} -> - hg_machine_action:schedule_timer(T); + prg_action:schedule_timer(T); {unset_timer, #mg_stateproc_UnsetTimerAction{}} -> suspend end diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index cf7c8741..5184a777 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -388,7 +388,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()}. @@ -2647,13 +2647,13 @@ get_action(?processed(), _Action, St) -> ?invoice_payment_flow_instant() -> timeout; ?invoice_payment_flow_hold(_, HeldUntil) -> - hg_machine_action:schedule_deadline(HeldUntil) + prg_action:schedule_deadline(HeldUntil) end; get_action(_Target, Action, _St) -> Action. set_timer(Timer, _Action) -> - hg_machine_action:schedule_timer(Timer). + prg_action:schedule_timer(Timer). get_provider_payment_terms(St, Revision) -> Opts = get_opts(St), diff --git a/apps/hellgate/src/hg_invoice_payment_chargeback.erl b/apps/hellgate/src/hg_invoice_payment_chargeback.erl index 8bc7e2e3..e3f84d82 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 diff --git a/apps/hellgate/src/hg_invoice_payment_refund.erl b/apps/hellgate/src/hg_invoice_payment_refund.erl index 7ff22dff..d44fb97f 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -103,7 +103,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()}. @@ -453,7 +453,7 @@ get_manual_refund_events(_) -> retry_session(_Action, Timeout) -> NewEvents = [hg_session:wrap_event(?refunded(), hg_session:create())], - NewAction = hg_machine_action:schedule_timer({timeout, Timeout}), + NewAction = prg_action:schedule_timer({timeout, Timeout}), {NewEvents, NewAction}. -spec check_retry_possibility(failure(), t()) -> diff --git a/apps/hellgate/src/hg_session.erl b/apps/hellgate/src/hg_session.erl index abaea40a..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'(). @@ -367,7 +367,7 @@ handle_proxy_intent(#proxy_provider_FinishIntent{status = {failure, Failure}}, A Events = [?session_finished(?session_failed({failure, Failure}))], {Events, Action}; handle_proxy_intent(#proxy_provider_SleepIntent{timer = Timer}, _Action0, _Session) -> - Action1 = hg_machine_action:schedule_timer(Timer), + Action1 = prg_action:schedule_timer(Timer), {[], Action1}; handle_proxy_intent( #proxy_provider_SuspendIntent{tag = Tag, timeout = Timer, timeout_behaviour = TimeoutBehaviour}, @@ -376,7 +376,7 @@ handle_proxy_intent( ) -> #{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:schedule_timer(Timer), + Action1 = prg_action:schedule_timer(Timer), Events = [?session_suspended(Tag, TimeoutBehaviour)], {Events, Action1}. diff --git a/apps/prg_machine/src/hg_machine_action.erl b/apps/prg_machine/src/prg_action.erl similarity index 92% rename from apps/prg_machine/src/hg_machine_action.erl rename to apps/prg_machine/src/prg_action.erl index 3ec269c8..0fdda24b 100644 --- a/apps/prg_machine/src/hg_machine_action.erl +++ b/apps/prg_machine/src/prg_action.erl @@ -1,6 +1,6 @@ --module(hg_machine_action). +-module(prg_action). -%%% Scheduling helpers for wire `action()` in processor intent. +%%% Scheduling helpers: domain timer/deadline → wire `action()` for processor intent. -include_lib("progressor/include/progressor.hrl"). diff --git a/docs/prg-invoice-single-collapse-goal.md b/docs/prg-invoice-single-collapse-goal.md deleted file mode 100644 index a98ae1a9..00000000 --- a/docs/prg-invoice-single-collapse-goal.md +++ /dev/null @@ -1,48 +0,0 @@ -Контекст собрал. Ключевые факты, на которых строится план: - -- Реплей: `prg_machine:collapse/2` → `dispatch_apply_event` → `hg_invoice:apply_event/4` → `apply_event_changes/3` → `collapse_changes/3` с `#{timestamp => Dt}` (**нестрого**: невалидный переход только warning). -- Пост-call валидация: `validate_changes/1` → `collapse_changes/3` напрямую с `#{validation => strict}` (**строго**: `validate_transition` бросает `{invalid_transition,...}`). Мимо `prg_machine` и мимо `apply_event/4`. -- В `process_call` свежие changes сворачиваются дважды: на 415 (`validate_changes` → нужен пост-state для `log_changes`) и на 710 (`to_prg_result` → результат выбрасывается, только guard). В `process_signal`/`process_repair` — один раз (только 710). -- Новые changes на момент валидации ещё не имеют `event_id`/`timestamp` — их назначает `marshal_new_events` в самом конце (`LastEventID+1...`, `system_time`). - -Вот план. - -## План варианта Б — единый путь сворачивания через `prg_machine` - -### Цель -Свежие (ещё не persisted) изменения после `handle_call/handle_signal/handle_repair` должны накатываться на состояние тем же механизмом `apply_event`, что и реплей истории, а не отдельным прямым вызовом `collapse_changes`. Один код-путь, одинаковые опции/таймстампы, явный режим валидации. - -### Этап 0. Зафиксировать инварианты (до правок) -- Записать, что реплей обязан остаться нестрогим (история доверенная, иначе старые машины перестанут подниматься), а валидация новых changes — строгой. Значит «единый путь» = одна функция фолда с параметром режима, а не одинаковое поведение. -- Подтвердить, что пост-state из валидации реально нужен только `log_changes` (в `to_prg_result` он выбрасывается). Это разрешает слить два прохода в один. - -### Этап 1. Ввести в `prg_machine` API применения новых изменений -- Добавить публичную функцию (рабочее имя `apply_new_events/3` или `fold_events/4`), которая принимает handler, текущую `Model` (collapsed `St`) и список новых event-bodies, и фолдит их через тот же `dispatch_apply_event`, что и `collapse/2`. -- Назначать провизорные `event_id`/`timestamp` так же, как `marshal_new_events`: `event_id` = `LastEventID + n`, `timestamp` = текущее время. Источник `LastEventID` — из `Model`/machine, чтобы `apply_event/4` корректно проставил `latest_event_id`. -- Прокинуть в функцию режим (`strict | lenient`), чтобы handler мог переключать строгость. `collapse/2` остаётся как есть (lenient). -- Решить, как режим доходит до `apply_event/4`: либо доп. аргумент колбэка (расширение опционального `apply_event/4` → `apply_event/5`, с сохранением обратной совместимости через `function_exported`), либо через временное поле в `Model`. Предпочтительно — явный режим в сигнатуре, чтобы не прятать состояние в модели. - -### Этап 2. Согласовать `apply_event` в `hg_invoice` с режимом -- `apply_event_changes/3` и `collapse_changes/3` уже принимают `Opts`; нужно, чтобы режим из Этапа 1 транслировался в `#{validation => strict}` (для новых changes) либо отсутствовал (для реплея). -- Убедиться, что `merge_change`/`merge_payment_change`/`validate_transition` получают `Opts` единообразно вне зависимости от того, пришёл ли change из истории или из свежего результата. -- Аккуратно с `timestamp`: для новых changes сейчас он берётся из обёртки `?payment_ev(... OccurredAt)` и из `Opts`. Проверить, что провизорный `Ts` из Этапа 1 не конфликтует с уже зашитым `OccurredAt` в changes (двойной источник времени). - -### Этап 3. Переписать `validate_changes` на новый API -- Заменить прямые `collapse_changes(Changes, St, #{validation => strict})` / `#{}` внутри `validate_changes/1` на вызов нового `prg_machine`-API с соответствующим режимом (`strict` по умолчанию, `lenient` для ветки `validate := false`, которой пользуется repair через `should_validate_transitions`). -- Сохранить контракт: функция по-прежнему возвращает пост-state (для `log_changes`) и бросает на невалидном переходе. - -### Этап 4. Убрать двойной фолд в `process_call` -- Свернуть changes один раз: вычислить пост-state через новый API однократно, передать его и в `log_changes`, и в построение результата. -- `to_prg_result/1` перестаёт вызывать `validate_changes` внутри (строка 710); валидация делается явно один раз в `process_call`. Для `process_signal`/`process_repair`, которые валидируют только через `to_prg_result`, ввести тот же единый вызов на их уровне, чтобы поведение (строгость, исключения) не поменялось. -- Следствие: `to_prg_result_/1` остаётся чистым маршалингом результата без побочной валидации. - -### Этап 5. Тесты и проверка эквивалентности -- Прогнать `hg_invoice_tests_SUITE` целиком — это основной регресс по сценариям call/signal/repair. -- Точечно проверить: (а) невалидная последовательность changes по-прежнему бросает `{invalid_transition,...}` на call; (б) repair с `validate_transitions = false` остаётся нестрогим; (в) реплей старых машин не начал бросать (lenient сохранён); (г) логи `log_changes` не изменились (тот же пост-state). -- Сверить, что `latest_event_id` после применения новых changes совпадает с тем, что назначит `marshal_new_events` (иначе разъедутся id событий). -- Проверить, что аналогичный паттерн не используется в FF-хендлерах (debt #5 — только HG invoice); если новый API в `prg_machine` общий, убедиться, что он не ломает контракт FF-доменов (они на `apply_event/2`). - -### Риски и развилки -- **Двойной источник timestamp** (провизорный из движка vs `OccurredAt` в обёртке change) — главный источник тонких багов; нужно выбрать единственный источник истины. -- **Расширение контракта `prg_machine`** (`apply_event/5` или новый колбэк) затрагивает всех хендлеров — держать строго опциональным/обратносовместимым. -- **Объём.** Этапы 1–2 — ядро (контракт движка + транзит режима), 3–4 — собственно дедупликация в invoice, 5 — верификация. Минимально полезный срез: можно сделать только Этап 4 (убрать внутрикольцевой двойной фолд) как промежуточный коммит, не трогая контракт движка, а Этапы 1–3 — отдельным шагом. diff --git a/docs/prg-machine-error-semantics-hg-ff.md b/docs/prg-machine-error-semantics-hg-ff.md deleted file mode 100644 index 90780339..00000000 --- a/docs/prg-machine-error-semantics-hg-ff.md +++ /dev/null @@ -1,276 +0,0 @@ -# `prg_machine`: семантика ошибок, HG vs FF, где не сошлось - -Документ фиксирует **как было** (machinery), **что поменяли** в Hellgate и Fistful при унификации на `{error, failed}`, **где сломалось** и **какой контракт считать целевым**. Чтобы не разбирать заново при каждом CT-прогоне. - -См. также: `docs/prg-machine-migration-context.md` (общая архитектура), `docs/prg-machine-remaining-debt.md` (техдолг collapse). - -*Обновлено: 2026-06-10* - ---- - -## 1. Три слоя ошибок (не путать) - -| Слой | Откуда | Пример | Кто обрабатывает | -|------|--------|--------|------------------| -| **A. Progressor API** | `progressor:call/init/repair/get` вернул `{error, Reason}` | `<<"process not found">>`, `<<"process is waiting">>`, `{exception, ...}` | `prg_machine:start/call/repair/get` | -| **B. Processor response** | Вызов progressor **успешен**, в теле ответа `{error, ...}` или `{exception, ...}` | `{ok, {error, invalid_callback}}` из `hg_invoice:process_call` | Домен (`hg_invoice`, FF handlers) | -| **C. Доменный throw** | Woody handler / callback host | `erlang:throw(#payproc_InvoiceNotFound{})` | `hg_*_handler`, `ff_*_handler` | - -Путаница между **A** и **B** — главный источник регрессий после миграции. - ---- - -## 2. Как было: `machinery_prg_backend` - -Файл: `_build/default/lib/machinery/src/machinery_prg_backend.erl` (в prod HG/FF уже не используется, но контракт оттуда «въелся» в тесты и ожидания). - -### 2.1. `call/5` — маппинг ошибок progressor - -```erlang -{error, <<"process not found">>} -> {error, notfound}; -{error, <<"process is init">>} -> {error, notfound}; -{error, {exception, _, _}} -> erlang:error({failed, NS, ID}); % raise -{error, <<"process is error">>} -> erlang:error({failed, NS, ID}); % raise -{error, _Reason} = Error -> {ok, Error}; %% <-- важно -``` - -**Следствие:** любая «неизвестная» ошибка progressor (в т.ч. `<<"process is waiting">>`, `<<"process is running">>`) превращалась в **успешный** вызов machinery с телом `{error, Reason}`. - -Дальше HG Thrift-слой (`hg_invoice_handler:call/3`): - -```erlang -{ok, Reply} -> Reply; % Reply может быть {error, <<"process is waiting">>} -{error, Error} -> erlang:error(Error); -``` - -То есть при статусе процесса `waiting` (таймер, фоновая обработка) внешний `call` **не падал на уровне API**, а возвращал `{error, Reason}` как обычный ответ домена. - -### 2.2. `repair/5` - -Явные ветки: `notfound`, `working` (`<<"process is running">>`), `failed` (exception / `process is error`), остальное — `{error, {failed, DecodedReason}}`. - -### 2.3. Actions / таймеры - -Machinery маршалил actions из списка `mg_stateproc` в `#{set_timer => ...}` **в микросекундах** (`system_time(microsecond)`). - -Сейчас домены отдают `progressor_action` напрямую (`set_deadline`, `set_timer`, `set_timeout`) — progressor сам нормализует единицы через `prg_utils:to_microseconds/1`. Это **не** причина HG-fail (таймеры в progressor работают с секундами/deadline корректно). - ---- - -## 3. Как стало: `prg_machine` (прямой client) - -Файл: `apps/prg_machine/src/prg_machine.erl` - -### 3.1. Целевой контракт `prg_machine` (client API) - -**Не затирать** причину ошибки. Атом `failed` — только для «битого» процесса в storage progressor, не для exception из домена. - -| Progressor | `prg_machine:call` / `start` | -|------------|------------------------------| -| `<<"process not found">>` / `<<"process is init">>` | `{error, notfound}` | -| `<<"process is error">>` | `{error, failed}` | -| `{exception, Class, Reason}` (и 4-tuple со stacktrace) | **`{error, {exception, ...}}` pass-through** | -| `<<"process is waiting">>` и прочие guard-ошибки | **`{error, Reason}` pass-through** | -| `<<"process already exists">>` (`start`) | `{error, exists}` | - -`repair`: те же явные ветки + `working` для `<<"process is running">>`; прочие ошибки (включая `{exception, ...}`) → `{error, {repair, {failed, Reason}}}` — **Reason сохраняется**. - -На внешней границе (woody adapter) exception по-прежнему можно сворачивать в `failed` для контракта провайдера — см. `ff_withdrawal_adapter_host`. - -### 3.2. Регрессия (июнь 2026) - -Временно в `call/6` стояло: - -```erlang -{error, _} -> {error, failed} % catch-all — ПЛОХО -``` - -Вместо: - -```erlang -{error, _} = Error -> Error % pass-through прочих ошибок progressor -``` - -**Эффект:** `<<"process is waiting">>`, `<<"process is running">>` (на call) и прочие guard-ошибки progressor превращались в атом `failed`. Внешние вызовы и callback-пути вели себя иначе, чем при machinery `{ok, {error, Reason}}` или pass-through. - -**Симптомы в CT** (`lib.hellgate`, прогон 2026-06-09): 11 FAIL в `hg_invoice_tests_SUITE`, паттерн: - -- `{badmatch, timeout}` в `next_change` / `next_changes` (таймаут ожидания события 12s) -- в списке ожидаемых payment events появлялся атом `timeout` вместо `payment_rollback_started` и т.п. -- `consistent_account_balances` — побочный эффект незавершённых платежей - -Типичные кейсы: `payment_hold_cancellation`, `payment_hold_auto_cancellation`, каскады (`payment_cascade_*`), `deadline_doesnt_affect_payment_refund`. - -FF transfer после правок `ff_ct_machine`: **88 OK / 0 FAIL** (тот же прогон). - -### 3.3. Текущее состояние ветки - -`prg_machine:call`: - -```erlang -{error, <<"process is error">>} -> {error, failed}; -{error, _} = Error -> Error. -``` - -`start`: `exists` + pass-through. `repair`: `notfound` / `working` / `failed` + `{repair, {failed, Reason}}` с исходным `Reason`. - -**Антипаттерн** (убран): `{error, {exception, _, _}} -> {error, failed}` — теряет class/reason для интроспекции и логов. - ---- - -## 4. Что правили в Hellgate - -| Модуль | Изменение | Зачем | -|--------|-----------|-------| -| `hg_invoicing_machine_client` | `{error, failed}` + **`{error, _} = Error -> Error`** | Проброс pass-through с `prg_machine:call` в Thrift | -| `hg_invoice_handler:call/3` | `{error, Error} -> erlang:error(Error)` | Без изменений по смыслу; `failed` — один из возможных `Error` | -| `hg_invoice` | `fail/1`: `{error, failed}` и `{error, {exception,...}}` → `ok` (тестовый хелпер, намеренный crash) | Согласовано с pass-through exception | -| `hg_invoice` | `process_callback` / `process_session_change_by_tag`: ветка `{ok, {error,_}} -> {error, failed}` (processor response, слой B) | Ответ процессора с ошибкой в теле | -| `hg_invoice_handler:repair/2` | `{error, working}`, `{error, Reason}` | Без `failed` catch-all | - -**Не трогали (и не надо без отдельного goal):** - -- `handle_payment_result` → `set_invoice_timer` при `?cancelled()` / `?failed()` — логика таймера invoice due после платежа -- двойной collapse (`validate_changes` + `to_prg_result`) — см. `prg-machine-remaining-debt.md` - -### 4.1. HG: обработка `failed` на границах - -```erlang -%% hg_invoice_handler:call/3 -{error, Error} -> erlang:error(Error) % в т.ч. failed, <<"process is waiting">> - -%% hg_proxy_host_provider:handle_callback_result/1 -{error, Reason} -> error(Reason) % failed уходит наружу как exception -``` - ---- - -## 5. Что правили в Fistful (FF) - -| Модуль | Изменение | Зачем | -|--------|-----------|-------| -| `ff_withdrawal_machine:call/2` | `notfound`, `failed`, **`{error,_}=Error`** | Симметрия с HG client | -| `ff_withdrawal_session_machine:call/2` | то же | | -| `ff_withdrawal_session_machine:process_callback/1` | spec: `failed` в union | Явная ветка `{error, failed}` | -| `ff_withdrawal_adapter_host` | `{error, failed} -> erlang:error(failed)` | Как HG provider callback | -| `ff_deposit_machine:repair/2` | `{error, failed}` → domain error tuple | Repair-контракт | - -### 5.1. FF CT: `ff_ct_machine` (meck `prg_machine:process/3`) - -Отдельная история — **не влияет на HG suites**, но ломала FF transfer. - -Проблемы: - -1. `meck:passthrough([prg_machine, process, [...]])` — неверная сигнатура, `function_clause` на `init` -2. `meck:passthrough` из mock `process/3` — `{badmatch, undefined}` в `get_current_call()` -3. `?MODULE:process(...)` — не exported - -**Рабочее решение:** - -```erlang -meck:new(prg_machine, [no_link, passthrough]), -meck:expect(prg_machine, process, fun process/3). - -%% внутри mock: -'prg_machine_meck_original':process(Call, Opts, BinCtx). -``` - -Плюс: идемпотентный `load/unload`, hook до `create`, `maybe_unload` в `after`, `await_withdrawal_activity` с `linear(50, 200)`. - -### 5.2. FF тесты на processor exception - -`ff_withdrawal_SUITE:session_repair_test`: - -```erlang -?assertMatch({error, {exception, _, _}}, call_process_callback(Callback)) -``` - -`failed` (атом) — только если progressor вернул `<<"process is error">>` (битый процесс), не crash домена. - ---- - -## 6. Матрица «кто что ожидает» (где не сошлось) - -| Ситуация | Machinery | `prg_machine` (правильно) | `prg_machine` (catch-all bug) | -|----------|-----------|---------------------------|-------------------------------| -| Processor crash (exception) | `raise {failed,NS,ID}` | `{error, {exception,...}}` | `{error, failed}` ✗ (потеря деталей) | -| `process is error` | `raise {failed,NS,ID}` | `{error, failed}` | `{error, failed}` ✓ | -| `process is waiting` на **call** | `{ok, {error, <<"process is waiting">>}}` | `{error, <<"process is waiting">>}` | `{error, failed}` ✗ | -| `process is running` на **call** | `{ok, {error, <<"process is running">>}}` | `{error, <<"process is running">>}` | `{error, failed}` ✗ | -| `process is running` на **repair** | `{error, working}` | `{error, working}` | `{error, working}` ✓ | -| Ответ процессора `{error, X}` в теле | `{ok, {error, X}}` (через machinery decode) | `{ok, {error, X}}` через `decode_term` | без изменений ✓ | - -**Важно:** полная эмуляция machinery `{ok, Error}` для слоя A **не** реализована в `prg_machine` — вместо этого HG handler принимает `{error, Reason}` на client API. Поведение **близко**, но не идентично: при `{error, <<"process is waiting">>}` handler делает `erlang:error(Reason)`, а не возвращает tuple клиенту. Тесты проходили с pass-through; с `failed` — нет. - -Если понадобится **бит-в-бит** как machinery для call: - -```erlang -%% гипотетически в prg_machine:call, только для «мягких» guard-ошибок: -{error, <<"process is ", _/binary>> = Reason} -> - {ok, {error, Reason}}; -``` - -Пока **не делали** — достаточно pass-through + обработка в `hg_invoicing_machine_client`. - ---- - -## 7. Статус CT (на момент документа) - -| Suite | Результат | Примечание | -|-------|-----------|------------| -| `lib.ff_transfer` | 88 OK / 0 FAIL | после `ff_ct_machine` + codec/timer fixes | -| `lib.hellgate` | 231 OK / **11 FAIL** | до fix pass-through в `prg_machine:call` | -| `lib.ff_server` | 41 OK / **4 FAIL** | отдельно не разбирали | - -Прогон: `_build/test/logs/index.html`, hellgate run `2026-06-09_22.10.35`. - -**11 FAIL (hellgate):** - -`payment_hold_cancellation`, `payment_hold_auto_cancellation`, `payment_cascade_fail_wo_route_candidates`, `payment_cascade_limit_overflow`, `payment_cascade_fail_wo_available_attempt_limit`, `payment_cascade_failures`, `payment_cascade_deadline_failures`, `payment_cascade_fail_provider_error`, `deadline_doesnt_affect_payment_refund`, `accept_payment_chargeback_exceeded`, `consistent_account_balances`. - -Перепрогон после fix pass-through — **нужен вручную** (`make wdeps-common-test` / docker). - -Локально `rebar3` может падать с `corrupt atom table` — использовать docker testrunner из Makefile. - ---- - -## 8. Чеклист при правках `prg_machine:call` - -1. **`failed` только для `<<"process is error">>`** — не для `{exception, ...}` и не catch-all `{error, _}`. -2. Client-обёртки: `{error, _} = Error -> Error` (HG/FF `*_machine`, `hg_invoicing_machine_client`). -3. Woody-граница: при необходимости сворачивать `{error, {exception,...}}` в `erlang:error(failed)` локально (`ff_withdrawal_adapter_host`). -4. Processor response (слой B) — в домене (`{ok, {error,_}}`), не в `prg_machine`. -5. CT: `session_repair_test` ожидает `{error, {exception, _, _}}`, не `{error, failed}`. -6. FF meck: только `'prg_machine_meck_original':process/3`. - ---- - -## 9. Ключевые файлы (быстрые ссылки) - -| Путь | Содержание | -|------|------------| -| `apps/prg_machine/src/prg_machine.erl` | `start/call/repair`, маппинг ошибок | -| `_build/.../machinery_prg_backend.erl` | эталон старого поведения call | -| `apps/hellgate/src/hg_invoicing_machine_client.erl` | Thrift → prg_machine | -| `apps/hellgate/src/hg_invoice_handler.erl` | `call/3`, `repair/2`, `ensure_started` | -| `apps/hellgate/src/hg_invoice.erl` | callbacks, `set_invoice_timer`, session/callback | -| `apps/hellgate/src/hg_proxy_host_provider.erl` | provider → `process_session_change_by_tag` | -| `apps/hellgate/test/hg_invoice_helper.erl` | `next_change`, timeout 12s | -| `apps/ff_transfer/test/ff_ct_machine.erl` | meck timeout hooks | -| `apps/ff_transfer/src/ff_withdrawal_machine.erl` | FF call wrapper | -| `apps/ff_server/src/ff_withdrawal_adapter_host.erl` | adapter `failed` | -| `_checkouts/progressor/src/progressor.erl` | `check_process_status`, call требует `<<"running">>` | -| `_checkouts/progressor/src/progressor_action.erl` | таймеры/deadline | - ---- - -## 10. Trace HTTP (`ff_machine_handler`) - -После миграции `progressor:trace/1` + `json:encode` ломался на сырых binary в `event_payload` (`invalid_byte, 131`). - -**Фикс:** `ff_machine_trace:trace/2` в `apps/ff_transfer` — порт `ff_machine:unmarshal_trace` (handler `unmarshal_event_body` + `json_compatible_value`). - -## 11. Открытые вопросы - -1. **Нужна ли эмуляция `{ok, {error, Reason}}`** для guard-ошибок progressor на call (как machinery) — или pass-through достаточен при текущих тестах. -2. **`repair_failed_session_with_failure`** — отдельно от trace: `{badmatch,{error,notfound}}` в session repair. diff --git a/docs/prg-machine-migration-context.md b/docs/prg-machine-migration-context.md deleted file mode 100644 index 32d44347..00000000 --- a/docs/prg-machine-migration-context.md +++ /dev/null @@ -1,330 +0,0 @@ -# Миграция на `prg_machine`: контекст для следующих доработок - -Документ фиксирует **цель**, **целевую архитектуру**, **фактическое состояние** ветки `add_prg_layer` (hellgate) и **открытые хвосты**. Merge target: `epic/monorepo`. - -Оркестрация Ralph: `/Users/artemfedorenko/Documents/paymentsols/ralph-2` (goal в `.ralph/goal.md`, задачи 1–36 verified, **37 — CT — не завершена**). - ---- - -## 1. Цель и направление - -**Единый runtime** для всех progressor namespace в Hellgate и Fistful: - -``` -woody handler (hg_*_handler, ff_*_handler) - → prg_machine:start | call | get | repair | notify | remove | trace - → progressor (storage + worker pool) - → prg_machine:process/3 - → domain module (-behaviour(prg_machine)) -``` - -**Убрать из prod path:** - -| Было | Статус | -|------|--------| -| `hg_machine` + `hg_progressor_handler` | **удалены** | -| `ff_machine` + `fistful` как machinery processor | **удалены** | -| `*_machinery_schema` в `ff_server` | **удалены** | -| `machinery_prg_backend` в `config/sys.config` для prod NS | **убран** | -| `hg_machine_action` | **удалён** → `prg_machine_action` | - -**Не в scope этой миграции (отдельные goals):** - -- Trace API на Thrift (`docs/trace-api-thrift.md`) -- ~~Hybrid MG↔progressor (`hg_hybrid`, machinegun)~~ — **удалено** -- Полное удаление зависимости `machinery` из `rebar.config` -- Дедупликация HG/FF утилит (`operation_context`, `ff_core`) — другой Ralph goal - -**Принципы:** - -- Один путь данных, без dual-write и адаптеров «старый API → новый» в проде -- Доменные границы HG/FF сохраняются; общее — только в `apps/prg_machine` -- `hg_proto` / Thrift encode **не** тащить в `prg_machine` (тонкий `hg_invoicing_machine_client` в hellgate) - ---- - -## 2. Как должно работать: `prg_machine` - -### 2.1. OTP app `apps/prg_machine` - -| Модуль | Роль | -|--------|------| -| `prg_machine` | behaviour, client API, `process/3`, `collapse`/`emit_events` | -| `prg_machine_registry` | gen_server — владелец ETS `prg_machine_dispatch`, `lookup/1` → `{unknown_namespace, NS}` | -| `prg_machine_action` | таймеры / remove → формат progressor | - -**Registry:** `prg_machine:get_child_spec([Module, …])` поднимает `prg_machine_registry`; при старте в ETS кладётся `{Namespace, HandlerModule}` по `Handler:namespace/0`. При рестарте реестра таблица пересоздаётся из того же списка handlers. - -**Client API** (`start`, `call`, `get`, `repair`, `notify`, `remove`, `trace`) — обёртки над `progressor:*` с encode/decode term и woody/otel context. - -**`process/3`** — callback progressor: - -1. `env_enter(WoodyCtx)` — поднять `operation_context` (HG или FF binding) -2. `unmarshal_machine` — history + aux_state из storage -3. `dispatch` → `Handler:init | process_call | process_signal | process_repair | process_notification` -4. `marshal_process_result` — events, action, aux_state (только при явном `auxst` от домена) обратно в progressor -5. `env_leave()` в `after` - -При необработанном исключении в домене: `{error, {exception, Class, Reason, Stacktrace}}` + structured log (`stacktrace`, `exception` в metadata). - -**Контекст RPC:** `application:set_env(prg_machine, woody_context_loader, Loader)` в `hellgate:start/2` и `ff_server:start/2` (fun или `{M,F}`), fallback на `woody_context:new()`. - -### 2.2. Behaviour (доменный модуль) - -Обязательные callbacks: - -- `namespace/0`, `init/2`, `process_signal/2`, `process_call/2`, `process_repair/2` -- `marshal_event_body/1`, `unmarshal_event_body/2`, `marshal_aux_state/1`, `unmarshal_aux_state/1` - -Опционально: `process_notification/2`. - -**Результат домена** (`result()`): - -```erlang -#{ - events => [EventBody, ...], - action => prg_machine_action:t(), % таймер / remove / undefined - auxst => term() % обычно #{ctx => ...} для FF, #{} для HG -} -``` - -**Actions:** `prg_machine_action` заменяет `hg_machine_action` и FF `continue`/`sleep`/`unset_timer`. Маппинг в progressor — `prg_machine_action:to_progressor/1`. - -### 2.3. Event-sourcing helpers - -| Функция | Назначение | -|---------|------------| -| `collapse/2` | fold по history: `apply_event/4` (EventID, Ts, Body, Model) если экспортирован, иначе `apply_event/2` (FF) | -| `emit_event/1`, `emit_events/1` | обёртка с timestamp для новых событий | -| `initial_model/2` | старт fold: `maps:get(model, AuxState, undefined)` при `is_map(AuxState)`, иначе `undefined` | - -**FF:** домены вызывают `prg_machine:collapse(Mod, Machine)` в `*_machine` и внутри домена. - -**HG invoice (хвост):** пока `collapse_st/1` / `collapse_history/1`; целевой паттерн — `prg_machine:collapse` + `apply_event/4` (см. `.ralph/goal-hg-collapse.md` в ralph-2). - -### 2.4. Конфиг progressor (`config/sys.config`) - -Единый шаблон для каждого NS (prod — 7 namespace: 2× HG + 5× FF): - -```erlang -processor => #{ - client => prg_machine, - options => #{ - ns => , - %% HG (strict) / FF (lenient) — см. operation_context:hellgate_binding/0, fistful_binding/0 - context_binding => #{ - registry_key => {p, l, stored_hg_context}, - cleanup_mode => strict - } - } -} -``` - -`prg_machine:process/3` поднимает RPC-контекст через `operation_context:env_enter/2` и снимает через `operation_context:env_leave/1` по `context_binding`, если в `options` не заданы явные хуки `env_enter` / `env_leave`. - -**Приоритет резолва** (`resolve_env_enter/1`, `resolve_env_leave/1` в `prg_machine.erl`): - -1. Явный fun в `env_enter` / `env_leave` — перекрывает всё (CT с кастомным `party_client` и т.п.) -2. `context_binding => Binding` — `operation_context:env_enter(WoodyCtx, Binding)` / `env_leave(Binding)` -3. noop — `fun(_) -> ok end` / `fun() -> ok end`, если ни fun, ни binding не заданы - -Стандартный enter: `woody_context` + `party_client:create_client()` в gproc по `registry_key` из binding. - -Без `handler`, `schema`, `machinery_prg_backend`. - -### 2.5. Тонкие обёртки - -| Слой | Паттерн | -|------|---------| -| `*_machine.erl` | только `prg_machine:*` client API | -| `ff_*_handler.erl` | woody → `*_machine` / домен, без `machinery:` | -| `hg_invoicing_machine_client` | Thrift RPC к invoice machines через `prg_machine:call/6` + `hg_proto_utils` | - ---- - -## 3. Что сделано (этапы P0–P5) - -| Этап | Состояние | Содержание | -|------|-----------|------------| -| **P0** | ✅ | `prg_machine` в `rebar.config` + `{applications}`; `woody_context_loader`; `rebar3 compile` | -| **P1** | ✅ | `ff/deposit_v1` end-to-end | -| **P2** | ✅ | 5 FF NS: deposit, source, destination, withdrawal, session | -| **P2b** | ⏸ вырезано | `ff/identity`, `ff/wallet_v2` — NS убраны из config (модулей в репо не было) | -| **P3** | ✅ | `invoice` на `prg_machine` | -| **P4** | ✅ частично | `invoice_template` на `prg_machine`; `customer`, `recurrent_paytools` — NS убраны из config | -| **P5** | ✅ частично | удалены `hg_machine`, `ff_machine`, `fistful.erl`, processor glue; **не** полный grep-gate по всему репо | - -### 3.1. Prod namespaces на `prg_machine` (7 шт.) - -| NS | Домен | Registry child spec | -|----|-------|---------------------| -| `invoice` | `hg_invoice` | `hellgate.erl` | -| `invoice_template` | `hg_invoice_template` | `hellgate.erl` | -| `ff/deposit_v1` | `ff_deposit` | `ff_server.erl` | -| `ff/source_v1` | `ff_source` | `ff_server.erl` | -| `ff/destination_v2` | `ff_destination` | `ff_server.erl` | -| `ff/withdrawal_v2` | `ff_withdrawal` | `ff_server.erl` | -| `ff/withdrawal/session_v2` | `ff_withdrawal_session` | `ff_server.erl` | - -### 3.2. Удалённые / ключевые изменения - -**Удалено:** - -- `apps/hellgate/src/hg_machine.erl`, `hg_machine_action.erl` -- `apps/fistful/src/ff_machine.erl`, `fistful.erl` -- `apps/hg_progressor/` (`hg_progressor_handler`, `hg_hybrid`, `call_automaton` glue) -- `apps/ff_server/src/ff_*_machinery_schema.erl` (×5) - -**Добавлено:** - -- `apps/prg_machine/` (`prg_machine.erl`, `prg_machine_action.erl`) -- `apps/ff_transfer/src/ff_machine_codec.erl` — marshal/unmarshal aux и общий codec -- `apps/hellgate/src/hg_invoicing_machine_client.erl` - -**Переписано:** - -- FF домены: `-behaviour(prg_machine)` (`ff_deposit`, `ff_source`, `ff_destination`, `ff_withdrawal`, `ff_withdrawal_session`) -- HG: `hg_invoice`, `hg_invoice_template` — behaviour + `prg_machine` client -- `ff_repair` — на `prg_machine:collapse` / `emit_events` -- `ff_machine_handler` — trace через `progressor:trace/1` (JSON HTTP; Thrift — отдельный goal, `docs/trace-api-thrift.md`) -- CT helper: `hg_ct_helper.erl` — progressor processor `client => prg_machine` - -### 3.3. Ralph verification - -- Задачи **1–34, 36** — verified -- Задача **37** (полный CT) — **не завершена** (прерывание ~37 мин, resource_exhausted) -- Integration gate: `rebar3 compile` OK, CT deferred -- Ветка: `add_prg_layer` → merge в `epic/monorepo`; PR ещё не открыт -- Runtime fixes (этапы 1, 3 review-plan): aux_state, registry, stacktrace — **в коде** - ---- - -## 4. Диаграмма потока (call) - -```mermaid -sequenceDiagram - participant WH as woody handler - participant PM as prg_machine client - participant PR as progressor - participant P3 as prg_machine:process/3 - participant DM as domain module - - WH->>PM: call(NS, ID, Args) - PM->>PR: progressor:call(Req) - PR->>P3: process({call, BinArgs, Process}, Opts, Ctx) - P3->>P3: env_enter, unmarshal_machine - P3->>DM: process_call(Args, Machine) - DM->>DM: collapse_st / collapse (state) - DM-->>P3: {Response, #{events, action, auxst}} - P3->>P3: marshal_process_result - P3->>P3: env_leave - P3-->>PR: {ok, Intent} - PR-->>PM: {ok, Response} - PM-->>WH: decode_term(Response) -``` - ---- - -## 5. Известные хвосты (следующие доработки) - -### 5.1. Блокеры перед merge - -| # | Хвост | Действие | -|---|-------|----------| -| 1 | **CT не прогонялись** | Запустить suites вручную (docker: postgres, party, dmt) | -| 2 | **Нет PR** | PR `add_prg_layer → epic/monorepo` после зелёного CT | - -**CT suites (минимум):** - -```bash -cd /Users/artemfedorenko/Documents/work/hellgate -rebar3 ct --suite apps/ff_server/test/ff_deposit_handler_SUITE -rebar3 ct --suite apps/ff_server/test/ff_withdrawal_handler_SUITE -rebar3 ct --suite apps/ff_server/test/ff_withdrawal_session_repair_SUITE -rebar3 ct --suite apps/hellgate/test/hg_invoice_lite_tests_SUITE -rebar3 ct --suite apps/hellgate/test/hg_invoice_tests_SUITE -rebar3 ct --suite apps/hellgate/test/hg_invoice_template_tests_SUITE -rebar3 ct --suite apps/hellgate/test/hg_direct_recurrent_tests_SUITE -``` - -### 5.2. Legacy machinery (вне prod NS, но в репо) - -| Модуль / config | Статус | -|-----------------|--------| -| `test/bender/sys.config`, `test/party-management/sys.config` | **намеренно** `machinery_prg_backend` — docker-sidecar сервисы bender / party-management (вне scope HG/FF) | -| `apps/ff_cth/src/ct_payment_system.erl` | **очищено** — убран мёртвый `{machinery_backend, progressor}` | -| `apps/machinery_extra/` | **удалён** — codec-слой в `ff_core` (`ff_msgpack`, `ff_machine_schema`) | -| `rebar.config` damsel pin | ждёт `progressor_trace.thrift` в damsel — см. `docs/trace-api-thrift.md` | - -### 5.3. Trace API - -- Сейчас: FF internal HTTP JSON (`ff_machine_handler` → `progressor:trace/1`) -- Цель (отдельно): Thrift по `progressor_trace.thrift`, см. `docs/trace-api-thrift.md` -- В git status были черновики `hg_progressor_trace*` — **не** в финальном дереве (app `hg_progressor` удалён) - -### 5.4. P2b — orphan NS - -`ff/identity`, `ff/wallet_v2`, HG `customer`, `recurrent_paytools` — убраны из `sys.config`. Если понадобятся в проде — отдельный PR с доменными модулями + `prg_machine`. - -### 5.5. Технический долг в runtime - -- ~~`marshal_intent` портит aux_state при отсутствии `auxst`~~ — **исправлено** (этап 1) -- ~~`initial_model/2` badmap на не-map aux_state~~ — **исправлено** (этап 1) -- ~~registry на пустом supervisor, `ets:lookup_element` badarg~~ — **исправлено** (`prg_machine_registry`, этап 3) -- ~~stacktrace теряется в `process/3`~~ — **исправлено** (4-tuple + log metadata, этап 3) -- ~~`binary_to_term` без `[safe]`~~ — **закрыто** -- `hg_invoice` двойной collapse — отложено (см. `prg-machine-remaining-debt.md` §5) -- ~~L1: разные timestamp в `marshal_event_body`~~ — **закрыто** (единый `{prg_machine:timestamp(), 0}`) - -### 5.6. Grep-инварианты (целевые после полного P5) - -```bash -rg 'hg_machine:' apps/hellgate --glob '*.erl' # 0 prod -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 -``` - ---- - -## 6. Чеклист для нового домена / NS - -1. `config/sys.config` — `processor => #{client => prg_machine, options => #{ns => ..., env_enter, env_leave}}` -2. Доменный модуль: `-behaviour(prg_machine)` + callbacks -3. `apply_event/2` (FF) или `apply_event/4` (HG: event_id + timestamp + body) для `prg_machine:collapse/2` -4. Codec событий в `marshal_event_body` / `unmarshal_event_body` (или `ff_machine_codec`) -5. `*_machine.erl` — только `prg_machine:*` -6. Woody handler — без `machinery` / `hg_machine` -7. Добавить модуль в `get_child_spec([...])` в `hellgate.erl` или `ff_server.erl` -8. CT suite для NS -9. `rebar3 compile` + grep gate - ---- - -## 7. Связанные файлы (точки входа) - -| Путь | Зачем смотреть | -|------|----------------| -| `apps/prg_machine/src/prg_machine.erl` | behaviour, process/3, collapse | -| `apps/prg_machine/src/prg_machine_registry.erl` | ETS registry owner | -| `docs/prg-machine-review-plan.md` | поэтапный план ревью и доработок | -| `apps/prg_machine/src/prg_machine_action.erl` | таймеры | -| `config/sys.config` | все prod NS | -| `apps/hellgate/src/hellgate.erl` | HG registry | -| `apps/ff_server/src/ff_server.erl` | FF registry + woody | -| `apps/hellgate/src/hg_invoice.erl` | образец HG behaviour | -| `apps/ff_transfer/src/ff_deposit.erl` | образец FF behaviour + collapse | -| `apps/hellgate/src/hg_invoicing_machine_client.erl` | Thrift → prg_machine | -| `apps/fistful/src/ff_repair.erl` | repair + collapse | -| `docs/trace-api-thrift.md` | следующий этап trace | -| `docs/prg-machine-error-semantics-hg-ff.md` | семантика ошибок, HG vs FF, CT-регрессии, meck | - ---- - -## 8. История Ralph (кратко) - -- **Goal:** `.ralph/goal.md` в ralph-2 -- **Completed:** tasks 1–36 (infra, FF×5, HG invoice+template, cleanup, CT helper fix) -- **Open:** task 37 — full CT; review round 1 не закрыт (`review_phase_completed: false`) -- **Устаревший артефакт:** `.ralph/summary.md` в ralph-2 — обновлён ссылкой на этот документ - -*Дата отчёта: 2026-06-07* diff --git a/docs/prg-machine-remaining-debt.md b/docs/prg-machine-remaining-debt.md deleted file mode 100644 index 0744b180..00000000 --- a/docs/prg-machine-remaining-debt.md +++ /dev/null @@ -1,48 +0,0 @@ -# `add_prg_layer`: что осталось и техдолг - -Актуально для ветки `add_prg_layer` → merge target `epic/monorepo`. - -## Закрыто - -| # | Пункт | Что сделано | -|---|-------|-------------| -| 1 | Зависимость `machinery` в FF prod | `ff_msgpack` + `ff_machine_schema` в `ff_core`; `machinery`/`machinery_extra` убраны из `ff_transfer.app.src`; app `machinery_extra` удалён | -| 2 | Мёртвый код в `machinery_extra` | `machinery_gensrv_backend*` удалены вместе с app | -| 3 | FF `marshal_event_body` — разные timestamp | Все FF-домены: `{prg_machine:timestamp(), 0}` | -| 4 | FF `process_notification` — пустые `#{}` | Явный noop: `#{events => [], action => progressor_action:instant()}` | -| 7 | `binary_to_term` без `[safe]` | Было закрыто ранее | -| — | Elvis / docs мусор | Убраны ignore для несуществующих модулей; review-plan и migration-context синхронизированы | - -`{machinery, …}` в корневом `rebar.config` **остаётся** — только для docker-sidecar тестов (`test/bender`, `test/party-management` → `machinery_prg_backend`). - ---- - -## Открытый техдолг (низкий приоритет) - -### 5. HG invoice — двойной collapse - -- `prg_machine:collapse` → `apply_event/4` → `collapse_changes` -- `handle_call` → `validate_changes` → снова `collapse_changes` по in-memory state - -Работает, но дублирование. Целевой паттерн — один путь через `prg_machine:collapse` (отдельный goal). - -### 6. Registry без ETS `heir` - -`prg_machine_registry` при падении пересоздаёт таблицу и перерегистрирует handlers из child_spec. Краткое окно без таблицы теоретически возможно. - -**Действие:** `heir` на supervisor — только если понадобится zero-downtime. - -### L1 (косметика). Фиктивная обёртка `{ev, Ts, Body}` в payload - -Progressor ставит timestamp в storage; в thrift-payload ts по-прежнему фиктивный, но единообразный. Полный отказ от обёртки потребует смены wire-формата `TimestampedChange`. - ---- - -## Grep-инварианты (prod FF/HG) - -```bash -rg 'machinery_msgpack|machinery_extra|machinery_time' apps/fistful apps/ff_transfer apps/ff_server --glob '*.erl' # 0 -rg 'machinery' apps/ff_transfer/src/ff_transfer.app.src # 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 -``` diff --git a/docs/prg-machine-review-plan.md b/docs/prg-machine-review-plan.md deleted file mode 100644 index 59bb9942..00000000 --- a/docs/prg-machine-review-plan.md +++ /dev/null @@ -1,80 +0,0 @@ -# Ревью ветки `add_prg_layer` (vs `epic/monorepo`) и план доработки - -Строгое ревью миграции HG/FF на единый `prg_machine`-runtime поверх `progressor`. -Diff `+3725 / −3979`, 91 файл, 11 коммитов. `rebar3 compile` проходит, grep-инварианты по prod-путям чистые. - -Дата ревью: 2026-06-07. - ---- - -## Осознанные решения (не блокеры) - -- **`progressor_action` собирается из локального `_checkouts/progressor`** (`v1.0.24` + 1 локальный коммит), а `rebar.config` пинит `{tag, "v1.0.24"}`; `_checkouts/` в `.gitignore`, `rebar.lock` не лочит progressor. На чистом клоне сборка/xref упадут (`undefined module progressor_action`). **Решено оставить как есть на текущем этапе** — будет закрыто отдельно (апстрим-тег в `valitydev/progressor` либо вендоринг `progressor_action` в `apps/prg_machine`). -- **`machinery` в корневом `rebar.config`** — только docker-sidecar тесты (`machinery_prg_backend` в `test/bender`, `test/party-management`). FF prod-путь на `ff_core` (`ff_msgpack`, `ff_machine_schema`). -- **Trace API** — сейчас FF internal HTTP JSON; перевод на Thrift (`docs/trace-api-thrift.md`) — отдельный goal. -- **Orphan NS** (`ff/identity`, `ff/wallet_v2`, HG `customer`, `recurrent_paytools`) — убраны из `sys.config`, вернутся отдельным PR при необходимости. - ---- - -## Блокеры перед merge - -| # | Проблема | Действие | -|---|----------|----------| -| B2 | CT не прогонялись (full-CT не завершена) | Поднять инфру и прогнать suites (см. Этап 2) | -| B3 | Нет PR, ветка не влита в `epic/monorepo` | Открыть PR после зелёных gate+CT (Этап 5) | - ---- - -## Высокий приоритет (корректность рантайма) — **закрыто** - -### H1. Порча `aux_state` на exception-пути HG invoice — **исправлено** -Подтверждено: `progressor` (`prg_worker.erl:624-630`) персистит `aux_state` из результата, если ключ присутствует. -Exception-ветка call в `hg_invoice.erl` возвращает голый `#{}` (без `auxst`); `marshal_intent/3` не эмитит `aux_state` → progressor сохраняет прежний aux_state. Раньше `to_prg_result(#{})` падал в `validate_changes` и/или насильно ставил `auxst => #{}`. -Бизнес-exception в invoice-call — штатная частая ситуация → ломает invoice после первого отклонённого вызова. Та же дыра: `dispatch_notification` без `process_notification/2` возвращает `#{}`. - -### H2. `initial_model/2` не защищён от не-map `aux_state` — **исправлено** -Guard `when is_map(...)` с fallback в `undefined`. - ---- - -## Средний приоритет — **закрыто** - -- **M1.** `marshal_intent` эмитит `aux_state` только при явном `auxst` — **исправлено**. -- **M2.** Реестр namespace — `prg_machine_registry` (gen_server owner) + `{unknown_namespace, NS}` — **исправлено**. ETS `heir` — отложено (см. `prg-machine-remaining-debt.md` §6). -- **M3.** Stacktrace в `process/3` (4-tuple + log metadata) — **исправлено**. -- **M4.** `ct_payment_system` очищен; `test/bender` / `test/party-management` на `machinery_prg_backend` — **намеренно** (docker-sidecar, вне HG/FF). - ---- - -## Низкий приоритет - -- **L1.** FF `marshal_event_body` — timestamp унифицирован (`{prg_machine:timestamp(), 0}`); фиктивная обёртка в wire остаётся — косметика. -- **L2.** Доки синхронизированы (`prg-machine-migration-context.md`, `prg-machine-remaining-debt.md`). -- **L3.** `rebar.config:39` TODO про bump damsel-тега после релиза `progressor_trace.thrift` — связать с Trace-API goal. - ---- - -## Пошаговый план - -### Этап 1 — корректность рантайма (H1, H2, M1) — можно делать в коде сразу -1. `prg_machine:marshal_intent/3`: эмитить ключ `aux_state` **только** когда домен вернул `auxst`; при отсутствии — не трогать сохранённый aux_state. -2. `prg_machine:initial_model/2`: guard `when is_map(AuxState)`, иначе `undefined`. -3. `hg_invoice.erl:412`: exception-ветку вернуть через `to_prg_result(#{})` (или явный `#{auxst => #{}}`). -4. Тест: invoice-call с бизнес-exception, затем `timeout`/повторный call — машина не должна уходить в error. Аналогично `notify` без `process_notification/2`. - -### Этап 2 — CT (B2) -5. Поднять инфру (postgres/progressor, party-management, dmt, bender), прогнать минимум: - - `ff_deposit_handler_SUITE`, `ff_withdrawal_handler_SUITE`, `ff_withdrawal_session_repair_SUITE` - - `hg_invoice_lite_tests_SUITE`, `hg_invoice_tests_SUITE`, `hg_invoice_template_tests_SUITE`, `hg_direct_recurrent_tests_SUITE` -6. Зафиксировать результаты. До зелёного CT merge не делать. - -### Этап 3 — устойчивость рантайма (M2, M3) -7. Упрочнить реестр namespace (устойчивый owner таблицы + понятная ошибка при отсутствии NS). -8. Сохранять stacktrace в `process/3` структурированно. - -### Этап 4 — чистка хвостов (M4, L*) -9. Убрать мёртвый `{machinery_backend, progressor}` из `ct_payment_system.erl`; решить судьбу `machinery_prg_backend` в `test/bender`, `test/party-management`. -10. Обновить доки (`prg-machine-migration-context.md`), связать L3 с Trace-API goal. - -### Этап 5 — PR (B3) -11. Открыть PR `add_prg_layer → epic/monorepo` после зелёных `compile`/`xref`/`lint`/`dialyzer`/CT, с описанием миграции и списком осознанных хвостов. diff --git a/docs/prg-machine.md b/docs/prg-machine.md new file mode 100644 index 00000000..98488dfd --- /dev/null +++ b/docs/prg-machine.md @@ -0,0 +1,220 @@ +# `prg_machine` в Hellgate / Fistful + +Единый runtime поверх progressor для HG и FF. Контракт `action()` в progressor: `progressor/docs/step-effect-migration.md`. + +*Обновлено: 2026-06-12. CI (compile, dialyzer, CT + compose) — green локально.* + +--- + +## 1. Поток данных + +``` +woody handler (hg_*_handler, ff_*_handler) + → prg_machine:start | call | get | repair | notify | remove | trace + → progressor + → prg_machine:process/3 + → domain module (-behaviour(prg_machine)) +``` + +```mermaid +sequenceDiagram + participant WH as woody handler + participant PM as prg_machine + participant PR as progressor + participant DM as domain + + 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. + +**Вне scope:** Trace на Thrift → `docs/trace-api-thrift.md`. `{machinery, …}` в `rebar.config` — только docker-sidecar тесты (`test/bender`, `test/party-management`). + +--- + +## 2. Модули `apps/prg_machine` + +| Модуль | Роль | +|--------|------| +| `prg_machine` | behaviour, client API, `process/3`, `collapse` / `emit_events` | +| `prg_machine_registry` | ETS `{Namespace, Handler}`; `{unknown_namespace, NS}` | +| `prg_action` | `{timeout, Sec}` / `{deadline, Dt}` → wire `action()` | + +**`process/3`:** `env_enter` → `unmarshal_machine` → `dispatch` → `marshal_process_result` → `env_leave`. Исключение в домене → `{error, {exception, Class, Reason, Stacktrace}}` + log. + +**Контекст RPC:** `woody_context_loader` в `hellgate` / `ff_server`; иначе `operation_context` по `context_binding` из `sys.config` (HG strict / FF lenient). + +--- + +## 3. Контракт домена + +### Callbacks + +`namespace/0`, `init/2`, `process_signal/2`, `process_call/2`, `process_repair/2`, marshal/unmarshal event + aux_state. Опционально: `process_notification/2`. + +### `result()` + +```erlang +#{ + events => [EventBody, ...], + action => action(), %% progressor.hrl; omit = idle + auxst => term() %% ключ пишется только при явном обновлении +} +``` + +### Wire `action()` + +```erlang +idle | suspend | timeout | remove +| {schedule, #{at := UnixSec, 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` | + +`prg_action` — **не** адаптер MG/repair, только timer tuple → `{schedule, ...}`. + +FF доменный `continue` / `sleep` / `{setup_timer, T}` → wire через `map_action/1` в каждом модуле. + +### Prod namespaces (7) + +| NS | Модуль | Registry | +|----|--------|----------| +| `invoice` | `hg_invoice` | `hellgate.erl` | +| `invoice_template` | `hg_invoice_template` | `hellgate.erl` | +| `ff/deposit_v1` | `ff_deposit` | `ff_server.erl` | +| `ff/source_v1` | `ff_source` | `ff_server.erl` | +| `ff/destination_v2` | `ff_destination` | `ff_server.erl` | +| `ff/withdrawal_v2` | `ff_withdrawal` | `ff_server.erl` | +| `ff/withdrawal/session_v2` | `ff_withdrawal_session` | `ff_server.erl` | + +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} + } +} +``` + +--- + +## 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, ...}` | **pass-through** `{error, {exception, ...}}` | +| прочие guard (`<<"process is waiting">>`, …) | **pass-through** `{error, Reason}` | +| `<<"process already exists">>` (`start`) | `{error, exists}` | + +`repair`: + `{error, working}` для `<<"process is running">>`; остальное → `{error, {repair, {failed, Reason}}}` с сохранением `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). +``` + +Processor crash в тестах: `{error, {exception, _, _}}`, не атом `failed`. + +--- + +## 5. Техдолг + +### До релиза + +- Progressor: CHANGELOG + tag `vX.Y.0` +- Hellgate: bump tag в `rebar.config` (сейчас branch `add_action_module`, ref `4f6d78a`) + +### Границы thrift / repair (не блокер) + +Прикладной код не обязан повторять семантику progressor. Сейчас conversion корректна, но не «нативный wire на входе»: + +| Место | Сейчас | +|-------|--------| +| `hg_invoice:action_to_prg/1`, `construct_repair_action/1` | inline `#mg_stateproc_ComplexAction{}` / `#repair_ComplexAction{}` → wire | +| `ff_codec` | unmarshal repair → `[set_timer \| remove]`, дальше `map_action/1` | + +HG repair timer + remove: `remove` побеждает timer — осознанная прикладная семантика. + +### HG invoice — двойной collapse + +Реплей: `prg_machine:collapse` (lenient). После call: `validate_changes` → `collapse_changes` strict **мимо** `collapse/2`, плюс повтор в `to_prg_result`. Цель — один фолд через `prg_machine` с параметром strict/lenient (`apply_new_events/3` + убрать двойной проход в `process_call`). Только HG invoice; FF на `apply_event/2`. + +### Прочее (низкий приоритет) + +- Registry без ETS `heir` — краткое окно при рестарте +- Фиктивная обёртка `{ev, Ts, Body}` в event payload +- Trace: сейчас HTTP JSON (`ff_machine_trace`); Thrift — `docs/trace-api-thrift.md` + +--- + +## 6. Новый namespace + +1. `sys.config` — `client => prg_machine` +2. `-behaviour(prg_machine)` + callbacks +3. `apply_event/2` (FF) или `apply_event/4` (HG) для `collapse/2` +4. `*_machine.erl` — только `prg_machine:*` +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 +``` + +--- + +## 8. Точки входа в коде + +| Путь | Зачем | +|------|-------| +| `apps/prg_machine/src/prg_machine.erl` | behaviour, errors, marshal_intent | +| `apps/prg_machine/src/prg_action.erl` | timer → wire | +| `apps/hellgate/src/hg_invoice.erl` | HG behaviour, repair, `action_to_prg` | +| `apps/ff_transfer/src/ff_deposit.erl` | FF behaviour | +| `apps/hellgate/src/hg_invoicing_machine_client.erl` | Thrift → prg_machine | +| `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/step-effect-hg-migration.md b/docs/step-effect-hg-migration.md deleted file mode 100644 index e29b6818..00000000 --- a/docs/step-effect-hg-migration.md +++ /dev/null @@ -1,221 +0,0 @@ -# Миграция hellgate на action() (новый progressor) - -План переработки hellgate / ff / prg_machine после релиза progressor с явной -алгеброй `action()` в `processor_intent`. - -План доработки progressor: `progressor/docs/step-effect-migration.md`. - -**Предпосылка:** progressor tag `vX.Y.0` — в runtime **только** wire `action()`; -`progressor_action` удалён; `normalize` внутри progressor **нет**. - ---- - -## Затронутые области - -~17 модулей (были `progressor_action:*`, переведены на wire `action()`): - -| Область | Модули (основные) | -|---------|-------------------| -| prg_machine | `prg_machine.erl`, test handlers | -| HG invoice/payment | `hg_invoice.erl`, `hg_invoice_payment.erl`, `hg_session.erl`, … | -| FF transfer | `ff_withdrawal.erl`, `ff_withdrawal_session.erl`, `ff_source.erl`, … | -| Codecs | `ff_codec.erl`, `ff_withdrawal_codec.erl`, … | - -Текущая зависимость: `rebar.config` → `progressor` branch `add_action_module` (заменить на tag). - ---- - -## Принцип - -**Legacy MG/map живёт только в hellgate, до marshal в progressor.** - -``` -┌─────────────────────────────────────┐ -│ hellgate / ff (домен) │ оркестрация шага, MG repair -│ hg_machine_action (новый) │ timer/deadline → wire action() -└──────────────┬──────────────────────┘ - │ action() (progressor.hrl) -┌──────────────▼──────────────────────┐ -│ prg_machine │ marshal_intent → processor_intent -└──────────────┬──────────────────────┘ - │ -┌──────────────▼──────────────────────┐ -│ progressor │ dispatch_action — без legacy -└─────────────────────────────────────┘ -``` - -- Поле в intent по-прежнему **`action`**, не `effect`. -- Wire-значения — атомы и `{schedule, ...}`; см. `progressor/include/progressor.hrl`. -- **`progressor_action` не вернётся** — замена локальным `hg_machine_action` (или прямым wire в домене). - -### Таблица legacy → wire (для адаптера и доменов) - -| Было (`progressor_action` / map) | Стало (`action()`) | -|----------------------------------|---------------------| -| `new()` / `undefined` | поле не писать (= `idle`) | -| `instant()` / `set_timeout(0, _)` | `timeout` | -| `unset_timer` | `suspend` | -| `remove()` / `#{remove := true}` | `remove` | -| `set_timeout(N, _)` / `#{set_timer => Ts}` | `{schedule, #{at => Ts, action => timeout}}` | -| `set_deadline(Dt, _)` | `{schedule, #{at => unix(Dt), action => timeout}}` | -| `#{set_timer => Ts, remove := true}` | `{schedule, #{at => Ts, action => remove}}` | - -`at` — абсолютный unix sec; относительное время считает автор (`erlang:system_time(second) + N`). - ---- - -## Фаза 0. Bump зависимости - -1. `rebar.config`: `{progressor, {git, "...", {tag, "vX.Y.0"}}}`. -2. `rebar3 upgrade progressor`, обновить `rebar.lock`. - -**Сразу после bump compile не зелёный** — это ожидаемо: `progressor_action` исчез из зависимости. - -**Критерий:** lock обновлён; список compile errors = карта работ (grep `progressor_action`). - ---- - -## Фаза 1. `hg_machine_action` + `prg_machine` ✓ - -### `hg_machine_action` - -Только хелперы планирования (без coerce/legacy): - -- `t() :: action()` (`progressor.hrl`); -- `marshal_timer/1`, `schedule_timer/1`, `schedule_after/1`, `schedule_deadline/1`; -- wire-атомы (`idle`, `timeout`, `suspend`, `remove`) — напрямую в доменах. - -### `prg_machine` - -```erlang --type result() :: #{ - events => [event_body()], - action => action(), - auxst => term() -}. -``` - -`marshal_intent/3`: `maps:get(action, Result, idle)` → в intent только wire; ключ `action` опускается для `idle`. - -Домены и FF переведены на wire; shim `progressor_action` и `from_legacy` удалены. - -**Критерий:** `rebar3 compile` green; `rebar3 eunit --module=prg_machine` green. - ---- - -## Фаза 2. Hellgate core - -Порядок по риску: - -| Модуль | ~вызовов | Заметки | -|--------|----------|---------| -| `hg_session.erl` | 10 | proxy/timer | -| `hg_invoice_payment_refund.erl` | 9 | | -| `hg_invoice_payment_chargeback.erl` | 8 | | -| `hg_invoice_payment.erl` | 31 | самый большой | -| `hg_invoice.erl` | 14+ | `action_to_prg`, repair | -| `hg_invoice_registered_payment.erl` | 3 | | -| `hg_invoice_repair.erl` | 1 | | - -Для каждого: - -- `progressor_action:*` → `hg_machine_action:*` или прямой wire (`timeout`, `suspend`, `{schedule, ...}`); -- `-type action()` в домене → `hg_machine_action:t()` или `action()`; -- аккумулятор `{Events, Action}` — перезапись `Action` на шаге, не `set_timeout(0, Old)`. - -### `hg_invoice` - -- `action_to_prg/1` → `hg_machine_action:from_mg/1`; -- `merge_repair_action/2` → `hg_machine_action:from_repair/2` (один исход, без затирания timer); -- `set_invoice_timer/2` → deadline → `{schedule, #{at => ..., action => timeout}}`. - -**Тест:** repair timer + remove → `{schedule, #{action => remove, at => ...}}`. - -**Критерий:** HG ct по invoice/payment green. - ---- - -## Фаза 3. FF transfer - -| Модуль | Паттерн | -|--------|---------| -| `ff_withdrawal.erl`, `ff_withdrawal_session.erl` | `map_action/1` → wire `action()` | -| `ff_source.erl`, `ff_destination.erl`, `ff_deposit.erl` | `instant` → `timeout` | - -```erlang -map_action(continue) -> timeout; -map_action(sleep) -> suspend; -map_action({setup_timer, T}) -> hg_machine_action:schedule_timeout(T). -``` - -**Критерий:** ff ct (withdrawal, destination suites) green. - ---- - -## Фаза 4. Codecs и repair API - -- `ff_codec.erl` — `repairer_ComplexAction` → `hg_machine_action:from_repair/2`; -- `ff_withdrawal_codec.erl`, `ff_*_codec.erl` — то же; -- `ff_repair.erl`, `hg_invoice_tests_SUITE` repair macros. - -**Критерий:** repair-тесты: timer only, remove only, timer + remove. - ---- - -## Фаза 5. Зачистка - -```bash -rg 'progressor_action' apps/ # → 0 ✓ -rg '#{set_timer' apps/ # → 0 ✓ -# unset_timer остаётся в thrift/repair unmarshal (MG), не в processor intent -``` - -1. ~~Удалить `from_legacy`~~ ✓ -2. Обновить `docs/prg-machine-migration-context.md`, `prg-machine-remaining-debt.md` при наличии. -3. Lock progressor на tag в `rebar.config` / `rebar.lock` (сейчас ref `4f6d78a`). - -**Критерий:** полный CI hellgate green. - ---- - -## Фаза 6. MG/thrift (опционально, отдельный PR) - -`#mg_stateproc_ComplexAction{}` в woody-путях — `hg_machine_action:from_mg/1`. - -Долгосрочно: thrift ComplexAction не протаскивается в progressor как map-ключи. - ---- - -## Порядок - -``` -progressor tag → hg_machine_action + prg_machine (фаза 1) - → HG domains (фаза 2) ∥ FF (фаза 3) - → codecs (фаза 4) → cleanup (фаза 5) -``` - -Фазы 2 и 3 частично параллелятся после готового `hg_machine_action`. - ---- - -## Не делать - -- `effect` / `prg_effect` / `normalize` **в progressor** — контракт уже другой. -- Возвращать `progressor_action` / legacy map в processor intent. -- Fold repair actions с затиранием timer — один `action()` на intent. -- Authoring `{timeout, N}` в intent progressor — только wire. - ---- - -## Чеклист приёмки - -- [x] progressor на ref `4f6d78a` (до tag) -- [x] `prg_machine:result()` — `action => action()` -- [x] `marshal_intent` — wire `action()` без coerce/legacy -- [x] нет `progressor_action` в apps/ -- [ ] repair timer + remove → `{schedule, #{action => remove, at => ...}}` (сейчас remove побеждает timer, как раньше) -- [ ] семантика `call_replace_timer` сохранена (новый schedule отменяет pending remove) -- [x] `from_legacy` / `coerce` удалены -- [x] `rebar3 compile` + `prg_machine` eunit green -- [ ] полный CI hellgate -- [ ] progressor tag в `rebar.config` From b5437c52a4b8feb016e35080a10727f24ff5116d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 12 Jun 2026 14:57:59 +0300 Subject: [PATCH 28/62] Refactor action type specifications across multiple modules to unify action handling under prg_action. Update unmarshal logic in ff_codec, ff_withdrawal_codec, and ff_transfer modules to enhance clarity and maintainability. Adjust event processing to utilize timeout actions consistently, improving overall code consistency. --- apps/ff_server/src/ff_codec.erl | 33 +++++---- apps/ff_server/src/ff_withdrawal_codec.erl | 4 +- apps/ff_transfer/src/ff_adjustment.erl | 12 ++-- apps/ff_transfer/src/ff_deposit.erl | 28 +++----- apps/ff_transfer/src/ff_destination.erl | 10 +-- apps/ff_transfer/src/ff_source.erl | 10 +-- apps/ff_transfer/src/ff_withdrawal.erl | 71 +++++++------------ .../ff_transfer/src/ff_withdrawal_session.erl | 46 ++++-------- apps/fistful/src/ff_repair.erl | 2 +- apps/hellgate/src/hg_invoice.erl | 61 +++------------- apps/prg_machine/src/prg_action.erl | 66 ++++++++++++++++- docs/prg-machine.md | 11 +-- 12 files changed, 157 insertions(+), 197 deletions(-) diff --git a/apps/ff_server/src/ff_codec.erl b/apps/ff_server/src/ff_codec.erl index 465ba10b..a179638f 100644 --- a/apps/ff_server/src/ff_codec.erl +++ b/apps/ff_server/src/ff_codec.erl @@ -363,25 +363,16 @@ 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); + prg_action:from_timer_remove( + unmarshal_repairer_timer_field(TimerAction), + unmarshal_repairer_remove_field(RemoveAction) + ); unmarshal(timer, {timeout, Timeout}) -> {timeout, unmarshal(integer, Timeout)}; unmarshal(timer, {deadline, Deadline}) -> @@ -579,6 +570,18 @@ unmarshal(range, #'fistful_base_EventRange'{ unmarshal(bool, V) when is_boolean(V) -> V. +unmarshal_repairer_timer_field(undefined) -> + undefined; +unmarshal_repairer_timer_field({set_timer, #repairer_SetTimerAction{timer = Timer}}) -> + {set_timer, unmarshal(timer, Timer)}; +unmarshal_repairer_timer_field({unset_timer, _}) -> + unset_timer. + +unmarshal_repairer_remove_field(undefined) -> + undefined; +unmarshal_repairer_remove_field(#repairer_RemoveAction{}) -> + remove. + maybe_unmarshal(_Type, undefined) -> undefined; maybe_unmarshal(Type, Value) -> diff --git a/apps/ff_server/src/ff_withdrawal_codec.erl b/apps/ff_server/src/ff_withdrawal_codec.erl index 84f4120c..54314d8a 100644 --- a/apps/ff_server/src/ff_withdrawal_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_codec.erl @@ -501,9 +501,7 @@ unmarshal_repair_scenario_test() -> events => [ {status_changed, pending} ], - action => [ - {set_timer, {timeout, 0}} - ] + action => timeout }}, unmarshal(repair_scenario, Scenario) ). diff --git a/apps/ff_transfer/src/ff_adjustment.erl b/apps/ff_transfer/src/ff_adjustment.erl index e2e265a7..82439cac 100644 --- a/apps/ff_transfer/src/ff_adjustment.erl +++ b/apps/ff_transfer/src/ff_adjustment.erl @@ -92,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() :: continue | undefined. +-type action() :: prg_action:t(). -type process_result() :: {action(), [event()]}. -type legacy_event() :: any(). -type external_id() :: id(). @@ -158,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 @@ -234,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). @@ -251,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_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index dae716f8..fcb2af10 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -151,7 +151,7 @@ -type is_negative() :: boolean(). -type cash() :: ff_cash:cash(). -type cash_range() :: ff_range:range(cash()). --type action() :: continue | undefined. +-type action() :: prg_action:t(). -type ctx() :: ff_entity_context:context(). -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). @@ -431,7 +431,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( @@ -440,10 +440,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) -> @@ -456,7 +456,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) -> @@ -488,16 +488,16 @@ process_limit_check(Deposit) -> }, [{limit_check, {wallet_receiver, {failed, 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) -> @@ -664,13 +664,13 @@ build_failure(limit_check, Deposit) -> process_transfer_result({Action, Events}, Machine) -> #{ events => Events, - action => map_action(Action), + action => Action, auxst => maps:get(aux_state, Machine, #{}) }. -type repair_result() :: #{ events := [term()], - action => continue | undefined, + action => action(), aux_state => term() }. @@ -678,16 +678,10 @@ process_transfer_result({Action, Events}, Machine) -> from_repair_result(#{events := Events} = Result, Machine) -> #{ events => repair_events_to_domain(Events), - action => map_action(maps:get(action, Result, undefined)), + action => maps:get(action, Result, idle), auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> prg_action:t(). -map_action(undefined) -> - idle; -map_action(continue) -> - timeout. - -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index d707b6d3..874551bf 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -303,7 +303,7 @@ marshal_aux_state(AuxSt) -> unmarshal_aux_state(Payload) when is_binary(Payload) -> ff_machine_codec:unmarshal_aux_state(Payload). --type action() :: continue | undefined. +-type action() :: prg_action:t(). -type repair_result() :: #{ events := [term()], @@ -315,16 +315,10 @@ unmarshal_aux_state(Payload) when is_binary(Payload) -> from_repair_result(#{events := Events} = Result, Machine) -> #{ events => repair_events_to_domain(Events), - action => map_action(maps:get(action, Result, undefined)), + action => maps:get(action, Result, idle), auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> prg_action:t(). -map_action(undefined) -> - idle; -map_action(continue) -> - timeout. - -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index 7c251446..f833ca64 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -279,7 +279,7 @@ marshal_aux_state(AuxSt) -> unmarshal_aux_state(Payload) when is_binary(Payload) -> ff_machine_codec:unmarshal_aux_state(Payload). --type action() :: continue | undefined. +-type action() :: prg_action:t(). -type repair_result() :: #{ events := [term()], @@ -291,16 +291,10 @@ unmarshal_aux_state(Payload) when is_binary(Payload) -> from_repair_result(#{events := Events} = Result, Machine) -> #{ events => repair_events_to_domain(Events), - action => map_action(maps:get(action, Result, undefined)), + action => maps:get(action, Result, idle), auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> prg_action:t(). -map_action(undefined) -> - idle; -map_action(continue) -> - timeout. - -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 9a36e045..c33d9546 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -187,14 +187,7 @@ -type invalid_withdrawal_status_error() :: {invalid_withdrawal_status, status()}. -%% Transfer-layer action before map_action/1. Adapter timers normally live on the -%% session machine; {setup_timer, _} is supported at this boundary for repair and -%% symmetry with ff_withdrawal_session. --type action() :: - sleep - | continue - | undefined - | {setup_timer, prg_action:timer()}. +-type action() :: prg_action:t(). -export_type([withdrawal/0]). -export_type([withdrawal_state/0]). @@ -529,7 +522,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()}. @@ -604,7 +597,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}; @@ -627,7 +620,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. @@ -770,7 +763,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)), @@ -778,12 +771,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) -> @@ -803,21 +796,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, @@ -936,14 +929,14 @@ process_limit_check(Withdrawal) -> }, [{limit_check, {wallet_sender, {failed, 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) -> @@ -979,7 +972,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) -> @@ -1005,11 +998,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) -> @@ -1017,11 +1010,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), @@ -1155,7 +1148,7 @@ build_party_varset(#{body := Body, wallet_id := WalletID, party_id := PartyID} = BinData = maps:get(bin_data, Params, undefined), PaymentTool = case {Destination, Resource} of - {undefined, _} -> + {idle, _} -> undefined; {_, Resource} -> construct_payment_tool(Resource) @@ -1690,11 +1683,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(). @@ -1763,17 +1756,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()]. @@ -1920,13 +1913,13 @@ unmarshal_aux_state(Payload) when is_binary(Payload) -> process_transfer_result({Action, Events}, Machine) -> #{ events => Events, - action => map_action(Action), + action => Action, auxst => maps:get(aux_state, Machine, #{}) }. -type repair_result() :: #{ events := [term()], - action => action() | undefined, + action => action(), aux_state => term() }. @@ -1934,20 +1927,10 @@ process_transfer_result({Action, Events}, Machine) -> from_repair_result(#{events := Events} = Result, Machine) -> #{ events => repair_events_to_domain(Events), - action => map_action(maps:get(action, Result, undefined)), + action => maps:get(action, Result, idle), auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action()) -> prg_action:t(). -map_action(undefined) -> - idle; -map_action(continue) -> - timeout; -map_action(sleep) -> - suspend; -map_action({setup_timer, Timer}) -> - prg_action:schedule_timer(Timer). - -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index b6c2c608..de406b6f 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -118,13 +118,7 @@ -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). --type action() :: - undefined - | continue - | {setup_callback, ff_withdrawal_callback:tag(), prg_action:timer()} - | {setup_timer, prg_action:timer()} - | retry - | finish. +-type action() :: prg_action:t(). -type process_result() :: {action(), [event()]}. @@ -228,7 +222,7 @@ 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, _} = Error -> erlang:error({unable_to_finish_session, Error}) end; @@ -267,13 +261,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. @@ -321,14 +315,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(?NS, 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), []}. %% @@ -467,43 +462,26 @@ unmarshal_aux_state(Payload) when is_binary(Payload) -> -spec process_session_result(process_result(), machine()) -> prg_result(). process_session_result({Action, Events}, Machine) -> - Session = prg_machine:collapse(?MODULE, Machine), #{ events => Events, - action => map_action(Action, Session), + action => Action, auxst => maps:get(aux_state, Machine, #{}) }. -type repair_result() :: #{ events := [term()], - action => action() | undefined, + action => action(), aux_state => term() }. -spec from_repair_result(repair_result(), machine()) -> prg_result(). from_repair_result(#{events := Events} = Result, Machine) -> - Session = prg_machine:collapse(?MODULE, Machine), #{ events => repair_events_to_domain(Events), - action => map_action(maps:get(action, Result, undefined), Session), + action => maps:get(action, Result, idle), auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) }. --spec map_action(action(), session_state()) -> prg_action:t(). -map_action(undefined, _Session) -> - idle; -map_action(continue, _Session) -> - timeout; -map_action({setup_callback, Tag, Timer}, Session) -> - ok = ff_machine_tag:create_binding(?NS, Tag, id(Session)), - prg_action:schedule_timer(Timer); -map_action({setup_timer, Timer}, _Session) -> - prg_action:schedule_timer(Timer); -map_action(finish, _Session) -> - suspend; -map_action(retry, _Session) -> - timeout. - -spec repair_events_to_domain([term()]) -> [event()]. repair_events_to_domain(Events) -> [event_body_from_timestamped(E) || E <- Events]. diff --git a/apps/fistful/src/ff_repair.erl b/apps/fistful/src/ff_repair.erl index 3f4f02e5..8b49dda0 100644 --- a/apps/fistful/src/ff_repair.erl +++ b/apps/fistful/src/ff_repair.erl @@ -17,7 +17,7 @@ -type repair_result() :: #{ events := [timestamped_event(model_event())], - action => term(), + action => prg_action:t(), aux_state => model_aux_state() }. diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index a44d235d..af324ee8 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -20,8 +20,6 @@ -include("hg_invoice.hrl"). -include_lib("damsel/include/dmsl_repair_thrift.hrl"). --include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). - -define(NS, invoice). -define(EVENT_FORMAT_VERSION, 1). @@ -326,7 +324,7 @@ handle_repair({changes, Changes, RepairAction, Params}, St) -> [] -> #{} end, - Action = construct_repair_action(RepairAction), + Action = prg_action:from_repair(RepairAction), Result#{ state => St, action => Action, @@ -358,23 +356,6 @@ handle_signal(timeout, #st{activity = invoice} = St) -> % invoice is expired handle_expiration(St). -construct_repair_action(CA) when CA /= undefined -> - case CA#repair_ComplexAction.remove of - #repair_RemoveAction{} -> - remove; - undefined -> - case CA#repair_ComplexAction.timer of - undefined -> - idle; - {set_timer, #repair_SetTimerAction{timer = Timer}} -> - prg_action:schedule_timer(Timer); - {unset_timer, #repair_UnsetTimerAction{}} -> - suspend - end - end; -construct_repair_action(undefined) -> - idle. - should_validate_transitions(#payproc_InvoiceRepairParams{validate_transitions = V}) when is_boolean(V) -> V; should_validate_transitions(undefined) -> @@ -443,7 +424,7 @@ handle_call({{'Invoicing', 'CapturePayment'}, {_InvoiceID, PaymentID, Params}}, #{ response => ok, changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => action_to_prg(Action), + action => Action, state => St }; handle_call({{'Invoicing', 'CancelPayment'}, {_InvoiceID, PaymentID, Reason}}, St0) -> @@ -454,7 +435,7 @@ handle_call({{'Invoicing', 'CancelPayment'}, {_InvoiceID, PaymentID, Reason}}, S #{ response => ok, changes => wrap_payment_changes(PaymentID, Changes, hg_datetime:format_now()), - action => action_to_prg(Action), + action => Action, state => St }; handle_call({{'Invoicing', 'Fulfill'}, {_InvoiceID, Reason}}, St0) -> @@ -591,7 +572,7 @@ do_register_payment(PaymentID, PaymentParams, St) -> #{ response => get_payment_state(PaymentSession), changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => action_to_prg(Action), + action => Action, state => St }. @@ -604,7 +585,7 @@ do_start_payment(PaymentID, PaymentParams, St) -> #{ response => get_payment_state(PaymentSession), changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => action_to_prg(Action), + action => Action, state => St }. @@ -635,7 +616,7 @@ handle_payment_result({next, {Changes, Action}}, PaymentID, _PaymentSession, St, #{timestamp := OccurredAt} = Opts, #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => action_to_prg(Action), + action => Action, state => St }; handle_payment_result({done, {Changes, Action}}, PaymentID, PaymentSession, St, Opts) -> @@ -648,7 +629,7 @@ handle_payment_result({done, {Changes, Action}}, PaymentID, PaymentSession, St, ?processed() -> #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => action_to_prg(Action), + action => Action, state => St }; ?captured() -> @@ -661,7 +642,7 @@ handle_payment_result({done, {Changes, Action}}, PaymentID, PaymentSession, St, end, #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt) ++ MaybePaid, - action => action_to_prg(Action), + action => Action, state => St }; ?refunded() -> @@ -677,13 +658,13 @@ handle_payment_result({done, {Changes, Action}}, PaymentID, PaymentSession, St, ?failed(_) -> #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => set_invoice_timer(action_to_prg(Action), St), + action => set_invoice_timer(Action, St), state => St }; ?cancelled() -> #{ changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => set_invoice_timer(action_to_prg(Action), St), + action => set_invoice_timer(Action, St), state => St } end. @@ -701,7 +682,7 @@ wrap_payment_impact(PaymentID, {Response, {Changes, Action}}, St, OccurredAt) -> #{ response => Response, changes => wrap_payment_changes(PaymentID, Changes, OccurredAt), - action => action_to_prg(Action), + action => Action, state => St }. @@ -1090,26 +1071,6 @@ changes_from_msgpack_data(#{format_version := V, data := Data}) -> changes_from_msgpack_data(Changes) when is_list(Changes) -> Changes. --spec action_to_prg(action() | undefined) -> action(). -action_to_prg(#mg_stateproc_ComplexAction{timer = Timer, remove = Remove}) -> - case Remove of - #mg_stateproc_RemoveAction{} -> - remove; - undefined -> - case Timer of - undefined -> - idle; - {set_timer, #mg_stateproc_SetTimerAction{timer = T}} -> - prg_action:schedule_timer(T); - {unset_timer, #mg_stateproc_UnsetTimerAction{}} -> - suspend - end - end; -action_to_prg(undefined) -> - idle; -action_to_prg(Action) -> - Action. - %% Marshalling -spec marshal_invoice(invoice()) -> binary(). diff --git a/apps/prg_machine/src/prg_action.erl b/apps/prg_machine/src/prg_action.erl index 0fdda24b..9c43a1eb 100644 --- a/apps/prg_machine/src/prg_action.erl +++ b/apps/prg_machine/src/prg_action.erl @@ -1,18 +1,24 @@ -module(prg_action). -%%% Scheduling helpers: domain timer/deadline → wire `action()` for processor intent. +%%% Wire `action()` helpers and thrift/MG → wire conversion at API boundaries. -include_lib("progressor/include/progressor.hrl"). +-include_lib("damsel/include/dmsl_repair_thrift.hrl"). +-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). -export([marshal_timer/1, schedule_timer/1, schedule_after/1, schedule_deadline/1]). +-export([from_timer_remove/2, from_mg/1, from_repair/1]). --export_type([t/0, timer/0, seconds/0]). +-export_type([t/0, timer/0, seconds/0, timer_field/0, remove_field/0]). -type seconds() :: timeout_sec(). -type datetime() :: calendar:datetime() | binary(). -type timer() :: {timeout, seconds()} | {deadline, datetime()}. -type t() :: action(). +-type timer_field() :: undefined | {set_timer, timer()} | unset_timer. +-type remove_field() :: undefined | remove. + -spec schedule_timer(timer()) -> t(). schedule_timer({timeout, 0}) -> timeout; @@ -40,3 +46,59 @@ marshal_timer({deadline, Bin}) when is_binary(Bin) -> calendar:rfc3339_to_system_time(unicode:characters_to_list(Bin), [{unit, second}]); marshal_timer(Other) -> error({invalid_timer, Other}). + +%% Thrift / MG → wire (HG policy: remove beats timer) + +-spec from_timer_remove(timer_field(), remove_field()) -> t(). +from_timer_remove(_, remove) -> + remove; +from_timer_remove(undefined, undefined) -> + idle; +from_timer_remove({set_timer, Timer}, undefined) -> + schedule_timer(Timer); +from_timer_remove(unset_timer, undefined) -> + suspend. + +-spec from_mg(undefined | mg_proto_state_processing_thrift:'ComplexAction'() | t()) -> t(). +from_mg(undefined) -> + idle; +from_mg(#mg_stateproc_ComplexAction{timer = Timer, remove = Remove}) -> + from_timer_remove(mg_timer_field(Timer), mg_remove_field(Remove)); +from_mg(Wire) when Wire =:= idle; Wire =:= suspend; Wire =:= timeout; Wire =:= remove -> + Wire; +from_mg({schedule, _} = Wire) -> + Wire. + +-spec from_repair(undefined | dmsl_repair_thrift:'ComplexAction'() | t()) -> t(). +from_repair(undefined) -> + idle; +from_repair(#repair_ComplexAction{timer = Timer, remove = Remove}) -> + from_timer_remove(repair_timer_field(Timer), repair_remove_field(Remove)); +from_repair(Wire) when Wire =:= idle; Wire =:= suspend; Wire =:= timeout; Wire =:= remove -> + Wire; +from_repair({schedule, _} = Wire) -> + Wire. + +mg_timer_field(undefined) -> + undefined; +mg_timer_field({set_timer, #mg_stateproc_SetTimerAction{timer = Timer}}) -> + {set_timer, Timer}; +mg_timer_field({unset_timer, _}) -> + unset_timer. + +mg_remove_field(undefined) -> + undefined; +mg_remove_field(#mg_stateproc_RemoveAction{}) -> + remove. + +repair_timer_field(undefined) -> + undefined; +repair_timer_field({set_timer, #repair_SetTimerAction{timer = Timer}}) -> + {set_timer, Timer}; +repair_timer_field({unset_timer, _}) -> + unset_timer. + +repair_remove_field(undefined) -> + undefined; +repair_remove_field(#repair_RemoveAction{}) -> + remove. diff --git a/docs/prg-machine.md b/docs/prg-machine.md index 98488dfd..f78f3e66 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -161,16 +161,9 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa - Progressor: CHANGELOG + tag `vX.Y.0` - Hellgate: bump tag в `rebar.config` (сейчас branch `add_action_module`, ref `4f6d78a`) -### Границы thrift / repair (не блокер) +### Thrift → wire -Прикладной код не обязан повторять семантику progressor. Сейчас conversion корректна, но не «нативный wire на входе»: - -| Место | Сейчас | -|-------|--------| -| `hg_invoice:action_to_prg/1`, `construct_repair_action/1` | inline `#mg_stateproc_ComplexAction{}` / `#repair_ComplexAction{}` → wire | -| `ff_codec` | unmarshal repair → `[set_timer \| remove]`, дальше `map_action/1` | - -HG repair timer + remove: `remove` побеждает timer — осознанная прикладная семантика. +`prg_action:from_mg/1`, `from_repair/1` — MG/damsel repair на границе. `ff_codec` — `repairer_ComplexAction` → wire. Домены FF/HG — только `prg_action:t()`. ### HG invoice — двойной collapse From f178fd139d289427f9a4ff6e95e85d5786660aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 12 Jun 2026 20:20:10 +0300 Subject: [PATCH 29/62] Refactor action handling in ff_codec, ff_withdrawal_codec, and ff_transfer modules to unify logic under prg_action. Enhance clarity and maintainability of unmarshal logic and event processing, ensuring consistent use of timeout actions across modules. --- docs/prg-machine-fix-plan.md | 237 +++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/prg-machine-fix-plan.md diff --git a/docs/prg-machine-fix-plan.md b/docs/prg-machine-fix-plan.md new file mode 100644 index 00000000..1698dd80 --- /dev/null +++ b/docs/prg-machine-fix-plan.md @@ -0,0 +1,237 @@ +# План правок по ревью `add_prg_layer` + +Статус: согласован по итогам ревью (2026-06-12). База диффа — `e035d3c1` (epic/monorepo). + +## Главный вывод: миграция данных НЕ нужна + +Все найденные несовместимости живут в нашем промежуточном слое (`prg_machine`, +`ff_machine_codec`, `hg_invoice`), а не в thrift-схемах и не в progressor: + +| Данные | Старый формат в БД | Что сломали | Лечится в слое | +|---|---|---|---| +| HG события | payload `t2b(msgpack {bin, Thrift})`, metadata `format_version` | читаем только ключ `format` | да: payload и так бинарно совместим, чинится только ключ метаданных | +| FF события | payload `t2b({bin, Thrift})`, metadata `format` | новый код ждёт сырой thrift | да: compat-чтение + запись в старом конверте | +| FF aux_state | `t2b(map)` | новый код ждёт msgpack-thrift | да: try-чтение обоих | +| HG aux_state | `t2b(#mg_stateproc_Content{})` | живёт на двух catch-ловушках | да: явная клауза | +| HG call-args задач | `{thrift_call, Service, FunRef, EncodedArgs}` в старой обёртке | новая форма `{FunRef, Args}` | да: compat-клауза чтения | + +Принцип этапа 1: **писать в старом формате, читать оба**. Тогда и rollback +безопасен (старый код читает всё, что записал новый). Унификация конверта +HG+FF («дальше одинаково для обоих») — отдельный финальный этап с bump'ом +версии формата, когда reader уже повсеместно выкачен. + +--- + +## Этап 1. Совместимость данных (блокер) + +### 1.1 Метаданные событий: оба ключа +`apps/prg_machine/src/prg_machine.erl` + +Важно: «рабочий» ключ у стеков разный. Старый HG (`hg_progressor`) писал +`<<"format_version">>`, старый FF (`machinery_prg_backend`) — `<<"format">>` +(и читает только его, с дефолтом 0). Возврат к одному из них ломает rollback +второго стека, поэтому: + +- Запись: `event_metadata/1` → `#{<<"format_version">> => V, <<"format">> => V}` + — оба старых ключа. Это разом чинит чтение старых HG-событий, event sink + (`prg_notifier` читает `format_version`) и rollback обоих стеков + (старый HG-reader найдёт `format_version`, старый FF-reader — `format`). +- Чтение: `unmarshal_event/2` — порядок `<<"format_version">>` → `<<"format">>` + → `format` → `undefined`. + +### 1.2 FF события: старый конверт на запись, sniff на чтение +`apps/ff_transfer/src/ff_machine_codec.erl` + +- `payload_to_binary`: вернуть старый конверт — `term_to_binary({bin, ThriftBin})` + (как писал `machinery_prg_backend` через `machinery_utils:encode(term, ...)`). +- `unmarshal_thrift_event` → принимать оба варианта: + - первый байт `131` → `binary_to_term` → `{bin, Bin}` → thrift из `Bin`; + любое другое msgpack-значение → понятная ошибка `{legacy_msgpack_event, ...}` + (по данным таких быть не должно — проверить на стейдже); + - иначе → сырой thrift (события, записанные текущей веткой в dev/test). +- То же чтение используется `ff_machine_trace` — автоматически чинится. + +Примечание: sniff по первому байту безопасен только в эту сторону +(thrift-струkt не начинается с `131`); для msgpack наоборот — fixmap(3) тоже +`0x83`, поэтому порядок проверки именно такой. + +### 1.3 FF aux_state: запись t2b, чтение try-оба +`apps/ff_transfer/src/ff_machine_codec.erl` + +- `marshal_aux_state` → `term_to_binary(AuxSt)` (старое поведение). +- `unmarshal_aux_state` → `try binary_to_term` (старый формат), на ошибке — + msgpack-путь (`binary_to_payload` + `ff_machine_schema:unmarshal`) для + записанного веткой. + +### 1.4 HG aux_state: явная клауза вместо catch-ловушек +`apps/hellgate/src/hg_invoice.erl` (`unmarshal_aux_state/1`) + +- Явно матчить `#mg_stateproc_Content{format_version = _, data = Data}` → + `mg_msgpack_marshalling:unmarshal(Data)`; убрать слепые `catch _:_`. +- Проверить ветку `dispatch(call, remove)` в `prg_machine`: туда уходит уже + размаршалленный aux_state — `marshal_aux_state` должен его переживать. + +### 1.5 Pending-задачи: compat-чтение args (решение — доработать, не откатывать) + +Заключение: формат менялся **только у HG** (FF всегда писал plain +`term_to_binary(Args)` — там регрессии нет). Новая форма `{FunRef, Args}` +проще и не тянет thrift-сериализацию в слой `prg_machine` — откатывать на +`{thrift_call, Service, FunRef, EncodedArgs}` не стоит. Достаточно +compat-чтения, окно риска — только незавершённые call/init задачи в момент +деплоя (timeout-задачи с пустыми args не затронуты): + +- `prg_machine:decode_term/1`: результат `{bin, Bin}` → `binary_to_term(Bin)` + (старая двойная обёртка). +- `apps/hellgate/src/hg_invoice.erl` (`process_call`): клауза для старой формы + `{thrift_call, Service, FunRef, EncodedArgs}` → `hg_proto_utils`-десериализация + args → дальше обычный путь `{FunRef, Args}`. Перед реализацией сверить точную + старую форму по `e035d3c1:apps/hellgate/src/hg_machine.erl` (строки ~137–143) + и старому клиентскому пути `hg_progressor:call`. + +### 1.6 Golden-тесты на старые форматы (критерий приёмки этапа) + +- Снять реальные бинари (payload/aux_state/metadata/args), сгенерированные кодом + базового коммита (или со стейджа), положить фикстурами. +- CT/eunit: чтение старого события, старого aux_state, старого call-арга — для + hg_invoice и каждого из 5 ff-неймспейсов; плюс симметричный тест «новая запись + читается старым форматом конверта» (rollback-инвариант). + +--- + +## Этап 2. Таймстемпы событий — вернуть микросекунды (мажор) + +PG-бэкенд progressor хранит `timestamptz` с микросекундами и сам детектит +юниты (`prg_utils:split_timestamp/to_microseconds`) — секунды сейчас режет +только `prg_machine`. + +- `prg_machine:marshal_new_events`: писать `timestamp => erlang:system_time(microsecond)` + (без `div 1000000`), один таймстемп на весь батч (как старый `emit_events`). +- Чтение: `event_timestamp_to_datetime` → возвращать `{calendar:datetime(), Micro}` + (machinery-формат); обновить тип `machine_event()` и спеки. +- HG: `hg_invoice:event_timestamp_to_binary` — форматировать с микро + (`hg_datetime`, как старый MG RFC3339). +- FF: `marshal_event_body` — в `{ev, {Dt, USec}, Body}` класть реальные микро + вместо захардкоженного `0`; «недостижимая» клауза `codec_timestamp({Dt, USec})` + в 5 `*_machine` модулях становится рабочей; `events/2` (GetEvents) наружу + отдаёт микро. +- В progressor при его ревью: поправить спеку `event() :: timestamp := timestamp_sec()` + → допускать `timestamp_us()` (фактически уже работает). + +--- + +## Этап 3. Ошибки и устойчивость (мажоры) + +### 3.1 `notify` / `remove` +- `prg_machine:notify/3`, `remove/2`: обработать все исходы + (`{error, failed}`, `{error, {exception, _, _}}`, прочие guard-ошибки) — + без `case_clause`. +- `ff_withdrawal_session:process_session`: восстановить старую семантику — + notify в сломанный withdrawal глотается с warning-логом (`{error, failed}` → + `ok`), сессия не заражается. Остальные ошибки — как сейчас, в error. + +### 3.2 `env_enter`/`env_leave` +- `prg_machine:process/3`: флаг «enter выполнен»; `after` зовёт Leave только + если был Enter. Ошибки до Enter возвращаются progressor'у как `{error, _}` + без маскировки исключением из `after`. + +### 3.3 Дефолтный woody deadline +- В `process/3` после восстановления контекста — `ensure_deadline_set` + (дефолт 30 с, конфигурируемо через опции неймспейса), как делал старый + `hg_progressor` через `hg_woody_service_wrapper:ensure_woody_deadline_set/2`. + +### 3.4 Repair-путь +- `prg_action:marshal_timer`: клауза `{deadline, {{_,_}=Dt, USec}}` (machinery-формат + из `ff_codec:unmarshal(timer, ...)`) — срезать USec, как в + `ff_adapter_withdrawal_codec:unmarshal_provider_timer/1`. Тест на deadline-таймер + в `ff_withdrawal_codec` (сейчас покрыт только `{timeout, 0}`). +- `prg_machine:repair/3`: декодировать `Reason` (`decode_term`) в ветке + `{error, {repair, {failed, Reason}}}` — наружу term, а не t2b-бинарь. +- Выправить спеки `ff_*_machine:repair/2` и хендлеры (`ff_withdrawal_repair` + и др.) под фактическую форму ошибки; убрать недостижимую ветку + `{error, failed} -> {failed, {invalid_result, unexpected_failure}}`. + +### 3.5 `hg_invoice_handler` +- `get_state/1`: `throw(#payproc_InvoiceNotFound{})` — голый рекорд, как в + `map_history_error`. +- `ensure_started/2`: вернуть ветку `{error, Reason} -> erlang:error(Reason)`. + +### 3.6 Контракт исключений процессора (решение) +- Форму `{error, {exception, Class, Reason}}` оставляем как есть — и на проводе + к progressor (его контракт: `is_retryable/5` по 3-tuple решает «не ретраить»), + и в клиентском API как pass-through. Переименование маркера (`failed` и т.п.) + — косметика, не делаем. +- Чиним только реальные баги: + - `prg_machine:get/3`: убрать `raise_exception` — возвращать + `{error, {exception, Class, Reason}}` как обычную ошибку, а не + re-raise с пустым stacktrace; + - убедиться, что потребители (`hg_invoicing_machine_client`, `ff_*_machine`, + repair-хендлеры) матчат эту форму: детали — в лог / маппинг в + thrift-ошибки, без `case_clause`. +- Голый `{error, failed}` остаётся для статусной ошибки + `<<"process is error">>` (процесс уже в error; деталей в ответе progressor + нет — при необходимости их даёт отдельный `get` с `detail` процесса). +- `docs/prg-machine.md`: убрать упоминание 4-tuple со stacktrace, описать + фактический контракт. + +--- + +## Этап 4. Контекст и конфиг + +### 4.1 Убрать глобальный `woody_context_loader` +- Удалить `application:set_env(prg_machine, woody_context_loader, fun ...)` из + `hellgate.erl` и `ff_server.erl` (анонимный fun в app env + общий ключ — + ломается на hot upgrade и при совместном старте в одном узле). +- `prg_machine:encode_rpc_context/0` → `operation_context:current_woody_context()`: + пробует hg-binding, затем ff-binding (gproc-ключи текущего процесса различны, + коллизий нет), fallback `woody_context:new()` с warning-логом. +- Добавить `operation_context` в `applications` у `prg_machine.app.src` + (зависимость уже фактическая — `resolve_env_enter`). + +### 4.2 `binary_to_term` без `[safe]` +- Старый стек (`machinery_utils:decode`, `hg_progressor`) работал без `[safe]` — + возвращаем как было: убрать `[safe]` во всех decode собственных данных + (`prg_machine:decode_term`, `unmarshal_event_body` fallback, + `unmarshal_aux_state`, `ff_machine_trace:decode_term`, `hg_invoice`). + +--- + +## Этап 5. Гигиена HG/FF + +- `hg_invoice`: `log_changes` для signal/repair (как старый `handle_result`); + убрать двойной `validate_changes` в `process_call` (заодно закрывает пункт + «двойной collapse» из техдолга в `docs/prg-machine.md`). +- FF копипаста ×5: вынести `to_repair_machine/1`, `from_repair_result/2`, + `repair_events_to_domain/1`, `event_body_from_timestamped/1`, + `history_times/1`, `history_to_events/1`, `codec_timestamp/1` в общий модуль + (`ff_machine_codec` или новый `ff_machine_lib`); удалить мёртвое поле `times` + из `st()` пяти `*_machine`, либо начать использовать. +- FF: вернуть no-op `process_notification` (`#{}`) у session/source/destination + вместо принудительного `action => timeout`; убрать лишний `action => timeout` + из `ff_destination:init`. +- FF `machine_to_st`: явная ветка для `aux_state = undefined` (сейчас дефолт + `maps:get(ctx, AuxState, #{})` мёртвый, падает `badmap`). +- `docs/prg-machine.md`: обновить разделы про ошибки/форматы по итогам этапов 1–4. + +Вне скоупа (решено): `hg_hybrid` не возвращаем; trace (`ff_machine_trace`, +дефолт формата, `<<>>`-args) — отдельный ПР с переездом на thrift; тег +progressor в `rebar.config` — после ревью progressor. + +--- + +## Этап 6 (опционально, отдельный ПР). Единый конверт HG+FF + +После выкатки этапов 1–5 и стабилизации: + +- format 2 = сырой thrift-binary payload для **обоих** стеков (HG уходит от + `t2b(msgpack)`, FF — от `t2b({bin, ...})`). +- Reader к этому моменту уже умеет оба формата (этап 1), поэтому включение + записи format 2 — отдельный маленький коммит; rollback-политика: откат только + на версии, содержащие reader этапа 1. +- Сюда же — переезд trace на thrift (`docs/trace-api-thrift.md`). + +## Порядок и критерии + +1 → 2 → 3 → 4 → 5 — каждый этап самостоятелен и мержибелен отдельно; 1 и 2 +трогают одни и те же функции маршалинга, их удобно делать подряд. Критерий +этапа 1 — golden-тесты (1.6) зелёные; критерий общий — CT + dialyzer + compose +зелёные, grep-инварианты из `docs/prg-machine.md` соблюдены. From 988aabd533dae66ec432973560242bf01ae93e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 13 Jun 2026 02:12:47 +0300 Subject: [PATCH 30/62] Refactor multiple modules to enhance clarity and maintainability by unifying action handling under prg_action. Update event processing and unmarshal logic in ff_transfer, ff_withdrawal, and related modules to ensure consistent use of timeout actions. Remove deprecated code and improve error handling for better robustness. --- apps/ff_server/src/ff_server.erl | 13 -- .../test/ff_destination_handler_SUITE.erl | 12 +- apps/ff_transfer/src/ff_deposit.erl | 41 +--- apps/ff_transfer/src/ff_deposit_machine.erl | 51 ++--- apps/ff_transfer/src/ff_destination.erl | 46 +---- .../src/ff_destination_machine.erl | 45 ++--- apps/ff_transfer/src/ff_machine_codec.erl | 68 ++++++- apps/ff_transfer/src/ff_machine_lib.erl | 53 +++++ apps/ff_transfer/src/ff_machine_trace.erl | 2 +- apps/ff_transfer/src/ff_source.erl | 45 +---- apps/ff_transfer/src/ff_source_machine.erl | 45 ++--- apps/ff_transfer/src/ff_withdrawal.erl | 41 +--- .../ff_transfer/src/ff_withdrawal_machine.erl | 53 ++--- .../ff_transfer/src/ff_withdrawal_session.erl | 52 ++--- .../src/ff_withdrawal_session_machine.erl | 51 ++--- apps/hellgate/src/hellgate.erl | 13 -- apps/hellgate/src/hg_datetime.erl | 1 + apps/hellgate/src/hg_invoice.erl | 60 +++++- apps/hellgate/src/hg_invoice_handler.erl | 8 +- .../src/operation_context.erl | 27 +++ apps/prg_machine/src/prg_action.erl | 24 ++- apps/prg_machine/src/prg_machine.app.src | 3 +- apps/prg_machine/src/prg_machine.erl | 185 +++++++++++++----- 23 files changed, 455 insertions(+), 484 deletions(-) create mode 100644 apps/ff_transfer/src/ff_machine_lib.erl diff --git a/apps/ff_server/src/ff_server.erl b/apps/ff_server/src/ff_server.erl index feae5755..2def3502 100644 --- a/apps/ff_server/src/ff_server.erl +++ b/apps/ff_server/src/ff_server.erl @@ -39,7 +39,6 @@ start() -> -spec start(normal, any()) -> {ok, pid()} | {error, any()}. start(_StartType, _StartArgs) -> ok = setup_metrics(), - ok = application:set_env(prg_machine, woody_context_loader, fun woody_rpc_context/0), supervisor:start_link({local, ?MODULE}, ?MODULE, []). -spec stop(any()) -> ok. @@ -133,15 +132,3 @@ wrap_handler(Handler, WrapperOpts) -> setup_metrics() -> ok = woody_ranch_prometheus_collector:setup(), ok = woody_hackney_prometheus_collector:setup(). - --spec woody_rpc_context() -> woody_context:ctx(). -woody_rpc_context() -> - try operation_context:load_fistful() of - Ctx -> - operation_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. diff --git a/apps/ff_server/test/ff_destination_handler_SUITE.erl b/apps/ff_server/test/ff_destination_handler_SUITE.erl index 12f56794..58639d4c 100644 --- a/apps/ff_server/test/ff_destination_handler_SUITE.erl +++ b/apps/ff_server/test/ff_destination_handler_SUITE.erl @@ -167,14 +167,14 @@ 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">> - }, - #{<<"task_status">> := <<"finished">>, <<"task_type">> := <<"timeout">>} + } ] = json:decode(Body), ok. diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index fcb2af10..1fe1b256 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -347,23 +347,23 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario) of + case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario) of {ok, {_Response, Result}} -> - from_repair_result(Result, Machine); + ff_machine_lib:from_repair_result(Result, Machine); {error, Reason} -> {error, Reason} end. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, + Timestamped = {ev, prg_machine:timestamp(), Body}, Encoded = ff_machine_codec:marshal_event(deposit, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> Timestamped = ff_machine_codec:unmarshal_event(deposit, ?EVENT_FORMAT_VERSION, Payload), - event_body_from_timestamped(Timestamped); + ff_machine_lib:event_body_from_timestamped(Timestamped); unmarshal_event_body(Format, _Payload) -> erlang:error({unknown_event_format, Format}). @@ -667,36 +667,3 @@ process_transfer_result({Action, Events}, Machine) -> action => Action, auxst => maps:get(aux_state, Machine, #{}) }. - --type repair_result() :: #{ - events := [term()], - action => action(), - aux_state => term() -}. - --spec from_repair_result(repair_result(), machine()) -> prg_result(). -from_repair_result(#{events := Events} = Result, Machine) -> - #{ - events => repair_events_to_domain(Events), - action => maps:get(action, Result, idle), - auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) - }. - --spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(Events) -> - [event_body_from_timestamped(E) || E <- Events]. - --spec event_body_from_timestamped(term()) -> event(). -event_body_from_timestamped({ev, _Timestamp, Change}) -> - Change; -event_body_from_timestamped(Change) -> - Change. - --spec to_repair_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 - }. diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index fc277a02..02bd761a 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -13,8 +13,7 @@ -type event() :: {integer(), timestamped_event(change())}. -type st() :: #{ model := deposit(), - ctx := ctx(), - times => {timestamp() | undefined, timestamp() | undefined} + ctx := ctx() }. -type deposit() :: ff_deposit:deposit_state(). -type external_id() :: id(). @@ -91,7 +90,9 @@ get(ID, {After, Limit}) -> {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> - {error, {unknown_deposit, ID}} + {error, {unknown_deposit, ID}}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. -spec events(id(), event_range()) -> @@ -100,9 +101,11 @@ get(ID, {After, Limit}) -> events(ID, {After, Limit}) -> case prg_machine:get_history(?NS, ID, After, Limit, forward) of {ok, History} -> - {ok, history_to_events(History)}; + {ok, ff_machine_lib:history_to_events(History)}; {error, notfound} -> - {error, {unknown_deposit, ID}} + {error, {unknown_deposit, ID}}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. -spec repair(id(), ff_repair:scenario()) -> @@ -115,10 +118,8 @@ repair(ID, Scenario) -> {error, notfound}; {error, working} -> {error, working}; - {error, failed} -> - {error, {failed, {invalid_result, unexpected_failure}}}; - {error, {repair, {failed, _Reason}}} = Error -> - Error + {error, {repair, {failed, Reason}}} -> + {error, {failed, Reason}} end. %% Accessors @@ -134,36 +135,12 @@ ctx(#{ctx := Ctx}) -> %% Internals -spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> +machine_to_st(#{aux_state := undefined} = Machine) -> + machine_to_st(Machine#{aux_state => #{}}); +machine_to_st(#{aux_state := AuxState} = Machine) -> Model = prg_machine:collapse(ff_deposit, Machine), Ctx = maps:get(ctx, AuxState, #{}), #{ model => Model, - ctx => Ctx, - times => history_times(History) + ctx => Ctx }. - --spec history_to_events(prg_machine:history()) -> [event()]. -history_to_events(History) -> - [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. - --spec history_times(prg_machine:history()) -> - {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. -history_times([]) -> - {undefined, undefined}; -history_times(History) -> - lists:foldl( - fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> - case Created of - undefined -> {Timestamp, Timestamp}; - _ -> {Created, Timestamp} - end - end, - {undefined, undefined}, - History - ). - -codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> - {DateTime, USec} = Timestamp; -codec_timestamp(DateTime) -> - {DateTime, 0}. diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index 874551bf..f5cdee3a 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -255,7 +255,6 @@ namespace() -> init({Events, Ctx}, _Machine) -> #{ events => Events, - action => timeout, auxst => #{ctx => Ctx} }. @@ -271,27 +270,27 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario) of + case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario) of {ok, {_Response, Result}} -> - from_repair_result(Result, Machine); + ff_machine_lib:from_repair_result(Result, Machine); {error, Reason} -> {error, Reason} end. -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{events => [], action => timeout}. + #{}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, + Timestamped = {ev, prg_machine:timestamp(), Body}, Encoded = ff_machine_codec:marshal_event(destination, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> Timestamped = ff_machine_codec:unmarshal_event(destination, ?EVENT_FORMAT_VERSION, Payload), - event_body_from_timestamped(Timestamped); + ff_machine_lib:event_body_from_timestamped(Timestamped); unmarshal_event_body(Format, _Payload) -> erlang:error({unknown_event_format, Format}). @@ -302,38 +301,3 @@ 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). - --type action() :: prg_action:t(). - --type repair_result() :: #{ - events := [term()], - action => action(), - aux_state => term() -}. - --spec from_repair_result(repair_result(), machine()) -> prg_result(). -from_repair_result(#{events := Events} = Result, Machine) -> - #{ - events => repair_events_to_domain(Events), - action => maps:get(action, Result, idle), - auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) - }. - --spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(Events) -> - [event_body_from_timestamped(E) || E <- Events]. - --spec event_body_from_timestamped(term()) -> event(). -event_body_from_timestamped({ev, _Timestamp, Change}) -> - Change; -event_body_from_timestamped(Change) -> - Change. - --spec to_repair_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 - }. diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index 29dacc94..e1c549d8 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -19,8 +19,7 @@ -type params() :: ff_destination:params(). -type st() :: #{ model := destination(), - ctx := ctx(), - times => {timestamp() | undefined, timestamp() | undefined} + ctx := ctx() }. -type repair_error() :: ff_repair:repair_error(). @@ -78,7 +77,9 @@ get(ID, {After, Limit}) -> {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> - {error, notfound} + {error, notfound}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. -spec events(id(), event_range()) -> @@ -87,9 +88,11 @@ get(ID, {After, Limit}) -> events(ID, {After, Limit}) -> case prg_machine:get_history(?NS, ID, After, Limit, forward) of {ok, History} -> - {ok, history_to_events(History)}; + {ok, ff_machine_lib:history_to_events(History)}; {error, notfound} -> - {error, notfound} + {error, notfound}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. %% Accessors @@ -105,36 +108,12 @@ ctx(#{ctx := Ctx}) -> %% Internals -spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> +machine_to_st(#{aux_state := undefined} = Machine) -> + machine_to_st(Machine#{aux_state => #{}}); +machine_to_st(#{aux_state := AuxState} = Machine) -> Model = prg_machine:collapse(ff_destination, Machine), Ctx = maps:get(ctx, AuxState, #{}), #{ model => Model, - ctx => Ctx, - times => history_times(History) + ctx => Ctx }. - --spec history_to_events(prg_machine:history()) -> [event()]. -history_to_events(History) -> - [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. - --spec history_times(prg_machine:history()) -> - {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. -history_times([]) -> - {undefined, undefined}; -history_times(History) -> - lists:foldl( - fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> - case Created of - undefined -> {Timestamp, Timestamp}; - _ -> {Created, Timestamp} - end - end, - {undefined, undefined}, - History - ). - -codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> - {DateTime, USec} = Timestamp; -codec_timestamp(DateTime) -> - {DateTime, 0}. diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl index fb731f0f..f1b644c1 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -89,22 +89,29 @@ unmarshal_event(withdrawal_session, 1, Payload) -> unmarshal_event(Domain, Format, _Payload) -> erlang:error({unknown_event_format, Domain, Format}). +%% aux_state: write in the legacy envelope term_to_binary(AuxSt) (as the old +%% machinery_prg_backend did). Reading tries both: legacy term first, then the +%% msgpack-thrift form this branch briefly wrote. -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - payload_to_binary(ff_machine_schema:marshal(AuxSt)). + term_to_binary(AuxSt). -spec unmarshal_aux_state(binary()) -> term(). unmarshal_aux_state(<<>>) -> #{}; unmarshal_aux_state(Payload) when is_binary(Payload) -> - ff_machine_schema:unmarshal(binary_to_payload(Payload)). + try + binary_to_term(Payload) + catch + error:badarg -> + ff_machine_schema:unmarshal(binary_to_payload(Payload)) + end. +%% Event payload: write the legacy envelope term_to_binary({bin, ThriftBin}) +%% (machinery_prg_backend used machinery_utils:encode(term, ...)). -spec payload_to_binary(ff_msgpack:t()) -> binary(). -payload_to_binary({bin, Bin}) when is_binary(Bin) -> - Bin; payload_to_binary(Payload) -> - {ok, Bin} = ff_msgpack:pack(Payload), - Bin. + term_to_binary(Payload). -spec binary_to_payload(binary()) -> ff_msgpack:t(). binary_to_payload(Bin) when is_binary(Bin) -> @@ -122,6 +129,20 @@ marshal_thrift_event(Timestamped, MarshalFun, ThriftModule, ThriftStruct) -> Type = {struct, struct, {ThriftModule, ThriftStruct}}, {bin, ff_proto_utils:serialize(Type, ThriftChange)}. +%% Sniff the stored payload: legacy events are term_to_binary({bin, ThriftBin}) +%% (first byte 131); events written by this branch are raw thrift. The sniff is +%% only safe in this direction — a thrift struct never starts with 131, whereas +%% msgpack fixmap(3) is 0x83, so we must check the term envelope first. +sniff_thrift_payload(<<131, _/binary>> = Payload) -> + case binary_to_term(Payload) of + {bin, Bin} when is_binary(Bin) -> + Bin; + Other -> + erlang:error({legacy_msgpack_event, Other}) + end; +sniff_thrift_payload(Payload) when is_binary(Payload) -> + Payload. + -spec unmarshal_thrift_event( binary(), fun((term()) -> timestamped_event()), @@ -129,6 +150,39 @@ marshal_thrift_event(Timestamped, MarshalFun, ThriftModule, ThriftStruct) -> atom() ) -> timestamped_event(). unmarshal_thrift_event(Payload, UnmarshalFun, ThriftModule, ThriftStruct) -> + ThriftBin = sniff_thrift_payload(Payload), Type = {struct, struct, {ThriftModule, ThriftStruct}}, - ThriftChange = ff_proto_utils:deserialize(Type, Payload), + 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 sniff_thrift_payload_reads_legacy_envelope_test() -> _. +sniff_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, sniff_thrift_payload(term_to_binary({bin, ThriftBin}))), + %% New format: raw thrift binary returned as-is. + ?assertEqual(ThriftBin, sniff_thrift_payload(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..879de082 --- /dev/null +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -0,0 +1,53 @@ +-module(ff_machine_lib). + +%%% Shared helpers for the ff_* prg_machine handlers (ff_withdrawal, ff_deposit, +%%% ff_source, ff_destination, ff_withdrawal_session) and their thin machine +%%% clients. Extracted to remove the per-namespace copy-paste. + +-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]). + +-type timestamp() :: prg_machine:timestamp(). +-type timestamped_event(T) :: {ev, timestamp(), T}. + +-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) -> + #{ + events => repair_events_to_domain(Events), + action => maps:get(action, Result, idle), + auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) + }. + +-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}. diff --git a/apps/ff_transfer/src/ff_machine_trace.erl b/apps/ff_transfer/src/ff_machine_trace.erl index 489820ef..03b02e57 100644 --- a/apps/ff_transfer/src/ff_machine_trace.erl +++ b/apps/ff_transfer/src/ff_machine_trace.erl @@ -88,7 +88,7 @@ decode_trace_value(Value) -> %% Same contract as prg_machine:decode_term/1. -spec decode_term(term()) -> term(). decode_term(Bin) when is_binary(Bin) -> - binary_to_term(Bin, [safe]); + binary_to_term(Bin); decode_term(Term) -> Term. diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index f833ca64..7d607124 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -247,27 +247,27 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario) of + case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario) of {ok, {_Response, Result}} -> - from_repair_result(Result, Machine); + ff_machine_lib:from_repair_result(Result, Machine); {error, Reason} -> {error, Reason} end. -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{events => [], action => timeout}. + #{}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, + Timestamped = {ev, prg_machine:timestamp(), Body}, Encoded = ff_machine_codec:marshal_event(source, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> Timestamped = ff_machine_codec:unmarshal_event(source, ?EVENT_FORMAT_VERSION, Payload), - event_body_from_timestamped(Timestamped); + ff_machine_lib:event_body_from_timestamped(Timestamped); unmarshal_event_body(Format, _Payload) -> erlang:error({unknown_event_format, Format}). @@ -278,38 +278,3 @@ 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). - --type action() :: prg_action:t(). - --type repair_result() :: #{ - events := [term()], - action => action(), - aux_state => term() -}. - --spec from_repair_result(repair_result(), machine()) -> prg_result(). -from_repair_result(#{events := Events} = Result, Machine) -> - #{ - events => repair_events_to_domain(Events), - action => maps:get(action, Result, idle), - auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) - }. - --spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(Events) -> - [event_body_from_timestamped(E) || E <- Events]. - --spec event_body_from_timestamped(term()) -> event(). -event_body_from_timestamped({ev, _Timestamp, Change}) -> - Change; -event_body_from_timestamped(Change) -> - Change. - --spec to_repair_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 - }. diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index 288848c7..dc484e4c 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -19,8 +19,7 @@ -type params() :: ff_source:params(). -type st() :: #{ model := source(), - ctx := ctx(), - times => {timestamp() | undefined, timestamp() | undefined} + ctx := ctx() }. -type repair_error() :: ff_repair:repair_error(). @@ -78,7 +77,9 @@ get(ID, {After, Limit}) -> {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> - {error, notfound} + {error, notfound}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. -spec events(id(), event_range()) -> @@ -87,9 +88,11 @@ get(ID, {After, Limit}) -> events(ID, {After, Limit}) -> case prg_machine:get_history(?NS, ID, After, Limit, forward) of {ok, History} -> - {ok, history_to_events(History)}; + {ok, ff_machine_lib:history_to_events(History)}; {error, notfound} -> - {error, notfound} + {error, notfound}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. %% Accessors @@ -105,36 +108,12 @@ ctx(#{ctx := Ctx}) -> %% Internals -spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> +machine_to_st(#{aux_state := undefined} = Machine) -> + machine_to_st(Machine#{aux_state => #{}}); +machine_to_st(#{aux_state := AuxState} = Machine) -> Model = prg_machine:collapse(ff_source, Machine), Ctx = maps:get(ctx, AuxState, #{}), #{ model => Model, - ctx => Ctx, - times => history_times(History) + ctx => Ctx }. - --spec history_to_events(prg_machine:history()) -> [event()]. -history_to_events(History) -> - [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. - --spec history_times(prg_machine:history()) -> - {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. -history_times([]) -> - {undefined, undefined}; -history_times(History) -> - lists:foldl( - fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> - case Created of - undefined -> {Timestamp, Timestamp}; - _ -> {Created, Timestamp} - end - end, - {undefined, undefined}, - History - ). - -codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> - {DateTime, USec} = Timestamp; -codec_timestamp(DateTime) -> - {DateTime, 0}. diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index c33d9546..ef5d6447 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -1871,9 +1871,9 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario) of + case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario) of {ok, {_Response, Result}} -> - from_repair_result(Result, Machine); + ff_machine_lib:from_repair_result(Result, Machine); {error, Reason} -> {error, Reason} end. @@ -1890,14 +1890,14 @@ process_notification({session_finished, SessionID, SessionResult}, Machine) -> -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, + Timestamped = {ev, prg_machine:timestamp(), Body}, Encoded = ff_machine_codec:marshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> Timestamped = ff_machine_codec:unmarshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Payload), - event_body_from_timestamped(Timestamped); + ff_machine_lib:event_body_from_timestamped(Timestamped); unmarshal_event_body(Format, _Payload) -> erlang:error({unknown_event_format, Format}). @@ -1917,39 +1917,6 @@ process_transfer_result({Action, Events}, Machine) -> auxst => maps:get(aux_state, Machine, #{}) }. --type repair_result() :: #{ - events := [term()], - action => action(), - aux_state => term() -}. - --spec from_repair_result(repair_result(), machine()) -> prg_result(). -from_repair_result(#{events := Events} = Result, Machine) -> - #{ - events => repair_events_to_domain(Events), - action => maps:get(action, Result, idle), - auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) - }. - --spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(Events) -> - [event_body_from_timestamped(E) || E <- Events]. - --spec event_body_from_timestamped(term()) -> event(). -event_body_from_timestamped({ev, _Timestamp, Change}) -> - Change; -event_body_from_timestamped(Change) -> - Change. - --spec to_repair_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 apply_event(event() | legacy_event(), ff_maybe:'maybe'(withdrawal_state())) -> withdrawal_state(). diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 14fc63ff..e8f2fcf1 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -13,8 +13,7 @@ -type event() :: {integer(), timestamped_event(change())}. -type st() :: #{ model := withdrawal(), - ctx := ctx(), - times => {timestamp() | undefined, timestamp() | undefined} + ctx := ctx() }. -type withdrawal() :: ff_withdrawal:withdrawal_state(). -type external_id() :: id(). @@ -109,7 +108,9 @@ get(ID, {After, Limit}) -> {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> - {error, {unknown_withdrawal, ID}} + {error, {unknown_withdrawal, ID}}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. -spec events(id(), event_range()) -> @@ -118,9 +119,11 @@ get(ID, {After, Limit}) -> events(ID, {After, Limit}) -> case prg_machine:get_history(?NS, ID, After, Limit, forward) of {ok, History} -> - {ok, history_to_events(History)}; + {ok, ff_machine_lib:history_to_events(History)}; {error, notfound} -> - {error, {unknown_withdrawal, ID}} + {error, {unknown_withdrawal, ID}}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. -spec repair(id(), ff_repair:scenario()) -> @@ -133,10 +136,8 @@ repair(ID, Scenario) -> {error, notfound}; {error, working} -> {error, working}; - {error, failed} -> - {error, {failed, {invalid_result, unexpected_failure}}}; - {error, {repair, {failed, _Reason}}} = Error -> - Error + {error, {repair, {failed, Reason}}} -> + {error, {failed, Reason}} end. -spec start_adjustment(id(), adjustment_params()) -> @@ -146,7 +147,7 @@ start_adjustment(WithdrawalID, Params) -> call(WithdrawalID, {start_adjustment, Params}). -spec notify(id(), notify_args()) -> - ok | {error, notfound} | no_return(). + ok | {error, notfound | failed} | no_return(). notify(ID, Args) -> prg_machine:notify(?NS, ID, Args). @@ -163,35 +164,16 @@ ctx(#{ctx := Ctx}) -> %% Internals -spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> +machine_to_st(#{aux_state := undefined} = Machine) -> + machine_to_st(Machine#{aux_state => #{}}); +machine_to_st(#{aux_state := AuxState} = Machine) -> Model = prg_machine:collapse(ff_withdrawal, Machine), Ctx = maps:get(ctx, AuxState, #{}), #{ model => Model, - ctx => Ctx, - times => history_times(History) + ctx => Ctx }. --spec history_to_events(prg_machine:history()) -> [event()]. -history_to_events(History) -> - [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. - --spec history_times(prg_machine:history()) -> - {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. -history_times([]) -> - {undefined, undefined}; -history_times(History) -> - lists:foldl( - fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> - case Created of - undefined -> {Timestamp, Timestamp}; - _ -> {Created, Timestamp} - end - end, - {undefined, undefined}, - History - ). - call(ID, Call) -> case prg_machine:call(?NS, ID, Call) of {ok, Reply} -> @@ -203,8 +185,3 @@ call(ID, Call) -> {error, _} = Error -> Error end. - -codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> - {DateTime, USec} = Timestamp; -codec_timestamp(DateTime) -> - {DateTime, 0}. diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index de406b6f..4412eb8c 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -223,6 +223,15 @@ process_session(#{status := {finished, _}, id := ID, result := Result, withdrawa case ff_withdrawal_machine:notify(WithdrawalID, {session_finished, ID, Result}) of ok -> {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, _} = Error -> erlang:error({unable_to_finish_session, Error}) end; @@ -428,27 +437,27 @@ process_repair(Scenario, Machine) -> {ok, {ok, #{action => Action, events => Events}}} end }, - case ff_repair:apply_scenario(?MODULE, to_repair_machine(Machine), Scenario, ScenarioProcessors) of + case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario, ScenarioProcessors) of {ok, {_Response, Result}} -> - from_repair_result(Result, Machine); + ff_machine_lib:from_repair_result(Result, Machine); {error, Reason} -> {error, Reason} end. -spec process_notification(term(), machine()) -> prg_result(). process_notification(_Args, _Machine) -> - #{events => [], action => timeout}. + #{}. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, {prg_machine:timestamp(), 0}, Body}, + Timestamped = {ev, prg_machine:timestamp(), Body}, Encoded = ff_machine_codec:marshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Timestamped), {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> Timestamped = ff_machine_codec:unmarshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Payload), - event_body_from_timestamped(Timestamped); + ff_machine_lib:event_body_from_timestamped(Timestamped); unmarshal_event_body(Format, _Payload) -> erlang:error({unknown_event_format, Format}). @@ -467,36 +476,3 @@ process_session_result({Action, Events}, Machine) -> action => Action, auxst => maps:get(aux_state, Machine, #{}) }. - --type repair_result() :: #{ - events := [term()], - action => action(), - aux_state => term() -}. - --spec from_repair_result(repair_result(), machine()) -> prg_result(). -from_repair_result(#{events := Events} = Result, Machine) -> - #{ - events => repair_events_to_domain(Events), - action => maps:get(action, Result, idle), - auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) - }. - --spec repair_events_to_domain([term()]) -> [event()]. -repair_events_to_domain(Events) -> - [event_body_from_timestamped(E) || E <- Events]. - --spec event_body_from_timestamped(term()) -> event(). -event_body_from_timestamped({ev, _Timestamp, Change}) -> - Change; -event_body_from_timestamped(Change) -> - Change. - --spec to_repair_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 - }. diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index b8494e05..bb2276d2 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -34,8 +34,7 @@ -type st() :: #{ model := session(), - ctx := ctx(), - times => {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined} + ctx := ctx() }. -type session() :: ff_withdrawal_session:session_state(). -type event() :: ff_withdrawal_session:event(). @@ -92,7 +91,9 @@ get(ID, {After, Limit}) -> {ok, Machine} -> {ok, machine_to_st(Machine)}; {error, notfound} -> - {error, notfound} + {error, notfound}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. -spec events(id(), event_range()) -> @@ -101,9 +102,11 @@ get(ID, {After, Limit}) -> events(ID, {After, Limit}) -> case prg_machine:get_history(?NS, ID, After, Limit, forward) of {ok, History} -> - {ok, history_to_events(History)}; + {ok, ff_machine_lib:history_to_events(History)}; {error, notfound} -> - {error, notfound} + {error, notfound}; + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}) end. -spec repair(id(), ff_repair:scenario()) -> @@ -116,10 +119,8 @@ repair(ID, Scenario) -> {error, notfound}; {error, working} -> {error, working}; - {error, failed} -> - {error, {failed, {invalid_result, unexpected_failure}}}; - {error, {repair, {failed, _Reason}}} = Error -> - Error + {error, {repair, {failed, Reason}}} -> + {error, {failed, Reason}} end. -spec process_callback(callback_params()) -> @@ -138,35 +139,16 @@ process_callback(#{tag := Tag} = Params) -> %% -spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{history := History, aux_state := AuxState} = Machine) -> +machine_to_st(#{aux_state := undefined} = Machine) -> + machine_to_st(Machine#{aux_state => #{}}); +machine_to_st(#{aux_state := AuxState} = Machine) -> Model = prg_machine:collapse(ff_withdrawal_session, Machine), Ctx = maps:get(ctx, AuxState, #{}), #{ model => Model, - ctx => Ctx, - times => history_times(History) + ctx => Ctx }. --spec history_to_events(prg_machine:history()) -> [{integer(), timestamped_event(event())}]. -history_to_events(History) -> - [{EventID, {ev, codec_timestamp(Timestamp), Body}} || {EventID, Timestamp, Body} <- History]. - --spec history_times(prg_machine:history()) -> - {prg_machine:timestamp() | undefined, prg_machine:timestamp() | undefined}. -history_times([]) -> - {undefined, undefined}; -history_times(History) -> - lists:foldl( - fun({_EventID, Timestamp, _Body}, {Created, _Updated}) -> - case Created of - undefined -> {Timestamp, Timestamp}; - _ -> {Created, Timestamp} - end - end, - {undefined, undefined}, - History - ). - call(Ref, Call) -> case prg_machine:call(?NS, Ref, Call) of {ok, Reply} -> @@ -178,8 +160,3 @@ call(Ref, Call) -> {error, _} = Error -> Error end. - -codec_timestamp({DateTime, USec} = Timestamp) when is_integer(USec) -> - {DateTime, USec} = Timestamp; -codec_timestamp(DateTime) -> - {DateTime, 0}. diff --git a/apps/hellgate/src/hellgate.erl b/apps/hellgate/src/hellgate.erl index 269499fa..b1243fbe 100644 --- a/apps/hellgate/src/hellgate.erl +++ b/apps/hellgate/src/hellgate.erl @@ -101,7 +101,6 @@ get_prometheus_route() -> -spec start(normal, any()) -> {ok, pid()} | {error, any()}. start(_StartType, _StartArgs) -> ok = setup_metrics(), - ok = application:set_env(prg_machine, woody_context_loader, fun woody_rpc_context/0), supervisor:start_link(?MODULE, []). -spec stop(any()) -> ok. @@ -113,15 +112,3 @@ stop(_State) -> setup_metrics() -> ok = woody_ranch_prometheus_collector:setup(), ok = woody_hackney_prometheus_collector:setup(). - --spec woody_rpc_context() -> woody_context:ctx(). -woody_rpc_context() -> - try operation_context:load_hellgate() of - Ctx -> - operation_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. 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 af324ee8..14f95d7d 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -20,6 +20,7 @@ -include("hg_invoice.hrl"). -include_lib("damsel/include/dmsl_repair_thrift.hrl"). +-include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). -define(NS, invoice). -define(EVENT_FORMAT_VERSION, 1). @@ -98,7 +99,7 @@ %% API --spec get(prg_machine:id()) -> {ok, st()} | {error, notfound}. +-spec get(prg_machine:id()) -> {ok, st()} | {error, prg_machine:get_error()}. get(ID) -> case prg_machine:get(?NS, ID) of {ok, Machine} -> @@ -389,11 +390,11 @@ handle_expiration(St) -> }. -spec process_call(call(), machine()) -> {prg_machine:response(), prg_result()}. -process_call(Call, Machine) -> +process_call(Call0, Machine) -> + Call = normalize_call(Call0), St = prg_machine:collapse(?MODULE, Machine), try CallResult = handle_call(Call, St), - _ = log_changes(maps:get(changes, CallResult, []), validate_changes(CallResult)), Response = maps:get(response, CallResult, ok), {call_response(Response), to_prg_result(CallResult)} catch @@ -401,6 +402,16 @@ process_call(Call, Machine) -> {{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), @@ -688,7 +699,10 @@ wrap_payment_impact(PaymentID, {Response, {Changes, Action}}, St, OccurredAt) -> -spec to_prg_result(handler_result()) -> prg_result(). to_prg_result(Result) -> - _ = validate_changes(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), to_prg_result_(Result). %% No `auxst` here: invoice sets it only in `init/2`; call/signal/repair must not touch aux_state (M1). @@ -1006,6 +1020,10 @@ apply_event(EventID, Ts, Changes, St0) -> 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). @@ -1038,11 +1056,14 @@ marshal_aux_state(AuxSt) -> unmarshal_aux_state(<<>>) -> #{}; unmarshal_aux_state(Payload) when is_binary(Payload) -> - try - mg_msgpack_marshalling:unmarshal(binary_to_term(Payload, [safe])) - catch - _:_ -> - binary_to_term(Payload, [safe]) + %% Legacy hg_progressor stored term_to_binary(#mg_stateproc_Content{data = Msgp}); + %% the current branch stores term_to_binary(mg_msgpack_marshalling:marshal(AuxSt)). + %% Both unwrap to a msgpack Value that mg_msgpack_marshalling:unmarshal decodes. + case binary_to_term(Payload) of + #mg_stateproc_Content{data = Data} -> + mg_msgpack_marshalling:unmarshal(Data); + Msgp -> + mg_msgpack_marshalling:unmarshal(Msgp) end. msgpack_payload_to_binary(Msgp) -> @@ -1058,7 +1079,7 @@ decode_event_body(Payload) -> try_unmarshal_msgpack_payload(Payload) -> try - {ok, mg_msgpack_marshalling:unmarshal(binary_to_term(Payload, [safe]))} + {ok, mg_msgpack_marshalling:unmarshal(binary_to_term(Payload))} catch _:_ -> {error, invalid_msgpack_payload} @@ -1143,4 +1164,23 @@ 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() -> + %% Legacy hg_progressor stored term_to_binary(#mg_stateproc_Content{data = Msgp}). + AuxSt = #{<<"legacy">> => 1}, + Msgp = mg_msgpack_marshalling:marshal(AuxSt), + Legacy = term_to_binary(#mg_stateproc_Content{format_version = 1, data = Msgp}), + ?assertEqual(AuxSt, unmarshal_aux_state(Legacy)). + -endif. diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index a97213e7..42f88eb5 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -150,7 +150,8 @@ ensure_started(ID, TemplateID, Params, Allocation, Mutations, DomainRevision) -> Invoice = hg_invoice:create(ID, TemplateID, Params, Allocation, Mutations, DomainRevision), case prg_machine:start(hg_invoice:namespace(), ID, hg_invoice:marshal_invoice(Invoice)) of {ok, _} -> ok; - {error, exists} -> ok + {error, exists} -> ok; + {error, Reason} -> erlang:error(Reason) end. call(ID, Function, Args) -> @@ -226,7 +227,10 @@ get_state(ID) -> {ok, St} -> St; {error, notfound} -> - throw({exception, #payproc_InvoiceNotFound{}}) + 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) -> diff --git a/apps/operation_context/src/operation_context.erl b/apps/operation_context/src/operation_context.erl index f8155746..4f98be12 100644 --- a/apps/operation_context/src/operation_context.erl +++ b/apps/operation_context/src/operation_context.erl @@ -25,6 +25,7 @@ -export([fistful_binding/0]). -export([env_enter/2]). -export([env_leave/1]). +-export([current_woody_context/0]). -type registry_key() :: {p, l, term()}. -type cleanup_mode() :: strict | lenient. @@ -153,6 +154,32 @@ env_enter(WoodyCtx, #{registry_key := RegistryKey}) -> 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([?HG_REGISTRY_KEY, ?FF_REGISTRY_KEY]) of + {ok, WoodyContext} -> + WoodyContext; + error -> + _ = logger:warning( + "operation_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. diff --git a/apps/prg_machine/src/prg_action.erl b/apps/prg_machine/src/prg_action.erl index 9c43a1eb..f34aa73a 100644 --- a/apps/prg_machine/src/prg_action.erl +++ b/apps/prg_machine/src/prg_action.erl @@ -12,7 +12,7 @@ -export_type([t/0, timer/0, seconds/0, timer_field/0, remove_field/0]). -type seconds() :: timeout_sec(). --type datetime() :: calendar:datetime() | binary(). +-type datetime() :: calendar:datetime() | {calendar:datetime(), non_neg_integer()} | binary(). -type timer() :: {timeout, seconds()} | {deadline, datetime()}. -type t() :: action(). @@ -29,21 +29,23 @@ schedule_timer(Timer) -> schedule_after(0) -> timeout; schedule_after(Seconds) when is_integer(Seconds), Seconds > 0 -> - {schedule, #{at => erlang:system_time(second) + Seconds, action => timeout}}. + {schedule, #{at => erlang:system_time(microsecond) + Seconds * 1000000, action => timeout}}. -spec schedule_deadline(datetime()) -> t(). schedule_deadline(Deadline) -> {schedule, #{at => marshal_timer({deadline, Deadline}), action => timeout}}. --spec marshal_timer(timer()) -> timestamp_sec(). +-spec marshal_timer(timer()) -> timestamp_us(). marshal_timer({timeout, 0}) -> - erlang:system_time(second); + erlang:system_time(microsecond); marshal_timer({timeout, Seconds}) when is_integer(Seconds), Seconds >= 0 -> - erlang:system_time(second) + Seconds; -marshal_timer({deadline, {_, _} = Dt}) -> - calendar:datetime_to_gregorian_seconds(Dt) - ?EPOCH_DIFF; + 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, second}]); + calendar:rfc3339_to_system_time(unicode:characters_to_list(Bin), [{unit, microsecond}]); marshal_timer(Other) -> error({invalid_timer, Other}). @@ -102,3 +104,9 @@ repair_remove_field(undefined) -> undefined; repair_remove_field(#repair_RemoveAction{}) -> remove. + +%% + +datetime_to_microseconds(Dt, USec) -> + Sec = calendar:datetime_to_gregorian_seconds(Dt) - ?EPOCH_DIFF, + Sec * 1000000 + USec. diff --git a/apps/prg_machine/src/prg_machine.app.src b/apps/prg_machine/src/prg_machine.app.src index 55fc0485..69a4e9e4 100644 --- a/apps/prg_machine/src/prg_machine.app.src +++ b/apps/prg_machine/src/prg_machine.app.src @@ -8,7 +8,8 @@ genlib, woody, scoper, - progressor + progressor, + operation_context ]}, {env, []}, {modules, []}, diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 7984901b..8005fb57 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -16,11 +16,16 @@ -type call() :: term(). -type response() :: ok | {ok, term()} | {error, term()} | {exception, term()}. --type timestamp() :: calendar:datetime(). +%% 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 get_error() :: + notfound + | {unknown_namespace, namespace()} + | {exception, atom(), term()}. -type machine() :: #{ namespace := namespace(), @@ -45,7 +50,8 @@ ns := namespace(), env_enter => env_enter_fun(), env_leave => fun(() -> ok), - context_binding => context_binding() + context_binding => context_binding(), + default_handling_timeout => timeout() }. -export_type([ @@ -61,6 +67,7 @@ machine_event/0, history/0, machine/0, + get_error/0, signal/0, result/0, process_options/0 @@ -193,10 +200,12 @@ repair(NS, ID, Args) -> {error, <<"process is error">>} -> {error, failed}; {error, Reason} -> - {error, {repair, {failed, Reason}}} + %% The repair-failed reason is our own term encoded by process/3 + %% (marshal_process_result -> encode_term); hand it back as a term. + {error, {repair, {failed, decode_term(Reason)}}} end. --spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, notfound | {unknown_namespace, namespace()}}. +-spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, get_error()}. get(NS, ID, Range) -> Req = request(NS, ID, undefined, Range), case progressor:get(Req) of @@ -209,27 +218,27 @@ get(NS, ID, Range) -> end; {error, <<"process not found">>} -> {error, notfound}; - {error, {exception, _, _} = Exception} -> - raise_exception(Exception); - {error, {exception, _, _, _} = Exception} -> - raise_exception(Exception) + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception}; + {error, {exception, Class, Reason, _Stacktrace}} -> + {error, {exception, Class, Reason}} end. --spec get(namespace(), id()) -> {ok, machine()} | {error, notfound}. +-spec get(namespace(), id()) -> {ok, machine()} | {error, get_error()}. get(NS, ID) -> get(NS, ID, #{direction => forward}). --spec get_history(namespace(), id()) -> {ok, history()} | {error, notfound}. +-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, notfound}. + {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, notfound}. + {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}} -> @@ -238,18 +247,20 @@ get_history(NS, ID, After, Limit, Direction) -> Error end. --spec notify(namespace(), id(), args()) -> ok | {error, notfound}. +-spec notify(namespace(), id(), args()) -> + ok | {error, notfound | failed | {exception, atom(), term()} | term()}. notify(NS, ID, Args) -> case call(NS, ID, {notify, Args}) of {ok, _} -> ok; - {error, notfound} = Error -> Error + {error, _} = Error -> Error end. --spec remove(namespace(), id()) -> ok | {error, notfound}. +-spec remove(namespace(), id()) -> + ok | {error, notfound | failed | {exception, atom(), term()} | term()}. remove(NS, ID) -> case call(NS, ID, remove) of {ok, _} -> ok; - {error, notfound} = Error -> Error + {error, _} = Error -> Error end. -spec history_range(undefined | event_id(), undefined | non_neg_integer(), forward | backward) -> @@ -270,13 +281,19 @@ process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> {error, _} = Error -> Error; {ok, Handler} -> - {WoodyCtx, OtelCtx} = decode_rpc_context(BinCtx), + {WoodyCtx0, OtelCtx} = decode_rpc_context(BinCtx), ok = woody_rpc_helper:attach_otel_context(OtelCtx), + WoodyCtx = ensure_deadline_set(WoodyCtx0, Opts), ok = run_env_enter(Enter, WoodyCtx), - 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) + %% Enter succeeded: from here Leave must run exactly once. Errors + %% raised before this point fall through to the outer catch and are + %% returned as {error, _} without being masked by a Leave exception. + run_with_env_leave(Leave, 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 -> @@ -287,8 +304,19 @@ process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> #{stacktrace => Stacktrace, exception => Exception} ), {error, Exception} - after - Leave() + 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. %% Registry @@ -324,7 +352,9 @@ emit_events(Events) -> -spec timestamp() -> timestamp(). timestamp() -> - calendar:universal_time(). + Now = erlang:system_time(microsecond), + {Seconds, Micro} = prg_utils:split_timestamp(Now), + {calendar:system_time_to_universal_time(Seconds, second), Micro}. %% Internals — dispatch @@ -406,20 +436,22 @@ unmarshal_event(Handler, #{ metadata := Meta, payload := Payload }) -> - Format = maps:get(<<"format">>, Meta, maps:get(format, Meta, undefined)), + Format = unmarshal_event_format(Meta), Body = unmarshal_event_body(Handler, Format, Payload), {EventID, event_timestamp_to_datetime(TsSec), Body}; unmarshal_event(_Handler, #{event_id := EventID} = Ev) -> erlang:error({missing_event_payload, EventID, maps:keys(Ev)}). marshal_new_events(Handler, LastEventID, Bodies) -> + %% One microsecond timestamp for the whole batch (as the old emit_events did). + %% The PG backend stores timestamptz with microseconds and auto-detects units. Ts = erlang:system_time(microsecond), lists:zipwith( fun(EventID, Body) -> {Format, Bin} = marshal_event_body(Handler, Body), #{ event_id => EventID, - timestamp => Ts div 1000000, + timestamp => Ts, metadata => event_metadata(Format), payload => Bin } @@ -442,7 +474,7 @@ unmarshal_event_body(Handler, Format, Payload) -> true -> Handler:unmarshal_event_body(Format, Payload); false -> - binary_to_term(Payload, [safe]) + binary_to_term(Payload) end. marshal_aux_state(Handler, AuxSt) -> @@ -460,19 +492,36 @@ unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> true -> Handler:unmarshal_aux_state(Bin); false -> - binary_to_term(Bin, [safe]) + binary_to_term(Bin) end. +%% 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) -> - #{<<"format">> => 0}; + event_metadata(0); event_metadata(Format) when is_integer(Format) -> - #{<<"format">> => Format}. + #{<<"format_version">> => Format, <<"format">> => Format}. + +%% Read order: legacy HG <<"format_version">> → legacy FF <<"format">> → +%% atom format (defensive) → undefined. +unmarshal_event_format(Meta) -> + maps:get( + <<"format_version">>, + Meta, + maps:get(<<"format">>, Meta, maps:get(format, Meta, undefined)) + ). +%% 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; + {Dt, 0}; +%% Integer timestamp stored by progressor — split into seconds + microseconds. event_timestamp_to_datetime(Ts) when is_integer(Ts) -> - TsSeconds = prg_utils:to_seconds(Ts), - calendar:system_time_to_universal_time(TsSeconds, second). + {Seconds, Micro} = prg_utils:split_timestamp(Ts), + {calendar:system_time_to_universal_time(Seconds, second), Micro}. dispatch_apply_event(Handler, EventID, Ts, Body, Model) -> case erlang:function_exported(Handler, apply_event, 4) of @@ -507,18 +556,7 @@ request(NS, ID, Args, Range) -> }). encode_rpc_context() -> - WoodyContext = - try application:get_env(prg_machine, woody_context_loader, undefined) of - {M, F} when is_atom(M), is_atom(F) -> - M:F(); - Loader when is_function(Loader, 0) -> - Loader(); - undefined -> - woody_context:new() - catch - _:_ -> - woody_context:new() - end, + WoodyContext = operation_context:current_woody_context(), encode_term(woody_rpc_helper:encode_rpc_context(WoodyContext, otel_ctx:get_current())). decode_rpc_context(<<>>) -> @@ -557,11 +595,25 @@ run_env_enter(Enter, WoodyCtx) when is_function(Enter, 1) -> run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> Enter(). +run_with_env_leave(Leave, Fun) when is_function(Leave, 0), is_function(Fun, 0) -> + try + Fun() + after + Leave() + end. + encode_term(Term) -> term_to_binary(Term). decode_term(Term) when is_binary(Term) -> - binary_to_term(Term, [safe]); + 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. @@ -577,12 +629,6 @@ range_from_process(#{range := Range = #{}}) -> range_from_process(_) -> #{direction => forward}. --spec raise_exception({exception, atom(), term()} | {exception, atom(), term(), list()}) -> no_return(). -raise_exception({exception, Class, Reason, Stacktrace}) when is_list(Stacktrace) -> - erlang:raise(Class, Reason, Stacktrace); -raise_exception({exception, Class, Reason}) -> - erlang:raise(Class, Reason, []). - -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). @@ -826,4 +872,39 @@ process_crash_conforms_progressor_exception() -> 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 unmarshal_event_format_reads_legacy_keys_test() -> _. +unmarshal_event_format_reads_legacy_keys_test() -> + %% New (both keys). + ?assertEqual(2, unmarshal_event_format(#{<<"format_version">> => 2, <<"format">> => 2})), + %% Legacy HG metadata: only <<"format_version">>. + ?assertEqual(1, unmarshal_event_format(#{<<"format_version">> => 1})), + %% Legacy FF metadata: only <<"format">>. + ?assertEqual(1, unmarshal_event_format(#{<<"format">> => 1})), + %% Defensive atom key and absence. + ?assertEqual(3, unmarshal_event_format(#{format => 3})), + ?assertEqual(undefined, unmarshal_event_format(#{})). + +-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. From 93bf7e665b2c4fbaa653dc3156b87245bea10409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 13 Jun 2026 12:30:13 +0300 Subject: [PATCH 31/62] Enhance error handling across multiple modules by adding specific error cases for repair scenarios. Update ff_deposit_repair, ff_withdrawal_repair, and ff_withdrawal_session_repair to handle failure reasons more robustly. Introduce new tests for unmarshal logic in ff_withdrawal_codec and prg_action to ensure consistent behavior with deadline timers and legacy content. --- apps/ff_server/src/ff_deposit_repair.erl | 4 ++- apps/ff_server/src/ff_withdrawal_codec.erl | 27 ++++++++++++++ apps/ff_server/src/ff_withdrawal_repair.erl | 4 ++- .../src/ff_withdrawal_session_repair.erl | 4 ++- apps/ff_transfer/src/ff_deposit_machine.erl | 4 ++- .../ff_transfer/src/ff_withdrawal_machine.erl | 4 ++- .../src/ff_withdrawal_session_machine.erl | 4 ++- apps/hellgate/src/hg_invoice_template.erl | 28 +++++++++++---- apps/prg_machine/src/prg_action.erl | 16 +++++++++ docs/prg-machine.md | 35 +++++++++++-------- 10 files changed, 103 insertions(+), 27 deletions(-) diff --git a/apps/ff_server/src/ff_deposit_repair.erl b/apps/ff_server/src/ff_deposit_repair.erl index ab3a4346..f0ce150c 100644 --- a/apps/ff_server/src/ff_deposit_repair.erl +++ b/apps/ff_server/src/ff_deposit_repair.erl @@ -21,5 +21,7 @@ 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) end. diff --git a/apps/ff_server/src/ff_withdrawal_codec.erl b/apps/ff_server/src/ff_withdrawal_codec.erl index 54314d8a..f0eba474 100644 --- a/apps/ff_server/src/ff_withdrawal_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_codec.erl @@ -506,4 +506,31 @@ unmarshal_repair_scenario_test() -> 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_repair.erl b/apps/ff_server/src/ff_withdrawal_repair.erl index 897a54d5..025538bc 100644 --- a/apps/ff_server/src/ff_withdrawal_repair.erl +++ b/apps/ff_server/src/ff_withdrawal_repair.erl @@ -21,5 +21,7 @@ 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) end. diff --git a/apps/ff_server/src/ff_withdrawal_session_repair.erl b/apps/ff_server/src/ff_withdrawal_session_repair.erl index 6eb716bf..3f34ac41 100644 --- a/apps/ff_server/src/ff_withdrawal_session_repair.erl +++ b/apps/ff_server/src/ff_withdrawal_session_repair.erl @@ -21,5 +21,7 @@ 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) end. diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index 02bd761a..a2068237 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -119,7 +119,9 @@ repair(ID, Scenario) -> {error, working} -> {error, working}; {error, {repair, {failed, Reason}}} -> - {error, {failed, Reason}} + {error, {failed, Reason}}; + {error, _} = Error -> + Error end. %% Accessors diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index e8f2fcf1..0c2c3a83 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -137,7 +137,9 @@ repair(ID, Scenario) -> {error, working} -> {error, working}; {error, {repair, {failed, Reason}}} -> - {error, {failed, Reason}} + {error, {failed, Reason}}; + {error, _} = Error -> + Error end. -spec start_adjustment(id(), adjustment_params()) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index bb2276d2..180ad7e1 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -120,7 +120,9 @@ repair(ID, Scenario) -> {error, working} -> {error, working}; {error, {repair, {failed, Reason}}} -> - {error, {failed, Reason}} + {error, {failed, Reason}}; + {error, _} = Error -> + Error end. -spec process_callback(callback_params()) -> diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index 5ad78cd3..1b935442 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -5,6 +5,7 @@ -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(EVENT_FORMAT_VERSION, 1). @@ -364,11 +365,12 @@ marshal_aux_state(AuxSt) -> unmarshal_aux_state(<<>>) -> #{}; unmarshal_aux_state(Payload) when is_binary(Payload) -> - try - mg_msgpack_marshalling:unmarshal(binary_to_term(Payload, [safe])) - catch - _:_ -> - binary_to_term(Payload, [safe]) + %% Same compat as hg_invoice: legacy #mg_stateproc_Content{} or current msgpack blob. + case binary_to_term(Payload) of + #mg_stateproc_Content{data = Data} -> + mg_msgpack_marshalling:unmarshal(Data); + Msgp -> + mg_msgpack_marshalling:unmarshal(Msgp) end. msgpack_payload_to_binary(Msgp) -> @@ -384,7 +386,7 @@ decode_event_body(Payload) -> try_unmarshal_msgpack_payload(Payload) -> try - {ok, mg_msgpack_marshalling:unmarshal(binary_to_term(Payload, [safe]))} + {ok, mg_msgpack_marshalling:unmarshal(binary_to_term(Payload))} catch _:_ -> {error, invalid_msgpack_payload} @@ -417,3 +419,17 @@ 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_reads_legacy_mg_content_test() -> _. +aux_state_reads_legacy_mg_content_test() -> + AuxSt = #{<<"legacy">> => 1}, + Msgp = mg_msgpack_marshalling:marshal(AuxSt), + Legacy = term_to_binary(#mg_stateproc_Content{format_version = 1, data = Msgp}), + ?assertEqual(AuxSt, unmarshal_aux_state(Legacy)). + +-endif. diff --git a/apps/prg_machine/src/prg_action.erl b/apps/prg_machine/src/prg_action.erl index f34aa73a..c47e13a9 100644 --- a/apps/prg_machine/src/prg_action.erl +++ b/apps/prg_machine/src/prg_action.erl @@ -110,3 +110,19 @@ repair_remove_field(#repair_RemoveAction{}) -> 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/docs/prg-machine.md b/docs/prg-machine.md index f78f3e66..f9c1c702 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -2,7 +2,7 @@ Единый runtime поверх progressor для HG и FF. Контракт `action()` в progressor: `progressor/docs/step-effect-migration.md`. -*Обновлено: 2026-06-12. CI (compile, dialyzer, CT + compose) — green локально.* +*Обновлено: 2026-06-13. CI (compile, dialyzer, CT + compose) — green локально.* --- @@ -45,10 +45,13 @@ sequenceDiagram | `prg_machine` | behaviour, client API, `process/3`, `collapse` / `emit_events` | | `prg_machine_registry` | ETS `{Namespace, Handler}`; `{unknown_namespace, NS}` | | `prg_action` | `{timeout, Sec}` / `{deadline, Dt}` → wire `action()` | +| `ff_machine_lib` | общие FF-хелперы: repair/history/timestamp для `*_machine` | -**`process/3`:** `env_enter` → `unmarshal_machine` → `dispatch` → `marshal_process_result` → `env_leave`. Исключение в домене → `{error, {exception, Class, Reason, Stacktrace}}` + log. +**`process/3`:** `env_enter` → `unmarshal_machine` → `dispatch` → `marshal_process_result` → `env_leave` (Leave только после успешного Enter). Исключение в домене → `{error, {exception, Class, Reason}}` + log (stacktrace только в логах, не на проводе). -**Контекст RPC:** `woody_context_loader` в `hellgate` / `ff_server`; иначе `operation_context` по `context_binding` из `sys.config` (HG strict / FF lenient). +**Контекст RPC:** `operation_context:current_woody_context/0` (hg-binding → ff-binding → fresh ctx + warning). В `process/3` — `env_enter`/`env_leave` по `context_binding` из `sys.config` (HG strict / FF lenient). + +**События:** timestamp в microsecond (`timestamp_us()`); metadata пишет оба ключа `<<"format_version">>` и `<<"format">>`. FF payload — legacy `term_to_binary({bin, ThriftBin})`; HG payload — `term_to_binary(msgpack)`. --- @@ -72,7 +75,7 @@ sequenceDiagram ```erlang idle | suspend | timeout | remove -| {schedule, #{at := UnixSec, action := timeout | remove}} +| {schedule, #{at := timestamp_us(), action := timeout | remove}} ``` | Было (legacy) | Стало | @@ -82,7 +85,7 @@ idle | suspend | timeout | remove | `remove()` | `remove` | | `set_timeout(N, _)` / deadline | `prg_action:schedule_timer/1`, `schedule_deadline/1` | -`prg_action` — **не** адаптер MG/repair, только timer tuple → `{schedule, ...}`. +`prg_action:from_mg/1`, `from_repair/1` — MG/damsel repair на границе (HG/FF handler → wire). `prg_action:marshal_timer/1` принимает `{deadline, {calendar:datetime(), Micro}}` (machinery-формат из `ff_codec:unmarshal(timer, ...)`). FF доменный `continue` / `sleep` / `{setup_timer, T}` → wire через `map_action/1` в каждом модуле. @@ -107,7 +110,8 @@ processor => #{ client => prg_machine, options => #{ ns => , - context_binding => #{registry_key => ..., cleanup_mode => strict | lenient} + context_binding => #{registry_key => ..., cleanup_mode => strict | lenient}, + default_handling_timeout => 30000 %% optional, woody deadline default } } ``` @@ -130,11 +134,11 @@ processor => #{ |------------|------------------| | `<<"process not found">>` / `<<"process is init">>` | `{error, notfound}` | | `<<"process is error">>` | `{error, failed}` | -| `{exception, ...}` | **pass-through** `{error, {exception, ...}}` | +| `{exception, Class, Reason}` (3-tuple) | **pass-through** `{error, {exception, Class, Reason}}` | | прочие guard (`<<"process is waiting">>`, …) | **pass-through** `{error, Reason}` | | `<<"process already exists">>` (`start`) | `{error, exists}` | -`repair`: + `{error, working}` для `<<"process is running">>`; остальное → `{error, {repair, {failed, Reason}}}` с сохранением `Reason`. +`repair`: + `{error, working}` для `<<"process is running">>`; `{error, {repair, {failed, Reason}}}` → `{error, {failed, Reason}}` в `ff_*_machine` / `erlang:error(Reason)` в woody repair handler. **Антипаттерн:** catch-all `{error, _} -> {error, failed}` — ломает HG CT (waiting/running превращаются в `failed`). @@ -159,21 +163,19 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa ### До релиза - Progressor: CHANGELOG + tag `vX.Y.0` -- Hellgate: bump tag в `rebar.config` (сейчас branch `add_action_module`, ref `4f6d78a`) - -### Thrift → wire - -`prg_action:from_mg/1`, `from_repair/1` — MG/damsel repair на границе. `ff_codec` — `repairer_ComplexAction` → wire. Домены FF/HG — только `prg_action:t()`. +- Hellgate: bump tag в `rebar.config` (сейчас branch `add_action_module`) ### HG invoice — двойной collapse -Реплей: `prg_machine:collapse` (lenient). После call: `validate_changes` → `collapse_changes` strict **мимо** `collapse/2`, плюс повтор в `to_prg_result`. Цель — один фолд через `prg_machine` с параметром strict/lenient (`apply_new_events/3` + убрать двойной проход в `process_call`). Только HG invoice; FF на `apply_event/2`. +Реплей: `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 на `apply_event/2`. ### Прочее (низкий приоритет) +- Golden-fixtures со стейджа для legacy payload/aux_state (см. `docs/prg-machine-fix-plan.md` §1.6) - Registry без ETS `heir` — краткое окно при рестарте - Фиктивная обёртка `{ev, Ts, Body}` в event payload - Trace: сейчас HTTP JSON (`ff_machine_trace`); Thrift — `docs/trace-api-thrift.md` +- Единый конверт HG+FF (format 2) — этап 6 fix-plan --- @@ -195,6 +197,7 @@ 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 ``` --- @@ -205,7 +208,9 @@ rg "client => machinery_prg_backend" config/sys.config # 0 |------|-------| | `apps/prg_machine/src/prg_machine.erl` | behaviour, errors, marshal_intent | | `apps/prg_machine/src/prg_action.erl` | timer → wire | -| `apps/hellgate/src/hg_invoice.erl` | HG behaviour, repair, `action_to_prg` | +| `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.erl` | FF behaviour | | `apps/hellgate/src/hg_invoicing_machine_client.erl` | Thrift → prg_machine | | `apps/fistful/src/ff_repair.erl` | repair scenarios | From 529c70133c69cd7f6e0a323b2d2b7831bf397e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 13 Jun 2026 14:11:33 +0300 Subject: [PATCH 32/62] bumped progressor --- rebar.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.lock b/rebar.lock index e59db8b5..85b19b83 100644 --- a/rebar.lock +++ b/rebar.lock @@ -117,7 +117,7 @@ 0}, {<<"progressor">>, {git,"https://github.com/valitydev/progressor.git", - {ref,"4f6d78a01854b8e03612719be4418dd8402a090e"}}, + {ref,"8f18b309279f0401283e8a18a0166825a8717980"}}, 0}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.11.0">>},0}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.9">>},0}, From 4af42b5b53f12891248299806f8f74c3f793cfdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sat, 13 Jun 2026 21:37:40 +0300 Subject: [PATCH 33/62] Enhance unmarshal logic in ff_adapter_withdrawal_codec to support microseconds in deadline timers. Add unit tests for unmarshal_provider_timer to ensure correct handling of datetime and microsecond values. Update ff_withdrawal to handle undefined destination cases. Improve unmarshal_aux_state in hg_invoice to handle empty content gracefully. --- .../src/ff_adapter_withdrawal_codec.erl | 26 +++- apps/ff_transfer/src/ff_withdrawal.erl | 2 +- apps/hellgate/src/hg_invoice.erl | 8 + .../test/legacy_fixture_golden_test.erl | 140 ++++++++++++++++++ apps/prg_machine/test/legacy_fixture_lib.erl | 72 +++++++++ .../legacy/ff_deposit_v1/latest/aux_state.bin | Bin 0 -> 16 bytes .../latest/events/0001.event_summary.term | 3 + .../latest/events/0001.metadata.term | 1 + .../latest/events/0001.payload.bin | Bin 0 -> 310 bytes .../latest/events/0002.event_summary.term | 3 + .../latest/events/0002.metadata.term | 1 + .../latest/events/0002.payload.bin | Bin 0 -> 64 bytes .../ff_deposit_v1/latest/process_id.txt | 1 + .../ff_deposit_v1/latest/process_summary.term | 4 + .../ff_destination_v2/latest/aux_state.bin | Bin 0 -> 28 bytes .../latest/events/0001.event_summary.term | 3 + .../latest/events/0001.metadata.term | 1 + .../latest/events/0001.payload.bin | Bin 0 -> 443 bytes .../latest/events/0002.event_summary.term | 3 + .../latest/events/0002.metadata.term | 1 + .../latest/events/0002.payload.bin | Bin 0 -> 135 bytes .../ff_destination_v2/latest/process_id.txt | 1 + .../latest/process_summary.term | 4 + .../legacy/ff_source_v1/latest/aux_state.bin | Bin 0 -> 28 bytes .../latest/events/0001.event_summary.term | 3 + .../latest/events/0001.metadata.term | 1 + .../latest/events/0001.payload.bin | Bin 0 -> 302 bytes .../latest/events/0002.event_summary.term | 3 + .../latest/events/0002.metadata.term | 1 + .../latest/events/0002.payload.bin | Bin 0 -> 135 bytes .../legacy/ff_source_v1/latest/process_id.txt | 1 + .../ff_source_v1/latest/process_summary.term | 4 + .../latest/aux_state.bin | Bin 0 -> 16 bytes .../latest/events/0001.event_summary.term | 3 + .../latest/events/0001.metadata.term | 1 + .../latest/events/0001.payload.bin | Bin 0 -> 537 bytes .../latest/events/0002.event_summary.term | 3 + .../latest/events/0002.metadata.term | 1 + .../latest/events/0002.payload.bin | Bin 0 -> 60 bytes .../latest/events/0003.event_summary.term | 3 + .../latest/events/0003.metadata.term | 1 + .../latest/events/0003.payload.bin | Bin 0 -> 290 bytes .../latest/events/0004.event_summary.term | 3 + .../latest/events/0004.metadata.term | 1 + .../latest/events/0004.payload.bin | Bin 0 -> 60 bytes .../latest/process_id.txt | 1 + .../latest/process_summary.term | 6 + .../ff_withdrawal_v2/latest/aux_state.bin | Bin 0 -> 16 bytes .../latest/events/0001.event_summary.term | 3 + .../latest/events/0001.metadata.term | 1 + .../latest/events/0001.payload.bin | Bin 0 -> 328 bytes .../latest/events/0002.event_summary.term | 3 + .../latest/events/0002.metadata.term | 1 + .../latest/events/0002.payload.bin | Bin 0 -> 64 bytes .../latest/events/0003.event_summary.term | 3 + .../latest/events/0003.metadata.term | 1 + .../latest/events/0003.payload.bin | Bin 0 -> 217 bytes .../ff_withdrawal_v2/latest/process_id.txt | 1 + .../latest/process_summary.term | 4 + .../legacy/hg_invoice/latest/aux_state.bin | Bin 0 -> 48 bytes .../latest/call_args_thrift_get.bin | Bin 0 -> 86 bytes .../latest/call_args_thrift_get.expected.term | 8 + .../latest/events/0001.event_summary.term | 3 + .../latest/events/0001.metadata.term | 1 + .../hg_invoice/latest/events/0001.payload.bin | Bin 0 -> 307 bytes .../legacy/hg_invoice/latest/process_id.txt | 1 + .../hg_invoice/latest/process_summary.term | 3 + 67 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 apps/prg_machine/test/legacy_fixture_golden_test.erl create mode 100644 apps/prg_machine/test/legacy_fixture_lib.erl create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/aux_state.bin create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0001.event_summary.term create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0001.metadata.term create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0001.payload.bin create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0002.event_summary.term create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0002.metadata.term create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0002.payload.bin create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/process_id.txt create mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/process_summary.term create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/aux_state.bin create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0001.event_summary.term create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0001.metadata.term create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0001.payload.bin create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0002.event_summary.term create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0002.metadata.term create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0002.payload.bin create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/process_id.txt create mode 100644 test/fixtures/legacy/ff_destination_v2/latest/process_summary.term create mode 100644 test/fixtures/legacy/ff_source_v1/latest/aux_state.bin create mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0001.event_summary.term create mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0001.metadata.term create mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0001.payload.bin create mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0002.event_summary.term create mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0002.metadata.term create mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0002.payload.bin create mode 100644 test/fixtures/legacy/ff_source_v1/latest/process_id.txt create mode 100644 test/fixtures/legacy/ff_source_v1/latest/process_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/aux_state.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.event_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.metadata.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.payload.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.event_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.metadata.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.payload.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.event_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.metadata.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.payload.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.event_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.metadata.term create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.payload.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_id.txt create mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/aux_state.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.event_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.metadata.term create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.payload.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.event_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.metadata.term create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.payload.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.event_summary.term create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.metadata.term create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.payload.bin create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/process_id.txt create mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/process_summary.term create mode 100644 test/fixtures/legacy/hg_invoice/latest/aux_state.bin create mode 100644 test/fixtures/legacy/hg_invoice/latest/call_args_thrift_get.bin create mode 100644 test/fixtures/legacy/hg_invoice/latest/call_args_thrift_get.expected.term create mode 100644 test/fixtures/legacy/hg_invoice/latest/events/0001.event_summary.term create mode 100644 test/fixtures/legacy/hg_invoice/latest/events/0001.metadata.term create mode 100644 test/fixtures/legacy/hg_invoice/latest/events/0001.payload.bin create mode 100644 test/fixtures/legacy/hg_invoice/latest/process_id.txt create mode 100644 test/fixtures/legacy/hg_invoice/latest/process_summary.term diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl index 12271855..e6f25716 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl @@ -421,10 +421,30 @@ 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() expects {deadline, calendar:datetime() | binary()}. +%% 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}}) -> - {deadline, DateTime}; +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_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index ef5d6447..d327ac52 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -1148,7 +1148,7 @@ build_party_varset(#{body := Body, wallet_id := WalletID, party_id := PartyID} = BinData = maps:get(bin_data, Params, undefined), PaymentTool = case {Destination, Resource} of - {idle, _} -> + {undefined, _} -> undefined; {_, Resource} -> construct_payment_tool(Resource) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 14f95d7d..0ddb839c 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -1060,6 +1060,8 @@ unmarshal_aux_state(Payload) when is_binary(Payload) -> %% the current branch stores term_to_binary(mg_msgpack_marshalling:marshal(AuxSt)). %% Both unwrap to a msgpack Value that mg_msgpack_marshalling:unmarshal decodes. case binary_to_term(Payload) of + #mg_stateproc_Content{data = {bin, <<>>}} -> + #{}; #mg_stateproc_Content{data = Data} -> mg_msgpack_marshalling:unmarshal(Data); Msgp -> @@ -1183,4 +1185,10 @@ aux_state_reads_legacy_mg_content_test() -> Legacy = term_to_binary(#mg_stateproc_Content{format_version = 1, data = Msgp}), ?assertEqual(AuxSt, unmarshal_aux_state(Legacy)). +-spec aux_state_reads_legacy_empty_content_test() -> _. +aux_state_reads_legacy_empty_content_test() -> + %% CT-captured empty invoice aux: Content with {bin, <<>>} data. + Legacy = term_to_binary(#mg_stateproc_Content{format_version = undefined, data = {bin, <<>>}}), + ?assertEqual(#{}, unmarshal_aux_state(Legacy)). + -endif. diff --git a/apps/prg_machine/test/legacy_fixture_golden_test.erl b/apps/prg_machine/test/legacy_fixture_golden_test.erl new file mode 100644 index 00000000..1856029a --- /dev/null +++ b/apps/prg_machine/test/legacy_fixture_golden_test.erl @@ -0,0 +1,140 @@ +-module(legacy_fixture_golden_test). + +-compile(nowarn_unused_function). +-compile(nowarn_missing_spec). + +-export([test/0]). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("hellgate/include/domain.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). + +-spec test() -> _. +test() -> + eunit:test(?MODULE, [verbose]). + +legacy_ff_event_test_() -> + [ + {fixture_id(Dir, "_event"), + fun() -> legacy_ff_event_test(Domain, Dir) end} + || {Domain, Dir} <- legacy_fixture_lib:ff_fixtures() + ]. + +legacy_ff_metadata_test_() -> + [ + {fixture_id(Dir, "_metadata"), + fun() -> legacy_ff_metadata_test(Dir) end} + || {_Domain, Dir} <- legacy_fixture_lib:ff_fixtures() + ]. + +legacy_ff_aux_state_test_() -> + [ + {fixture_id(Dir, "_aux_state"), + fun() -> legacy_ff_aux_state_test(Dir) end} + || {_Domain, Dir} <- legacy_fixture_lib:ff_fixtures() + ]. + +legacy_ff_rollback_test_() -> + [ + {fixture_id(Dir, "_rollback"), + fun() -> legacy_ff_rollback_roundtrip_test(Domain, Dir) end} + || {Domain, Dir} <- legacy_fixture_lib:ff_fixtures() + ]. + +legacy_hg_invoice_event_test_() -> + {"hg_invoice_event", fun legacy_hg_invoice_event_test/0}. + +legacy_hg_invoice_metadata_test_() -> + {"hg_invoice_metadata", fun legacy_hg_invoice_metadata_test/0}. + +legacy_hg_invoice_aux_state_test_() -> + {"hg_invoice_aux_state", fun legacy_hg_invoice_aux_state_test/0}. + +legacy_hg_call_args_test_() -> + {"hg_call_args", fun legacy_hg_call_args_test/0}. + +legacy_hg_event_rollback_test_() -> + {"hg_invoice_event_rollback", fun legacy_hg_event_rollback_test/0}. + +%% + +legacy_ff_event_test(Domain, Dir) -> + Payload = legacy_fixture_lib:read_event_payload(Dir, 1), + Meta = legacy_fixture_lib:read_event_metadata(Dir, 1), + ?assertEqual(1, maps:get(<<"format">>, Meta)), + ?assertNot(maps:is_key(<<"format_version">>, Meta)), + ?assertMatch(<<131, _/binary>>, Payload), + {bin, _} = binary_to_term(Payload), + Timestamped = ff_machine_codec:unmarshal_event(Domain, 1, Payload), + Change = ff_machine_lib:event_body_from_timestamped(Timestamped), + ?assertMatch({created, _}, Change). + +legacy_ff_metadata_test(Dir) -> + Meta = legacy_fixture_lib:read_event_metadata(Dir, 1), + ?assertEqual(#{<<"format">> => 1}, Meta). + +legacy_ff_aux_state_test(Dir) -> + Aux = legacy_fixture_lib:read_aux_state(Dir), + ?assertMatch(#{ctx := _}, ff_machine_codec:unmarshal_aux_state(Aux)). + +legacy_ff_rollback_roundtrip_test(Domain, Dir) -> + LegacyPayload = legacy_fixture_lib:read_event_payload(Dir, 1), + Timestamped = ff_machine_codec:unmarshal_event(Domain, 1, LegacyPayload), + Encoded = ff_machine_codec:marshal_event(Domain, 1, Timestamped), + NewPayload = ff_machine_codec:payload_to_binary(Encoded), + ?assertEqual(LegacyPayload, NewPayload). + +legacy_hg_invoice_event_test() -> + Dir = legacy_fixture_lib:hg_invoice_dir(), + Payload = legacy_fixture_lib:read_event_payload(Dir, 1), + Meta = legacy_fixture_lib:read_event_metadata(Dir, 1), + InvoiceID = trim_binary(legacy_fixture_lib:read_bin(Dir, "process_id.txt")), + ?assertEqual(#{<<"format_version">> => 1}, Meta), + ?assertNot(maps:is_key(<<"format">>, Meta)), + Changes = hg_invoice:unmarshal_event_body(1, Payload), + ?assertMatch([{invoice_created, _}], Changes), + [{invoice_created, {payproc_InvoiceCreated, Invoice}}] = Changes, + ?assertEqual(InvoiceID, Invoice#domain_Invoice.id). + +legacy_hg_invoice_metadata_test() -> + Meta = legacy_fixture_lib:read_event_metadata(legacy_fixture_lib:hg_invoice_dir(), 1), + ?assertEqual(#{<<"format_version">> => 1}, Meta). + +legacy_hg_invoice_aux_state_test() -> + Dir = legacy_fixture_lib:hg_invoice_dir(), + Aux = legacy_fixture_lib:read_aux_state(Dir), + ?assertEqual(#{}, hg_invoice:unmarshal_aux_state(Aux)). + +legacy_hg_call_args_test() -> + Dir = legacy_fixture_lib:hg_invoice_dir(), + Bin = legacy_fixture_lib:read_bin(Dir, "call_args_thrift_get.bin"), + Expected = legacy_fixture_lib:read_term(Dir, "call_args_thrift_get.expected.term"), + Inner = decode_legacy_call_args(Bin), + ?assertEqual(maps:get(inner_call, Expected), Inner), + {thrift_call, invoicing, FunRef, EncodedArgs} = Inner, + {Module, _Service} = hg_proto:get_service(invoicing), + FullFunctionRef = {Module, FunRef}, + Args = hg_proto_utils:deserialize_function_args(FullFunctionRef, EncodedArgs), + ?assertEqual(maps:get(normalized_call, Expected), {FunRef, Args}). + +legacy_hg_event_rollback_test() -> + Dir = legacy_fixture_lib:hg_invoice_dir(), + LegacyPayload = legacy_fixture_lib:read_event_payload(Dir, 1), + Changes = hg_invoice:unmarshal_event_body(1, LegacyPayload), + {Format, NewPayload} = hg_invoice:marshal_event_body(Changes), + ?assertEqual(1, Format), + ?assertEqual(LegacyPayload, NewPayload). + +fixture_id(Dir, Suffix) -> + Dir ++ Suffix. + +decode_legacy_call_args(Bin) when is_binary(Bin) -> + case binary_to_term(Bin) of + {bin, Inner} when is_binary(Inner) -> + binary_to_term(Inner); + Term -> + Term + end. + +trim_binary(Bin) -> + re:replace(Bin, "[\\s]+$", "", [{return, binary}, global]). diff --git a/apps/prg_machine/test/legacy_fixture_lib.erl b/apps/prg_machine/test/legacy_fixture_lib.erl new file mode 100644 index 00000000..7779ba91 --- /dev/null +++ b/apps/prg_machine/test/legacy_fixture_lib.erl @@ -0,0 +1,72 @@ +-module(legacy_fixture_lib). + +-compile(nowarn_missing_spec). + +%%% Load CT-captured legacy progressor bytes from test/fixtures/legacy/. + +-export([root/0]). +-export([read_bin/2]). +-export([read_term/2]). +-export([read_event_payload/2]). +-export([read_event_metadata/2]). +-export([read_aux_state/1]). +-export([ff_fixtures/0]). +-export([hg_invoice_dir/0]). + +-type fixture_dir() :: string(). +-type domain() :: deposit | source | destination | withdrawal | withdrawal_session. + +-spec root() -> file:filename(). +root() -> + filename:absname( + filename:join([ + filename:dirname(?FILE), + "..", + "..", + "..", + "test", + "fixtures", + "legacy" + ]) + ). + +-spec ff_fixtures() -> [{domain(), fixture_dir()}]. +ff_fixtures() -> + [ + {deposit, "ff_deposit_v1"}, + {source, "ff_source_v1"}, + {destination, "ff_destination_v2"}, + {withdrawal, "ff_withdrawal_v2"}, + {withdrawal_session, "ff_withdrawal_session_v2"} + ]. + +-spec hg_invoice_dir() -> fixture_dir(). +hg_invoice_dir() -> + "hg_invoice". + +-spec read_bin(fixture_dir(), file:filename()) -> binary(). +read_bin(Dir, Name) -> + Path = filename:join([root(), Dir, "latest", Name]), + {ok, Bin} = file:read_file(Path), + Bin. + +-spec read_term(fixture_dir(), file:filename()) -> term(). +read_term(Dir, Name) -> + Path = filename:join([root(), Dir, "latest", Name]), + {ok, [Term]} = file:consult(Path), + Term. + +-spec read_event_payload(fixture_dir(), pos_integer()) -> binary(). +read_event_payload(Dir, Index) -> + read_bin(Dir, event_name(Index, "payload.bin")). + +-spec read_event_metadata(fixture_dir(), pos_integer()) -> map(). +read_event_metadata(Dir, Index) -> + read_term(Dir, event_name(Index, "metadata.term")). + +-spec read_aux_state(fixture_dir()) -> binary(). +read_aux_state(Dir) -> + read_bin(Dir, "aux_state.bin"). + +event_name(Index, Suffix) -> + filename:join(["events", lists:flatten(io_lib:format("~4..0w.", [Index])) ++ Suffix]). diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/aux_state.bin b/test/fixtures/legacy/ff_deposit_v1/latest/aux_state.bin new file mode 100644 index 0000000000000000000000000000000000000000..79b493f59a13fc97177d60f3622e93c4428410ae GIT binary patch literal 16 UcmZoJVPIfjEN4zGsQ|GU03R>|HUIzs literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.event_summary.term b/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.event_summary.term new file mode 100644 index 00000000..3ea7b5d6 --- /dev/null +++ b/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.event_summary.term @@ -0,0 +1,3 @@ +#{index => 1,timestamp => 1781366441967720,event_id => 1, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 310}. diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..adcef61f0f37979d913ec245a8cf00958ffac081 GIT binary patch literal 310 zcmaKnJ5R$f5P&ZUFV)#tkXTt5jQoh5q#L0{tq7`2iDL>FBF+iLMOg_P#I(W@FQ2hwm7eKP%0dty4m;oaX5jSG hc>WlA4_N|$;K*M3*Y?;DXMesfMBafD)3!lu`2zLiKC%D+ literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.event_summary.term b/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.event_summary.term new file mode 100644 index 00000000..e794c3a6 --- /dev/null +++ b/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.event_summary.term @@ -0,0 +1,3 @@ +#{index => 2,timestamp => 1781366441967740,event_id => 2, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 64}. diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..939787a8e925f6ea49246ba0b07c760bac4f8ec7 GIT binary patch literal 64 zcmZq9U@B)$%FN4UU|=xjW?%$T(nbbGX1WGux`xIfhGtd<23979dX{DuMuz55JPb@= L#K;3y%D?~scrpn| literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/process_id.txt b/test/fixtures/legacy/ff_deposit_v1/latest/process_id.txt new file mode 100644 index 00000000..e6bfcff0 --- /dev/null +++ b/test/fixtures/legacy/ff_deposit_v1/latest/process_id.txt @@ -0,0 +1 @@ +4yX9AB6Zel5jIe7L3ijkQj6y2SH \ No newline at end of file diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/process_summary.term b/test/fixtures/legacy/ff_deposit_v1/latest/process_summary.term new file mode 100644 index 00000000..941290d6 --- /dev/null +++ b/test/fixtures/legacy/ff_deposit_v1/latest/process_summary.term @@ -0,0 +1,4 @@ +#{extra => #{domain => deposit}, + status => <<"running">>,namespace => 'ff/deposit_v1', + process_id => <<"4yX9AB6Zel5jIe7L3ijkQj6y2SH">>,last_event_id => 2, + history_len => 2}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/aux_state.bin b/test/fixtures/legacy/ff_destination_v2/latest/aux_state.bin new file mode 100644 index 0000000000000000000000000000000000000000..7a2c44d49c2f1f64713e7eb51fefc49e6129fa41 GIT binary patch literal 28 ccmZoJVPIfjEN4zGsQ|Nbfm|lPU=W)D08vQ 1,timestamp => 1781366442089605,event_id => 1, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 443}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_destination_v2/latest/events/0001.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_destination_v2/latest/events/0001.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_destination_v2/latest/events/0001.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..dcbdcc1705f501f4254f68034bf42c6485c0501e GIT binary patch literal 443 zcmZut!A`#MB@svr(Q=Us*>+dJ(k&K*aQ1)vFaN?R z5Dv!KWGDOf<-IraH8*y}aG?``!Z(2e05lxxcsBKH)*3UfLurRQceG6z^(Ph>78Fjr zQ8xHke>&lCwI2yJ>&_?fAc%W?ne9w4Pzi}eY&Wbeg=qP%Et%`vT=0Q?CICw{UOnB zx&|h(kdv>_Ow>k5u@N<>SO@UvXs**lHDk3uC8!?WIT6!9^*lWZl*YK@G*+4%R#|&d Mky>m|7+L`Q0;TR%3IG5A literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0002.event_summary.term b/test/fixtures/legacy/ff_destination_v2/latest/events/0002.event_summary.term new file mode 100644 index 00000000..434c7567 --- /dev/null +++ b/test/fixtures/legacy/ff_destination_v2/latest/events/0002.event_summary.term @@ -0,0 +1,3 @@ +#{index => 2,timestamp => 1781366442089619,event_id => 2, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 135}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_destination_v2/latest/events/0002.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_destination_v2/latest/events/0002.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_destination_v2/latest/events/0002.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..9067a79e2678ac99561161ef784be38bf32021e6 GIT binary patch literal 135 zcmZq9U@B)$%FN4UU|^`?W?%$T(nbbGX1WGux`xIfhGtd<2396UdIpvTh6d(QJPb@= z#0XKUVrZF?m}qQns+(eHYOZUNXr8Q_WRhZ_o04c^U~Xn;YG#sZ#KFJ>G?kHuff=HS SIVjYLfs26!C #{domain => destination}, + status => <<"running">>,namespace => 'ff/destination_v2', + process_id => <<"OrIqsu2bJpyOaegAhZkISkDHdrw">>,last_event_id => 2, + history_len => 2}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/aux_state.bin b/test/fixtures/legacy/ff_source_v1/latest/aux_state.bin new file mode 100644 index 0000000000000000000000000000000000000000..7a2c44d49c2f1f64713e7eb51fefc49e6129fa41 GIT binary patch literal 28 ccmZoJVPIfjEN4zGsQ|Nbfm|lPU=W)D08vQ 1,timestamp => 1781366442029049,event_id => 1, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 302}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_source_v1/latest/events/0001.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_source_v1/latest/events/0001.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_source_v1/latest/events/0001.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..52d0bcc3231c534c52cbbcb93f01b47bb5c84999 GIT binary patch literal 302 zcmZvX%}c~E5XGn6)mGBI3tkkwMv`n2n!Sm-2o*(?ReGDG%b)(u2sr~ zLS#x3IB@8+!`2cw^yeS%tBw4+`xGDL<&YZlwtQ%^E7OVPia_Lu 2,timestamp => 1781366442029064,event_id => 2, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 135}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_source_v1/latest/events/0002.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_source_v1/latest/events/0002.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_source_v1/latest/events/0002.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..de0883ee47b144654243978a0ec2e80ae1ec55e4 GIT binary patch literal 135 zcmZq9U@B)$%FN4UU|^`?W?%$T(nbbGX1WGux`xIfhGtd<2396UdIm<8rpA_0JPb@= z#0XKUVwq%`VwPrPsGDkOYN>0InwY3-k&X!ok1 #{domain => source}, + status => <<"running">>,namespace => 'ff/source_v1', + process_id => <<"EtH1KKKcf4giWhZX4ogD6Vlo69S">>,last_event_id => 2, + history_len => 2}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/aux_state.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/aux_state.bin new file mode 100644 index 0000000000000000000000000000000000000000..79b493f59a13fc97177d60f3622e93c4428410ae GIT binary patch literal 16 UcmZoJVPIfjEN4zGsQ|GU03R>|HUIzs literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.event_summary.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.event_summary.term new file mode 100644 index 00000000..5f0e4fad --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.event_summary.term @@ -0,0 +1,3 @@ +#{index => 1,timestamp => 1781366446616059,event_id => 1, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 537}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..a1187903fd21c1591999f674c98e9a0bcbd1a08d GIT binary patch literal 537 zcma)2Jx{|h5PkVj32~4rB*eg4F(4W2#BtJYu(7s zMwfz@P8zOCQ_xLFZbY%P$_ZmlaUxa`q9c4YZkHeq;3Wh2M?YY+fK&Unu+^?!ZC`4)TQt?iB{+*ac`D-*3C{bE zhmZHO+ZIa@#NCM6q~03~rVn=@(TLEIuH;guWy*`Owp^)P@lutN+stILu$gmp%5Z!* vwe@oFsN? 2,timestamp => 1781366446622962,event_id => 2, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 60}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..eaa28fbe049525b1e2804f6f493f263b513c2646 GIT binary patch literal 60 zcmZq9U@B)$%FN4UU|`VaW?%$T(nbbGX1WGux`xIfhGtd<2396!dS*t(rUn*KJPb@= I1X9NU08!crGynhq literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.event_summary.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.event_summary.term new file mode 100644 index 00000000..94afa299 --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.event_summary.term @@ -0,0 +1,3 @@ +#{index => 3,timestamp => 1781366446622982,event_id => 3, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 290}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.metadata.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.payload.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..72ece02e0db07e1390f4bd916a82fb0e0e60313f GIT binary patch literal 290 zcmZvWI}XAy5Jbn$hk#8<#TguJoM2hL0HUEFsZvrw0WAmM;>5%vTQJo=X=ZlbH~Gr0 z_vZr;I|Tt?Z^=4C&NS`NxIh#-S3BDt$d4LQqtFnuwd{w>>3md>Dc2cljP(QgU}LnV uuf{Dhr6y18Pod?~$p6&+y0gWpkaH+FlpIzZDh_K78xC<_dRjqGFzW;DUn% 4,timestamp => 1781366446622988,event_id => 4, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 60}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.metadata.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.payload.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..8adee7732120ba88830804d5b306701a4618a2a1 GIT binary patch literal 60 zcmZq9U@B)$%FN4UU|`VaW?%$T(nbbGX1WGux`xIfhGtd<2396!dS*t(rUn*KJPb@c K49q-WbqoMe;t4eX literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_id.txt b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_id.txt new file mode 100644 index 00000000..c88b9875 --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_id.txt @@ -0,0 +1 @@ +5b8ab680-8e1b-48b7-8e01-b07fc4e0bcb7/1 \ No newline at end of file diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_summary.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_summary.term new file mode 100644 index 00000000..c8228c0e --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_summary.term @@ -0,0 +1,6 @@ +#{extra => + #{domain => withdrawal_session, + withdrawal_id => <<"5b8ab680-8e1b-48b7-8e01-b07fc4e0bcb7">>}, + status => <<"running">>,namespace => 'ff/withdrawal/session_v2', + process_id => <<"5b8ab680-8e1b-48b7-8e01-b07fc4e0bcb7/1">>, + last_event_id => 4,history_len => 4}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/aux_state.bin b/test/fixtures/legacy/ff_withdrawal_v2/latest/aux_state.bin new file mode 100644 index 0000000000000000000000000000000000000000..79b493f59a13fc97177d60f3622e93c4428410ae GIT binary patch literal 16 UcmZoJVPIfjEN4zGsQ|GU03R>|HUIzs literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.event_summary.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.event_summary.term new file mode 100644 index 00000000..ec60515c --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.event_summary.term @@ -0,0 +1,3 @@ +#{index => 1,timestamp => 1781366444329720,event_id => 1, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 328}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..4889ba7668c77b35a56ad1c442d7bb057d9793f9 GIT binary patch literal 328 zcma)%F;BxV5QQ%dh^p=nKw|A`baouaE>n 2,timestamp => 1781366444329752,event_id => 2, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 64}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..e32165b3c068bd565f8ebe89c14e61c68d4e7969 GIT binary patch literal 64 zcmZq9U@B)$%FN4UU|=xjW?%$T(nbbGX1WGux`xIfhGtd<2395}dd9{EhGr&FJPb@= L#K;3y%D?~scM%CZ literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.event_summary.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.event_summary.term new file mode 100644 index 00000000..86e17791 --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.event_summary.term @@ -0,0 +1,3 @@ +#{index => 3,timestamp => 1781366444329796,event_id => 3, + metadata_keys => [<<"format">>], + payload_first_byte => 131,payload_size => 217}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.metadata.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.metadata.term new file mode 100644 index 00000000..a034155d --- /dev/null +++ b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.metadata.term @@ -0,0 +1 @@ +#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.payload.bin b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..69f1e26accadd5a7f894856a0494adde65bd1ab8 GIT binary patch literal 217 zcmZq9U@B)$%FN4UU|=}I&AT)W%ybRRbPbI|49%*? z7}$6i7~u$_N5I6;)WFyh1PlxefS8+s8E6z6M3|d_1;`Q5LIWHOY(U{v+zjkMilsOy zwTOp-iy3G(8v_&jO9pNrWMB|VE{RW0EK149&q+xwiqA{TP32(_ #{domain => withdrawal}, + status => <<"running">>,namespace => 'ff/withdrawal_v2', + process_id => <<"4fb902cf-e5e0-400e-871d-6587492acd01">>,last_event_id => 3, + history_len => 3}. diff --git a/test/fixtures/legacy/hg_invoice/latest/aux_state.bin b/test/fixtures/legacy/hg_invoice/latest/aux_state.bin new file mode 100644 index 0000000000000000000000000000000000000000..8064dcf68016b3a3e4937d99eca792147bcb0df4 GIT binary patch literal 48 zcmZq9U@jNQO^+`wNi0b%D9TTcch1i%NzE%M=Pb=jNlnYlOHIjODrZj0%*$l}0sy#L B5O@Fp literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/hg_invoice/latest/call_args_thrift_get.bin b/test/fixtures/legacy/hg_invoice/latest/call_args_thrift_get.bin new file mode 100644 index 0000000000000000000000000000000000000000..deeab2cd9c308b7f1e6129669c51df8ad766ecec GIT binary patch literal 86 zcmZq9U@B)$%FN4UU|{fU&R{9$F3Bj$Oe={`PRz+E=giD2%g;>C%u5F< + {thrift_call,invoicing, + {'Invoicing','Get'}, + <<11,0,2,0,0,0,11,50,72,72,89,90,88,57,102,122,107,48,12,0, + 3,0,0>>}, + normalized_call => + {{'Invoicing','Get'}, + {<<"2HHYZX9fzk0">>,{payproc_EventRange,undefined,undefined}}}}. diff --git a/test/fixtures/legacy/hg_invoice/latest/events/0001.event_summary.term b/test/fixtures/legacy/hg_invoice/latest/events/0001.event_summary.term new file mode 100644 index 00000000..86d4834c --- /dev/null +++ b/test/fixtures/legacy/hg_invoice/latest/events/0001.event_summary.term @@ -0,0 +1,3 @@ +#{index => 1,timestamp => 1781366490607970,event_id => 1, + metadata_keys => [<<"format_version">>], + payload_first_byte => 131,payload_size => 307}. diff --git a/test/fixtures/legacy/hg_invoice/latest/events/0001.metadata.term b/test/fixtures/legacy/hg_invoice/latest/events/0001.metadata.term new file mode 100644 index 00000000..f057ad2c --- /dev/null +++ b/test/fixtures/legacy/hg_invoice/latest/events/0001.metadata.term @@ -0,0 +1 @@ +#{<<"format_version">> => 1}. diff --git a/test/fixtures/legacy/hg_invoice/latest/events/0001.payload.bin b/test/fixtures/legacy/hg_invoice/latest/events/0001.payload.bin new file mode 100644 index 0000000000000000000000000000000000000000..4b8f8d0240a9547f4f4429cd4e041f027c1d73c1 GIT binary patch literal 307 zcmZXQO>crg5QYb6)xy?#=)uIhH`)aW29L%jo{dK9g>1KrZghdbHd?R#mHto%xR`XZ zhxvM+*_rF!JXv2dRRG5D1q=e{koq}bVBq-k`R8^MC6@!W!Ng2u5drJIzMic<0tEyf z{$*2e;1MMQpZcLoL)RNEy>LRk$%wuM<0$fiErAx!1k|@1efKDKjQ_fcJnNrAAfkha z*L%dcL7?`@u78RRe?0Ku+Q@RC^^$IOW!lDg?39_2&SBRr$L!15i3#f ZJxg_5=7P&a^7xx+hLGn@J)yb>zzu67HCO-u literal 0 HcmV?d00001 diff --git a/test/fixtures/legacy/hg_invoice/latest/process_id.txt b/test/fixtures/legacy/hg_invoice/latest/process_id.txt new file mode 100644 index 00000000..5a4359ba --- /dev/null +++ b/test/fixtures/legacy/hg_invoice/latest/process_id.txt @@ -0,0 +1 @@ +2HHYZX9fzk0 \ No newline at end of file diff --git a/test/fixtures/legacy/hg_invoice/latest/process_summary.term b/test/fixtures/legacy/hg_invoice/latest/process_summary.term new file mode 100644 index 00000000..9bcfdcba --- /dev/null +++ b/test/fixtures/legacy/hg_invoice/latest/process_summary.term @@ -0,0 +1,3 @@ +#{extra => #{domain => hg_invoice}, + status => <<"running">>,namespace => invoice, + process_id => <<"2HHYZX9fzk0">>,last_event_id => 1,history_len => 1}. From 31f741940701aa1d7bf508fbc2a2977a373fbcdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Sun, 14 Jun 2026 12:00:08 +0300 Subject: [PATCH 34/62] Enhance error handling in ff_deposit and ff_withdrawal modules by adding specific cases for invalid terms in wallet limit checks. Update validate_wallet_limits function to return structured error messages for better clarity. Improve process_limit_check to handle terms violations consistently. --- apps/ff_transfer/src/ff_deposit.erl | 11 ++++++---- apps/ff_transfer/src/ff_withdrawal.erl | 11 ++++++---- apps/hellgate/src/hg_invoice_template.erl | 2 ++ apps/prg_machine/src/prg_machine.erl | 20 +++++++++++++++++-- .../test/legacy_fixture_golden_test.erl | 12 ++++------- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index 1fe1b256..a2c928be 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -486,7 +486,9 @@ 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, {timeout, Events}. @@ -630,15 +632,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_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index d327ac52..bbd73f35 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -927,7 +927,9 @@ 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, {timeout, Events}. @@ -1474,15 +1476,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 diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index 1b935442..5488f474 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -367,6 +367,8 @@ unmarshal_aux_state(<<>>) -> unmarshal_aux_state(Payload) when is_binary(Payload) -> %% Same compat as hg_invoice: legacy #mg_stateproc_Content{} or current msgpack blob. case binary_to_term(Payload) of + #mg_stateproc_Content{data = {bin, <<>>}} -> + #{}; #mg_stateproc_Content{data = Data} -> mg_msgpack_marshalling:unmarshal(Data); Msgp -> diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 8005fb57..851f2da2 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -596,10 +596,26 @@ run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> Enter(). run_with_env_leave(Leave, Fun) when is_function(Leave, 0), is_function(Fun, 0) -> + try Fun() of + Result -> + safe_env_leave(Leave), + Result + catch + Class:Reason:Stacktrace -> + safe_env_leave(Leave), + erlang:raise(Class, Reason, Stacktrace) + end. + +safe_env_leave(Leave) -> try - Fun() - after Leave() + catch + Class:Reason:Stacktrace -> + logger:error( + "prg_machine env_leave failed: ~p:~p", + [Class, Reason], + #{stacktrace => Stacktrace} + ) end. encode_term(Term) -> diff --git a/apps/prg_machine/test/legacy_fixture_golden_test.erl b/apps/prg_machine/test/legacy_fixture_golden_test.erl index 1856029a..21a5e7c6 100644 --- a/apps/prg_machine/test/legacy_fixture_golden_test.erl +++ b/apps/prg_machine/test/legacy_fixture_golden_test.erl @@ -15,29 +15,25 @@ test() -> legacy_ff_event_test_() -> [ - {fixture_id(Dir, "_event"), - fun() -> legacy_ff_event_test(Domain, Dir) end} + {fixture_id(Dir, "_event"), fun() -> legacy_ff_event_test(Domain, Dir) end} || {Domain, Dir} <- legacy_fixture_lib:ff_fixtures() ]. legacy_ff_metadata_test_() -> [ - {fixture_id(Dir, "_metadata"), - fun() -> legacy_ff_metadata_test(Dir) end} + {fixture_id(Dir, "_metadata"), fun() -> legacy_ff_metadata_test(Dir) end} || {_Domain, Dir} <- legacy_fixture_lib:ff_fixtures() ]. legacy_ff_aux_state_test_() -> [ - {fixture_id(Dir, "_aux_state"), - fun() -> legacy_ff_aux_state_test(Dir) end} + {fixture_id(Dir, "_aux_state"), fun() -> legacy_ff_aux_state_test(Dir) end} || {_Domain, Dir} <- legacy_fixture_lib:ff_fixtures() ]. legacy_ff_rollback_test_() -> [ - {fixture_id(Dir, "_rollback"), - fun() -> legacy_ff_rollback_roundtrip_test(Domain, Dir) end} + {fixture_id(Dir, "_rollback"), fun() -> legacy_ff_rollback_roundtrip_test(Domain, Dir) end} || {Domain, Dir} <- legacy_fixture_lib:ff_fixtures() ]. From 369e6f212106e139da806049a28bf6d6e51979cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 10:45:24 +0300 Subject: [PATCH 35/62] Add op_context application with initial configuration and implementation - Introduced `rebar.config` for build configuration, including dependencies on gproc, woody, and party_client. - Created `op_context.app.src` to define the application structure and metadata. - Implemented core functionality in `op_context.erl`, including context management and environment handling for woody and party client contexts. - Added internal types and API specifications for context operations, ensuring robust context creation, loading, and cleanup mechanisms. --- apps/{operation_context => op_context}/rebar.config | 0 .../src/op_context.app.src} | 0 .../src/operation_context.erl => op_context/src/op_context.erl} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename apps/{operation_context => op_context}/rebar.config (100%) rename apps/{operation_context/src/operation_context.app.src => op_context/src/op_context.app.src} (100%) rename apps/{operation_context/src/operation_context.erl => op_context/src/op_context.erl} (100%) diff --git a/apps/operation_context/rebar.config b/apps/op_context/rebar.config similarity index 100% rename from apps/operation_context/rebar.config rename to apps/op_context/rebar.config diff --git a/apps/operation_context/src/operation_context.app.src b/apps/op_context/src/op_context.app.src similarity index 100% rename from apps/operation_context/src/operation_context.app.src rename to apps/op_context/src/op_context.app.src diff --git a/apps/operation_context/src/operation_context.erl b/apps/op_context/src/op_context.erl similarity index 100% rename from apps/operation_context/src/operation_context.erl rename to apps/op_context/src/op_context.erl From 38961c08ebfca459ad712e6b16c66f13b13adadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 10:45:43 +0300 Subject: [PATCH 36/62] Refactor operation_context references to op_context across multiple modules - Updated application dependencies to replace `operation_context` with `op_context` in various modules, enhancing consistency and maintainability. - Refactored context management functions to utilize the new `op_context` API for saving, loading, and cleaning up contexts in both hellgate and fistful scopes. - Adjusted related bindings and context retrieval methods to align with the new structure, ensuring seamless integration with existing functionality. --- apps/ff_core/src/ff_core.app.src | 4 +- apps/ff_cth/src/ct_helper.erl | 7 +- apps/ff_cth/src/ct_payment_system.erl | 10 +- apps/ff_server/src/ff_woody_wrapper.erl | 6 +- apps/ff_transfer/src/ff_transfer.app.src | 2 +- apps/fistful/src/ff_machine_tag.erl | 4 +- apps/fistful/src/ff_party.erl | 6 +- apps/fistful/src/ff_woody_client.erl | 2 +- apps/fistful/src/fistful.app.src | 2 +- apps/hellgate/src/hellgate.app.src | 2 +- apps/hellgate/src/hg_customer_client.erl | 2 +- apps/hellgate/src/hg_invoice_payment.erl | 6 +- apps/hellgate/src/hg_machine_tag.erl | 4 +- apps/hellgate/src/hg_party.erl | 6 +- apps/hellgate/src/hg_payment_institution.erl | 6 +- apps/hellgate/test/hg_ct_fixture.erl | 12 +- apps/hellgate/test/hg_ct_helper.erl | 4 +- .../test/hg_direct_recurrent_tests_SUITE.erl | 4 +- .../test/hg_invoice_lite_tests_SUITE.erl | 6 +- .../test/hg_invoice_template_tests_SUITE.erl | 4 +- apps/hellgate/test/hg_invoice_tests_SUITE.erl | 12 +- .../test/hg_route_rules_tests_SUITE.erl | 6 +- .../hg_proto/src/hg_woody_service_wrapper.erl | 10 +- apps/hg_proto/src/hg_woody_wrapper.erl | 2 +- apps/op_context/src/op_context.app.src | 2 +- apps/op_context/src/op_context.erl | 107 +++++++----------- apps/prg_machine/src/prg_machine.app.src | 2 +- apps/prg_machine/src/prg_machine.erl | 12 +- apps/routing/src/hg_route_collector.erl | 26 ++--- docs/prg-machine-fix-plan.md | 4 +- docs/prg-machine.md | 2 +- 31 files changed, 129 insertions(+), 155 deletions(-) diff --git a/apps/ff_core/src/ff_core.app.src b/apps/ff_core/src/ff_core.app.src index 4c3921d4..6bf479a1 100644 --- a/apps/ff_core/src/ff_core.app.src +++ b/apps/ff_core/src/ff_core.app.src @@ -5,9 +5,7 @@ {applications, [ kernel, stdlib, - genlib, - mg_proto, - thrift + genlib ]}, {env, []}, {modules, []}, diff --git a/apps/ff_cth/src/ct_helper.erl b/apps/ff_cth/src/ct_helper.erl index 81fd0b2c..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 = operation_context:save_fistful( - operation_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 = operation_context:cleanup_fistful(). + ok = op_context:cleanup(fistful). %% diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index d5cd0fc3..2001725a 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -261,7 +261,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/source_v1', - context_binding => operation_context:fistful_binding() + context_binding => op_context:binding(fistful) } } }, @@ -270,7 +270,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/destination_v2', - context_binding => operation_context:fistful_binding() + context_binding => op_context:binding(fistful) } } }, @@ -279,7 +279,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/deposit_v1', - context_binding => operation_context:fistful_binding() + context_binding => op_context:binding(fistful) } } }, @@ -288,7 +288,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/withdrawal_v2', - context_binding => operation_context:fistful_binding() + context_binding => op_context:binding(fistful) } } }, @@ -297,7 +297,7 @@ progressor_namespaces() -> client => prg_machine, options => #{ ns => 'ff/withdrawal/session_v2', - context_binding => operation_context:fistful_binding() + context_binding => op_context:binding(fistful) } } } diff --git a/apps/ff_server/src/ff_woody_wrapper.erl b/apps/ff_server/src/ff_woody_wrapper.erl index f0ca6c1d..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 = operation_context:save_fistful(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 - operation_context:cleanup_fistful() + 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) }, - operation_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_transfer/src/ff_transfer.app.src b/apps/ff_transfer/src/ff_transfer.app.src index 6ab8798c..90749508 100644 --- a/apps/ff_transfer/src/ff_transfer.app.src +++ b/apps/ff_transfer/src/ff_transfer.app.src @@ -8,7 +8,7 @@ genlib, ff_core, progressor, - operation_context, + op_context, prg_machine, damsel, fistful, diff --git a/apps/fistful/src/ff_machine_tag.erl b/apps/fistful/src/ff_machine_tag.erl index f898eca1..f575599a 100644 --- a/apps/fistful/src/ff_machine_tag.erl +++ b/apps/fistful/src/ff_machine_tag.erl @@ -11,7 +11,7 @@ -spec get_binding(ns(), tag()) -> {ok, entity_id()} | {error, not_found}. get_binding(NS, Tag) -> - WoodyContext = operation_context:get_woody_context(operation_context:load_fistful()), + 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 = operation_context:get_woody_context(operation_context:load_fistful()), + 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 a19b8ff0..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 = operation_context:load_fistful(), - Client = operation_context:get_party_client(Context), - ClientContext = operation_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_woody_client.erl b/apps/fistful/src/ff_woody_client.erl index 47332234..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, operation_context:get_woody_context(operation_context:load_fistful())). + 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 5d381d92..561fc99c 100644 --- a/apps/fistful/src/fistful.app.src +++ b/apps/fistful/src/fistful.app.src @@ -11,7 +11,7 @@ ff_core, snowflake, progressor, - operation_context, + op_context, prg_machine, woody, uuid, diff --git a/apps/hellgate/src/hellgate.app.src b/apps/hellgate/src/hellgate.app.src index cee3bbc3..93881fa3 100644 --- a/apps/hellgate/src/hellgate.app.src +++ b/apps/hellgate/src/hellgate.app.src @@ -10,7 +10,7 @@ fault_detector_proto, herd, progressor, - operation_context, + op_context, prg_machine, hg_proto, routing, diff --git a/apps/hellgate/src/hg_customer_client.erl b/apps/hellgate/src/hg_customer_client.erl index 4594e490..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 - operation_context:get_woody_context(operation_context:load_hellgate()) + 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_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 5184a777..e940eaa0 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -4098,9 +4098,9 @@ get_message(invoice_payment_status_changed) -> "Invoice payment status is changed". get_party_client() -> - HgContext = operation_context:load_hellgate(), - Client = operation_context:get_party_client(HgContext), - Context = operation_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_machine_tag.erl b/apps/hellgate/src/hg_machine_tag.erl index 289cef93..7fa90c98 100644 --- a/apps/hellgate/src/hg_machine_tag.erl +++ b/apps/hellgate/src/hg_machine_tag.erl @@ -13,7 +13,7 @@ -spec get_binding(ns(), tag()) -> {ok, entity_id(), machine_id()} | {error, notfound}. get_binding(NS, Tag) -> - WoodyContext = operation_context:get_woody_context(operation_context:load_hellgate()), + 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 = operation_context:get_woody_context(operation_context:load_hellgate()), + 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; diff --git a/apps/hellgate/src/hg_party.erl b/apps/hellgate/src/hg_party.erl index 1830ade2..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 = operation_context:load_hellgate(), - Client = operation_context:get_party_client(HgContext), - Context = operation_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 e43f83f2..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 = operation_context:load_hellgate(), - Client = operation_context:get_party_client(HgContext), - Context = operation_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/test/hg_ct_fixture.erl b/apps/hellgate/test/hg_ct_fixture.erl index 3be78a65..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 = operation_context:save_hellgate(operation_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 ), - _ = operation_context:cleanup_hellgate(), + _ = 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 = operation_context:save_hellgate(operation_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), - operation_context:cleanup_hellgate(), + 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 = operation_context:save_hellgate(operation_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), - operation_context:cleanup_hellgate(), + 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 5933bcd0..61d30cf4 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -324,7 +324,7 @@ start_app(progressor = AppName) -> client => prg_machine, options => #{ ns => invoice, - context_binding => operation_context:hellgate_binding() + context_binding => op_context:binding(hellgate) } }, worker_pool_size => 150 @@ -334,7 +334,7 @@ start_app(progressor = AppName) -> client => prg_machine, options => #{ ns => invoice_template, - context_binding => operation_context:hellgate_binding() + context_binding => op_context:binding(hellgate) } } } diff --git a/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl b/apps/hellgate/test/hg_direct_recurrent_tests_SUITE.erl index 6361178a..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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), + ok = op_context:cleanup(hellgate), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), C1 = [ diff --git a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl index ba7612c5..03cba0ed 100644 --- a/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_lite_tests_SUITE.erl @@ -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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), + ok = op_context:cleanup(hellgate), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), ok = hg_invoice_helper:start_kv_store(SupPid), @@ -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 = operation_context:save_hellgate(operation_context:create()), + ok = op_context:save(op_context:key(hellgate), op_context:create()), [ {client, Client} | C diff --git a/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_template_tests_SUITE.erl index b1de5225..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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), + ok = op_context:cleanup(hellgate), [ {party_config_ref, PartyConfigRef}, {party_client, Client}, diff --git a/apps/hellgate/test/hg_invoice_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_tests_SUITE.erl index 5bc70aca..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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), + ok = op_context:cleanup(hellgate), {ok, SupPid} = supervisor:start_link(?MODULE, []), _ = unlink(SupPid), @@ -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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), + ok = op_context:cleanup(hellgate), _ = case cfg(original_domain_revision, C) of Revision when is_integer(Revision) -> @@ -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 = operation_context:save_hellgate(operation_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 = operation_context:cleanup_hellgate(), + 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 00f90da3..db7b7a94 100644 --- a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl +++ b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl @@ -132,13 +132,13 @@ end_per_group(_GroupName, _C) -> -spec init_per_testcase(test_case_name(), config()) -> config(). init_per_testcase(_, C) -> - Ctx = operation_context:set_party_client(cfg(party_client, C), operation_context:create()), - ok = operation_context:save_hellgate(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 = operation_context:cleanup_hellgate(), + ok = op_context:cleanup(hellgate), ok. cfg(Key, C) -> diff --git a/apps/hg_proto/src/hg_woody_service_wrapper.erl b/apps/hg_proto/src/hg_woody_service_wrapper.erl index d82d3896..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 = operation_context:save_hellgate(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 - operation_context:cleanup_hellgate() + 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()) -> operation_context:context(). +-spec create_context(woody_context:ctx(), handler_opts()) -> op_context:context(). create_context(WoodyContext, Opts) -> ContextOptions = #{ woody_context => WoodyContext }, - Context = operation_context:create(ContextOptions), + Context = op_context:create(ContextOptions), configure_party_client(Context, Opts). configure_party_client(Context0, #{party_client := PartyClient}) -> - operation_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 c1008812..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 = operation_context:get_woody_context(operation_context:load_hellgate()), + 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/op_context/src/op_context.app.src b/apps/op_context/src/op_context.app.src index 12b34b67..9104cc91 100644 --- a/apps/op_context/src/op_context.app.src +++ b/apps/op_context/src/op_context.app.src @@ -1,4 +1,4 @@ -{application, operation_context, [ +{application, op_context, [ {description, "Process-scoped operation context (woody + party_client) for HG and FF"}, {vsn, "0.1.0"}, {registered, []}, diff --git a/apps/op_context/src/op_context.erl b/apps/op_context/src/op_context.erl index 4f98be12..99b1cff4 100644 --- a/apps/op_context/src/op_context.erl +++ b/apps/op_context/src/op_context.erl @@ -1,18 +1,14 @@ --module(operation_context). +-module(op_context). -export([create/0]). -export([create/1]). -export([save/2]). -export([load/1]). +-export([cleanup/1]). -export([cleanup/2]). --export([save_hellgate/1]). --export([load_hellgate/0]). --export([cleanup_hellgate/0]). - --export([save_fistful/1]). --export([load_fistful/0]). --export([cleanup_fistful/0]). +-export([key/1]). +-export([binding/1]). -export([get_woody_context/1]). -export([set_woody_context/2]). @@ -21,12 +17,11 @@ -export([get_party_client/1]). -export([set_party_client/2]). --export([hellgate_binding/0]). --export([fistful_binding/0]). -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. @@ -48,6 +43,7 @@ }. -export_type([ + scope/0, registry_key/0, cleanup_mode/0, binding/0, @@ -61,9 +57,6 @@ -type party_client() :: party_client:client(). -type party_client_context() :: party_client:context(). --define(HG_REGISTRY_KEY, {p, l, stored_hg_context}). --define(FF_REGISTRY_KEY, {p, l, {ff_context, stored_context}}). - %% API -spec create() -> context(). @@ -90,6 +83,10 @@ save(RegistryKey, Context) -> 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), @@ -102,42 +99,17 @@ cleanup(RegistryKey, lenient) -> end, ok. --spec save_hellgate(context()) -> ok. -save_hellgate(Context) -> - save(?HG_REGISTRY_KEY, Context). - --spec load_hellgate() -> context() | no_return(). -load_hellgate() -> - load(?HG_REGISTRY_KEY). - --spec cleanup_hellgate() -> ok. -cleanup_hellgate() -> - cleanup(?HG_REGISTRY_KEY, strict). - --spec save_fistful(context()) -> ok. -save_fistful(Context) -> - save(?FF_REGISTRY_KEY, Context). - --spec load_fistful() -> context() | no_return(). -load_fistful() -> - load(?FF_REGISTRY_KEY). +-spec key(scope()) -> registry_key(). +key(hellgate) -> + {p, l, stored_hg_context}; +key(fistful) -> + {p, l, {ff_context, stored_context}}. --spec cleanup_fistful() -> ok. -cleanup_fistful() -> - cleanup(?FF_REGISTRY_KEY, lenient). - --spec hellgate_binding() -> binding(). -hellgate_binding() -> - #{ - registry_key => ?HG_REGISTRY_KEY, - cleanup_mode => strict - }. - --spec fistful_binding() -> binding(). -fistful_binding() -> +-spec binding(scope()) -> binding(). +binding(Scope) -> #{ - registry_key => ?FF_REGISTRY_KEY, - cleanup_mode => lenient + registry_key => key(Scope), + cleanup_mode => cleanup_mode(Scope) }. -spec env_enter(woody_context(), binding()) -> ok. @@ -160,12 +132,12 @@ env_leave(#{registry_key := RegistryKey, cleanup_mode := CleanupMode}) -> %% global prg_machine woody_context_loader app-env hook. -spec current_woody_context() -> woody_context(). current_woody_context() -> - case try_load_woody_context([?HG_REGISTRY_KEY, ?FF_REGISTRY_KEY]) of + case try_load_woody_context([key(hellgate), key(fistful)]) of {ok, WoodyContext} -> WoodyContext; error -> _ = logger:warning( - "operation_context: no woody context bound to the current process, using a fresh one" + "op_context: no woody context bound to the current process, using a fresh one" ), woody_context:new() end. @@ -208,6 +180,12 @@ set_party_client_context(PartyContext, Context) -> %% 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; @@ -223,9 +201,6 @@ ensure_party_context_exists(#{woody_context := WoodyContext} = Options) -> -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). --define(HG_TEST_KEY, {p, l, stored_hg_context}). --define(FF_TEST_KEY, {p, l, {ff_context, stored_context}}). - -spec test() -> _. -spec colocated_keys_isolated_test() -> _. @@ -237,23 +212,23 @@ colocated_keys_isolated_test() -> try CtxHg = create(#{woody_context => WoodyHg}), CtxFf = create(#{woody_context => WoodyFf}), - ok = save(?HG_TEST_KEY, CtxHg), - ok = save(?FF_TEST_KEY, CtxFf), - CtxHgLoaded = load(?HG_TEST_KEY), - CtxFfLoaded = load(?FF_TEST_KEY), + 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(?HG_TEST_KEY, strict), - CtxFfAfterHgCleanup = load(?FF_TEST_KEY), + ok = cleanup(hellgate), + CtxFfAfterHgCleanup = load(key(fistful)), ?assertEqual(WoodyFf, get_woody_context(CtxFfAfterHgCleanup)), - ok = cleanup(?FF_TEST_KEY, lenient) + ok = cleanup(fistful) after - cleanup(?HG_TEST_KEY, lenient), - cleanup(?FF_TEST_KEY, lenient) + cleanup(key(hellgate), lenient), + cleanup(fistful) end. -spec scoped_helpers_test() -> _. @@ -262,12 +237,12 @@ scoped_helpers_test() -> {ok, _} = application:ensure_all_started(woody), WoodyCtx = woody_context:new(), try - ok = save_fistful(create(#{woody_context => WoodyCtx})), - ?assertEqual(WoodyCtx, get_woody_context(load_fistful())), - ok = cleanup_fistful(), - ok = cleanup_fistful() + 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() + cleanup(fistful) end. -endif. diff --git a/apps/prg_machine/src/prg_machine.app.src b/apps/prg_machine/src/prg_machine.app.src index 69a4e9e4..37cd3092 100644 --- a/apps/prg_machine/src/prg_machine.app.src +++ b/apps/prg_machine/src/prg_machine.app.src @@ -9,7 +9,7 @@ woody, scoper, progressor, - operation_context + op_context ]}, {env, []}, {modules, []}, diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 851f2da2..93d3bd7f 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -44,7 +44,7 @@ -type env_enter_fun() :: fun(() -> ok) | fun((woody_context:ctx()) -> ok). --type context_binding() :: operation_context:binding(). +-type context_binding() :: op_context:binding(). -type process_options() :: #{ ns := namespace(), @@ -556,7 +556,7 @@ request(NS, ID, Args, Range) -> }). encode_rpc_context() -> - WoodyContext = operation_context:current_woody_context(), + WoodyContext = op_context:current_woody_context(), encode_term(woody_rpc_helper:encode_rpc_context(WoodyContext, otel_ctx:get_current())). decode_rpc_context(<<>>) -> @@ -571,7 +571,7 @@ resolve_env_enter(Opts) -> false -> case maps:get(context_binding, Opts, undefined) of Binding when is_map(Binding) -> - fun(WoodyCtx) -> operation_context:env_enter(WoodyCtx, Binding) end; + fun(WoodyCtx) -> op_context:env_enter(WoodyCtx, Binding) end; _ -> fun(_) -> ok end end @@ -584,7 +584,7 @@ resolve_env_leave(Opts) -> false -> case maps:get(context_binding, Opts, undefined) of Binding when is_map(Binding) -> - fun() -> operation_context:env_leave(Binding) end; + fun() -> op_context:env_leave(Binding) end; _ -> fun() -> ok end end @@ -729,7 +729,7 @@ setup_env_hook_test() -> {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(operation_context), + {ok, _} = application:ensure_all_started(op_context), _ = ensure_env_hook_dispatch_table(), true = ets:insert(?TABLE, {?TEST_NS, prg_machine_env_mock_handler}), ok = prg_machine_env_mock_context:reset(), @@ -738,7 +738,7 @@ setup_env_hook_test() -> -spec cleanup_env_hook_test(_) -> ok. cleanup_env_hook_test(_) -> _ = ets:delete(?TABLE, ?TEST_NS), - operation_context:cleanup(?TEST_REGISTRY_KEY, lenient), + op_context:cleanup(?TEST_REGISTRY_KEY, lenient), ok. -spec ensure_woody_available() -> ok. diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index 34a5b41c..5717ec5b 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 = operation_context:load_hellgate(), + HgContext = op_context:load(op_context:key(hellgate)), try genlib_pmap:map( fun(Route) -> - ok = operation_context:save_hellgate(HgContext), + ok = op_context:save(op_context:key(hellgate), HgContext), hg_inspector:fill_blacklist(Route, BlCtx) end, Routes, @@ -268,9 +268,9 @@ acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision) -> end. get_party_client() -> - HgContext = operation_context:load_hellgate(), - Client = operation_context:get_party_client(HgContext), - Context = operation_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}. check_terms_acceptability(payment, Terms, VS) -> @@ -455,7 +455,7 @@ setup_fill_blacklist_test() -> -spec cleanup_fill_blacklist_test(_) -> ok. cleanup_fill_blacklist_test(_Ok) -> try - operation_context:cleanup_hellgate() + op_context:cleanup(hellgate) catch _:_ -> ok end, @@ -468,15 +468,15 @@ cleanup_fill_blacklist_test(_Ok) -> -spec fill_blacklist_preserves_hg_context_in_workers() -> _. fill_blacklist_preserves_hg_context_in_workers() -> - HgCtx = operation_context:create(#{woody_context => woody_context:new()}), - ok = operation_context:save_hellgate(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, operation_context:load_hellgate()), + ?assertEqual(HgCtx, op_context:load(op_context:key(hellgate))), Parent ! {worker_done, self(), Ref}, Route end), @@ -487,15 +487,15 @@ fill_blacklist_preserves_hg_context_in_workers() -> ?assert(lists:all(fun(Pid) -> Pid =/= Parent end, WorkerPids)) after ok = meck:unload(hg_inspector), - ok = operation_context:cleanup_hellgate() + 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 = operation_context:create(#{woody_context => woody_context:new()}), - ok = operation_context:save_hellgate(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), @@ -514,7 +514,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 = operation_context:cleanup_hellgate() + ok = op_context:cleanup(hellgate) end. -spec collect_worker_pids(reference(), non_neg_integer()) -> [pid()]. diff --git a/docs/prg-machine-fix-plan.md b/docs/prg-machine-fix-plan.md index 1698dd80..3d068c36 100644 --- a/docs/prg-machine-fix-plan.md +++ b/docs/prg-machine-fix-plan.md @@ -181,10 +181,10 @@ PG-бэкенд progressor хранит `timestamptz` с микросекунд - Удалить `application:set_env(prg_machine, woody_context_loader, fun ...)` из `hellgate.erl` и `ff_server.erl` (анонимный fun в app env + общий ключ — ломается на hot upgrade и при совместном старте в одном узле). -- `prg_machine:encode_rpc_context/0` → `operation_context:current_woody_context()`: +- `prg_machine:encode_rpc_context/0` → `op_context:current_woody_context()`: пробует hg-binding, затем ff-binding (gproc-ключи текущего процесса различны, коллизий нет), fallback `woody_context:new()` с warning-логом. -- Добавить `operation_context` в `applications` у `prg_machine.app.src` +- Добавить `op_context` в `applications` у `prg_machine.app.src` (зависимость уже фактическая — `resolve_env_enter`). ### 4.2 `binary_to_term` без `[safe]` diff --git a/docs/prg-machine.md b/docs/prg-machine.md index f9c1c702..3e2ef578 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -49,7 +49,7 @@ sequenceDiagram **`process/3`:** `env_enter` → `unmarshal_machine` → `dispatch` → `marshal_process_result` → `env_leave` (Leave только после успешного Enter). Исключение в домене → `{error, {exception, Class, Reason}}` + log (stacktrace только в логах, не на проводе). -**Контекст RPC:** `operation_context:current_woody_context/0` (hg-binding → ff-binding → fresh ctx + warning). В `process/3` — `env_enter`/`env_leave` по `context_binding` из `sys.config` (HG strict / FF lenient). +**Контекст RPC:** `op_context:current_woody_context/0` (hg-binding → ff-binding → fresh ctx + warning). В `process/3` — `env_enter`/`env_leave` по `context_binding` из `sys.config` (HG strict / FF lenient). **События:** timestamp в microsecond (`timestamp_us()`); metadata пишет оба ключа `<<"format_version">>` и `<<"format">>`. FF payload — legacy `term_to_binary({bin, ThriftBin})`; HG payload — `term_to_binary(msgpack)`. From 9d73c12426e558c6eaf57bcbc3bd48ba1a6797b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 11:27:22 +0300 Subject: [PATCH 37/62] Refactor error handling in ff_deposit_repair, ff_withdrawal_repair, and ff_withdrawal_session_repair modules to include additional cases for error scenarios. Update ff_withdrawal_adapter_host to improve exception handling. Simplify database host configuration in ct_payment_system. Remove unnecessary code comments from ff_deposit_handler. --- apps/ff_cth/src/ct_payment_system.erl | 10 +--------- apps/ff_server/src/ff_deposit_handler.erl | 4 ---- apps/ff_server/src/ff_deposit_repair.erl | 8 +++++++- apps/ff_server/src/ff_withdrawal_adapter_host.erl | 8 ++++---- apps/ff_server/src/ff_withdrawal_repair.erl | 8 +++++++- apps/ff_server/src/ff_withdrawal_session_repair.erl | 8 +++++++- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index 2001725a..65cacbe9 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -206,18 +206,10 @@ services(Options) -> }, maps:get(services, Options, Default). -postgres_host() -> - case inet:gethostbyname("db") of - {ok, _} -> - "db"; - {error, _} -> - "127.0.0.1" - end. - epg_databases() -> #{ default_db => #{ - host => postgres_host(), + host => "db", port => 5432, database => "fistful", username => "fistful", diff --git a/apps/ff_server/src/ff_deposit_handler.erl b/apps/ff_server/src/ff_deposit_handler.erl index 0f97b048..74c3481d 100644 --- a/apps/ff_server/src/ff_deposit_handler.erl +++ b/apps/ff_server/src/ff_deposit_handler.erl @@ -1,7 +1,3 @@ -%%% -%%% Deposit woody handler — ff_deposit_machine (prg_machine runtime). -%%% - -module(ff_deposit_handler). -behaviour(ff_woody_wrapper). diff --git a/apps/ff_server/src/ff_deposit_repair.erl b/apps/ff_server/src/ff_deposit_repair.erl index f0ce150c..fa4d33c1 100644 --- a/apps/ff_server/src/ff_deposit_repair.erl +++ b/apps/ff_server/src/ff_deposit_repair.erl @@ -23,5 +23,11 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, working} -> woody_error:raise(business, #fistful_MachineAlreadyWorking{}); {error, {failed, Reason}} -> - erlang:error(Reason) + erlang:error(Reason); + {error, failed} -> + erlang:error(failed); + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}); + {error, {exception, Class, Reason, _Stacktrace}} -> + erlang:error({process_exception, Class, Reason}) end. diff --git a/apps/ff_server/src/ff_withdrawal_adapter_host.erl b/apps/ff_server/src/ff_withdrawal_adapter_host.erl index 4d57a803..af4d240e 100644 --- a/apps/ff_server/src/ff_withdrawal_adapter_host.erl +++ b/apps/ff_server/src/ff_withdrawal_adapter_host.erl @@ -33,10 +33,10 @@ handle_function_('ProcessCallback', {Callback}, _Opts) -> woody_error:raise(business, #wthd_provider_SessionNotFound{}); {error, failed} -> erlang:error(failed); - {error, {exception, _, _}} -> - erlang:error(failed); - {error, {exception, _, _, _}} -> - erlang:error(failed) + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}); + {error, {exception, Class, Reason, _Stacktrace}} -> + erlang:error({process_exception, Class, Reason}) end. %% diff --git a/apps/ff_server/src/ff_withdrawal_repair.erl b/apps/ff_server/src/ff_withdrawal_repair.erl index 025538bc..df7570f8 100644 --- a/apps/ff_server/src/ff_withdrawal_repair.erl +++ b/apps/ff_server/src/ff_withdrawal_repair.erl @@ -23,5 +23,11 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, working} -> woody_error:raise(business, #fistful_MachineAlreadyWorking{}); {error, {failed, Reason}} -> - erlang:error(Reason) + erlang:error(Reason); + {error, failed} -> + erlang:error(failed); + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}); + {error, {exception, Class, Reason, _Stacktrace}} -> + erlang:error({process_exception, Class, Reason}) end. diff --git a/apps/ff_server/src/ff_withdrawal_session_repair.erl b/apps/ff_server/src/ff_withdrawal_session_repair.erl index 3f34ac41..0d6486a2 100644 --- a/apps/ff_server/src/ff_withdrawal_session_repair.erl +++ b/apps/ff_server/src/ff_withdrawal_session_repair.erl @@ -23,5 +23,11 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, working} -> woody_error:raise(business, #fistful_MachineAlreadyWorking{}); {error, {failed, Reason}} -> - erlang:error(Reason) + erlang:error(Reason); + {error, failed} -> + erlang:error(failed); + {error, {exception, Class, Reason}} -> + erlang:error({process_exception, Class, Reason}); + {error, {exception, Class, Reason, _Stacktrace}} -> + erlang:error({process_exception, Class, Reason}) end. From bd7d26d5dc06367feb7a5722bf118090ca217690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 12:10:15 +0300 Subject: [PATCH 38/62] Refactor error handling in ff_deposit, ff_withdrawal, and ff_withdrawal_session modules to improve robustness. Update repair functions to include specific error cases for better clarity and consistency. Enhance the ff_machine_lib to support new error types and streamline the handling of repair scenarios across multiple machines. --- apps/ff_server/src/ff_deposit_repair.erl | 2 - apps/ff_server/src/ff_server.erl | 10 +- apps/ff_server/src/ff_withdrawal_repair.erl | 2 - .../src/ff_withdrawal_session_repair.erl | 2 - apps/ff_transfer/src/ff_deposit.erl | 85 ------------- apps/ff_transfer/src/ff_deposit_machine.erl | 93 +++++++++++++- apps/ff_transfer/src/ff_destination.erl | 79 ------------ .../src/ff_destination_machine.erl | 79 +++++++++++- apps/ff_transfer/src/ff_machine_lib.erl | 14 ++- apps/ff_transfer/src/ff_source.erl | 80 ------------ apps/ff_transfer/src/ff_source_machine.erl | 80 +++++++++++- apps/ff_transfer/src/ff_withdrawal.erl | 106 ---------------- .../ff_transfer/src/ff_withdrawal_machine.erl | 113 ++++++++++++++++- .../ff_transfer/src/ff_withdrawal_session.erl | 105 +--------------- .../src/ff_withdrawal_session_machine.erl | 118 +++++++++++++++++- .../test/ff_withdrawal_limits_SUITE.erl | 2 +- apps/prg_machine/src/prg_machine.erl | 22 +++- 17 files changed, 506 insertions(+), 486 deletions(-) diff --git a/apps/ff_server/src/ff_deposit_repair.erl b/apps/ff_server/src/ff_deposit_repair.erl index fa4d33c1..90f700f3 100644 --- a/apps/ff_server/src/ff_deposit_repair.erl +++ b/apps/ff_server/src/ff_deposit_repair.erl @@ -27,7 +27,5 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, failed} -> erlang:error(failed); {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}); - {error, {exception, Class, Reason, _Stacktrace}} -> erlang:error({process_exception, Class, Reason}) end. diff --git a/apps/ff_server/src/ff_server.erl b/apps/ff_server/src/ff_server.erl index 2def3502..8cb12afd 100644 --- a/apps/ff_server/src/ff_server.erl +++ b/apps/ff_server/src/ff_server.erl @@ -99,11 +99,11 @@ init([]) -> ), PartyClientSpec = party_client:child_spec(party_client, PartyClient), PrgMachineSpec = prg_machine:get_child_spec([ - ff_deposit, - ff_source, - ff_destination, - ff_withdrawal, - ff_withdrawal_session + ff_deposit_machine, + ff_source_machine, + ff_destination_machine, + ff_withdrawal_machine, + ff_withdrawal_session_machine ]), % TODO % - Zero thoughts given while defining this strategy. diff --git a/apps/ff_server/src/ff_withdrawal_repair.erl b/apps/ff_server/src/ff_withdrawal_repair.erl index df7570f8..dcf3c101 100644 --- a/apps/ff_server/src/ff_withdrawal_repair.erl +++ b/apps/ff_server/src/ff_withdrawal_repair.erl @@ -27,7 +27,5 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, failed} -> erlang:error(failed); {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}); - {error, {exception, Class, Reason, _Stacktrace}} -> erlang:error({process_exception, Class, Reason}) end. diff --git a/apps/ff_server/src/ff_withdrawal_session_repair.erl b/apps/ff_server/src/ff_withdrawal_session_repair.erl index 0d6486a2..b68e1502 100644 --- a/apps/ff_server/src/ff_withdrawal_session_repair.erl +++ b/apps/ff_server/src/ff_withdrawal_session_repair.erl @@ -27,7 +27,5 @@ handle_function('Repair', {ID, Scenario}, _Opts) -> {error, failed} -> erlang:error(failed); {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}); - {error, {exception, Class, Reason, _Stacktrace}} -> erlang:error({process_exception, Class, Reason}) end. diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index a2c928be..e248ccec 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -4,13 +4,8 @@ -module(ff_deposit). --behaviour(prg_machine). - -include_lib("damsel/include/dmsl_domain_thrift.hrl"). --define(NS, 'ff/deposit_v1'). --define(EVENT_FORMAT_VERSION, 1). - -type id() :: binary(). -type description() :: binary(). @@ -123,18 +118,6 @@ -export([apply_event/2]). -%% prg_machine - --export([namespace/0]). --export([init/2]). --export([process_signal/2]). --export([process_call/2]). --export([process_repair/2]). --export([marshal_event_body/1]). --export([unmarshal_event_body/2]). --export([marshal_aux_state/1]). --export([unmarshal_aux_state/1]). - %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). @@ -152,9 +135,6 @@ -type cash() :: ff_cash:cash(). -type cash_range() :: ff_range:range(cash()). -type action() :: prg_action:t(). --type ctx() :: ff_entity_context:context(). --type machine() :: prg_machine:machine(). --type prg_result() :: prg_machine:result(). -type p_transfer() :: ff_postings_transfer:transfer(). -type currency_id() :: ff_currency:id(). -type external_id() :: id(). @@ -320,61 +300,6 @@ is_finished(#{status := {failed, _}}) -> is_finished(#{status := pending}) -> false. -%% prg_machine - --spec namespace() -> prg_machine:namespace(). -namespace() -> - ?NS. - --spec init({[event()], ctx()}, machine()) -> prg_result(). -init({Events, Ctx}, _Machine) -> - #{ - events => Events, - action => timeout, - auxst => #{ctx => Ctx} - }. - --spec process_signal(prg_machine:signal(), machine()) -> prg_result(). -process_signal(timeout, Machine) -> - Deposit = prg_machine:collapse(?MODULE, Machine), - process_transfer_result(process_transfer(Deposit), Machine); -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). - --spec process_call(term(), machine()) -> no_return(). -process_call(CallArgs, _Machine) -> - erlang:error({unexpected_call, CallArgs}). - --spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. -process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. - --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(deposit, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. - --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(deposit, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). - --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). - %% Events utils -spec apply_event(event(), deposit_state() | undefined) -> deposit_state(). @@ -660,13 +585,3 @@ build_failure(limit_check, Deposit) -> code => <<"amount">> } }. - -%% prg_machine helpers - --spec process_transfer_result(process_result(), machine()) -> prg_result(). -process_transfer_result({Action, Events}, Machine) -> - #{ - events => Events, - action => Action, - auxst => maps:get(aux_state, Machine, #{}) - }. diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index a2068237..e17ef083 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -1,9 +1,13 @@ %%% -%%% Deposit machine — thin prg_machine client +%%% Deposit machine %%% -module(ff_deposit_machine). +-behaviour(prg_machine). + +-define(EVENT_FORMAT_VERSION, 1). + %% API -type id() :: prg_machine:id(). @@ -26,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()}. @@ -40,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 @@ -54,6 +60,18 @@ -export([deposit/1]). -export([ctx/1]). +%% prg_machine + +-export([namespace/0]). +-export([init/2]). +-export([process_signal/2]). +-export([process_call/2]). +-export([process_repair/2]). +-export([marshal_event_body/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% Pipeline -import(ff_pipeline, [do/1, unwrap/1]). @@ -61,6 +79,8 @@ %% Internal types -type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). -define(NS, 'ff/deposit_v1'). @@ -109,7 +129,7 @@ events(ID, {After, Limit}) -> end. -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) -> case prg_machine:repair(?NS, ID, Scenario) of {ok, Response} -> @@ -120,8 +140,10 @@ repair(ID, Scenario) -> {error, working}; {error, {repair, {failed, Reason}}} -> {error, {failed, Reason}}; - {error, _} = Error -> - Error + {error, failed} = Error -> + Error; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception} end. %% Accessors @@ -134,6 +156,61 @@ deposit(#{model := Model}) -> ctx(#{ctx := Ctx}) -> Ctx. +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init({[change()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + #{ + events => Events, + action => timeout, + auxst => #{ctx => Ctx} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Deposit = prg_machine:collapse(ff_deposit, Machine), + process_transfer_result(ff_deposit:process_transfer(Deposit), Machine); +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + case ff_repair:apply_scenario(ff_deposit, ff_machine_lib:to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + ff_machine_lib:from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, prg_machine:timestamp(), Body}, + Encoded = ff_machine_codec:marshal_event(deposit, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(deposit, ?EVENT_FORMAT_VERSION, Payload), + ff_machine_lib:event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + %% Internals -spec machine_to_st(prg_machine:machine()) -> st(). @@ -146,3 +223,11 @@ machine_to_st(#{aux_state := AuxState} = Machine) -> model => Model, ctx => Ctx }. + +-spec process_transfer_result({prg_action:t(), [change()]}, machine()) -> prg_result(). +process_transfer_result({Action, Events}, Machine) -> + #{ + events => Events, + action => Action, + auxst => maps:get(aux_state, Machine, #{}) + }. diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index f5cdee3a..eb4b210d 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -8,11 +8,6 @@ -module(ff_destination). --behaviour(prg_machine). - --define(NS, 'ff/destination_v2'). --define(EVENT_FORMAT_VERSION, 1). - -type id() :: binary(). -type token() :: binary(). -type name() :: binary(). @@ -114,27 +109,10 @@ -export([is_accessible/1]). -export([apply_event/2]). -%% prg_machine - --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/1]). --export([unmarshal_event_body/2]). --export([marshal_aux_state/1]). --export([unmarshal_aux_state/1]). - %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). --type ctx() :: ff_entity_context:context(). --type machine() :: prg_machine:machine(). --type prg_result() :: prg_machine:result(). - %% Accessors -spec party_id(destination_state()) -> party_id(). @@ -244,60 +222,3 @@ 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}). - -%% prg_machine - --spec namespace() -> prg_machine:namespace(). -namespace() -> - ?NS. - --spec init({[event()], ctx()}, machine()) -> prg_result(). -init({Events, Ctx}, _Machine) -> - #{ - events => Events, - auxst => #{ctx => Ctx} - }. - --spec process_signal(prg_machine:signal(), machine()) -> prg_result(). -process_signal(timeout, _Machine) -> - #{}; -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). - --spec process_call(term(), machine()) -> no_return(). -process_call(CallArgs, _Machine) -> - erlang:error({unexpected_call, CallArgs}). - --spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. -process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. - --spec process_notification(term(), machine()) -> prg_result(). -process_notification(_Args, _Machine) -> - #{}. - --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(destination, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. - --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(destination, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). - --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). diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index e1c549d8..73fa93d7 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -1,9 +1,13 @@ %%% -%%% Destination machine — thin prg_machine client +%%% Destination machine %%% -module(ff_destination_machine). +-behaviour(prg_machine). + +-define(EVENT_FORMAT_VERSION, 1). + %% API -type id() :: prg_machine:id(). @@ -45,10 +49,26 @@ -export([destination/1]). -export([ctx/1]). +%% prg_machine + +-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/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% Pipeline -import(ff_pipeline, [do/1, unwrap/1]). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). + -define(NS, 'ff/destination_v2'). %% API @@ -105,6 +125,63 @@ destination(#{model := Model}) -> ctx(#{ctx := Ctx}) -> Ctx. +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init({[change()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + #{ + events => Events, + auxst => #{ctx => Ctx} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, _Machine) -> + #{}; +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + case ff_repair:apply_scenario(ff_destination, ff_machine_lib:to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + ff_machine_lib:from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_notification(term(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, prg_machine:timestamp(), Body}, + Encoded = ff_machine_codec:marshal_event(destination, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(destination, ?EVENT_FORMAT_VERSION, Payload), + ff_machine_lib:event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + %% Internals -spec machine_to_st(prg_machine:machine()) -> st(). diff --git a/apps/ff_transfer/src/ff_machine_lib.erl b/apps/ff_transfer/src/ff_machine_lib.erl index 879de082..f6aec738 100644 --- a/apps/ff_transfer/src/ff_machine_lib.erl +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -1,7 +1,6 @@ -module(ff_machine_lib). -%%% Shared helpers for the ff_* prg_machine handlers (ff_withdrawal, ff_deposit, -%%% ff_source, ff_destination, ff_withdrawal_session) and their thin machine +%%% Shared helpers for the ff_* prg_machine handlers and their thin machine %%% clients. Extracted to remove the per-namespace copy-paste. -export([to_repair_machine/1]). @@ -11,9 +10,20 @@ -export([history_to_events/1]). -export([codec_timestamp/1]). +-export_type([repair_call_error/0]). + -type timestamp() :: prg_machine:timestamp(). -type timestamped_event(T) :: {ev, timestamp(), T}. +-type processor_error() :: {exception, atom(), term()}. + +-type repair_call_error() :: + notfound + | working + | failed + | {failed, ff_repair:repair_error()} + | processor_error(). + -spec to_repair_machine(prg_machine:machine()) -> ff_repair:machine(). to_repair_machine(#{namespace := NS, id := ID, history := History, aux_state := AuxState}) -> #{ diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index 7d607124..e5b2c3bb 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -7,11 +7,6 @@ -module(ff_source). --behaviour(prg_machine). - --define(NS, 'ff/source_v1'). --define(EVENT_FORMAT_VERSION, 1). - -type id() :: binary(). -type name() :: binary(). -type account() :: ff_account:account(). @@ -100,27 +95,10 @@ -export([is_accessible/1]). -export([apply_event/2]). -%% prg_machine - --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/1]). --export([unmarshal_event_body/2]). --export([marshal_aux_state/1]). --export([unmarshal_aux_state/1]). - %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). --type ctx() :: ff_entity_context:context(). --type machine() :: prg_machine:machine(). --type prg_result() :: prg_machine:result(). - %% Accessors -spec id(source_state()) -> id(). @@ -220,61 +198,3 @@ 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}). - -%% prg_machine - --spec namespace() -> prg_machine:namespace(). -namespace() -> - ?NS. - --spec init({[event()], ctx()}, machine()) -> prg_result(). -init({Events, Ctx}, _Machine) -> - #{ - events => Events, - action => timeout, - auxst => #{ctx => Ctx} - }. - --spec process_signal(prg_machine:signal(), machine()) -> prg_result(). -process_signal(timeout, _Machine) -> - #{}; -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). - --spec process_call(term(), machine()) -> no_return(). -process_call(CallArgs, _Machine) -> - erlang:error({unexpected_call, CallArgs}). - --spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. -process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. - --spec process_notification(term(), machine()) -> prg_result(). -process_notification(_Args, _Machine) -> - #{}. - --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(source, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. - --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(source, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). - --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). diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index dc484e4c..e1106fc9 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -1,9 +1,13 @@ %%% -%%% Source machine — thin prg_machine client +%%% Source machine %%% -module(ff_source_machine). +-behaviour(prg_machine). + +-define(EVENT_FORMAT_VERSION, 1). + %% API -type id() :: prg_machine:id(). @@ -45,10 +49,26 @@ -export([source/1]). -export([ctx/1]). +%% prg_machine + +-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/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% Pipeline -import(ff_pipeline, [do/1, unwrap/1]). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). + -define(NS, 'ff/source_v1'). %% API @@ -105,6 +125,64 @@ source(#{model := Model}) -> ctx(#{ctx := Ctx}) -> Ctx. +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init({[change()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + #{ + events => Events, + action => timeout, + auxst => #{ctx => Ctx} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, _Machine) -> + #{}; +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-spec process_call(term(), machine()) -> no_return(). +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + case ff_repair:apply_scenario(ff_source, ff_machine_lib:to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + ff_machine_lib:from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_notification(term(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, prg_machine:timestamp(), Body}, + Encoded = ff_machine_codec:marshal_event(source, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(source, ?EVENT_FORMAT_VERSION, Payload), + ff_machine_lib:event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + %% Internals -spec machine_to_st(prg_machine:machine()) -> st(). diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index bbd73f35..2bcfcb0b 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -4,13 +4,8 @@ -module(ff_withdrawal). --behaviour(prg_machine). - -include_lib("damsel/include/dmsl_domain_thrift.hrl"). --define(NS, 'ff/withdrawal_v2'). --define(EVENT_FORMAT_VERSION, 1). - -type id() :: binary(). -define(ACTUAL_FORMAT_VERSION, 4). @@ -255,19 +250,6 @@ -export([apply_event/2]). -%% prg_machine - --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/1]). --export([unmarshal_event_body/2]). --export([marshal_aux_state/1]). --export([unmarshal_aux_state/1]). - %% Pipeline -import(ff_pipeline, [do/1, unwrap/1, unwrap/2]). @@ -308,10 +290,6 @@ -type legacy_event() :: any(). --type ctx() :: ff_entity_context:context(). --type machine() :: prg_machine:machine(). --type prg_result() :: prg_machine:result(). - -type transfer_params() :: #{ party_id := party_id(), wallet_id := wallet_id(), @@ -1838,90 +1816,6 @@ get_quote_field(provider_id, #{route := Route}) -> get_quote_field(terminal_id, #{route := Route}) -> ff_withdrawal_routing:get_terminal(Route). -%% prg_machine - --spec namespace() -> prg_machine:namespace(). -namespace() -> - ?NS. - --spec init({[event()], ctx()}, machine()) -> prg_result(). -init({Events, Ctx}, _Machine) -> - #{ - events => Events, - action => timeout, - auxst => #{ctx => Ctx} - }. - --spec process_signal(prg_machine:signal(), machine()) -> prg_result(). -process_signal(timeout, Machine) -> - Withdrawal = prg_machine:collapse(?MODULE, Machine), - process_transfer_result(process_transfer(Withdrawal), Machine); -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). - --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 start_adjustment(Params, Withdrawal) of - {ok, Result} -> - {ok, process_transfer_result(Result, Machine)}; - {error, _Reason} = Error -> - {Error, #{}} - end; -process_call(CallArgs, _Machine) -> - erlang:error({unexpected_call, CallArgs}). - --spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. -process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. - --spec process_notification({session_finished, session_id(), session_result()}, machine()) -> prg_result(). -process_notification({session_finished, SessionID, SessionResult}, Machine) -> - Withdrawal = prg_machine:collapse(?MODULE, Machine), - case finalize_session(SessionID, SessionResult, Withdrawal) of - {ok, Result} -> - process_transfer_result(Result, Machine); - {error, Reason} -> - erlang:error({unable_to_finalize_session, Reason}) - end. - --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. - --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). - --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). - --spec process_transfer_result(process_result(), machine()) -> prg_result(). -process_transfer_result({Action, Events}, Machine) -> - #{ - events => Events, - action => Action, - auxst => maps:get(aux_state, Machine, #{}) - }. - -%% - -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 0c2c3a83..70cd8abf 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -1,9 +1,13 @@ %%% -%%% Withdrawal machine — thin prg_machine client +%%% Withdrawal machine %%% -module(ff_withdrawal_machine). +-behaviour(prg_machine). + +-define(EVENT_FORMAT_VERSION, 1). + %% API -type id() :: prg_machine:id(). @@ -26,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_withdrawal_error() :: {unknown_withdrawal, id()}. @@ -55,6 +60,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 @@ -72,6 +78,19 @@ -export([withdrawal/1]). -export([ctx/1]). +%% prg_machine + +-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/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% Pipeline -import(ff_pipeline, [do/1, unwrap/1]). @@ -79,6 +98,8 @@ %% Internal types -type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). -define(NS, 'ff/withdrawal_v2'). @@ -127,7 +148,7 @@ events(ID, {After, Limit}) -> end. -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) -> case prg_machine:repair(?NS, ID, Scenario) of {ok, Response} -> @@ -138,8 +159,10 @@ repair(ID, Scenario) -> {error, working}; {error, {repair, {failed, Reason}}} -> {error, {failed, Reason}}; - {error, _} = Error -> - Error + {error, failed} = Error -> + Error; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception} end. -spec start_adjustment(id(), adjustment_params()) -> @@ -163,6 +186,80 @@ withdrawal(#{model := Model}) -> ctx(#{ctx := Ctx}) -> Ctx. +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init({[change()], ctx()}, machine()) -> prg_result(). +init({Events, Ctx}, _Machine) -> + #{ + events => Events, + action => timeout, + auxst => #{ctx => Ctx} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), + process_transfer_result(ff_withdrawal:process_transfer(Withdrawal), Machine); +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-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(ff_withdrawal, Machine), + case ff_withdrawal:start_adjustment(Params, Withdrawal) of + {ok, Result} -> + {ok, process_transfer_result(Result, Machine)}; + {error, _Reason} = Error -> + {Error, #{}} + end; +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + case ff_repair:apply_scenario(ff_withdrawal, ff_machine_lib:to_repair_machine(Machine), Scenario) of + {ok, {_Response, Result}} -> + ff_machine_lib:from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_notification(notify_args(), machine()) -> prg_result(). +process_notification({session_finished, SessionID, SessionResult}, Machine) -> + Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), + case ff_withdrawal:finalize_session(SessionID, SessionResult, Withdrawal) of + {ok, Result} -> + process_transfer_result(Result, Machine); + {error, Reason} -> + erlang:error({unable_to_finalize_session, Reason}) + end. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, prg_machine:timestamp(), Body}, + Encoded = ff_machine_codec:marshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Payload), + ff_machine_lib:event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + %% Internals -spec machine_to_st(prg_machine:machine()) -> st(). @@ -176,6 +273,14 @@ machine_to_st(#{aux_state := AuxState} = Machine) -> ctx => Ctx }. +-spec process_transfer_result({prg_action:t(), [change()]}, machine()) -> prg_result(). +process_transfer_result({Action, Events}, Machine) -> + #{ + events => Events, + action => Action, + auxst => maps:get(aux_state, Machine, #{}) + }. + call(ID, Call) -> case prg_machine:call(?NS, ID, Call) of {ok, Reply} -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index 4412eb8c..a35c63db 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -4,11 +4,6 @@ -module(ff_withdrawal_session). --behaviour(prg_machine). - --define(NS, 'ff/withdrawal/session_v2'). --define(EVENT_FORMAT_VERSION, 1). - %% Accessors -export([id/1]). @@ -34,19 +29,6 @@ %% ff_repair -export([set_session_result/2]). -%% prg_machine - --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/1]). --export([unmarshal_event_body/2]). --export([marshal_aux_state/1]). --export([unmarshal_aux_state/1]). - %% %% Types %% @@ -115,8 +97,6 @@ }. -type id() :: binary(). --type machine() :: prg_machine:machine(). --type prg_result() :: prg_machine:result(). -type action() :: prg_action:t(). @@ -328,7 +308,7 @@ process_adapter_intent({finish, {success, _TransactionInfo}}, _Session) -> process_adapter_intent({finish, Result}, _Session) -> {timeout, [{finished, Result}]}; process_adapter_intent({sleep, #{timer := Timer, tag := Tag}}, Session) -> - ok = ff_machine_tag:create_binding(?NS, Tag, id(Session)), + ok = ff_machine_tag:create_binding(ff_withdrawal_session_machine:namespace(), Tag, id(Session)), Events = create_callback(Tag, Session), {prg_action:schedule_timer(Timer), Events}; process_adapter_intent({sleep, #{timer := Timer}}, _Session) -> @@ -393,86 +373,3 @@ create_adapter_withdrawal( -spec set_callbacks_index(callbacks_index(), session_state()) -> session_state(). set_callbacks_index(Callbacks, Session) -> Session#{callbacks => Callbacks}. - -%% prg_machine - --spec namespace() -> prg_machine:namespace(). -namespace() -> - ?NS. - --spec init([event()], machine()) -> prg_result(). -init(Events, _Machine) -> - #{ - events => Events, - action => timeout, - auxst => #{ctx => ff_entity_context:new()} - }. - --spec process_signal(prg_machine:signal(), machine()) -> prg_result(). -process_signal(timeout, Machine) -> - Session = prg_machine:collapse(?MODULE, Machine), - process_session_result(process_session(Session), Machine); -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). - --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 process_callback(Params, Session) of - {ok, {Response, Result}} -> - {{ok, Response}, process_session_result(Result, Machine)}; - {error, {Reason, _Result}} -> - {{error, Reason}, #{}} - end; -process_call(CallArgs, _Machine) -> - erlang:error({unexpected_call, CallArgs}). - --spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. -process_repair(Scenario, Machine) -> - ScenarioProcessors = #{ - set_session_result => fun(Args, RMachine) -> - Session = prg_machine:collapse(?MODULE, ff_repair:to_prg_machine(RMachine)), - {Action, Events} = set_session_result(Args, Session), - {ok, {ok, #{action => Action, events => Events}}} - end - }, - case ff_repair:apply_scenario(?MODULE, ff_machine_lib:to_repair_machine(Machine), Scenario, ScenarioProcessors) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. - --spec process_notification(term(), machine()) -> prg_result(). -process_notification(_Args, _Machine) -> - #{}. - --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. - --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). - --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). - --spec process_session_result(process_result(), machine()) -> prg_result(). -process_session_result({Action, Events}, Machine) -> - #{ - events => Events, - action => Action, - auxst => maps:get(aux_state, Machine, #{}) - }. diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 180ad7e1..63c38dd9 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -1,10 +1,13 @@ %%% -%%% Withdrawal session machine — thin prg_machine client +%%% Withdrawal session machine %%% -module(ff_withdrawal_session_machine). +-behaviour(prg_machine). + -define(NS, 'ff/withdrawal/session_v2'). +-define(EVENT_FORMAT_VERSION, 1). %% API @@ -18,19 +21,35 @@ -export([repair/2]). -export([process_callback/1]). +%% prg_machine + +-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/1]). +-export([unmarshal_event_body/2]). +-export([marshal_aux_state/1]). +-export([unmarshal_aux_state/1]). + %% %% Types %% -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([repair_error/0]). -export_type([repair_response/0]). +-export_type([repair_call_error/0]). -type id() :: prg_machine:id(). -type data() :: ff_withdrawal_session:data(). -type params() :: ff_withdrawal_session:params(). +-type change() :: ff_withdrawal_session:event(). -type st() :: #{ model := session(), @@ -53,6 +72,8 @@ | {exception, atom(), term(), list()}. -type ctx() :: ff_entity_context:context(). +-type machine() :: prg_machine:machine(). +-type prg_result() :: prg_machine:result(). %% Pipeline @@ -110,7 +131,7 @@ events(ID, {After, Limit}) -> end. -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) -> case prg_machine:repair(?NS, ID, Scenario) of {ok, Response} -> @@ -121,8 +142,10 @@ repair(ID, Scenario) -> {error, working}; {error, {repair, {failed, Reason}}} -> {error, {failed, Reason}}; - {error, _} = Error -> - Error + {error, failed} = Error -> + Error; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception} end. -spec process_callback(callback_params()) -> @@ -136,6 +159,85 @@ process_callback(#{tag := Tag} = Params) -> {error, {unknown_session, {tag, Tag}}} end. +%% prg_machine + +-spec namespace() -> prg_machine:namespace(). +namespace() -> + ?NS. + +-spec init([change()], machine()) -> prg_result(). +init(Events, _Machine) -> + #{ + events => Events, + action => timeout, + auxst => #{ctx => ff_entity_context:new()} + }. + +-spec process_signal(prg_machine:signal(), machine()) -> prg_result(). +process_signal(timeout, Machine) -> + Session = prg_machine:collapse(ff_withdrawal_session, Machine), + process_session_result(ff_withdrawal_session:process_session(Session), Machine); +process_signal({repair, _Args}, _Machine) -> + erlang:error({unexpected_signal, repair}). + +-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(ff_withdrawal_session, Machine), + case ff_withdrawal_session:process_callback(Params, Session) of + {ok, {Response, Result}} -> + {{ok, Response}, process_session_result(Result, Machine)}; + {error, {Reason, _Result}} -> + {{error, Reason}, #{}} + end; +process_call(CallArgs, _Machine) -> + erlang:error({unexpected_call, CallArgs}). + +-spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. +process_repair(Scenario, Machine) -> + ScenarioProcessors = #{ + set_session_result => fun(Args, RMachine) -> + Session = prg_machine:collapse(ff_withdrawal_session, ff_repair:to_prg_machine(RMachine)), + {Action, Events} = ff_withdrawal_session:set_session_result(Args, Session), + {ok, {ok, #{action => Action, events => Events}}} + end + }, + case + ff_repair:apply_scenario( + ff_withdrawal_session, ff_machine_lib:to_repair_machine(Machine), Scenario, ScenarioProcessors + ) + of + {ok, {_Response, Result}} -> + ff_machine_lib:from_repair_result(Result, Machine); + {error, Reason} -> + {error, Reason} + end. + +-spec process_notification(term(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. +marshal_event_body(Body) -> + Timestamped = {ev, prg_machine:timestamp(), Body}, + Encoded = ff_machine_codec:marshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Timestamped), + {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + +-spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). +unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Payload), + ff_machine_lib:event_body_from_timestamped(Timestamped); +unmarshal_event_body(Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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). + %% %% Internals %% @@ -151,6 +253,14 @@ machine_to_st(#{aux_state := AuxState} = Machine) -> ctx => Ctx }. +-spec process_session_result(ff_withdrawal_session:process_result(), machine()) -> prg_result(). +process_session_result({Action, Events}, Machine) -> + #{ + events => Events, + action => Action, + auxst => maps:get(aux_state, Machine, #{}) + }. + call(Ref, Call) -> case prg_machine:call(?NS, Ref, Call) of {ok, Reply} -> diff --git a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl index 461837ab..eb032826 100644 --- a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl @@ -548,7 +548,7 @@ await_provider_retry(FirstAmount, SecondAmount, TotalAmount, C) -> ok = ff_ct_machine:set_hook( timeout, fun - (Machine, ff_withdrawal, _Args) -> + (Machine, ff_withdrawal_machine, _Args) -> Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), case {ff_withdrawal:id(Withdrawal), ff_withdrawal:activity(Withdrawal)} of {WithdrawalID1, Activity} -> diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 93d3bd7f..0df8fbb2 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -6,8 +6,6 @@ -include_lib("progressor/include/progressor.hrl"). -define(TABLE, prg_machine_dispatch). -%% progressor is_retryable/5 and machinery_prg_backend expect a 3-tuple; stacktrace stays in logs only. --define(PROCESSOR_EXCEPTION(Class, Reason, _Stacktrace), {exception, Class, Reason}). %% Types @@ -27,6 +25,17 @@ | {unknown_namespace, namespace()} | {exception, atom(), term()}. +-type processor_error() :: + {exception, atom(), term()} + | {exception, atom(), term(), list()}. + +-type repair_error() :: + notfound + | working + | failed + | processor_error() + | {repair, {failed, term()}}. + -type machine() :: #{ namespace := namespace(), id := id(), @@ -68,6 +77,7 @@ history/0, machine/0, get_error/0, + repair_error/0, signal/0, result/0, process_options/0 @@ -180,7 +190,7 @@ call(NS, ID, CallArgs, After, Limit, Direction) -> end. -spec repair(namespace(), id(), args()) -> - {ok, term()} | {error, notfound | working | failed | {repair, {failed, term()}}}. + {ok, term()} | {error, repair_error()}. repair(NS, ID, Args) -> Req = #{ ns => NS, @@ -199,6 +209,10 @@ repair(NS, ID, Args) -> {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, Reason} -> %% The repair-failed reason is our own term encoded by process/3 %% (marshal_process_result -> encode_term); hand it back as a term. @@ -297,7 +311,7 @@ process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> end catch Class:Reason:Stacktrace -> - Exception = ?PROCESSOR_EXCEPTION(Class, Reason, Stacktrace), + Exception = {exception, Class, Reason}, logger:error( "prg_machine process failed: ~p:~p", [Class, Reason], From 68352f64c7bf416dfffeec646a70013cdfbbcabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 12:33:06 +0300 Subject: [PATCH 39/62] Refactor process_signal function across multiple machines to remove error handling for unexpected signals. Simplify the implementation by directly returning an empty map for timeout signals in ff_deposit_machine, ff_destination_machine, ff_source_machine, ff_withdrawal_machine, and ff_withdrawal_session_machine modules. --- apps/ff_transfer/src/ff_deposit_machine.erl | 4 +--- apps/ff_transfer/src/ff_destination_machine.erl | 4 +--- apps/ff_transfer/src/ff_source_machine.erl | 4 +--- apps/ff_transfer/src/ff_withdrawal_machine.erl | 4 +--- apps/ff_transfer/src/ff_withdrawal_session_machine.erl | 4 +--- apps/hellgate/src/hg_invoice_template.erl | 2 -- apps/prg_machine/src/prg_machine.erl | 2 +- 7 files changed, 6 insertions(+), 18 deletions(-) diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index e17ef083..9f52acff 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -173,9 +173,7 @@ init({Events, Ctx}, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, Machine) -> Deposit = prg_machine:collapse(ff_deposit, Machine), - process_transfer_result(ff_deposit:process_transfer(Deposit), Machine); -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). + process_transfer_result(ff_deposit:process_transfer(Deposit), Machine). -spec process_call(term(), machine()) -> no_return(). process_call(CallArgs, _Machine) -> diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index 73fa93d7..825b55ba 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -140,9 +140,7 @@ init({Events, Ctx}, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, _Machine) -> - #{}; -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). + #{}. -spec process_call(term(), machine()) -> no_return(). process_call(CallArgs, _Machine) -> diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index e1106fc9..cbcbfdfe 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -141,9 +141,7 @@ init({Events, Ctx}, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, _Machine) -> - #{}; -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). + #{}. -spec process_call(term(), machine()) -> no_return(). process_call(CallArgs, _Machine) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 70cd8abf..f9b05496 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -203,9 +203,7 @@ init({Events, Ctx}, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, Machine) -> Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), - process_transfer_result(ff_withdrawal:process_transfer(Withdrawal), Machine); -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). + process_transfer_result(ff_withdrawal:process_transfer(Withdrawal), Machine). -spec process_call({start_adjustment, adjustment_params()}, machine()) -> {ok | {error, start_adjustment_error()}, prg_result()}. diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 63c38dd9..5b5f6fb1 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -176,9 +176,7 @@ init(Events, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, Machine) -> Session = prg_machine:collapse(ff_withdrawal_session, Machine), - process_session_result(ff_withdrawal_session:process_session(Session), Machine); -process_signal({repair, _Args}, _Machine) -> - erlang:error({unexpected_signal, repair}). + process_session_result(ff_withdrawal_session:process_session(Session), Machine). -spec process_call({process_callback, callback_params()}, machine()) -> {{ok, process_callback_response()} | {error, process_callback_error()}, prg_result()}. diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index 5488f474..e842335b 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -262,8 +262,6 @@ process_repair(_Args, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, _Machine) -> - #{}; -process_signal({repair, _}, _Machine) -> #{}. -spec process_call(call(), machine()) -> {prg_machine:response(), prg_result()}. diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 0df8fbb2..d1d540a7 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -44,7 +44,7 @@ range => history_range() }. --type signal() :: timeout | {repair, args()}. +-type signal() :: timeout. -type result() :: #{ events => [event_body()], action => action(), From 3c3171ae76a508f734a6de3fd458ce036a041748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 17:22:35 +0300 Subject: [PATCH 40/62] Refactor error handling in ff_withdrawal_adapter_host and ff_withdrawal_session_machine to simplify exception management. Update processor_error type definitions in ff_machine_lib and prg_machine for consistency. Adjust test cases in ff_withdrawal_SUITE to reflect new error handling logic. --- .../src/ff_withdrawal_adapter_host.erl | 6 +----- apps/ff_transfer/src/ff_machine_lib.erl | 2 +- .../src/ff_withdrawal_session_machine.erl | 8 +++----- apps/ff_transfer/test/ff_withdrawal_SUITE.erl | 2 +- .../test/ff_withdrawal_limits_SUITE.erl | 5 ++--- apps/prg_machine/src/prg_machine.erl | 17 ++++++++++------- 6 files changed, 18 insertions(+), 22 deletions(-) diff --git a/apps/ff_server/src/ff_withdrawal_adapter_host.erl b/apps/ff_server/src/ff_withdrawal_adapter_host.erl index af4d240e..6c65d9ab 100644 --- a/apps/ff_server/src/ff_withdrawal_adapter_host.erl +++ b/apps/ff_server/src/ff_withdrawal_adapter_host.erl @@ -32,11 +32,7 @@ handle_function_('ProcessCallback', {Callback}, _Opts) -> {error, {unknown_session, _Ref}} -> woody_error:raise(business, #wthd_provider_SessionNotFound{}); {error, failed} -> - erlang:error(failed); - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}); - {error, {exception, Class, Reason, _Stacktrace}} -> - erlang:error({process_exception, Class, Reason}) + erlang:error(failed) end. %% diff --git a/apps/ff_transfer/src/ff_machine_lib.erl b/apps/ff_transfer/src/ff_machine_lib.erl index f6aec738..fc5045d4 100644 --- a/apps/ff_transfer/src/ff_machine_lib.erl +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -15,7 +15,7 @@ -type timestamp() :: prg_machine:timestamp(). -type timestamped_event(T) :: {ev, timestamp(), T}. --type processor_error() :: {exception, atom(), term()}. +-type processor_error() :: prg_machine:processor_error(). -type repair_call_error() :: notfound diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 5b5f6fb1..26a516cc 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -67,10 +67,6 @@ {unknown_session, {tag, id()}} | ff_withdrawal_session:process_callback_error(). --type processor_error() :: - {exception, atom(), term()} - | {exception, atom(), term(), list()}. - -type ctx() :: ff_entity_context:context(). -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). @@ -150,7 +146,7 @@ repair(ID, Scenario) -> -spec process_callback(callback_params()) -> {ok, process_callback_response()} - | {error, process_callback_error() | processor_error() | failed}. + | {error, process_callback_error() | failed}. process_callback(#{tag := Tag} = Params) -> case ff_machine_tag:get_binding(?NS, Tag) of {ok, EntityID} -> @@ -267,6 +263,8 @@ call(Ref, Call) -> {error, {unknown_session, Ref}}; {error, failed} -> {error, failed}; + {error, {exception, _, _}} -> + {error, failed}; {error, _} = Error -> Error end. diff --git a/apps/ff_transfer/test/ff_withdrawal_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_SUITE.erl index f3b02590..232d3be3 100644 --- a/apps/ff_transfer/test/ff_withdrawal_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_SUITE.erl @@ -886,7 +886,7 @@ session_repair_test(C) -> ?assertEqual(pending, await_session_processing_status(WithdrawalID, pending)), SessionID = get_session_id(WithdrawalID), ?assertEqual(<<"callback_processing">>, await_session_adapter_state(SessionID, <<"callback_processing">>)), - ?assertMatch({error, {exception, _, _}}, call_process_callback(Callback)), + ?assertEqual({error, failed}, call_process_callback(Callback)), timer:sleep(3000), ?assertEqual(pending, await_session_processing_status(WithdrawalID, pending)), ok = repair_withdrawal_session(WithdrawalID), diff --git a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl index eb032826..ec32f4ee 100644 --- a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl @@ -72,14 +72,13 @@ groups() -> -spec init_per_suite(config()) -> config(). init_per_suite(C) -> - C1 = ct_helper:makeup_cfg( + ct_helper:makeup_cfg( [ ct_helper:test_case_name(init), ct_payment_system:setup() ], C - ), - C1. + ). -spec end_per_suite(config()) -> _. end_per_suite(C) -> diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index d1d540a7..cd3e9beb 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -20,14 +20,12 @@ %% 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 | {unknown_namespace, namespace()} - | {exception, atom(), term()}. - --type processor_error() :: - {exception, atom(), term()} - | {exception, atom(), term(), list()}. + | processor_error(). -type repair_error() :: notfound @@ -77,6 +75,7 @@ history/0, machine/0, get_error/0, + processor_error/0, repair_error/0, signal/0, result/0, @@ -185,6 +184,10 @@ call(NS, ID, CallArgs, After, Limit, Direction) -> {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, _} = Error -> Error end. @@ -262,7 +265,7 @@ get_history(NS, ID, After, Limit, Direction) -> end. -spec notify(namespace(), id(), args()) -> - ok | {error, notfound | failed | {exception, atom(), term()} | term()}. + ok | {error, notfound | failed | processor_error() | term()}. notify(NS, ID, Args) -> case call(NS, ID, {notify, Args}) of {ok, _} -> ok; @@ -270,7 +273,7 @@ notify(NS, ID, Args) -> end. -spec remove(namespace(), id()) -> - ok | {error, notfound | failed | {exception, atom(), term()} | term()}. + ok | {error, notfound | failed | processor_error() | term()}. remove(NS, ID) -> case call(NS, ID, remove) of {ok, _} -> ok; From b75b9dee3e286fd40a6e30c906bc557a1ec43b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 17:26:51 +0300 Subject: [PATCH 41/62] Refactor error handling in ff_withdrawal_adapter_host and ff_withdrawal_session_machine to streamline exception management. Update test cases in ff_withdrawal_SUITE to align with new error handling logic, ensuring consistent error reporting. --- apps/ff_server/src/ff_withdrawal_adapter_host.erl | 4 +--- apps/ff_transfer/src/ff_withdrawal_session_machine.erl | 6 +++--- apps/ff_transfer/test/ff_withdrawal_SUITE.erl | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/ff_server/src/ff_withdrawal_adapter_host.erl b/apps/ff_server/src/ff_withdrawal_adapter_host.erl index 6c65d9ab..7413f3f3 100644 --- a/apps/ff_server/src/ff_withdrawal_adapter_host.erl +++ b/apps/ff_server/src/ff_withdrawal_adapter_host.erl @@ -30,9 +30,7 @@ handle_function_('ProcessCallback', {Callback}, _Opts) -> {error, {session_already_finished, Context}} -> {ok, marshal(process_callback_result, {finished, Context})}; {error, {unknown_session, _Ref}} -> - woody_error:raise(business, #wthd_provider_SessionNotFound{}); - {error, failed} -> - erlang:error(failed) + woody_error:raise(business, #wthd_provider_SessionNotFound{}) end. %% diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 26a516cc..f7e4a1e8 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -146,7 +146,7 @@ repair(ID, Scenario) -> -spec process_callback(callback_params()) -> {ok, process_callback_response()} - | {error, process_callback_error() | failed}. + | {error, process_callback_error()}. process_callback(#{tag := Tag} = Params) -> case ff_machine_tag:get_binding(?NS, Tag) of {ok, EntityID} -> @@ -262,9 +262,9 @@ call(Ref, Call) -> {error, notfound} -> {error, {unknown_session, Ref}}; {error, failed} -> - {error, failed}; + erlang:error({failed, ?NS, Ref}); {error, {exception, _, _}} -> - {error, failed}; + erlang:error({failed, ?NS, Ref}); {error, _} = Error -> Error end. diff --git a/apps/ff_transfer/test/ff_withdrawal_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_SUITE.erl index 232d3be3..ce5ad64d 100644 --- a/apps/ff_transfer/test/ff_withdrawal_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_SUITE.erl @@ -886,7 +886,7 @@ session_repair_test(C) -> ?assertEqual(pending, await_session_processing_status(WithdrawalID, pending)), SessionID = get_session_id(WithdrawalID), ?assertEqual(<<"callback_processing">>, await_session_adapter_state(SessionID, <<"callback_processing">>)), - ?assertEqual({error, failed}, call_process_callback(Callback)), + ?assertError({failed, _, _}, call_process_callback(Callback)), timer:sleep(3000), ?assertEqual(pending, await_session_processing_status(WithdrawalID, pending)), ok = repair_withdrawal_session(WithdrawalID), From c8cad6cbde8d037d0ee15497dc40a8a519051606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 18:20:00 +0300 Subject: [PATCH 42/62] Remove TODO.md file and refactor hg_invoice_handler and hg_proto modules. Update invoice event timestamp handling in hg_invoice_handler. Simplify service retrieval in hg_proto by removing unused service definitions. --- TODO.md | 13 ------------- apps/hellgate/src/hg_invoice_handler.erl | 5 +---- apps/hg_proto/src/hg_proto.erl | 12 ++---------- 3 files changed, 3 insertions(+), 27 deletions(-) delete mode 100644 TODO.md 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/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl index 42f88eb5..d4bff72d 100644 --- a/apps/hellgate/src/hg_invoice_handler.erl +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -254,13 +254,10 @@ publish_invoice_event(InvoiceID, {ID, Dt, Event}) -> #payproc_Event{ id = ID, source = {invoice_id, InvoiceID}, - created_at = format_event_timestamp(Dt), + created_at = Dt, payload = ?invoice_ev(Event) }. -format_event_timestamp(Dt) -> - Dt. - map_history_error({ok, Result}) -> Result; map_history_error({error, notfound}) -> diff --git a/apps/hg_proto/src/hg_proto.erl b/apps/hg_proto/src/hg_proto.erl index 387b7797..2da3e413 100644 --- a/apps/hg_proto/src/hg_proto.erl +++ b/apps/hg_proto/src/hg_proto.erl @@ -45,11 +45,7 @@ get_service(party_config) -> get_service(customer_management) -> {dmsl_customer_thrift, 'CustomerManagement'}; get_service(bank_card_storage) -> - {dmsl_customer_thrift, 'BankCardStorage'}; -get_service(invoice_trace) -> - {dmsl_progressor_trace_thrift, 'InvoiceTrace'}; -get_service(invoice_template_trace) -> - {dmsl_progressor_trace_thrift, 'InvoiceTemplateTrace'}. + {dmsl_customer_thrift, 'BankCardStorage'}. -spec get_service_spec(Name :: atom()) -> service_spec(). get_service_spec(Name) -> @@ -63,8 +59,4 @@ get_service_spec(invoice_templating = Name, #{}) -> get_service_spec(processor = Name, #{namespace := Ns}) when is_binary(Ns) -> {?VERSION_PREFIX ++ "/stateproc/" ++ binary_to_list(Ns), get_service(Name)}; get_service_spec(proxy_host_provider = Name, #{}) -> - {?VERSION_PREFIX ++ "/proxyhost/provider", get_service(Name)}; -get_service_spec(invoice_trace = Name, #{}) -> - {?VERSION_PREFIX ++ "/trace/invoice", get_service(Name)}; -get_service_spec(invoice_template_trace = Name, #{}) -> - {?VERSION_PREFIX ++ "/trace/invoice_template", get_service(Name)}. + {?VERSION_PREFIX ++ "/proxyhost/provider", get_service(Name)}. From 4fac72bab3efa19d9682bf61d37b7a717893f11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 15 Jun 2026 19:43:44 +0300 Subject: [PATCH 43/62] Refactor auxiliary state marshaling and unmarshaling in hg_invoice and hg_invoice_template modules. Introduce new helper functions for content handling and add tests for roundtrip serialization and legacy content management. Ensure compatibility with legacy data formats while improving code clarity. --- apps/hellgate/src/hg_invoice.erl | 26 ++++++++++++------ apps/hellgate/src/hg_invoice_template.erl | 27 ++++++++++++++++--- .../test/legacy_fixture_golden_test.erl | 8 ++++++ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 0ddb839c..680fb33a 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -1050,15 +1050,22 @@ unmarshal_event_body(Format, _Payload) -> -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - msgpack_payload_to_binary(mg_msgpack_marshalling:marshal(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}); - %% the current branch stores term_to_binary(mg_msgpack_marshalling:marshal(AuxSt)). - %% Both unwrap to a msgpack Value that mg_msgpack_marshalling:unmarshal decodes. + %% 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, <<>>}} -> #{}; @@ -1179,15 +1186,18 @@ aux_state_empty_test() -> -spec aux_state_reads_legacy_mg_content_test() -> _. aux_state_reads_legacy_mg_content_test() -> - %% Legacy hg_progressor stored term_to_binary(#mg_stateproc_Content{data = Msgp}). AuxSt = #{<<"legacy">> => 1}, - Msgp = mg_msgpack_marshalling:marshal(AuxSt), - Legacy = term_to_binary(#mg_stateproc_Content{format_version = 1, data = Msgp}), + 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() -> - %% CT-captured empty invoice aux: Content with {bin, <<>>} data. Legacy = term_to_binary(#mg_stateproc_Content{format_version = undefined, data = {bin, <<>>}}), ?assertEqual(#{}, unmarshal_aux_state(Legacy)). diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index e842335b..b9507cd7 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -357,13 +357,22 @@ unmarshal_event_body(Format, _Payload) -> -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - msgpack_payload_to_binary(mg_msgpack_marshalling:marshal(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) -> - %% Same compat as hg_invoice: legacy #mg_stateproc_Content{} or current msgpack blob. + %% 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, <<>>}} -> #{}; @@ -425,11 +434,21 @@ unmarshal_event_payload(#{format_version := 1, data := {bin, Changes}}) -> -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}, - Msgp = mg_msgpack_marshalling:marshal(AuxSt), - Legacy = term_to_binary(#mg_stateproc_Content{format_version = 1, data = Msgp}), + 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/prg_machine/test/legacy_fixture_golden_test.erl b/apps/prg_machine/test/legacy_fixture_golden_test.erl index 21a5e7c6..20e9ec68 100644 --- a/apps/prg_machine/test/legacy_fixture_golden_test.erl +++ b/apps/prg_machine/test/legacy_fixture_golden_test.erl @@ -46,6 +46,9 @@ legacy_hg_invoice_metadata_test_() -> legacy_hg_invoice_aux_state_test_() -> {"hg_invoice_aux_state", fun legacy_hg_invoice_aux_state_test/0}. +legacy_hg_aux_state_rollback_test_() -> + {"hg_invoice_aux_state_rollback", fun legacy_hg_aux_state_rollback_test/0}. + legacy_hg_call_args_test_() -> {"hg_call_args", fun legacy_hg_call_args_test/0}. @@ -101,6 +104,11 @@ legacy_hg_invoice_aux_state_test() -> Aux = legacy_fixture_lib:read_aux_state(Dir), ?assertEqual(#{}, hg_invoice:unmarshal_aux_state(Aux)). +legacy_hg_aux_state_rollback_test() -> + Dir = legacy_fixture_lib:hg_invoice_dir(), + LegacyAux = legacy_fixture_lib:read_aux_state(Dir), + ?assertEqual(LegacyAux, hg_invoice:marshal_aux_state(#{})). + legacy_hg_call_args_test() -> Dir = legacy_fixture_lib:hg_invoice_dir(), Bin = legacy_fixture_lib:read_bin(Dir, "call_args_thrift_get.bin"), From 9bd367209664022ddfc953dfc701d54e89f56b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 16 Jun 2026 16:28:05 +0300 Subject: [PATCH 44/62] Refactor ff_transfer modules to utilize ff_machine_lib for common operations. Simplify event handling, state marshaling, and error management across ff_deposit_machine, ff_destination_machine, ff_source_machine, ff_withdrawal_machine, and ff_withdrawal_session_machine. Enhance code clarity and maintainability by reducing redundancy and improving consistency in function calls. --- .../src/ff_withdrawal_session_codec.erl | 2 +- apps/ff_transfer/src/ff_deposit_machine.erl | 88 ++-------- .../src/ff_destination_machine.erl | 73 ++------ apps/ff_transfer/src/ff_machine_codec.erl | 40 ++--- apps/ff_transfer/src/ff_machine_lib.erl | 156 +++++++++++++++++- apps/ff_transfer/src/ff_source_machine.erl | 74 ++------- .../ff_transfer/src/ff_withdrawal_machine.erl | 92 ++--------- .../src/ff_withdrawal_session_machine.erl | 106 +++--------- 8 files changed, 225 insertions(+), 406 deletions(-) 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_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index 9f52acff..4e326c46 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -72,10 +72,6 @@ -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -%% Pipeline - --import(ff_pipeline, [do/1, unwrap/1]). - %% Internal types -type ctx() :: ff_entity_context:context(). @@ -90,11 +86,7 @@ ok | {error, ff_deposit:create_error() | exists}. create(Params, Ctx) -> - do(fun() -> - #{id := ID} = Params, - Events = unwrap(ff_deposit:create(Params)), - unwrap(prg_machine:start(?NS, ID, {Events, Ctx})) - end). + ff_machine_lib:create(?NS, fun ff_deposit:create/1, Params, Ctx). -spec get(id()) -> {ok, st()} @@ -106,45 +98,18 @@ get(ID) -> {ok, st()} | {error, unknown_deposit_error()}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of - {ok, Machine} -> - {ok, machine_to_st(Machine)}; - {error, notfound} -> - {error, {unknown_deposit, ID}}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:get(?NS, ID, {After, Limit}, ff_deposit, {unknown_deposit, ID}). -spec events(id(), event_range()) -> {ok, [event()]} | {error, unknown_deposit_error()}. events(ID, {After, Limit}) -> - case prg_machine:get_history(?NS, ID, After, Limit, forward) of - {ok, History} -> - {ok, ff_machine_lib:history_to_events(History)}; - {error, notfound} -> - {error, {unknown_deposit, ID}}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:events(?NS, ID, {After, Limit}, {unknown_deposit, ID}). -spec repair(id(), ff_repair:scenario()) -> {ok, repair_response()} | {error, repair_call_error()}. repair(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. + ff_machine_lib:repair(?NS, ID, Scenario). %% Accessors @@ -173,7 +138,7 @@ init({Events, Ctx}, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, Machine) -> Deposit = prg_machine:collapse(ff_deposit, Machine), - process_transfer_result(ff_deposit:process_transfer(Deposit), Machine). + ff_machine_lib:to_prg_result(ff_deposit:process_transfer(Deposit)). -spec process_call(term(), machine()) -> no_return(). process_call(CallArgs, _Machine) -> @@ -181,51 +146,20 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(ff_deposit, ff_machine_lib:to_repair_machine(Machine), Scenario) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. + ff_machine_lib:process_repair(ff_deposit, Machine, Scenario). -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(deposit, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + ff_machine_lib:marshal_event_body(deposit, ?EVENT_FORMAT_VERSION, Body). -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(deposit, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). +unmarshal_event_body(Format, Payload) -> + ff_machine_lib:unmarshal_event_body(deposit, ?EVENT_FORMAT_VERSION, Format, Payload). -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - ff_machine_codec: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_codec:unmarshal_aux_state(Payload). - -%% Internals - --spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{aux_state := undefined} = Machine) -> - machine_to_st(Machine#{aux_state => #{}}); -machine_to_st(#{aux_state := AuxState} = Machine) -> - Model = prg_machine:collapse(ff_deposit, Machine), - Ctx = maps:get(ctx, AuxState, #{}), - #{ - model => Model, - ctx => Ctx - }. - --spec process_transfer_result({prg_action:t(), [change()]}, machine()) -> prg_result(). -process_transfer_result({Action, Events}, Machine) -> - #{ - events => Events, - action => Action, - auxst => maps:get(aux_state, Machine, #{}) - }. + ff_machine_lib:unmarshal_aux_state(Payload). diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index 825b55ba..e2461202 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -56,16 +56,11 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). --export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/2]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -%% Pipeline - --import(ff_pipeline, [do/1, unwrap/1]). - -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). @@ -77,11 +72,7 @@ ok | {error, ff_destination:create_error() | exists}. create(Params, Ctx) -> - do(fun() -> - #{id := ID} = Params, - Events = unwrap(ff_destination:create(Params)), - unwrap(prg_machine:start(?NS, ID, {Events, Ctx})) - end). + ff_machine_lib:create(?NS, fun ff_destination:create/1, Params, Ctx). -spec get(id()) -> {ok, st()} @@ -93,27 +84,13 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of - {ok, Machine} -> - {ok, machine_to_st(Machine)}; - {error, notfound} -> - {error, notfound}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:get(?NS, ID, {After, Limit}, ff_destination, notfound). -spec events(id(), event_range()) -> {ok, events()} | {error, notfound}. events(ID, {After, Limit}) -> - case prg_machine:get_history(?NS, ID, After, Limit, forward) of - {ok, History} -> - {ok, ff_machine_lib:history_to_events(History)}; - {error, notfound} -> - {error, notfound}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:events(?NS, ID, {After, Limit}, notfound). %% Accessors @@ -133,10 +110,7 @@ namespace() -> -spec init({[change()], ctx()}, machine()) -> prg_result(). init({Events, Ctx}, _Machine) -> - #{ - events => Events, - auxst => #{ctx => Ctx} - }. + ff_machine_lib:init_result(Events, Ctx). -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, _Machine) -> @@ -148,47 +122,20 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(ff_destination, ff_machine_lib:to_repair_machine(Machine), Scenario) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. - --spec process_notification(term(), machine()) -> prg_result(). -process_notification(_Args, _Machine) -> - #{}. + ff_machine_lib:process_repair(ff_destination, Machine, Scenario). -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(destination, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + ff_machine_lib:marshal_event_body(destination, ?EVENT_FORMAT_VERSION, Body). -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(destination, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). +unmarshal_event_body(Format, Payload) -> + ff_machine_lib:unmarshal_event_body(destination, ?EVENT_FORMAT_VERSION, Format, Payload). -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - ff_machine_codec: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_codec:unmarshal_aux_state(Payload). - -%% Internals - --spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{aux_state := undefined} = Machine) -> - machine_to_st(Machine#{aux_state => #{}}); -machine_to_st(#{aux_state := AuxState} = Machine) -> - Model = prg_machine:collapse(ff_destination, Machine), - Ctx = maps:get(ctx, AuxState, #{}), - #{ - model => Model, - ctx => Ctx - }. + 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 index f1b644c1..de57fca8 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -5,7 +5,8 @@ -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -export([payload_to_binary/1]). --export([binary_to_payload/1]). + +-export_type([domain/0]). -type domain() :: deposit | source | destination | withdrawal | withdrawal_session. -type format_version() :: pos_integer(). @@ -89,9 +90,7 @@ unmarshal_event(withdrawal_session, 1, Payload) -> unmarshal_event(Domain, Format, _Payload) -> erlang:error({unknown_event_format, Domain, Format}). -%% aux_state: write in the legacy envelope term_to_binary(AuxSt) (as the old -%% machinery_prg_backend did). Reading tries both: legacy term first, then the -%% msgpack-thrift form this branch briefly wrote. +%% 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). @@ -100,12 +99,7 @@ marshal_aux_state(AuxSt) -> unmarshal_aux_state(<<>>) -> #{}; unmarshal_aux_state(Payload) when is_binary(Payload) -> - try - binary_to_term(Payload) - catch - error:badarg -> - ff_machine_schema:unmarshal(binary_to_payload(Payload)) - end. + binary_to_term(Payload). %% Event payload: write the legacy envelope term_to_binary({bin, ThriftBin}) %% (machinery_prg_backend used machinery_utils:encode(term, ...)). @@ -113,11 +107,6 @@ unmarshal_aux_state(Payload) when is_binary(Payload) -> payload_to_binary(Payload) -> term_to_binary(Payload). --spec binary_to_payload(binary()) -> ff_msgpack:t(). -binary_to_payload(Bin) when is_binary(Bin) -> - {ok, Payload} = ff_msgpack:unpack(Bin), - Payload. - -spec marshal_thrift_event( timestamped_event(), fun((timestamped_event()) -> term()), @@ -129,19 +118,14 @@ marshal_thrift_event(Timestamped, MarshalFun, ThriftModule, ThriftStruct) -> Type = {struct, struct, {ThriftModule, ThriftStruct}}, {bin, ff_proto_utils:serialize(Type, ThriftChange)}. -%% Sniff the stored payload: legacy events are term_to_binary({bin, ThriftBin}) -%% (first byte 131); events written by this branch are raw thrift. The sniff is -%% only safe in this direction — a thrift struct never starts with 131, whereas -%% msgpack fixmap(3) is 0x83, so we must check the term envelope first. -sniff_thrift_payload(<<131, _/binary>> = Payload) -> +%% 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; -sniff_thrift_payload(Payload) when is_binary(Payload) -> - Payload. + end. -spec unmarshal_thrift_event( binary(), @@ -150,7 +134,7 @@ sniff_thrift_payload(Payload) when is_binary(Payload) -> atom() ) -> timestamped_event(). unmarshal_thrift_event(Payload, UnmarshalFun, ThriftModule, ThriftStruct) -> - ThriftBin = sniff_thrift_payload(Payload), + ThriftBin = legacy_thrift_payload(Payload), Type = {struct, struct, {ThriftModule, ThriftStruct}}, ThriftChange = ff_proto_utils:deserialize(Type, ThriftBin), UnmarshalFun(ThriftChange). @@ -177,12 +161,10 @@ aux_state_reads_legacy_term_to_binary_test() -> AuxSt = #{ctx => #{}, model => legacy}, ?assertEqual(AuxSt, unmarshal_aux_state(term_to_binary(AuxSt))). --spec sniff_thrift_payload_reads_legacy_envelope_test() -> _. -sniff_thrift_payload_reads_legacy_envelope_test() -> +-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, sniff_thrift_payload(term_to_binary({bin, ThriftBin}))), - %% New format: raw thrift binary returned as-is. - ?assertEqual(ThriftBin, sniff_thrift_payload(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 index fc5045d4..cd546d35 100644 --- a/apps/ff_transfer/src/ff_machine_lib.erl +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -3,17 +3,35 @@ %%% 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([unmarshal_event_body/4]). +-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(). @@ -24,6 +42,102 @@ | {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}, Domain, NotFoundError) -> + case prg_machine:get(NS, ID, prg_machine:history_range(After, Limit, forward)) of + {ok, Machine} -> + {ok, machine_to_st(Domain, 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(Domain, #{aux_state := AuxState} = Machine) -> + #{ + model => prg_machine:collapse(Domain, 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(Domain, Machine, Scenario) -> + case ff_repair:apply_scenario(Domain, 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(Domain, Machine, Scenario, ScenarioProcessors) -> + case ff_repair:apply_scenario(Domain, 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}) -> #{ @@ -34,12 +148,14 @@ to_repair_machine(#{namespace := NS, id := ID, history := History, aux_state := }. -spec from_repair_result(ff_repair:scenario_result(), prg_machine:machine()) -> prg_machine:result(). -from_repair_result(#{events := Events} = Result, Machine) -> - #{ - events => repair_events_to_domain(Events), - action => maps:get(action, Result, idle), - auxst => maps:get(aux_state, Result, maps:get(aux_state, Machine, #{})) - }. +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) -> @@ -61,3 +177,31 @@ 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) -> + Timestamped = {ev, prg_machine: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(), pos_integer(), pos_integer(), binary()) -> + prg_machine:event_body(). +unmarshal_event_body(Domain, Format, Format, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(Domain, Format, Payload), + event_body_from_timestamped(Timestamped); +unmarshal_event_body(_Domain, _ExpectedFormat, Format, _Payload) -> + erlang:error({unknown_event_format, Format}). + +-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_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index cbcbfdfe..84d2f2b3 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -56,16 +56,11 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). --export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/2]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -%% Pipeline - --import(ff_pipeline, [do/1, unwrap/1]). - -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). @@ -77,11 +72,7 @@ ok | {error, ff_source:create_error() | exists}. create(Params, Ctx) -> - do(fun() -> - #{id := ID} = Params, - Events = unwrap(ff_source:create(Params)), - unwrap(prg_machine:start(?NS, ID, {Events, Ctx})) - end). + ff_machine_lib:create(?NS, fun ff_source:create/1, Params, Ctx). -spec get(id()) -> {ok, st()} @@ -93,27 +84,13 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of - {ok, Machine} -> - {ok, machine_to_st(Machine)}; - {error, notfound} -> - {error, notfound}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:get(?NS, ID, {After, Limit}, ff_source, notfound). -spec events(id(), event_range()) -> {ok, events()} | {error, notfound}. events(ID, {After, Limit}) -> - case prg_machine:get_history(?NS, ID, After, Limit, forward) of - {ok, History} -> - {ok, ff_machine_lib:history_to_events(History)}; - {error, notfound} -> - {error, notfound}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:events(?NS, ID, {After, Limit}, notfound). %% Accessors @@ -133,11 +110,7 @@ namespace() -> -spec init({[change()], ctx()}, machine()) -> prg_result(). init({Events, Ctx}, _Machine) -> - #{ - events => Events, - action => timeout, - auxst => #{ctx => Ctx} - }. + ff_machine_lib:init_result(Events, Ctx). -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, _Machine) -> @@ -149,47 +122,20 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(ff_source, ff_machine_lib:to_repair_machine(Machine), Scenario) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. - --spec process_notification(term(), machine()) -> prg_result(). -process_notification(_Args, _Machine) -> - #{}. + ff_machine_lib:process_repair(ff_source, Machine, Scenario). -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(source, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + ff_machine_lib:marshal_event_body(source, ?EVENT_FORMAT_VERSION, Body). -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(source, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). +unmarshal_event_body(Format, Payload) -> + ff_machine_lib:unmarshal_event_body(source, ?EVENT_FORMAT_VERSION, Format, Payload). -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - ff_machine_codec: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_codec:unmarshal_aux_state(Payload). - -%% Internals - --spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{aux_state := undefined} = Machine) -> - machine_to_st(Machine#{aux_state => #{}}); -machine_to_st(#{aux_state := AuxState} = Machine) -> - Model = prg_machine:collapse(ff_source, Machine), - Ctx = maps:get(ctx, AuxState, #{}), - #{ - model => Model, - ctx => Ctx - }. + ff_machine_lib:unmarshal_aux_state(Payload). diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index f9b05496..abaf5b88 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -91,10 +91,6 @@ -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -%% Pipeline - --import(ff_pipeline, [do/1, unwrap/1]). - %% Internal types -type ctx() :: ff_entity_context:context(). @@ -109,11 +105,7 @@ ok | {error, ff_withdrawal:create_error() | exists}. create(Params, Ctx) -> - do(fun() -> - #{id := ID} = Params, - Events = unwrap(ff_withdrawal:create(Params)), - unwrap(prg_machine:start(?NS, ID, {Events, Ctx})) - end). + ff_machine_lib:create(?NS, fun ff_withdrawal:create/1, Params, Ctx). -spec get(id()) -> {ok, st()} @@ -125,45 +117,18 @@ get(ID) -> {ok, st()} | {error, unknown_withdrawal_error()}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of - {ok, Machine} -> - {ok, machine_to_st(Machine)}; - {error, notfound} -> - {error, {unknown_withdrawal, ID}}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:get(?NS, ID, {After, Limit}, ff_withdrawal, {unknown_withdrawal, ID}). -spec events(id(), event_range()) -> {ok, [event()]} | {error, unknown_withdrawal_error()}. events(ID, {After, Limit}) -> - case prg_machine:get_history(?NS, ID, After, Limit, forward) of - {ok, History} -> - {ok, ff_machine_lib:history_to_events(History)}; - {error, notfound} -> - {error, {unknown_withdrawal, ID}}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:events(?NS, ID, {After, Limit}, {unknown_withdrawal, ID}). -spec repair(id(), ff_repair:scenario()) -> {ok, repair_response()} | {error, repair_call_error()}. repair(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. + ff_machine_lib:repair(?NS, ID, Scenario). -spec start_adjustment(id(), adjustment_params()) -> ok @@ -203,7 +168,7 @@ init({Events, Ctx}, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, Machine) -> Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), - process_transfer_result(ff_withdrawal:process_transfer(Withdrawal), Machine). + ff_machine_lib:to_prg_result(ff_withdrawal:process_transfer(Withdrawal)). -spec process_call({start_adjustment, adjustment_params()}, machine()) -> {ok | {error, start_adjustment_error()}, prg_result()}. @@ -211,7 +176,7 @@ process_call({start_adjustment, Params}, Machine) -> Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), case ff_withdrawal:start_adjustment(Params, Withdrawal) of {ok, Result} -> - {ok, process_transfer_result(Result, Machine)}; + {ok, ff_machine_lib:to_prg_result(Result)}; {error, _Reason} = Error -> {Error, #{}} end; @@ -220,64 +185,33 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - case ff_repair:apply_scenario(ff_withdrawal, ff_machine_lib:to_repair_machine(Machine), Scenario) of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. + ff_machine_lib:process_repair(ff_withdrawal, Machine, Scenario). -spec process_notification(notify_args(), machine()) -> prg_result(). process_notification({session_finished, SessionID, SessionResult}, Machine) -> Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), case ff_withdrawal:finalize_session(SessionID, SessionResult, Withdrawal) of {ok, Result} -> - process_transfer_result(Result, Machine); + ff_machine_lib:to_prg_result(Result); {error, Reason} -> erlang:error({unable_to_finalize_session, Reason}) end. -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + ff_machine_lib:marshal_event_body(withdrawal, ?EVENT_FORMAT_VERSION, Body). -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(withdrawal, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). +unmarshal_event_body(Format, Payload) -> + ff_machine_lib:unmarshal_event_body(withdrawal, ?EVENT_FORMAT_VERSION, Format, Payload). -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - ff_machine_codec: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_codec:unmarshal_aux_state(Payload). - -%% Internals - --spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{aux_state := undefined} = Machine) -> - machine_to_st(Machine#{aux_state => #{}}); -machine_to_st(#{aux_state := AuxState} = Machine) -> - Model = prg_machine:collapse(ff_withdrawal, Machine), - Ctx = maps:get(ctx, AuxState, #{}), - #{ - model => Model, - ctx => Ctx - }. - --spec process_transfer_result({prg_action:t(), [change()]}, machine()) -> prg_result(). -process_transfer_result({Action, Events}, Machine) -> - #{ - events => Events, - action => Action, - auxst => maps:get(aux_state, Machine, #{}) - }. + ff_machine_lib:unmarshal_aux_state(Payload). call(ID, Call) -> case prg_machine:call(?NS, ID, Call) of diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index f7e4a1e8..062e94a9 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -28,7 +28,6 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). --export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/2]). -export([marshal_aux_state/1]). @@ -42,6 +41,11 @@ -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]). @@ -56,7 +60,7 @@ ctx := ctx() }. -type session() :: ff_withdrawal_session:session_state(). --type event() :: ff_withdrawal_session:event(). +-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}. @@ -104,45 +108,18 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - case prg_machine:get(?NS, ID, prg_machine:history_range(After, Limit, forward)) of - {ok, Machine} -> - {ok, machine_to_st(Machine)}; - {error, notfound} -> - {error, notfound}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:get(?NS, ID, {After, Limit}, ff_withdrawal_session, notfound). -spec events(id(), event_range()) -> - {ok, [{integer(), timestamped_event(event())}]} + {ok, [event()]} | {error, notfound}. events(ID, {After, Limit}) -> - case prg_machine:get_history(?NS, ID, After, Limit, forward) of - {ok, History} -> - {ok, ff_machine_lib:history_to_events(History)}; - {error, notfound} -> - {error, notfound}; - {error, {exception, Class, Reason}} -> - erlang:error({process_exception, Class, Reason}) - end. + ff_machine_lib:events(?NS, ID, {After, Limit}, notfound). -spec repair(id(), ff_repair:scenario()) -> {ok, repair_response()} | {error, repair_call_error()}. repair(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. + ff_machine_lib:repair(?NS, ID, Scenario). -spec process_callback(callback_params()) -> {ok, process_callback_response()} @@ -163,16 +140,12 @@ namespace() -> -spec init([change()], machine()) -> prg_result(). init(Events, _Machine) -> - #{ - events => Events, - action => timeout, - auxst => #{ctx => ff_entity_context:new()} - }. + 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(ff_withdrawal_session, Machine), - process_session_result(ff_withdrawal_session:process_session(Session), 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()}. @@ -180,7 +153,7 @@ process_call({process_callback, Params}, Machine) -> Session = prg_machine:collapse(ff_withdrawal_session, Machine), case ff_withdrawal_session:process_callback(Params, Session) of {ok, {Response, Result}} -> - {{ok, Response}, process_session_result(Result, Machine)}; + {{ok, Response}, ff_machine_lib:to_prg_result(Result)}; {error, {Reason, _Result}} -> {{error, Reason}, #{}} end; @@ -196,64 +169,23 @@ process_repair(Scenario, Machine) -> {ok, {ok, #{action => Action, events => Events}}} end }, - case - ff_repair:apply_scenario( - ff_withdrawal_session, ff_machine_lib:to_repair_machine(Machine), Scenario, ScenarioProcessors - ) - of - {ok, {_Response, Result}} -> - ff_machine_lib:from_repair_result(Result, Machine); - {error, Reason} -> - {error, Reason} - end. - --spec process_notification(term(), machine()) -> prg_result(). -process_notification(_Args, _Machine) -> - #{}. + ff_machine_lib:process_repair(ff_withdrawal_session, Machine, Scenario, ScenarioProcessors). -spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> - Timestamped = {ev, prg_machine:timestamp(), Body}, - Encoded = ff_machine_codec:marshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Timestamped), - {?EVENT_FORMAT_VERSION, ff_machine_codec:payload_to_binary(Encoded)}. + ff_machine_lib:marshal_event_body(withdrawal_session, ?EVENT_FORMAT_VERSION, Body). -spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(withdrawal_session, ?EVENT_FORMAT_VERSION, Payload), - ff_machine_lib:event_body_from_timestamped(Timestamped); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). +unmarshal_event_body(Format, Payload) -> + ff_machine_lib:unmarshal_event_body(withdrawal_session, ?EVENT_FORMAT_VERSION, Format, Payload). -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> - ff_machine_codec: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_codec:unmarshal_aux_state(Payload). - -%% -%% Internals -%% - --spec machine_to_st(prg_machine:machine()) -> st(). -machine_to_st(#{aux_state := undefined} = Machine) -> - machine_to_st(Machine#{aux_state => #{}}); -machine_to_st(#{aux_state := AuxState} = Machine) -> - Model = prg_machine:collapse(ff_withdrawal_session, Machine), - Ctx = maps:get(ctx, AuxState, #{}), - #{ - model => Model, - ctx => Ctx - }. - --spec process_session_result(ff_withdrawal_session:process_result(), machine()) -> prg_result(). -process_session_result({Action, Events}, Machine) -> - #{ - events => Events, - action => Action, - auxst => maps:get(aux_state, Machine, #{}) - }. + ff_machine_lib:unmarshal_aux_state(Payload). call(Ref, Call) -> case prg_machine:call(?NS, Ref, Call) of From f4a966627c3f4f9876ae398ef4158179af4784c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 16 Jun 2026 17:45:58 +0300 Subject: [PATCH 45/62] minor --- apps/ff_server/test/ff_source_handler_SUITE.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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. From f8eaa842d9c0a1124dc6c0ac803c16c61b27c641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 16 Jun 2026 21:08:09 +0300 Subject: [PATCH 46/62] Enhance apply_event function across multiple modules to support additional parameters. Update specifications for apply_event in ff_deposit, ff_destination, ff_source, ff_withdrawal_session, and ff_withdrawal modules to include event_id and timestamp. This change improves event handling consistency and prepares for future enhancements in event processing. --- apps/ff_server/src/ff_codec.erl | 2 +- apps/ff_transfer/src/ff_deposit.erl | 6 + apps/ff_transfer/src/ff_destination.erl | 6 + apps/ff_transfer/src/ff_source.erl | 6 + apps/ff_transfer/src/ff_withdrawal.erl | 10 + .../ff_transfer/src/ff_withdrawal_session.erl | 6 + apps/prg_machine/src/prg_machine.erl | 501 +++--------------- apps/prg_machine/src/prg_machine_client.erl | 187 +++++++ apps/prg_machine/src/prg_machine_codec.erl | 21 + apps/prg_machine/src/prg_machine_env.erl | 93 ++++ apps/prg_machine/src/prg_machine_events.erl | 135 +++++ .../prg_machine/src/prg_machine_processor.erl | 87 +++ apps/prg_machine/src/prg_machine_registry.erl | 37 +- docs/prg-machine-fix-plan.md | 237 --------- docs/prg-machine.md | 29 +- 15 files changed, 672 insertions(+), 691 deletions(-) create mode 100644 apps/prg_machine/src/prg_machine_client.erl create mode 100644 apps/prg_machine/src/prg_machine_codec.erl create mode 100644 apps/prg_machine/src/prg_machine_env.erl create mode 100644 apps/prg_machine/src/prg_machine_events.erl create mode 100644 apps/prg_machine/src/prg_machine_processor.erl delete mode 100644 docs/prg-machine-fix-plan.md diff --git a/apps/ff_server/src/ff_codec.erl b/apps/ff_server/src/ff_codec.erl index a179638f..94365b6d 100644 --- a/apps/ff_server/src/ff_codec.erl +++ b/apps/ff_server/src/ff_codec.erl @@ -301,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) -> diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index e248ccec..f6c77817 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -117,6 +117,7 @@ %% Event source -export([apply_event/2]). +-export([apply_event/4]). %% Pipeline @@ -306,6 +307,11 @@ is_finished(#{status := pending}) -> apply_event(Ev, T0) -> apply_event_(Ev, T0). +-spec apply_event(prg_machine:event_id(), prg_machine:timestamp(), event(), deposit_state() | undefined) -> + deposit_state(). +apply_event(_EventID, _Timestamp, Ev, T) -> + apply_event(Ev, T). + -spec apply_event_(event(), deposit_state() | undefined) -> deposit_state(). apply_event_({created, T}, undefined) -> apply_negative_body(T); diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index eb4b210d..78d03527 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -108,6 +108,7 @@ -export([create/1]). -export([is_accessible/1]). -export([apply_event/2]). +-export([apply_event/4]). %% Pipeline @@ -222,3 +223,8 @@ 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}). + +-spec apply_event(prg_machine:event_id(), prg_machine:timestamp(), event(), ff_maybe:'maybe'(destination_state())) -> + destination_state(). +apply_event(_EventID, _Timestamp, Ev, Destination) -> + apply_event(Ev, Destination). diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index e5b2c3bb..6658fb6b 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -94,6 +94,7 @@ -export([create/1]). -export([is_accessible/1]). -export([apply_event/2]). +-export([apply_event/4]). %% Pipeline @@ -198,3 +199,8 @@ 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}). + +-spec apply_event(prg_machine:event_id(), prg_machine:timestamp(), event(), ff_maybe:'maybe'(source_state())) -> + source_state(). +apply_event(_EventID, _Timestamp, Ev, Source) -> + apply_event(Ev, Source). diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 2bcfcb0b..cbf127fa 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -249,6 +249,7 @@ %% Event source -export([apply_event/2]). +-export([apply_event/4]). %% Pipeline @@ -1822,6 +1823,15 @@ apply_event(Ev, T0) -> T2 = save_adjustable_info(Ev, T1), T2. +-spec apply_event( + prg_machine:event_id(), + prg_machine:timestamp(), + event() | legacy_event(), + ff_maybe:'maybe'(withdrawal_state()) +) -> withdrawal_state(). +apply_event(_EventID, _Timestamp, Ev, T0) -> + apply_event(Ev, T0). + -spec apply_event_(event(), ff_maybe:'maybe'(withdrawal_state())) -> withdrawal_state(). apply_event_({created, T}, undefined) -> make_state(T); diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index a35c63db..f9dc7fa7 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -25,6 +25,7 @@ %% ff_machine -export([apply_event/2]). +-export([apply_event/4]). %% ff_repair -export([set_session_result/2]). @@ -196,6 +197,11 @@ apply_event({callback, _Ev} = WrappedEvent, Session) -> Callbacks1 = ff_withdrawal_callback_utils:apply_event(WrappedEvent, Callbacks0), set_callbacks_index(Callbacks1, Session). +-spec apply_event(prg_machine:event_id(), prg_machine:timestamp(), event(), undefined | session_state()) -> + session_state(). +apply_event(_EventID, _Timestamp, Ev, Session) -> + apply_event(Ev, Session). + -spec process_session(session_state()) -> process_result(). process_session(#{status := {finished, _}, id := ID, result := Result, withdrawal := Withdrawal}) -> % Session has finished, it should notify the withdrawal machine about the fact diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index cd3e9beb..1a94fe67 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -1,12 +1,10 @@ -module(prg_machine). -%%% Unified runtime: HTTP/woody handlers -> domain (-behaviour(prg_machine)) -> progressor. -%%% Replaces hg_machine, ff_machine, machinery client/backend stack for progressor. +%%% Public facade for the unified progressor machine runtime. The implementation +%%% is split by role: client API, processor, env/context, codec and event fold. -include_lib("progressor/include/progressor.hrl"). --define(TABLE, prg_machine_dispatch). - %% Types -type namespace() :: namespace_id(). @@ -104,7 +102,8 @@ -callback unmarshal_aux_state(binary()) -> term(). -%% Optional: collapse passes event_id and timestamp (HG invoice). Default: apply_event/2. +%% Canonical collapse callback. Domain modules passed to collapse/2 adapt legacy +%% event folds at their boundary and expose only this arity to the runtime. -callback apply_event(event_id(), timestamp(), event_body(), term()) -> term(). -optional_callbacks([ @@ -141,6 +140,19 @@ -export([handler_namespace/1]). -export([unmarshal_event_body/3]). +%% Callback dispatch. Keep dynamic behaviour calls in this module: Elvis allows +%% them here because this is the module that defines the callbacks. + +-export([callback_init/3]). +-export([callback_process_signal/3]). +-export([callback_process_call/3]). +-export([callback_process_repair/3]). +-export([callback_process_notification/3]). +-export([callback_apply_event/5]). +-export([callback_marshal_event_body/2]). +-export([callback_marshal_aux_state/2]). +-export([callback_unmarshal_aux_state/2]). + %% Event-sourcing helpers (replaces ff_machine) -export([collapse/2]). @@ -152,189 +164,66 @@ -spec start(namespace(), id(), args()) -> {ok, ok} | {error, exists | 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, _} = Error -> - Error - end. + prg_machine_client:start(NS, ID, Args). -spec call(namespace(), id(), call()) -> {ok, response()} | {error, notfound | failed | term()}. call(NS, ID, CallArgs) -> - call(NS, ID, CallArgs, undefined, undefined, forward). + prg_machine_client:call(NS, ID, CallArgs). -spec call(namespace(), id(), call(), event_id() | undefined, non_neg_integer() | undefined, forward | backward) -> {ok, response()} | {error, notfound | failed | 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, _} = Error -> - Error - end. + prg_machine_client:call(NS, ID, CallArgs, After, Limit, Direction). -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, Reason} -> - %% The repair-failed reason is our own term encoded by process/3 - %% (marshal_process_result -> encode_term); hand it back as a term. - {error, {repair, {failed, decode_term(Reason)}}} - end. + prg_machine_client:repair(NS, ID, Args). -spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, get_error()}. get(NS, ID, Range) -> - Req = request(NS, ID, undefined, Range), - case progressor:get(Req) of - {ok, Process} -> - case get_handler_module(NS) of - {ok, Handler} -> - {ok, unmarshal_machine(Handler, NS, Process)}; - {error, _} = Error -> - Error - end; - {error, <<"process not found">>} -> - {error, notfound}; - {error, {exception, _Class, _Reason} = Exception} -> - {error, Exception}; - {error, {exception, Class, Reason, _Stacktrace}} -> - {error, {exception, Class, Reason}} - end. + prg_machine_client:get(NS, ID, Range). -spec get(namespace(), id()) -> {ok, machine()} | {error, get_error()}. get(NS, ID) -> - get(NS, ID, #{direction => forward}). + prg_machine_client:get(NS, ID). -spec get_history(namespace(), id()) -> {ok, history()} | {error, get_error()}. get_history(NS, ID) -> - get_history(NS, ID, undefined, undefined, forward). + prg_machine_client:get_history(NS, ID). -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). + prg_machine_client:get_history(NS, ID, After, Limit). -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. + prg_machine_client:get_history(NS, ID, After, Limit, Direction). -spec notify(namespace(), id(), args()) -> ok | {error, notfound | failed | processor_error() | term()}. notify(NS, ID, Args) -> - case call(NS, ID, {notify, Args}) of - {ok, _} -> ok; - {error, _} = Error -> Error - end. + prg_machine_client:notify(NS, ID, Args). -spec remove(namespace(), id()) -> ok | {error, notfound | failed | processor_error() | term()}. remove(NS, ID) -> - case call(NS, ID, remove) of - {ok, _} -> ok; - {error, _} = Error -> Error - end. + prg_machine_client:remove(NS, ID). -spec history_range(undefined | event_id(), undefined | non_neg_integer(), forward | backward) -> history_range(). history_range(Offset, Limit, Direction) -> - encode_range(Offset, Limit, Direction). + prg_machine_client:history_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) -> - Enter = resolve_env_enter(Opts), - Leave = resolve_env_leave(Opts), - try - case get_handler_module(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), - ok = run_env_enter(Enter, WoodyCtx), - %% Enter succeeded: from here Leave must run exactly once. Errors - %% raised before this point fall through to the outer catch and are - %% returned as {error, _} without being masked by a Leave exception. - run_with_env_leave(Leave, 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. +process(Call, Opts, BinCtx) -> + prg_machine_processor:process(Call, Opts, BinCtx). %% Registry @@ -346,62 +235,24 @@ get_child_spec(Handlers) -> handler_namespace(Handler) -> Handler:namespace(). -%% Event-sourcing (replaces ff_machine collapse/emit) +-spec callback_init(module(), args(), machine()) -> result(). +callback_init(Handler, Args, Machine) -> + Handler:init(Args, Machine). --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 callback_process_signal(module(), signal(), machine()) -> result(). +callback_process_signal(Handler, Signal, Machine) -> + Handler:process_signal(Signal, Machine). --spec emit_event(term()) -> [{ev, timestamp(), term()}]. -emit_event(Event) -> - emit_events([Event]). +-spec callback_process_call(module(), call(), machine()) -> {response(), result()}. +callback_process_call(Handler, Call, Machine) -> + Handler:process_call(Call, Machine). --spec emit_events([term()]) -> [{ev, timestamp(), term()}]. -emit_events(Events) -> - Ts = timestamp(), - [{ev, Ts, Body} || Body <- Events]. +-spec callback_process_repair(module(), args(), machine()) -> result() | {error, term()}. +callback_process_repair(Handler, Args, Machine) -> + Handler:process_repair(Args, Machine). --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), - dispatch_notification(Handler, Args, Machine); -dispatch(Handler, call, BinArgs, Machine) -> - case decode_term(BinArgs) of - {notify, Args} -> - dispatch_notification(Handler, 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. - -dispatch_notification(Handler, Args, Machine) -> +-spec callback_process_notification(module(), args(), machine()) -> result(). +callback_process_notification(Handler, Args, Machine) -> case erlang:function_exported(Handler, process_notification, 2) of true -> Handler:process_notification(Args, Machine); @@ -409,75 +260,12 @@ dispatch_notification(Handler, Args, Machine) -> #{} 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. +-spec callback_apply_event(module(), event_id(), timestamp(), event_body(), term()) -> term(). +callback_apply_event(Handler, EventID, Ts, Body, Model) -> + Handler:apply_event(EventID, Ts, Body, Model). -%% 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 := TsSec, - metadata := Meta, - payload := Payload -}) -> - Format = unmarshal_event_format(Meta), - Body = unmarshal_event_body(Handler, Format, Payload), - {EventID, event_timestamp_to_datetime(TsSec), Body}; -unmarshal_event(_Handler, #{event_id := EventID} = Ev) -> - erlang:error({missing_event_payload, EventID, maps:keys(Ev)}). - -marshal_new_events(Handler, LastEventID, Bodies) -> - %% One microsecond timestamp for the whole batch (as the old emit_events did). - %% The PG backend stores timestamptz with microseconds and auto-detects units. - Ts = erlang:system_time(microsecond), - lists:zipwith( - fun(EventID, Body) -> - {Format, Bin} = marshal_event_body(Handler, Body), - #{ - event_id => EventID, - timestamp => Ts, - metadata => event_metadata(Format), - payload => Bin - } - end, - lists:seq(LastEventID + 1, LastEventID + length(Bodies)), - Bodies - ). - -marshal_event_body(Handler, Body) -> +-spec callback_marshal_event_body(module(), event_body()) -> {undefined | pos_integer(), binary()}. +callback_marshal_event_body(Handler, Body) -> case erlang:function_exported(Handler, marshal_event_body, 1) of true -> Handler:marshal_event_body(Body); @@ -494,7 +282,8 @@ unmarshal_event_body(Handler, Format, Payload) -> binary_to_term(Payload) end. -marshal_aux_state(Handler, AuxSt) -> +-spec callback_marshal_aux_state(module(), term()) -> binary(). +callback_marshal_aux_state(Handler, AuxSt) -> case erlang:function_exported(Handler, marshal_aux_state, 1) of true -> Handler:marshal_aux_state(AuxSt); @@ -502,9 +291,10 @@ marshal_aux_state(Handler, AuxSt) -> term_to_binary(AuxSt) end. -unmarshal_aux_state(_Handler, undefined) -> +-spec callback_unmarshal_aux_state(module(), undefined | binary()) -> term(). +callback_unmarshal_aux_state(_Handler, undefined) -> undefined; -unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> +callback_unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> case erlang:function_exported(Handler, unmarshal_aux_state, 1) of true -> Handler:unmarshal_aux_state(Bin); @@ -512,159 +302,28 @@ unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> binary_to_term(Bin) end. -%% 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}. - -%% Read order: legacy HG <<"format_version">> → legacy FF <<"format">> → -%% atom format (defensive) → undefined. -unmarshal_event_format(Meta) -> - maps:get( - <<"format_version">>, - Meta, - maps:get(<<"format">>, Meta, maps:get(format, Meta, undefined)) - ). - -%% 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) -> - case erlang:function_exported(Handler, apply_event, 4) of - true -> - Handler:apply_event(EventID, Ts, Body, Model); - false -> - case erlang:function_exported(Handler, apply_event, 2) of - true -> - Handler:apply_event(Body, Model); - false -> - erlang:error({apply_event_not_defined, Handler}) - end - end. - -initial_model(_Handler, AuxState) when is_map(AuxState) -> - maps:get(model, AuxState, undefined); -initial_model(_Handler, _AuxState) -> - undefined. - -get_handler_module(NS) -> - prg_machine_registry:lookup(NS). - -%% 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)). - -resolve_env_enter(Opts) -> - case maps:is_key(env_enter, Opts) of - true -> - maps:get(env_enter, Opts); - false -> - case maps:get(context_binding, Opts, undefined) of - Binding when is_map(Binding) -> - fun(WoodyCtx) -> op_context:env_enter(WoodyCtx, Binding) end; - _ -> - fun(_) -> ok end - end - end. +%% Event-sourcing (replaces ff_machine collapse/emit) -resolve_env_leave(Opts) -> - case maps:is_key(env_leave, Opts) of - true -> - maps:get(env_leave, Opts); - false -> - case maps:get(context_binding, Opts, undefined) of - Binding when is_map(Binding) -> - fun() -> op_context:env_leave(Binding) end; - _ -> - fun() -> ok end - end - end. +-spec collapse(module(), machine()) -> term(). +collapse(Handler, Machine) -> + prg_machine_events:collapse(Handler, Machine). -run_env_enter(Enter, WoodyCtx) when is_function(Enter, 1) -> - Enter(WoodyCtx); -run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> - Enter(). - -run_with_env_leave(Leave, Fun) when is_function(Leave, 0), is_function(Fun, 0) -> - try Fun() of - Result -> - safe_env_leave(Leave), - Result - catch - Class:Reason:Stacktrace -> - safe_env_leave(Leave), - erlang:raise(Class, Reason, Stacktrace) - end. +-spec emit_event(term()) -> [{ev, timestamp(), term()}]. +emit_event(Event) -> + prg_machine_events:emit_event(Event). -safe_env_leave(Leave) -> - try - Leave() - catch - Class:Reason:Stacktrace -> - logger:error( - "prg_machine env_leave failed: ~p:~p", - [Class, Reason], - #{stacktrace => Stacktrace} - ) - end. +-spec emit_events([term()]) -> [{ev, timestamp(), term()}]. +emit_events(Events) -> + prg_machine_events:emit_events(Events). -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}. +-spec timestamp() -> timestamp(). +timestamp() -> + prg_machine_events:timestamp(). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). +-define(TABLE, prg_machine_dispatch). -define(TEST_NS, env_test_ns). -define(TEST_REGISTRY_KEY, {p, l, prg_machine_env_test_context}). -define(TEST_BINDING, #{ @@ -799,7 +458,7 @@ cleanup_aux_state_test(_) -> -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, #{}), + Intent = prg_machine_processor:marshal_intent(prg_machine_aux_state_test_handler, 0, #{}), ?assertNot(maps:is_key(aux_state, Intent)), ?assertEqual([], maps:get(events, Intent)). @@ -911,20 +570,20 @@ process_crash_conforms_progressor_exception() -> 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)). + ?assertEqual(#{<<"format_version">> => 1, <<"format">> => 1}, prg_machine_events:event_metadata(1)), + ?assertEqual(#{<<"format_version">> => 0, <<"format">> => 0}, prg_machine_events:event_metadata(undefined)). -spec unmarshal_event_format_reads_legacy_keys_test() -> _. unmarshal_event_format_reads_legacy_keys_test() -> %% New (both keys). - ?assertEqual(2, unmarshal_event_format(#{<<"format_version">> => 2, <<"format">> => 2})), + ?assertEqual(2, prg_machine_events:unmarshal_event_format(#{<<"format_version">> => 2, <<"format">> => 2})), %% Legacy HG metadata: only <<"format_version">>. - ?assertEqual(1, unmarshal_event_format(#{<<"format_version">> => 1})), + ?assertEqual(1, prg_machine_events:unmarshal_event_format(#{<<"format_version">> => 1})), %% Legacy FF metadata: only <<"format">>. - ?assertEqual(1, unmarshal_event_format(#{<<"format">> => 1})), + ?assertEqual(1, prg_machine_events:unmarshal_event_format(#{<<"format">> => 1})), %% Defensive atom key and absence. - ?assertEqual(3, unmarshal_event_format(#{format => 3})), - ?assertEqual(undefined, unmarshal_event_format(#{})). + ?assertEqual(3, prg_machine_events:unmarshal_event_format(#{format => 3})), + ?assertEqual(undefined, prg_machine_events:unmarshal_event_format(#{})). -spec decode_term_reads_legacy_double_envelope_test() -> _. decode_term_reads_legacy_double_envelope_test() -> @@ -932,12 +591,12 @@ decode_term_reads_legacy_double_envelope_test() -> %% 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)), + ?assertEqual(Args, prg_machine_codec:decode_term(Legacy)), %% New single envelope still works (rollback/forward invariant). - ?assertEqual(Args, decode_term(encode_term(Args))), + ?assertEqual(Args, prg_machine_codec:decode_term(prg_machine_codec: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}))). + ?assertEqual({bin, not_a_binary}, prg_machine_codec:decode_term(term_to_binary({bin, not_a_binary}))). -endif. diff --git a/apps/prg_machine/src/prg_machine_client.erl b/apps/prg_machine/src/prg_machine_client.erl new file mode 100644 index 00000000..5ccd0f44 --- /dev/null +++ b/apps/prg_machine/src/prg_machine_client.erl @@ -0,0 +1,187 @@ +-module(prg_machine_client). + +-include_lib("progressor/include/progressor.hrl"). + +-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([remove/2]). +-export([history_range/3]). + +-spec start(prg_machine:namespace(), id(), prg_machine:args()) -> {ok, ok} | {error, exists | term()}. +start(NS, ID, Args) -> + Req = #{ + ns => NS, + id => ID, + args => prg_machine_codec:encode_term(Args), + context => prg_machine_env:encode_rpc_context() + }, + case progressor:init(Req) of + {ok, ok} = Ok -> + Ok; + {error, <<"process already exists">>} -> + {error, exists}; + {error, _} = Error -> + Error + end. + +-spec call(prg_machine:namespace(), id(), prg_machine:call()) -> + {ok, prg_machine:response()} | {error, notfound | failed | term()}. +call(NS, ID, CallArgs) -> + call(NS, ID, CallArgs, undefined, undefined, forward). + +-spec call( + prg_machine:namespace(), + id(), + prg_machine:call(), + prg_machine:event_id() | undefined, + non_neg_integer() | undefined, + forward | backward +) -> + {ok, prg_machine:response()} | {error, notfound | failed | 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, prg_machine_codec: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, _} = Error -> + Error + end. + +-spec repair(prg_machine:namespace(), id(), prg_machine:args()) -> + {ok, term()} | {error, prg_machine:repair_error()}. +repair(NS, ID, Args) -> + Req = #{ + ns => NS, + id => ID, + args => prg_machine_codec:encode_term(Args), + context => prg_machine_env:encode_rpc_context() + }, + case progressor:repair(Req) of + {ok, Response} -> + {ok, prg_machine_codec: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, Reason} -> + %% The repair-failed reason is our own term encoded by process/3 + %% (marshal_process_result -> encode_term); hand it back as a term. + {error, {repair, {failed, prg_machine_codec:decode_term(Reason)}}} + end. + +-spec get(prg_machine:namespace(), id(), history_range()) -> + {ok, prg_machine:machine()} | {error, prg_machine:get_error()}. +get(NS, ID, Range) -> + Req = request(NS, ID, undefined, Range), + case progressor:get(Req) of + {ok, Process} -> + case prg_machine_registry:lookup(NS) of + {ok, Handler} -> + {ok, prg_machine_events:unmarshal_machine(Handler, NS, Process)}; + {error, _} = Error -> + Error + end; + {error, <<"process not found">>} -> + {error, notfound}; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception}; + {error, {exception, Class, Reason, _Stacktrace}} -> + {error, {exception, Class, Reason}} + end. + +-spec get(prg_machine:namespace(), id()) -> {ok, prg_machine:machine()} | {error, prg_machine:get_error()}. +get(NS, ID) -> + get(NS, ID, #{direction => forward}). + +-spec get_history(prg_machine:namespace(), id()) -> {ok, prg_machine:history()} | {error, prg_machine:get_error()}. +get_history(NS, ID) -> + get_history(NS, ID, undefined, undefined, forward). + +-spec get_history( + prg_machine:namespace(), + id(), + prg_machine:event_id() | undefined, + non_neg_integer() | undefined +) -> + {ok, prg_machine:history()} | {error, prg_machine:get_error()}. +get_history(NS, ID, After, Limit) -> + get_history(NS, ID, After, Limit, forward). + +-spec get_history( + prg_machine:namespace(), + id(), + prg_machine:event_id() | undefined, + non_neg_integer() | undefined, + forward | backward +) -> + {ok, prg_machine:history()} | {error, prg_machine: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(prg_machine:namespace(), id(), prg_machine:args()) -> + ok | {error, notfound | failed | prg_machine:processor_error() | term()}. +notify(NS, ID, Args) -> + case call(NS, ID, {notify, Args}) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. + +-spec remove(prg_machine:namespace(), id()) -> + ok | {error, notfound | failed | prg_machine:processor_error() | term()}. +remove(NS, ID) -> + case call(NS, ID, remove) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. + +-spec history_range(undefined | prg_machine:event_id(), undefined | non_neg_integer(), forward | backward) -> + history_range(). +history_range(Offset, Limit, Direction) -> + encode_range(Offset, Limit, Direction). + +request(NS, ID, Args, Range) -> + genlib_map:compact(#{ + ns => NS, + id => ID, + args => prg_machine_codec:encode_term(Args), + context => prg_machine_env:encode_rpc_context(), + range => Range + }). + +encode_range(After, Limit, Direction) -> + genlib_map:compact(#{ + offset => After, + limit => Limit, + direction => Direction + }). diff --git a/apps/prg_machine/src/prg_machine_codec.erl b/apps/prg_machine/src/prg_machine_codec.erl new file mode 100644 index 00000000..09a66496 --- /dev/null +++ b/apps/prg_machine/src/prg_machine_codec.erl @@ -0,0 +1,21 @@ +-module(prg_machine_codec). + +-export([encode_term/1]). +-export([decode_term/1]). + +-spec encode_term(term()) -> binary(). +encode_term(Term) -> + term_to_binary(Term). + +-spec decode_term(term()) -> 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. diff --git a/apps/prg_machine/src/prg_machine_env.erl b/apps/prg_machine/src/prg_machine_env.erl new file mode 100644 index 00000000..624ba23b --- /dev/null +++ b/apps/prg_machine/src/prg_machine_env.erl @@ -0,0 +1,93 @@ +-module(prg_machine_env). + +-export([encode_rpc_context/0]). +-export([run/3]). + +%% Default woody deadline (30s, configurable per namespace via opts), restoring +%% the old hg_progressor behaviour. +-define(DEFAULT_HANDLING_TIMEOUT, 30000). + +-spec encode_rpc_context() -> binary(). +encode_rpc_context() -> + WoodyContext = op_context:current_woody_context(), + prg_machine_codec:encode_term(woody_rpc_helper:encode_rpc_context(WoodyContext, otel_ctx:get_current())). + +-spec run(binary(), prg_machine:process_options(), fun(() -> Result)) -> Result. +run(BinCtx, Opts, Fun) when is_function(Fun, 0) -> + Enter = resolve_env_enter(Opts), + Leave = resolve_env_leave(Opts), + {WoodyCtx0, OtelCtx} = decode_rpc_context(BinCtx), + ok = woody_rpc_helper:attach_otel_context(OtelCtx), + WoodyCtx = ensure_deadline_set(WoodyCtx0, Opts), + ok = run_env_enter(Enter, WoodyCtx), + %% Enter succeeded: from here Leave must run exactly once. Errors raised + %% before this point fall through to the processor catch unchanged. + run_with_env_leave(Leave, Fun). + +decode_rpc_context(<<>>) -> + woody_rpc_helper:decode_rpc_context(#{}); +decode_rpc_context(Bin) -> + woody_rpc_helper:decode_rpc_context(prg_machine_codec:decode_term(Bin)). + +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. + +resolve_env_enter(Opts) -> + case maps:is_key(env_enter, Opts) of + true -> + maps:get(env_enter, Opts); + false -> + case maps:get(context_binding, Opts, undefined) of + Binding when is_map(Binding) -> + fun(WoodyCtx) -> op_context:env_enter(WoodyCtx, Binding) end; + _ -> + fun(_) -> ok end + end + end. + +resolve_env_leave(Opts) -> + case maps:is_key(env_leave, Opts) of + true -> + maps:get(env_leave, Opts); + false -> + case maps:get(context_binding, Opts, undefined) of + Binding when is_map(Binding) -> + fun() -> op_context:env_leave(Binding) end; + _ -> + fun() -> ok end + end + end. + +run_env_enter(Enter, WoodyCtx) when is_function(Enter, 1) -> + Enter(WoodyCtx); +run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> + Enter(). + +run_with_env_leave(Leave, Fun) when is_function(Leave, 0), is_function(Fun, 0) -> + try Fun() of + Result -> + safe_env_leave(Leave), + Result + catch + Class:Reason:Stacktrace -> + safe_env_leave(Leave), + erlang:raise(Class, Reason, Stacktrace) + end. + +safe_env_leave(Leave) -> + try + Leave() + catch + Class:Reason:Stacktrace -> + logger:error( + "prg_machine env_leave failed: ~p:~p", + [Class, Reason], + #{stacktrace => Stacktrace} + ) + end. diff --git a/apps/prg_machine/src/prg_machine_events.erl b/apps/prg_machine/src/prg_machine_events.erl new file mode 100644 index 00000000..c15c6ea4 --- /dev/null +++ b/apps/prg_machine/src/prg_machine_events.erl @@ -0,0 +1,135 @@ +-module(prg_machine_events). + +-export([collapse/2]). +-export([emit_event/1]). +-export([emit_events/1]). +-export([timestamp/0]). +-export([unmarshal_machine/3]). +-export([marshal_new_events/3]). +-export([marshal_aux_state/2]). + +-ifdef(TEST). +-export([event_metadata/1]). +-export([unmarshal_event_format/1]). +-endif. + +-spec collapse(module(), prg_machine:machine()) -> term(). +collapse(Handler, #{history := History, aux_state := AuxState}) -> + lists:foldl( + fun({EventID, Ts, Body}, Model) -> + prg_machine:callback_apply_event(Handler, EventID, Ts, Body, Model) + end, + initial_model(AuxState), + History + ). + +-spec emit_event(term()) -> [{ev, prg_machine:timestamp(), term()}]. +emit_event(Event) -> + emit_events([Event]). + +-spec emit_events([term()]) -> [{ev, prg_machine:timestamp(), term()}]. +emit_events(Events) -> + Ts = timestamp(), + [{ev, Ts, Body} || Body <- Events]. + +-spec timestamp() -> prg_machine:timestamp(). +timestamp() -> + Now = erlang:system_time(microsecond), + {Seconds, Micro} = prg_utils:split_timestamp(Now), + {calendar:system_time_to_universal_time(Seconds, second), Micro}. + +-spec unmarshal_machine(module(), prg_machine:namespace(), map()) -> prg_machine: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 + }. + +-spec marshal_new_events(module(), non_neg_integer(), [prg_machine:event_body()]) -> [map()]. +marshal_new_events(Handler, LastEventID, Bodies) -> + %% One microsecond timestamp for the whole batch (as the old emit_events did). + %% The PG backend stores timestamptz with microseconds and auto-detects units. + Ts = erlang:system_time(microsecond), + lists:zipwith( + fun(EventID, Body) -> + {Format, Bin} = marshal_event_body(Handler, Body), + #{ + event_id => EventID, + timestamp => Ts, + metadata => event_metadata(Format), + payload => Bin + } + end, + lists:seq(LastEventID + 1, LastEventID + length(Bodies)), + Bodies + ). + +unmarshal_event(Handler, #{ + event_id := EventID, + timestamp := TsSec, + metadata := Meta, + payload := Payload +}) -> + Format = unmarshal_event_format(Meta), + Body = prg_machine:unmarshal_event_body(Handler, Format, Payload), + {EventID, event_timestamp_to_datetime(TsSec), Body}; +unmarshal_event(_Handler, #{event_id := EventID} = Ev) -> + erlang:error({missing_event_payload, EventID, maps:keys(Ev)}). + +marshal_event_body(Handler, Body) -> + prg_machine:callback_marshal_event_body(Handler, Body). + +-spec marshal_aux_state(module(), term()) -> binary(). +marshal_aux_state(Handler, AuxSt) -> + prg_machine:callback_marshal_aux_state(Handler, AuxSt). + +unmarshal_aux_state(_Handler, undefined) -> + undefined; +unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> + prg_machine:callback_unmarshal_aux_state(Handler, 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">>). +-spec event_metadata(undefined | non_neg_integer()) -> map(). +event_metadata(undefined) -> + event_metadata(0); +event_metadata(Format) when is_integer(Format) -> + #{<<"format_version">> => Format, <<"format">> => Format}. + +%% Read order: legacy HG <<"format_version">> -> legacy FF <<"format">> -> +%% atom format (defensive) -> undefined. +-spec unmarshal_event_format(map()) -> undefined | non_neg_integer(). +unmarshal_event_format(Meta) -> + maps:get( + <<"format_version">>, + Meta, + maps:get(<<"format">>, Meta, maps:get(format, Meta, undefined)) + ). + +%% 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}. + +initial_model(AuxState) when is_map(AuxState) -> + maps:get(model, AuxState, undefined); +initial_model(_AuxState) -> + undefined. + +range_from_process(#{range := Range = #{}}) -> + Range; +range_from_process(_) -> + #{direction => forward}. diff --git a/apps/prg_machine/src/prg_machine_processor.erl b/apps/prg_machine/src/prg_machine_processor.erl new file mode 100644 index 00000000..6eba3b08 --- /dev/null +++ b/apps/prg_machine/src/prg_machine_processor.erl @@ -0,0 +1,87 @@ +-module(prg_machine_processor). + +-export([process/3]). + +-ifdef(TEST). +-export([marshal_intent/3]). +-endif. + +-spec process({init | call | repair | notify | timeout, binary(), map()}, prg_machine: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} -> + prg_machine_env:run(BinCtx, Opts, fun() -> + LastEventID = maps:get(last_event_id, Process), + Machine = prg_machine_events: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. + +dispatch(Handler, init, BinArgs, Machine) -> + Args = prg_machine_codec:decode_term(BinArgs), + prg_machine:callback_init(Handler, Args, Machine); +dispatch(Handler, timeout, _BinArgs, Machine) -> + prg_machine:callback_process_signal(Handler, timeout, Machine); +dispatch(Handler, notify, BinArgs, Machine) -> + Args = prg_machine_codec:decode_term(BinArgs), + dispatch_notification(Handler, Args, Machine); +dispatch(Handler, call, BinArgs, Machine) -> + case prg_machine_codec:decode_term(BinArgs) of + {notify, Args} -> + dispatch_notification(Handler, Args, Machine); + remove -> + #{events => [], action => remove, auxst => maps:get(aux_state, Machine)}; + Call -> + prg_machine:callback_process_call(Handler, Call, Machine) + end; +dispatch(Handler, repair, BinArgs, Machine) -> + Args = prg_machine_codec:decode_term(BinArgs), + case prg_machine:callback_process_repair(Handler, Args, Machine) of + {error, Reason} -> + {error, Reason}; + Result when is_map(Result) -> + Result + end. + +dispatch_notification(Handler, Args, Machine) -> + prg_machine:callback_process_notification(Handler, Args, Machine). + +marshal_process_result(Handler, LastEventID, {Response, Result}) when is_map(Result) -> + Intent = marshal_intent(Handler, LastEventID, Result), + {ok, Intent#{response => prg_machine_codec: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, prg_machine_codec:encode_term(Reason)}. + +-spec marshal_intent(module(), non_neg_integer(), prg_machine:result()) -> map(). +marshal_intent(Handler, LastEventID, Result) when is_map(Result) -> + Base0 = #{events => prg_machine_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 => prg_machine_events:marshal_aux_state(Handler, maps:get(auxst, Result))}; + false -> + Base1 + end. diff --git a/apps/prg_machine/src/prg_machine_registry.erl b/apps/prg_machine/src/prg_machine_registry.erl index 92f03659..a6185742 100644 --- a/apps/prg_machine/src/prg_machine_registry.erl +++ b/apps/prg_machine/src/prg_machine_registry.erl @@ -2,8 +2,6 @@ %%% Namespace -> handler module registry (ETS owner). --behaviour(gen_server). - -define(TABLE, prg_machine_dispatch). -define(SERVER, ?MODULE). @@ -11,14 +9,7 @@ -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{}. +-export([init/1]). -spec get_child_spec([module()]) -> supervisor:child_spec(). get_child_spec(Handlers) -> @@ -33,7 +24,7 @@ get_child_spec(Handlers) -> -spec start_link([module()]) -> {ok, pid()} | {error, term()}. start_link(Handlers) -> - gen_server:start_link({local, ?SERVER}, ?MODULE, Handlers, []). + proc_lib:start_link(?MODULE, init, [Handlers]). -spec lookup(prg_machine:namespace()) -> {ok, module()} | {error, {unknown_namespace, prg_machine:namespace()}}. lookup(NS) -> @@ -59,20 +50,18 @@ ensure_table() -> ok end. --spec init([module()]) -> {ok, state()}. +-spec init([module()]) -> ok. init(Handlers) -> + true = register(?SERVER, self()), ok = ensure_table(), true = ets:insert(?TABLE, [{prg_machine:handler_namespace(H), H} || H <- Handlers]), - {ok, #state{handlers = Handlers}}. + proc_lib:init_ack({ok, self()}), + loop(). --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}. +loop() -> + receive + stop -> + ok; + _Msg -> + loop() + end. diff --git a/docs/prg-machine-fix-plan.md b/docs/prg-machine-fix-plan.md deleted file mode 100644 index 3d068c36..00000000 --- a/docs/prg-machine-fix-plan.md +++ /dev/null @@ -1,237 +0,0 @@ -# План правок по ревью `add_prg_layer` - -Статус: согласован по итогам ревью (2026-06-12). База диффа — `e035d3c1` (epic/monorepo). - -## Главный вывод: миграция данных НЕ нужна - -Все найденные несовместимости живут в нашем промежуточном слое (`prg_machine`, -`ff_machine_codec`, `hg_invoice`), а не в thrift-схемах и не в progressor: - -| Данные | Старый формат в БД | Что сломали | Лечится в слое | -|---|---|---|---| -| HG события | payload `t2b(msgpack {bin, Thrift})`, metadata `format_version` | читаем только ключ `format` | да: payload и так бинарно совместим, чинится только ключ метаданных | -| FF события | payload `t2b({bin, Thrift})`, metadata `format` | новый код ждёт сырой thrift | да: compat-чтение + запись в старом конверте | -| FF aux_state | `t2b(map)` | новый код ждёт msgpack-thrift | да: try-чтение обоих | -| HG aux_state | `t2b(#mg_stateproc_Content{})` | живёт на двух catch-ловушках | да: явная клауза | -| HG call-args задач | `{thrift_call, Service, FunRef, EncodedArgs}` в старой обёртке | новая форма `{FunRef, Args}` | да: compat-клауза чтения | - -Принцип этапа 1: **писать в старом формате, читать оба**. Тогда и rollback -безопасен (старый код читает всё, что записал новый). Унификация конверта -HG+FF («дальше одинаково для обоих») — отдельный финальный этап с bump'ом -версии формата, когда reader уже повсеместно выкачен. - ---- - -## Этап 1. Совместимость данных (блокер) - -### 1.1 Метаданные событий: оба ключа -`apps/prg_machine/src/prg_machine.erl` - -Важно: «рабочий» ключ у стеков разный. Старый HG (`hg_progressor`) писал -`<<"format_version">>`, старый FF (`machinery_prg_backend`) — `<<"format">>` -(и читает только его, с дефолтом 0). Возврат к одному из них ломает rollback -второго стека, поэтому: - -- Запись: `event_metadata/1` → `#{<<"format_version">> => V, <<"format">> => V}` - — оба старых ключа. Это разом чинит чтение старых HG-событий, event sink - (`prg_notifier` читает `format_version`) и rollback обоих стеков - (старый HG-reader найдёт `format_version`, старый FF-reader — `format`). -- Чтение: `unmarshal_event/2` — порядок `<<"format_version">>` → `<<"format">>` - → `format` → `undefined`. - -### 1.2 FF события: старый конверт на запись, sniff на чтение -`apps/ff_transfer/src/ff_machine_codec.erl` - -- `payload_to_binary`: вернуть старый конверт — `term_to_binary({bin, ThriftBin})` - (как писал `machinery_prg_backend` через `machinery_utils:encode(term, ...)`). -- `unmarshal_thrift_event` → принимать оба варианта: - - первый байт `131` → `binary_to_term` → `{bin, Bin}` → thrift из `Bin`; - любое другое msgpack-значение → понятная ошибка `{legacy_msgpack_event, ...}` - (по данным таких быть не должно — проверить на стейдже); - - иначе → сырой thrift (события, записанные текущей веткой в dev/test). -- То же чтение используется `ff_machine_trace` — автоматически чинится. - -Примечание: sniff по первому байту безопасен только в эту сторону -(thrift-струkt не начинается с `131`); для msgpack наоборот — fixmap(3) тоже -`0x83`, поэтому порядок проверки именно такой. - -### 1.3 FF aux_state: запись t2b, чтение try-оба -`apps/ff_transfer/src/ff_machine_codec.erl` - -- `marshal_aux_state` → `term_to_binary(AuxSt)` (старое поведение). -- `unmarshal_aux_state` → `try binary_to_term` (старый формат), на ошибке — - msgpack-путь (`binary_to_payload` + `ff_machine_schema:unmarshal`) для - записанного веткой. - -### 1.4 HG aux_state: явная клауза вместо catch-ловушек -`apps/hellgate/src/hg_invoice.erl` (`unmarshal_aux_state/1`) - -- Явно матчить `#mg_stateproc_Content{format_version = _, data = Data}` → - `mg_msgpack_marshalling:unmarshal(Data)`; убрать слепые `catch _:_`. -- Проверить ветку `dispatch(call, remove)` в `prg_machine`: туда уходит уже - размаршалленный aux_state — `marshal_aux_state` должен его переживать. - -### 1.5 Pending-задачи: compat-чтение args (решение — доработать, не откатывать) - -Заключение: формат менялся **только у HG** (FF всегда писал plain -`term_to_binary(Args)` — там регрессии нет). Новая форма `{FunRef, Args}` -проще и не тянет thrift-сериализацию в слой `prg_machine` — откатывать на -`{thrift_call, Service, FunRef, EncodedArgs}` не стоит. Достаточно -compat-чтения, окно риска — только незавершённые call/init задачи в момент -деплоя (timeout-задачи с пустыми args не затронуты): - -- `prg_machine:decode_term/1`: результат `{bin, Bin}` → `binary_to_term(Bin)` - (старая двойная обёртка). -- `apps/hellgate/src/hg_invoice.erl` (`process_call`): клауза для старой формы - `{thrift_call, Service, FunRef, EncodedArgs}` → `hg_proto_utils`-десериализация - args → дальше обычный путь `{FunRef, Args}`. Перед реализацией сверить точную - старую форму по `e035d3c1:apps/hellgate/src/hg_machine.erl` (строки ~137–143) - и старому клиентскому пути `hg_progressor:call`. - -### 1.6 Golden-тесты на старые форматы (критерий приёмки этапа) - -- Снять реальные бинари (payload/aux_state/metadata/args), сгенерированные кодом - базового коммита (или со стейджа), положить фикстурами. -- CT/eunit: чтение старого события, старого aux_state, старого call-арга — для - hg_invoice и каждого из 5 ff-неймспейсов; плюс симметричный тест «новая запись - читается старым форматом конверта» (rollback-инвариант). - ---- - -## Этап 2. Таймстемпы событий — вернуть микросекунды (мажор) - -PG-бэкенд progressor хранит `timestamptz` с микросекундами и сам детектит -юниты (`prg_utils:split_timestamp/to_microseconds`) — секунды сейчас режет -только `prg_machine`. - -- `prg_machine:marshal_new_events`: писать `timestamp => erlang:system_time(microsecond)` - (без `div 1000000`), один таймстемп на весь батч (как старый `emit_events`). -- Чтение: `event_timestamp_to_datetime` → возвращать `{calendar:datetime(), Micro}` - (machinery-формат); обновить тип `machine_event()` и спеки. -- HG: `hg_invoice:event_timestamp_to_binary` — форматировать с микро - (`hg_datetime`, как старый MG RFC3339). -- FF: `marshal_event_body` — в `{ev, {Dt, USec}, Body}` класть реальные микро - вместо захардкоженного `0`; «недостижимая» клауза `codec_timestamp({Dt, USec})` - в 5 `*_machine` модулях становится рабочей; `events/2` (GetEvents) наружу - отдаёт микро. -- В progressor при его ревью: поправить спеку `event() :: timestamp := timestamp_sec()` - → допускать `timestamp_us()` (фактически уже работает). - ---- - -## Этап 3. Ошибки и устойчивость (мажоры) - -### 3.1 `notify` / `remove` -- `prg_machine:notify/3`, `remove/2`: обработать все исходы - (`{error, failed}`, `{error, {exception, _, _}}`, прочие guard-ошибки) — - без `case_clause`. -- `ff_withdrawal_session:process_session`: восстановить старую семантику — - notify в сломанный withdrawal глотается с warning-логом (`{error, failed}` → - `ok`), сессия не заражается. Остальные ошибки — как сейчас, в error. - -### 3.2 `env_enter`/`env_leave` -- `prg_machine:process/3`: флаг «enter выполнен»; `after` зовёт Leave только - если был Enter. Ошибки до Enter возвращаются progressor'у как `{error, _}` - без маскировки исключением из `after`. - -### 3.3 Дефолтный woody deadline -- В `process/3` после восстановления контекста — `ensure_deadline_set` - (дефолт 30 с, конфигурируемо через опции неймспейса), как делал старый - `hg_progressor` через `hg_woody_service_wrapper:ensure_woody_deadline_set/2`. - -### 3.4 Repair-путь -- `prg_action:marshal_timer`: клауза `{deadline, {{_,_}=Dt, USec}}` (machinery-формат - из `ff_codec:unmarshal(timer, ...)`) — срезать USec, как в - `ff_adapter_withdrawal_codec:unmarshal_provider_timer/1`. Тест на deadline-таймер - в `ff_withdrawal_codec` (сейчас покрыт только `{timeout, 0}`). -- `prg_machine:repair/3`: декодировать `Reason` (`decode_term`) в ветке - `{error, {repair, {failed, Reason}}}` — наружу term, а не t2b-бинарь. -- Выправить спеки `ff_*_machine:repair/2` и хендлеры (`ff_withdrawal_repair` - и др.) под фактическую форму ошибки; убрать недостижимую ветку - `{error, failed} -> {failed, {invalid_result, unexpected_failure}}`. - -### 3.5 `hg_invoice_handler` -- `get_state/1`: `throw(#payproc_InvoiceNotFound{})` — голый рекорд, как в - `map_history_error`. -- `ensure_started/2`: вернуть ветку `{error, Reason} -> erlang:error(Reason)`. - -### 3.6 Контракт исключений процессора (решение) -- Форму `{error, {exception, Class, Reason}}` оставляем как есть — и на проводе - к progressor (его контракт: `is_retryable/5` по 3-tuple решает «не ретраить»), - и в клиентском API как pass-through. Переименование маркера (`failed` и т.п.) - — косметика, не делаем. -- Чиним только реальные баги: - - `prg_machine:get/3`: убрать `raise_exception` — возвращать - `{error, {exception, Class, Reason}}` как обычную ошибку, а не - re-raise с пустым stacktrace; - - убедиться, что потребители (`hg_invoicing_machine_client`, `ff_*_machine`, - repair-хендлеры) матчат эту форму: детали — в лог / маппинг в - thrift-ошибки, без `case_clause`. -- Голый `{error, failed}` остаётся для статусной ошибки - `<<"process is error">>` (процесс уже в error; деталей в ответе progressor - нет — при необходимости их даёт отдельный `get` с `detail` процесса). -- `docs/prg-machine.md`: убрать упоминание 4-tuple со stacktrace, описать - фактический контракт. - ---- - -## Этап 4. Контекст и конфиг - -### 4.1 Убрать глобальный `woody_context_loader` -- Удалить `application:set_env(prg_machine, woody_context_loader, fun ...)` из - `hellgate.erl` и `ff_server.erl` (анонимный fun в app env + общий ключ — - ломается на hot upgrade и при совместном старте в одном узле). -- `prg_machine:encode_rpc_context/0` → `op_context:current_woody_context()`: - пробует hg-binding, затем ff-binding (gproc-ключи текущего процесса различны, - коллизий нет), fallback `woody_context:new()` с warning-логом. -- Добавить `op_context` в `applications` у `prg_machine.app.src` - (зависимость уже фактическая — `resolve_env_enter`). - -### 4.2 `binary_to_term` без `[safe]` -- Старый стек (`machinery_utils:decode`, `hg_progressor`) работал без `[safe]` — - возвращаем как было: убрать `[safe]` во всех decode собственных данных - (`prg_machine:decode_term`, `unmarshal_event_body` fallback, - `unmarshal_aux_state`, `ff_machine_trace:decode_term`, `hg_invoice`). - ---- - -## Этап 5. Гигиена HG/FF - -- `hg_invoice`: `log_changes` для signal/repair (как старый `handle_result`); - убрать двойной `validate_changes` в `process_call` (заодно закрывает пункт - «двойной collapse» из техдолга в `docs/prg-machine.md`). -- FF копипаста ×5: вынести `to_repair_machine/1`, `from_repair_result/2`, - `repair_events_to_domain/1`, `event_body_from_timestamped/1`, - `history_times/1`, `history_to_events/1`, `codec_timestamp/1` в общий модуль - (`ff_machine_codec` или новый `ff_machine_lib`); удалить мёртвое поле `times` - из `st()` пяти `*_machine`, либо начать использовать. -- FF: вернуть no-op `process_notification` (`#{}`) у session/source/destination - вместо принудительного `action => timeout`; убрать лишний `action => timeout` - из `ff_destination:init`. -- FF `machine_to_st`: явная ветка для `aux_state = undefined` (сейчас дефолт - `maps:get(ctx, AuxState, #{})` мёртвый, падает `badmap`). -- `docs/prg-machine.md`: обновить разделы про ошибки/форматы по итогам этапов 1–4. - -Вне скоупа (решено): `hg_hybrid` не возвращаем; trace (`ff_machine_trace`, -дефолт формата, `<<>>`-args) — отдельный ПР с переездом на thrift; тег -progressor в `rebar.config` — после ревью progressor. - ---- - -## Этап 6 (опционально, отдельный ПР). Единый конверт HG+FF - -После выкатки этапов 1–5 и стабилизации: - -- format 2 = сырой thrift-binary payload для **обоих** стеков (HG уходит от - `t2b(msgpack)`, FF — от `t2b({bin, ...})`). -- Reader к этому моменту уже умеет оба формата (этап 1), поэтому включение - записи format 2 — отдельный маленький коммит; rollback-политика: откат только - на версии, содержащие reader этапа 1. -- Сюда же — переезд trace на thrift (`docs/trace-api-thrift.md`). - -## Порядок и критерии - -1 → 2 → 3 → 4 → 5 — каждый этап самостоятелен и мержибелен отдельно; 1 и 2 -трогают одни и те же функции маршалинга, их удобно делать подряд. Критерий -этапа 1 — golden-тесты (1.6) зелёные; критерий общий — CT + dialyzer + compose -зелёные, grep-инварианты из `docs/prg-machine.md` соблюдены. diff --git a/docs/prg-machine.md b/docs/prg-machine.md index 3e2ef578..55fa3394 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -2,7 +2,7 @@ Единый runtime поверх progressor для HG и FF. Контракт `action()` в progressor: `progressor/docs/step-effect-migration.md`. -*Обновлено: 2026-06-13. CI (compile, dialyzer, CT + compose) — green локально.* +*Обновлено: 2026-06-16. CI (compile, dialyzer, CT + compose) — см. текущий PR.* --- @@ -42,8 +42,13 @@ sequenceDiagram | Модуль | Роль | |--------|------| -| `prg_machine` | behaviour, client API, `process/3`, `collapse` / `emit_events` | -| `prg_machine_registry` | ETS `{Namespace, Handler}`; `{unknown_namespace, NS}` | +| `prg_machine` | публичный фасад: behaviour, client API, `process/3`, `collapse` / `emit_events` | +| `prg_machine_client` | progressor client API: `start` / `call` / `get` / `repair` / history | +| `prg_machine_processor` | progressor `process/3`: dispatch доменного callback и сбор intent | +| `prg_machine_events` | event fold, event payload/metadata, aux_state codec defaults | +| `prg_machine_env` | woody/otel context, deadline, `env_enter` / `env_leave` | +| `prg_machine_codec` | term envelope и legacy double-envelope decode | +| `prg_machine_registry` | ETS `{Namespace, Handler}` под простым owner-процессом | | `prg_action` | `{timeout, Sec}` / `{deadline, Dt}` → wire `action()` | | `ff_machine_lib` | общие FF-хелперы: repair/history/timestamp для `*_machine` | @@ -61,6 +66,14 @@ sequenceDiagram `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 @@ -167,15 +180,15 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa ### 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 на `apply_event/2`. +Реплей: `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`. ### Прочее (низкий приоритет) -- Golden-fixtures со стейджа для legacy payload/aux_state (см. `docs/prg-machine-fix-plan.md` §1.6) -- Registry без ETS `heir` — краткое окно при рестарте +- Golden-fixtures со стейджа для legacy payload/aux_state +- 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 fix-plan +- Единый конверт HG+FF (format 2) --- @@ -183,7 +196,7 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa 1. `sys.config` — `client => prg_machine` 2. `-behaviour(prg_machine)` + callbacks -3. `apply_event/2` (FF) или `apply_event/4` (HG) для `collapse/2` +3. `apply_event/4` для `collapse/2` 4. `*_machine.erl` — только `prg_machine:*` 5. Handler в `get_child_spec` (`hellgate.erl` / `ff_server.erl`) 6. CT suite From 1b4f07a9464c79dda439de2c8166139ddbcbd0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 17 Jun 2026 13:03:10 +0300 Subject: [PATCH 47/62] Refactor unmarshal_event_body function across multiple modules to simplify its signature by removing the format parameter. Update specifications in ff_machine_lib, ff_deposit_machine, ff_destination_machine, ff_source_machine, ff_withdrawal_machine, and hg_invoice modules for consistency. This change enhances code clarity and prepares for improved event handling. --- apps/ff_transfer/src/ff_deposit.erl | 6 - apps/ff_transfer/src/ff_deposit_machine.erl | 8 +- apps/ff_transfer/src/ff_destination.erl | 6 - .../src/ff_destination_machine.erl | 8 +- apps/ff_transfer/src/ff_machine_lib.erl | 12 +- apps/ff_transfer/src/ff_machine_trace.erl | 4 +- apps/ff_transfer/src/ff_source.erl | 6 - apps/ff_transfer/src/ff_source_machine.erl | 8 +- apps/ff_transfer/src/ff_withdrawal.erl | 10 - .../ff_transfer/src/ff_withdrawal_machine.erl | 8 +- .../ff_transfer/src/ff_withdrawal_session.erl | 6 - .../src/ff_withdrawal_session_machine.erl | 8 +- apps/hellgate/src/hg_invoice.erl | 10 +- apps/hellgate/src/hg_invoice_template.erl | 10 +- apps/prg_machine/src/prg_machine.erl | 506 ++++++++++++++---- apps/prg_machine/src/prg_machine_client.erl | 187 ------- apps/prg_machine/src/prg_machine_codec.erl | 21 - apps/prg_machine/src/prg_machine_env.erl | 93 ---- apps/prg_machine/src/prg_machine_events.erl | 135 ----- .../prg_machine/src/prg_machine_processor.erl | 87 --- apps/prg_machine/src/prg_machine_registry.erl | 37 +- .../test/legacy_fixture_golden_test.erl | 4 +- docs/prg-machine-fix-plan.md | 237 ++++++++ docs/prg-machine.md | 29 +- 24 files changed, 717 insertions(+), 729 deletions(-) delete mode 100644 apps/prg_machine/src/prg_machine_client.erl delete mode 100644 apps/prg_machine/src/prg_machine_codec.erl delete mode 100644 apps/prg_machine/src/prg_machine_env.erl delete mode 100644 apps/prg_machine/src/prg_machine_events.erl delete mode 100644 apps/prg_machine/src/prg_machine_processor.erl create mode 100644 docs/prg-machine-fix-plan.md diff --git a/apps/ff_transfer/src/ff_deposit.erl b/apps/ff_transfer/src/ff_deposit.erl index f6c77817..e248ccec 100644 --- a/apps/ff_transfer/src/ff_deposit.erl +++ b/apps/ff_transfer/src/ff_deposit.erl @@ -117,7 +117,6 @@ %% Event source -export([apply_event/2]). --export([apply_event/4]). %% Pipeline @@ -307,11 +306,6 @@ is_finished(#{status := pending}) -> apply_event(Ev, T0) -> apply_event_(Ev, T0). --spec apply_event(prg_machine:event_id(), prg_machine:timestamp(), event(), deposit_state() | undefined) -> - deposit_state(). -apply_event(_EventID, _Timestamp, Ev, T) -> - apply_event(Ev, T). - -spec apply_event_(event(), deposit_state() | undefined) -> deposit_state(). apply_event_({created, T}, undefined) -> apply_negative_body(T); diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index 4e326c46..2f950813 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -68,7 +68,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([marshal_event_body/1]). --export([unmarshal_event_body/2]). +-export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -152,9 +152,9 @@ process_repair(Scenario, Machine) -> marshal_event_body(Body) -> ff_machine_lib:marshal_event_body(deposit, ?EVENT_FORMAT_VERSION, Body). --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(Format, Payload) -> - ff_machine_lib:unmarshal_event_body(deposit, ?EVENT_FORMAT_VERSION, Format, Payload). +-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) -> diff --git a/apps/ff_transfer/src/ff_destination.erl b/apps/ff_transfer/src/ff_destination.erl index 78d03527..eb4b210d 100644 --- a/apps/ff_transfer/src/ff_destination.erl +++ b/apps/ff_transfer/src/ff_destination.erl @@ -108,7 +108,6 @@ -export([create/1]). -export([is_accessible/1]). -export([apply_event/2]). --export([apply_event/4]). %% Pipeline @@ -223,8 +222,3 @@ 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}). - --spec apply_event(prg_machine:event_id(), prg_machine:timestamp(), event(), ff_maybe:'maybe'(destination_state())) -> - destination_state(). -apply_event(_EventID, _Timestamp, Ev, Destination) -> - apply_event(Ev, Destination). diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index e2461202..48b1152b 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -57,7 +57,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([marshal_event_body/1]). --export([unmarshal_event_body/2]). +-export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -128,9 +128,9 @@ process_repair(Scenario, Machine) -> marshal_event_body(Body) -> ff_machine_lib:marshal_event_body(destination, ?EVENT_FORMAT_VERSION, Body). --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(Format, Payload) -> - ff_machine_lib:unmarshal_event_body(destination, ?EVENT_FORMAT_VERSION, Format, Payload). +-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) -> diff --git a/apps/ff_transfer/src/ff_machine_lib.erl b/apps/ff_transfer/src/ff_machine_lib.erl index cd546d35..b1a21b14 100644 --- a/apps/ff_transfer/src/ff_machine_lib.erl +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -20,7 +20,7 @@ -export([history_to_events/1]). -export([codec_timestamp/1]). -export([marshal_event_body/3]). --export([unmarshal_event_body/4]). +-export([unmarshal_event_body/2]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -185,13 +185,11 @@ marshal_event_body(Domain, Format, 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(), pos_integer(), pos_integer(), binary()) -> +-spec unmarshal_event_body(ff_machine_codec:domain(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(Domain, Format, Format, Payload) -> - Timestamped = ff_machine_codec:unmarshal_event(Domain, Format, Payload), - event_body_from_timestamped(Timestamped); -unmarshal_event_body(_Domain, _ExpectedFormat, Format, _Payload) -> - erlang:error({unknown_event_format, Format}). +unmarshal_event_body(Domain, Payload) -> + Timestamped = ff_machine_codec:unmarshal_event(Domain, 1, Payload), + event_body_from_timestamped(Timestamped). -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> diff --git a/apps/ff_transfer/src/ff_machine_trace.erl b/apps/ff_transfer/src/ff_machine_trace.erl index 03b02e57..9291c191 100644 --- a/apps/ff_transfer/src/ff_machine_trace.erl +++ b/apps/ff_transfer/src/ff_machine_trace.erl @@ -44,11 +44,9 @@ unmarshal_trace_events(Events, Handler) -> unmarshal_trace_event(Event, Handler) -> Payload = maps:get(event_payload, Event), - Meta = maps:get(event_metadata, Event, #{}), - Format = maps:get(<<"format">>, Meta, maps:get(format, Meta, 1)), EventID = maps:get(event_id, Event), Ts = maps:get(event_timestamp, Event), - Body = prg_machine:unmarshal_event_body(Handler, Format, Payload), + Body = prg_machine:unmarshal_event_body(Handler, Payload), #{ event_id => EventID, event_payload => json_compatible_value(Body), diff --git a/apps/ff_transfer/src/ff_source.erl b/apps/ff_transfer/src/ff_source.erl index 6658fb6b..e5b2c3bb 100644 --- a/apps/ff_transfer/src/ff_source.erl +++ b/apps/ff_transfer/src/ff_source.erl @@ -94,7 +94,6 @@ -export([create/1]). -export([is_accessible/1]). -export([apply_event/2]). --export([apply_event/4]). %% Pipeline @@ -199,8 +198,3 @@ 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}). - --spec apply_event(prg_machine:event_id(), prg_machine:timestamp(), event(), ff_maybe:'maybe'(source_state())) -> - source_state(). -apply_event(_EventID, _Timestamp, Ev, Source) -> - apply_event(Ev, Source). diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index 84d2f2b3..df7296e5 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -57,7 +57,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([marshal_event_body/1]). --export([unmarshal_event_body/2]). +-export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -128,9 +128,9 @@ process_repair(Scenario, Machine) -> marshal_event_body(Body) -> ff_machine_lib:marshal_event_body(source, ?EVENT_FORMAT_VERSION, Body). --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(Format, Payload) -> - ff_machine_lib:unmarshal_event_body(source, ?EVENT_FORMAT_VERSION, Format, Payload). +-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) -> diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index cbf127fa..2bcfcb0b 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -249,7 +249,6 @@ %% Event source -export([apply_event/2]). --export([apply_event/4]). %% Pipeline @@ -1823,15 +1822,6 @@ apply_event(Ev, T0) -> T2 = save_adjustable_info(Ev, T1), T2. --spec apply_event( - prg_machine:event_id(), - prg_machine:timestamp(), - event() | legacy_event(), - ff_maybe:'maybe'(withdrawal_state()) -) -> withdrawal_state(). -apply_event(_EventID, _Timestamp, Ev, T0) -> - apply_event(Ev, T0). - -spec apply_event_(event(), ff_maybe:'maybe'(withdrawal_state())) -> withdrawal_state(). apply_event_({created, T}, undefined) -> make_state(T); diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index abaf5b88..79dd5bb9 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -87,7 +87,7 @@ -export([process_repair/2]). -export([process_notification/2]). -export([marshal_event_body/1]). --export([unmarshal_event_body/2]). +-export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -201,9 +201,9 @@ process_notification({session_finished, SessionID, SessionResult}, Machine) -> marshal_event_body(Body) -> ff_machine_lib:marshal_event_body(withdrawal, ?EVENT_FORMAT_VERSION, Body). --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(Format, Payload) -> - ff_machine_lib:unmarshal_event_body(withdrawal, ?EVENT_FORMAT_VERSION, Format, Payload). +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + ff_machine_lib:unmarshal_event_body(withdrawal, Payload). -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index f9dc7fa7..a35c63db 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -25,7 +25,6 @@ %% ff_machine -export([apply_event/2]). --export([apply_event/4]). %% ff_repair -export([set_session_result/2]). @@ -197,11 +196,6 @@ apply_event({callback, _Ev} = WrappedEvent, Session) -> Callbacks1 = ff_withdrawal_callback_utils:apply_event(WrappedEvent, Callbacks0), set_callbacks_index(Callbacks1, Session). --spec apply_event(prg_machine:event_id(), prg_machine:timestamp(), event(), undefined | session_state()) -> - session_state(). -apply_event(_EventID, _Timestamp, Ev, Session) -> - apply_event(Ev, Session). - -spec process_session(session_state()) -> process_result(). process_session(#{status := {finished, _}, id := ID, result := Result, withdrawal := Withdrawal}) -> % Session has finished, it should notify the withdrawal machine about the fact diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 062e94a9..9a3bf7c3 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -29,7 +29,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([marshal_event_body/1]). --export([unmarshal_event_body/2]). +-export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -175,9 +175,9 @@ process_repair(Scenario, Machine) -> marshal_event_body(Body) -> ff_machine_lib:marshal_event_body(withdrawal_session, ?EVENT_FORMAT_VERSION, Body). --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(Format, Payload) -> - ff_machine_lib:unmarshal_event_body(withdrawal_session, ?EVENT_FORMAT_VERSION, Format, Payload). +-spec unmarshal_event_body(binary()) -> prg_machine:event_body(). +unmarshal_event_body(Payload) -> + ff_machine_lib:unmarshal_event_body(withdrawal_session, Payload). -spec marshal_aux_state(term()) -> binary(). marshal_aux_state(AuxSt) -> diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 680fb33a..c7031c95 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -55,7 +55,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([marshal_event_body/1]). --export([unmarshal_event_body/2]). +-export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -export([apply_event/4]). @@ -1042,11 +1042,9 @@ marshal_event_body(Changes) when is_list(Changes) -> Msgp = mg_msgpack_marshalling:marshal(Data), {?EVENT_FORMAT_VERSION, msgpack_payload_to_binary(Msgp)}. --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - decode_event_body(Payload); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). +-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) -> diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index b9507cd7..95a90773 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -25,7 +25,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([marshal_event_body/1]). --export([unmarshal_event_body/2]). +-export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -export([apply_event/4]). @@ -349,11 +349,9 @@ marshal_event_body(Changes) when is_list(Changes) -> Msgp = mg_msgpack_marshalling:marshal(Data), {?EVENT_FORMAT_VERSION, msgpack_payload_to_binary(Msgp)}. --spec unmarshal_event_body(pos_integer(), binary()) -> prg_machine:event_body(). -unmarshal_event_body(?EVENT_FORMAT_VERSION, Payload) -> - decode_event_body(Payload); -unmarshal_event_body(Format, _Payload) -> - erlang:error({unknown_event_format, Format}). +-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) -> diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 1a94fe67..ee64cf7e 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -1,10 +1,12 @@ -module(prg_machine). -%%% Public facade for the unified progressor machine runtime. The implementation -%%% is split by role: client API, processor, env/context, codec and event fold. +%%% 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"). +-define(TABLE, prg_machine_dispatch). + %% Types -type namespace() :: namespace_id(). @@ -96,20 +98,19 @@ -callback marshal_event_body(event_body()) -> {undefined | pos_integer(), binary()}. --callback unmarshal_event_body(undefined | pos_integer(), binary()) -> event_body(). +-callback unmarshal_event_body(binary()) -> event_body(). -callback marshal_aux_state(term()) -> binary(). -callback unmarshal_aux_state(binary()) -> term(). -%% Canonical collapse callback. Domain modules passed to collapse/2 adapt legacy -%% event folds at their boundary and expose only this arity to the runtime. +%% Optional: collapse passes event_id and timestamp (HG invoice). Default: apply_event/2. -callback apply_event(event_id(), timestamp(), event_body(), term()) -> term(). -optional_callbacks([ process_notification/2, marshal_event_body/1, - unmarshal_event_body/2, + unmarshal_event_body/1, marshal_aux_state/1, unmarshal_aux_state/1, apply_event/4 @@ -138,20 +139,7 @@ -export([get_child_spec/1]). -export([handler_namespace/1]). --export([unmarshal_event_body/3]). - -%% Callback dispatch. Keep dynamic behaviour calls in this module: Elvis allows -%% them here because this is the module that defines the callbacks. - --export([callback_init/3]). --export([callback_process_signal/3]). --export([callback_process_call/3]). --export([callback_process_repair/3]). --export([callback_process_notification/3]). --export([callback_apply_event/5]). --export([callback_marshal_event_body/2]). --export([callback_marshal_aux_state/2]). --export([callback_unmarshal_aux_state/2]). +-export([unmarshal_event_body/2]). %% Event-sourcing helpers (replaces ff_machine) @@ -164,66 +152,189 @@ -spec start(namespace(), id(), args()) -> {ok, ok} | {error, exists | term()}. start(NS, ID, Args) -> - prg_machine_client: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, _} = Error -> + Error + end. -spec call(namespace(), id(), call()) -> {ok, response()} | {error, notfound | failed | term()}. call(NS, ID, CallArgs) -> - prg_machine_client: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 | term()}. call(NS, ID, CallArgs, After, Limit, Direction) -> - prg_machine_client: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, _} = Error -> + Error + end. -spec repair(namespace(), id(), args()) -> {ok, term()} | {error, repair_error()}. repair(NS, ID, Args) -> - prg_machine_client: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, Reason} -> + %% The repair-failed reason is our own term encoded by process/3 + %% (marshal_process_result -> encode_term); hand it back as a term. + {error, {repair, {failed, decode_term(Reason)}}} + end. -spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, get_error()}. get(NS, ID, Range) -> - prg_machine_client:get(NS, ID, Range). + Req = request(NS, ID, undefined, Range), + case progressor:get(Req) of + {ok, Process} -> + case get_handler_module(NS) of + {ok, Handler} -> + {ok, unmarshal_machine(Handler, NS, Process)}; + {error, _} = Error -> + Error + end; + {error, <<"process not found">>} -> + {error, notfound}; + {error, {exception, _Class, _Reason} = Exception} -> + {error, Exception}; + {error, {exception, Class, Reason, _Stacktrace}} -> + {error, {exception, Class, Reason}} + end. -spec get(namespace(), id()) -> {ok, machine()} | {error, get_error()}. get(NS, ID) -> - prg_machine_client:get(NS, ID). + get(NS, ID, #{direction => forward}). -spec get_history(namespace(), id()) -> {ok, history()} | {error, get_error()}. get_history(NS, ID) -> - prg_machine_client: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) -> - prg_machine_client: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) -> - prg_machine_client: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 | processor_error() | term()}. notify(NS, ID, Args) -> - prg_machine_client:notify(NS, ID, Args). + case call(NS, ID, {notify, Args}) of + {ok, _} -> ok; + {error, _} = Error -> Error + end. -spec remove(namespace(), id()) -> ok | {error, notfound | failed | processor_error() | term()}. remove(NS, ID) -> - prg_machine_client:remove(NS, ID). + case call(NS, ID, remove) 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) -> - prg_machine_client: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(Call, Opts, BinCtx) -> - prg_machine_processor:process(Call, Opts, BinCtx). +process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> + Enter = resolve_env_enter(Opts), + Leave = resolve_env_leave(Opts), + try + case get_handler_module(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), + ok = run_env_enter(Enter, WoodyCtx), + %% Enter succeeded: from here Leave must run exactly once. Errors + %% raised before this point fall through to the outer catch and are + %% returned as {error, _} without being masked by a Leave exception. + run_with_env_leave(Leave, 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. %% Registry @@ -235,24 +346,62 @@ get_child_spec(Handlers) -> handler_namespace(Handler) -> Handler:namespace(). --spec callback_init(module(), args(), machine()) -> result(). -callback_init(Handler, Args, Machine) -> - Handler:init(Args, Machine). +%% Event-sourcing (replaces ff_machine collapse/emit) --spec callback_process_signal(module(), signal(), machine()) -> result(). -callback_process_signal(Handler, Signal, Machine) -> - Handler:process_signal(Signal, Machine). +-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 callback_process_call(module(), call(), machine()) -> {response(), result()}. -callback_process_call(Handler, Call, Machine) -> - Handler:process_call(Call, Machine). +-spec emit_event(term()) -> [{ev, timestamp(), term()}]. +emit_event(Event) -> + emit_events([Event]). --spec callback_process_repair(module(), args(), machine()) -> result() | {error, term()}. -callback_process_repair(Handler, Args, Machine) -> - Handler:process_repair(Args, Machine). +-spec emit_events([term()]) -> [{ev, timestamp(), term()}]. +emit_events(Events) -> + Ts = timestamp(), + [{ev, Ts, Body} || Body <- Events]. --spec callback_process_notification(module(), args(), machine()) -> result(). -callback_process_notification(Handler, Args, Machine) -> +-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), + dispatch_notification(Handler, Args, Machine); +dispatch(Handler, call, BinArgs, Machine) -> + case decode_term(BinArgs) of + {notify, Args} -> + dispatch_notification(Handler, 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. + +dispatch_notification(Handler, Args, Machine) -> case erlang:function_exported(Handler, process_notification, 2) of true -> Handler:process_notification(Args, Machine); @@ -260,12 +409,73 @@ callback_process_notification(Handler, Args, Machine) -> #{} end. --spec callback_apply_event(module(), event_id(), timestamp(), event_body(), term()) -> term(). -callback_apply_event(Handler, EventID, Ts, Body, Model) -> - Handler:apply_event(EventID, Ts, Body, Model). +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. --spec callback_marshal_event_body(module(), event_body()) -> {undefined | pos_integer(), binary()}. -callback_marshal_event_body(Handler, Body) -> +%% 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 := TsSec, + payload := Payload +}) -> + Body = unmarshal_event_body(Handler, Payload), + {EventID, event_timestamp_to_datetime(TsSec), Body}; +unmarshal_event(_Handler, #{event_id := EventID} = Ev) -> + erlang:error({missing_event_payload, EventID, maps:keys(Ev)}). + +marshal_new_events(Handler, LastEventID, Bodies) -> + %% One microsecond timestamp for the whole batch (as the old emit_events did). + %% The PG backend stores timestamptz with microseconds and auto-detects units. + Ts = erlang:system_time(microsecond), + lists:zipwith( + fun(EventID, Body) -> + {Format, Bin} = marshal_event_body(Handler, Body), + #{ + event_id => EventID, + timestamp => Ts, + metadata => event_metadata(Format), + payload => Bin + } + end, + lists:seq(LastEventID + 1, LastEventID + length(Bodies)), + Bodies + ). + +marshal_event_body(Handler, Body) -> case erlang:function_exported(Handler, marshal_event_body, 1) of true -> Handler:marshal_event_body(Body); @@ -273,17 +483,16 @@ callback_marshal_event_body(Handler, Body) -> {undefined, term_to_binary(Body)} end. --spec unmarshal_event_body(module(), undefined | pos_integer(), binary()) -> event_body(). -unmarshal_event_body(Handler, Format, Payload) -> - case erlang:function_exported(Handler, unmarshal_event_body, 2) of +-spec unmarshal_event_body(module(), binary()) -> event_body(). +unmarshal_event_body(Handler, Payload) -> + case erlang:function_exported(Handler, unmarshal_event_body, 1) of true -> - Handler:unmarshal_event_body(Format, Payload); + Handler:unmarshal_event_body(Payload); false -> binary_to_term(Payload) end. --spec callback_marshal_aux_state(module(), term()) -> binary(). -callback_marshal_aux_state(Handler, AuxSt) -> +marshal_aux_state(Handler, AuxSt) -> case erlang:function_exported(Handler, marshal_aux_state, 1) of true -> Handler:marshal_aux_state(AuxSt); @@ -291,10 +500,9 @@ callback_marshal_aux_state(Handler, AuxSt) -> term_to_binary(AuxSt) end. --spec callback_unmarshal_aux_state(module(), undefined | binary()) -> term(). -callback_unmarshal_aux_state(_Handler, undefined) -> +unmarshal_aux_state(_Handler, undefined) -> undefined; -callback_unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> +unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> case erlang:function_exported(Handler, unmarshal_aux_state, 1) of true -> Handler:unmarshal_aux_state(Bin); @@ -302,28 +510,150 @@ callback_unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> binary_to_term(Bin) end. -%% Event-sourcing (replaces ff_machine collapse/emit) +%% 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) -> + case erlang:function_exported(Handler, apply_event, 4) of + true -> + Handler:apply_event(EventID, Ts, Body, Model); + false -> + case erlang:function_exported(Handler, apply_event, 2) of + true -> + Handler:apply_event(Body, Model); + false -> + erlang:error({apply_event_not_defined, Handler}) + end + end. --spec collapse(module(), machine()) -> term(). -collapse(Handler, Machine) -> - prg_machine_events:collapse(Handler, Machine). +initial_model(_Handler, AuxState) when is_map(AuxState) -> + maps:get(model, AuxState, undefined); +initial_model(_Handler, _AuxState) -> + undefined. --spec emit_event(term()) -> [{ev, timestamp(), term()}]. -emit_event(Event) -> - prg_machine_events:emit_event(Event). +get_handler_module(NS) -> + prg_machine_registry:lookup(NS). --spec emit_events([term()]) -> [{ev, timestamp(), term()}]. -emit_events(Events) -> - prg_machine_events:emit_events(Events). +%% RPC / terms --spec timestamp() -> timestamp(). -timestamp() -> - prg_machine_events:timestamp(). +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)). + +resolve_env_enter(Opts) -> + case maps:is_key(env_enter, Opts) of + true -> + maps:get(env_enter, Opts); + false -> + case maps:get(context_binding, Opts, undefined) of + Binding when is_map(Binding) -> + fun(WoodyCtx) -> op_context:env_enter(WoodyCtx, Binding) end; + _ -> + fun(_) -> ok end + end + end. + +resolve_env_leave(Opts) -> + case maps:is_key(env_leave, Opts) of + true -> + maps:get(env_leave, Opts); + false -> + case maps:get(context_binding, Opts, undefined) of + Binding when is_map(Binding) -> + fun() -> op_context:env_leave(Binding) end; + _ -> + fun() -> ok end + end + end. + +run_env_enter(Enter, WoodyCtx) when is_function(Enter, 1) -> + Enter(WoodyCtx); +run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> + Enter(). + +run_with_env_leave(Leave, Fun) when is_function(Leave, 0), is_function(Fun, 0) -> + try Fun() of + Result -> + safe_env_leave(Leave), + Result + catch + Class:Reason:Stacktrace -> + safe_env_leave(Leave), + erlang:raise(Class, Reason, Stacktrace) + end. + +safe_env_leave(Leave) -> + try + Leave() + 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}. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). --define(TABLE, prg_machine_dispatch). -define(TEST_NS, env_test_ns). -define(TEST_REGISTRY_KEY, {p, l, prg_machine_env_test_context}). -define(TEST_BINDING, #{ @@ -458,7 +788,7 @@ cleanup_aux_state_test(_) -> -spec marshal_intent_omits_aux_state_without_auxst() -> _. marshal_intent_omits_aux_state_without_auxst() -> - Intent = prg_machine_processor:marshal_intent(prg_machine_aux_state_test_handler, 0, #{}), + Intent = marshal_intent(prg_machine_aux_state_test_handler, 0, #{}), ?assertNot(maps:is_key(aux_state, Intent)), ?assertEqual([], maps:get(events, Intent)). @@ -570,20 +900,8 @@ process_crash_conforms_progressor_exception() -> 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}, prg_machine_events:event_metadata(1)), - ?assertEqual(#{<<"format_version">> => 0, <<"format">> => 0}, prg_machine_events:event_metadata(undefined)). - --spec unmarshal_event_format_reads_legacy_keys_test() -> _. -unmarshal_event_format_reads_legacy_keys_test() -> - %% New (both keys). - ?assertEqual(2, prg_machine_events:unmarshal_event_format(#{<<"format_version">> => 2, <<"format">> => 2})), - %% Legacy HG metadata: only <<"format_version">>. - ?assertEqual(1, prg_machine_events:unmarshal_event_format(#{<<"format_version">> => 1})), - %% Legacy FF metadata: only <<"format">>. - ?assertEqual(1, prg_machine_events:unmarshal_event_format(#{<<"format">> => 1})), - %% Defensive atom key and absence. - ?assertEqual(3, prg_machine_events:unmarshal_event_format(#{format => 3})), - ?assertEqual(undefined, prg_machine_events:unmarshal_event_format(#{})). + ?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() -> @@ -591,12 +909,12 @@ decode_term_reads_legacy_double_envelope_test() -> %% 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, prg_machine_codec:decode_term(Legacy)), + ?assertEqual(Args, decode_term(Legacy)), %% New single envelope still works (rollback/forward invariant). - ?assertEqual(Args, prg_machine_codec:decode_term(prg_machine_codec:encode_term(Args))), + ?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}, prg_machine_codec:decode_term(term_to_binary({bin, not_a_binary}))). + ?assertEqual({bin, not_a_binary}, decode_term(term_to_binary({bin, not_a_binary}))). -endif. diff --git a/apps/prg_machine/src/prg_machine_client.erl b/apps/prg_machine/src/prg_machine_client.erl deleted file mode 100644 index 5ccd0f44..00000000 --- a/apps/prg_machine/src/prg_machine_client.erl +++ /dev/null @@ -1,187 +0,0 @@ --module(prg_machine_client). - --include_lib("progressor/include/progressor.hrl"). - --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([remove/2]). --export([history_range/3]). - --spec start(prg_machine:namespace(), id(), prg_machine:args()) -> {ok, ok} | {error, exists | term()}. -start(NS, ID, Args) -> - Req = #{ - ns => NS, - id => ID, - args => prg_machine_codec:encode_term(Args), - context => prg_machine_env:encode_rpc_context() - }, - case progressor:init(Req) of - {ok, ok} = Ok -> - Ok; - {error, <<"process already exists">>} -> - {error, exists}; - {error, _} = Error -> - Error - end. - --spec call(prg_machine:namespace(), id(), prg_machine:call()) -> - {ok, prg_machine:response()} | {error, notfound | failed | term()}. -call(NS, ID, CallArgs) -> - call(NS, ID, CallArgs, undefined, undefined, forward). - --spec call( - prg_machine:namespace(), - id(), - prg_machine:call(), - prg_machine:event_id() | undefined, - non_neg_integer() | undefined, - forward | backward -) -> - {ok, prg_machine:response()} | {error, notfound | failed | 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, prg_machine_codec: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, _} = Error -> - Error - end. - --spec repair(prg_machine:namespace(), id(), prg_machine:args()) -> - {ok, term()} | {error, prg_machine:repair_error()}. -repair(NS, ID, Args) -> - Req = #{ - ns => NS, - id => ID, - args => prg_machine_codec:encode_term(Args), - context => prg_machine_env:encode_rpc_context() - }, - case progressor:repair(Req) of - {ok, Response} -> - {ok, prg_machine_codec: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, Reason} -> - %% The repair-failed reason is our own term encoded by process/3 - %% (marshal_process_result -> encode_term); hand it back as a term. - {error, {repair, {failed, prg_machine_codec:decode_term(Reason)}}} - end. - --spec get(prg_machine:namespace(), id(), history_range()) -> - {ok, prg_machine:machine()} | {error, prg_machine:get_error()}. -get(NS, ID, Range) -> - Req = request(NS, ID, undefined, Range), - case progressor:get(Req) of - {ok, Process} -> - case prg_machine_registry:lookup(NS) of - {ok, Handler} -> - {ok, prg_machine_events:unmarshal_machine(Handler, NS, Process)}; - {error, _} = Error -> - Error - end; - {error, <<"process not found">>} -> - {error, notfound}; - {error, {exception, _Class, _Reason} = Exception} -> - {error, Exception}; - {error, {exception, Class, Reason, _Stacktrace}} -> - {error, {exception, Class, Reason}} - end. - --spec get(prg_machine:namespace(), id()) -> {ok, prg_machine:machine()} | {error, prg_machine:get_error()}. -get(NS, ID) -> - get(NS, ID, #{direction => forward}). - --spec get_history(prg_machine:namespace(), id()) -> {ok, prg_machine:history()} | {error, prg_machine:get_error()}. -get_history(NS, ID) -> - get_history(NS, ID, undefined, undefined, forward). - --spec get_history( - prg_machine:namespace(), - id(), - prg_machine:event_id() | undefined, - non_neg_integer() | undefined -) -> - {ok, prg_machine:history()} | {error, prg_machine:get_error()}. -get_history(NS, ID, After, Limit) -> - get_history(NS, ID, After, Limit, forward). - --spec get_history( - prg_machine:namespace(), - id(), - prg_machine:event_id() | undefined, - non_neg_integer() | undefined, - forward | backward -) -> - {ok, prg_machine:history()} | {error, prg_machine: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(prg_machine:namespace(), id(), prg_machine:args()) -> - ok | {error, notfound | failed | prg_machine:processor_error() | term()}. -notify(NS, ID, Args) -> - case call(NS, ID, {notify, Args}) of - {ok, _} -> ok; - {error, _} = Error -> Error - end. - --spec remove(prg_machine:namespace(), id()) -> - ok | {error, notfound | failed | prg_machine:processor_error() | term()}. -remove(NS, ID) -> - case call(NS, ID, remove) of - {ok, _} -> ok; - {error, _} = Error -> Error - end. - --spec history_range(undefined | prg_machine:event_id(), undefined | non_neg_integer(), forward | backward) -> - history_range(). -history_range(Offset, Limit, Direction) -> - encode_range(Offset, Limit, Direction). - -request(NS, ID, Args, Range) -> - genlib_map:compact(#{ - ns => NS, - id => ID, - args => prg_machine_codec:encode_term(Args), - context => prg_machine_env:encode_rpc_context(), - range => Range - }). - -encode_range(After, Limit, Direction) -> - genlib_map:compact(#{ - offset => After, - limit => Limit, - direction => Direction - }). diff --git a/apps/prg_machine/src/prg_machine_codec.erl b/apps/prg_machine/src/prg_machine_codec.erl deleted file mode 100644 index 09a66496..00000000 --- a/apps/prg_machine/src/prg_machine_codec.erl +++ /dev/null @@ -1,21 +0,0 @@ --module(prg_machine_codec). - --export([encode_term/1]). --export([decode_term/1]). - --spec encode_term(term()) -> binary(). -encode_term(Term) -> - term_to_binary(Term). - --spec decode_term(term()) -> 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. diff --git a/apps/prg_machine/src/prg_machine_env.erl b/apps/prg_machine/src/prg_machine_env.erl deleted file mode 100644 index 624ba23b..00000000 --- a/apps/prg_machine/src/prg_machine_env.erl +++ /dev/null @@ -1,93 +0,0 @@ --module(prg_machine_env). - --export([encode_rpc_context/0]). --export([run/3]). - -%% Default woody deadline (30s, configurable per namespace via opts), restoring -%% the old hg_progressor behaviour. --define(DEFAULT_HANDLING_TIMEOUT, 30000). - --spec encode_rpc_context() -> binary(). -encode_rpc_context() -> - WoodyContext = op_context:current_woody_context(), - prg_machine_codec:encode_term(woody_rpc_helper:encode_rpc_context(WoodyContext, otel_ctx:get_current())). - --spec run(binary(), prg_machine:process_options(), fun(() -> Result)) -> Result. -run(BinCtx, Opts, Fun) when is_function(Fun, 0) -> - Enter = resolve_env_enter(Opts), - Leave = resolve_env_leave(Opts), - {WoodyCtx0, OtelCtx} = decode_rpc_context(BinCtx), - ok = woody_rpc_helper:attach_otel_context(OtelCtx), - WoodyCtx = ensure_deadline_set(WoodyCtx0, Opts), - ok = run_env_enter(Enter, WoodyCtx), - %% Enter succeeded: from here Leave must run exactly once. Errors raised - %% before this point fall through to the processor catch unchanged. - run_with_env_leave(Leave, Fun). - -decode_rpc_context(<<>>) -> - woody_rpc_helper:decode_rpc_context(#{}); -decode_rpc_context(Bin) -> - woody_rpc_helper:decode_rpc_context(prg_machine_codec:decode_term(Bin)). - -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. - -resolve_env_enter(Opts) -> - case maps:is_key(env_enter, Opts) of - true -> - maps:get(env_enter, Opts); - false -> - case maps:get(context_binding, Opts, undefined) of - Binding when is_map(Binding) -> - fun(WoodyCtx) -> op_context:env_enter(WoodyCtx, Binding) end; - _ -> - fun(_) -> ok end - end - end. - -resolve_env_leave(Opts) -> - case maps:is_key(env_leave, Opts) of - true -> - maps:get(env_leave, Opts); - false -> - case maps:get(context_binding, Opts, undefined) of - Binding when is_map(Binding) -> - fun() -> op_context:env_leave(Binding) end; - _ -> - fun() -> ok end - end - end. - -run_env_enter(Enter, WoodyCtx) when is_function(Enter, 1) -> - Enter(WoodyCtx); -run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> - Enter(). - -run_with_env_leave(Leave, Fun) when is_function(Leave, 0), is_function(Fun, 0) -> - try Fun() of - Result -> - safe_env_leave(Leave), - Result - catch - Class:Reason:Stacktrace -> - safe_env_leave(Leave), - erlang:raise(Class, Reason, Stacktrace) - end. - -safe_env_leave(Leave) -> - try - Leave() - catch - Class:Reason:Stacktrace -> - logger:error( - "prg_machine env_leave failed: ~p:~p", - [Class, Reason], - #{stacktrace => Stacktrace} - ) - end. diff --git a/apps/prg_machine/src/prg_machine_events.erl b/apps/prg_machine/src/prg_machine_events.erl deleted file mode 100644 index c15c6ea4..00000000 --- a/apps/prg_machine/src/prg_machine_events.erl +++ /dev/null @@ -1,135 +0,0 @@ --module(prg_machine_events). - --export([collapse/2]). --export([emit_event/1]). --export([emit_events/1]). --export([timestamp/0]). --export([unmarshal_machine/3]). --export([marshal_new_events/3]). --export([marshal_aux_state/2]). - --ifdef(TEST). --export([event_metadata/1]). --export([unmarshal_event_format/1]). --endif. - --spec collapse(module(), prg_machine:machine()) -> term(). -collapse(Handler, #{history := History, aux_state := AuxState}) -> - lists:foldl( - fun({EventID, Ts, Body}, Model) -> - prg_machine:callback_apply_event(Handler, EventID, Ts, Body, Model) - end, - initial_model(AuxState), - History - ). - --spec emit_event(term()) -> [{ev, prg_machine:timestamp(), term()}]. -emit_event(Event) -> - emit_events([Event]). - --spec emit_events([term()]) -> [{ev, prg_machine:timestamp(), term()}]. -emit_events(Events) -> - Ts = timestamp(), - [{ev, Ts, Body} || Body <- Events]. - --spec timestamp() -> prg_machine:timestamp(). -timestamp() -> - Now = erlang:system_time(microsecond), - {Seconds, Micro} = prg_utils:split_timestamp(Now), - {calendar:system_time_to_universal_time(Seconds, second), Micro}. - --spec unmarshal_machine(module(), prg_machine:namespace(), map()) -> prg_machine: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 - }. - --spec marshal_new_events(module(), non_neg_integer(), [prg_machine:event_body()]) -> [map()]. -marshal_new_events(Handler, LastEventID, Bodies) -> - %% One microsecond timestamp for the whole batch (as the old emit_events did). - %% The PG backend stores timestamptz with microseconds and auto-detects units. - Ts = erlang:system_time(microsecond), - lists:zipwith( - fun(EventID, Body) -> - {Format, Bin} = marshal_event_body(Handler, Body), - #{ - event_id => EventID, - timestamp => Ts, - metadata => event_metadata(Format), - payload => Bin - } - end, - lists:seq(LastEventID + 1, LastEventID + length(Bodies)), - Bodies - ). - -unmarshal_event(Handler, #{ - event_id := EventID, - timestamp := TsSec, - metadata := Meta, - payload := Payload -}) -> - Format = unmarshal_event_format(Meta), - Body = prg_machine:unmarshal_event_body(Handler, Format, Payload), - {EventID, event_timestamp_to_datetime(TsSec), Body}; -unmarshal_event(_Handler, #{event_id := EventID} = Ev) -> - erlang:error({missing_event_payload, EventID, maps:keys(Ev)}). - -marshal_event_body(Handler, Body) -> - prg_machine:callback_marshal_event_body(Handler, Body). - --spec marshal_aux_state(module(), term()) -> binary(). -marshal_aux_state(Handler, AuxSt) -> - prg_machine:callback_marshal_aux_state(Handler, AuxSt). - -unmarshal_aux_state(_Handler, undefined) -> - undefined; -unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> - prg_machine:callback_unmarshal_aux_state(Handler, 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">>). --spec event_metadata(undefined | non_neg_integer()) -> map(). -event_metadata(undefined) -> - event_metadata(0); -event_metadata(Format) when is_integer(Format) -> - #{<<"format_version">> => Format, <<"format">> => Format}. - -%% Read order: legacy HG <<"format_version">> -> legacy FF <<"format">> -> -%% atom format (defensive) -> undefined. --spec unmarshal_event_format(map()) -> undefined | non_neg_integer(). -unmarshal_event_format(Meta) -> - maps:get( - <<"format_version">>, - Meta, - maps:get(<<"format">>, Meta, maps:get(format, Meta, undefined)) - ). - -%% 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}. - -initial_model(AuxState) when is_map(AuxState) -> - maps:get(model, AuxState, undefined); -initial_model(_AuxState) -> - undefined. - -range_from_process(#{range := Range = #{}}) -> - Range; -range_from_process(_) -> - #{direction => forward}. diff --git a/apps/prg_machine/src/prg_machine_processor.erl b/apps/prg_machine/src/prg_machine_processor.erl deleted file mode 100644 index 6eba3b08..00000000 --- a/apps/prg_machine/src/prg_machine_processor.erl +++ /dev/null @@ -1,87 +0,0 @@ --module(prg_machine_processor). - --export([process/3]). - --ifdef(TEST). --export([marshal_intent/3]). --endif. - --spec process({init | call | repair | notify | timeout, binary(), map()}, prg_machine: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} -> - prg_machine_env:run(BinCtx, Opts, fun() -> - LastEventID = maps:get(last_event_id, Process), - Machine = prg_machine_events: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. - -dispatch(Handler, init, BinArgs, Machine) -> - Args = prg_machine_codec:decode_term(BinArgs), - prg_machine:callback_init(Handler, Args, Machine); -dispatch(Handler, timeout, _BinArgs, Machine) -> - prg_machine:callback_process_signal(Handler, timeout, Machine); -dispatch(Handler, notify, BinArgs, Machine) -> - Args = prg_machine_codec:decode_term(BinArgs), - dispatch_notification(Handler, Args, Machine); -dispatch(Handler, call, BinArgs, Machine) -> - case prg_machine_codec:decode_term(BinArgs) of - {notify, Args} -> - dispatch_notification(Handler, Args, Machine); - remove -> - #{events => [], action => remove, auxst => maps:get(aux_state, Machine)}; - Call -> - prg_machine:callback_process_call(Handler, Call, Machine) - end; -dispatch(Handler, repair, BinArgs, Machine) -> - Args = prg_machine_codec:decode_term(BinArgs), - case prg_machine:callback_process_repair(Handler, Args, Machine) of - {error, Reason} -> - {error, Reason}; - Result when is_map(Result) -> - Result - end. - -dispatch_notification(Handler, Args, Machine) -> - prg_machine:callback_process_notification(Handler, Args, Machine). - -marshal_process_result(Handler, LastEventID, {Response, Result}) when is_map(Result) -> - Intent = marshal_intent(Handler, LastEventID, Result), - {ok, Intent#{response => prg_machine_codec: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, prg_machine_codec:encode_term(Reason)}. - --spec marshal_intent(module(), non_neg_integer(), prg_machine:result()) -> map(). -marshal_intent(Handler, LastEventID, Result) when is_map(Result) -> - Base0 = #{events => prg_machine_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 => prg_machine_events:marshal_aux_state(Handler, maps:get(auxst, Result))}; - false -> - Base1 - end. diff --git a/apps/prg_machine/src/prg_machine_registry.erl b/apps/prg_machine/src/prg_machine_registry.erl index a6185742..92f03659 100644 --- a/apps/prg_machine/src/prg_machine_registry.erl +++ b/apps/prg_machine/src/prg_machine_registry.erl @@ -2,6 +2,8 @@ %%% Namespace -> handler module registry (ETS owner). +-behaviour(gen_server). + -define(TABLE, prg_machine_dispatch). -define(SERVER, ?MODULE). @@ -9,7 +11,14 @@ -export([start_link/1]). -export([lookup/1]). -export([ensure_table/0]). --export([init/1]). + +-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) -> @@ -24,7 +33,7 @@ get_child_spec(Handlers) -> -spec start_link([module()]) -> {ok, pid()} | {error, term()}. start_link(Handlers) -> - proc_lib:start_link(?MODULE, init, [Handlers]). + gen_server:start_link({local, ?SERVER}, ?MODULE, Handlers, []). -spec lookup(prg_machine:namespace()) -> {ok, module()} | {error, {unknown_namespace, prg_machine:namespace()}}. lookup(NS) -> @@ -50,18 +59,20 @@ ensure_table() -> ok end. --spec init([module()]) -> ok. +-spec init([module()]) -> {ok, state()}. init(Handlers) -> - true = register(?SERVER, self()), ok = ensure_table(), true = ets:insert(?TABLE, [{prg_machine:handler_namespace(H), H} || H <- Handlers]), - proc_lib:init_ack({ok, self()}), - loop(). + {ok, #state{handlers = Handlers}}. -loop() -> - receive - stop -> - ok; - _Msg -> - loop() - end. +-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/legacy_fixture_golden_test.erl b/apps/prg_machine/test/legacy_fixture_golden_test.erl index 20e9ec68..6f3271ef 100644 --- a/apps/prg_machine/test/legacy_fixture_golden_test.erl +++ b/apps/prg_machine/test/legacy_fixture_golden_test.erl @@ -90,7 +90,7 @@ legacy_hg_invoice_event_test() -> InvoiceID = trim_binary(legacy_fixture_lib:read_bin(Dir, "process_id.txt")), ?assertEqual(#{<<"format_version">> => 1}, Meta), ?assertNot(maps:is_key(<<"format">>, Meta)), - Changes = hg_invoice:unmarshal_event_body(1, Payload), + Changes = hg_invoice:unmarshal_event_body(Payload), ?assertMatch([{invoice_created, _}], Changes), [{invoice_created, {payproc_InvoiceCreated, Invoice}}] = Changes, ?assertEqual(InvoiceID, Invoice#domain_Invoice.id). @@ -124,7 +124,7 @@ legacy_hg_call_args_test() -> legacy_hg_event_rollback_test() -> Dir = legacy_fixture_lib:hg_invoice_dir(), LegacyPayload = legacy_fixture_lib:read_event_payload(Dir, 1), - Changes = hg_invoice:unmarshal_event_body(1, LegacyPayload), + Changes = hg_invoice:unmarshal_event_body(LegacyPayload), {Format, NewPayload} = hg_invoice:marshal_event_body(Changes), ?assertEqual(1, Format), ?assertEqual(LegacyPayload, NewPayload). diff --git a/docs/prg-machine-fix-plan.md b/docs/prg-machine-fix-plan.md new file mode 100644 index 00000000..3d068c36 --- /dev/null +++ b/docs/prg-machine-fix-plan.md @@ -0,0 +1,237 @@ +# План правок по ревью `add_prg_layer` + +Статус: согласован по итогам ревью (2026-06-12). База диффа — `e035d3c1` (epic/monorepo). + +## Главный вывод: миграция данных НЕ нужна + +Все найденные несовместимости живут в нашем промежуточном слое (`prg_machine`, +`ff_machine_codec`, `hg_invoice`), а не в thrift-схемах и не в progressor: + +| Данные | Старый формат в БД | Что сломали | Лечится в слое | +|---|---|---|---| +| HG события | payload `t2b(msgpack {bin, Thrift})`, metadata `format_version` | читаем только ключ `format` | да: payload и так бинарно совместим, чинится только ключ метаданных | +| FF события | payload `t2b({bin, Thrift})`, metadata `format` | новый код ждёт сырой thrift | да: compat-чтение + запись в старом конверте | +| FF aux_state | `t2b(map)` | новый код ждёт msgpack-thrift | да: try-чтение обоих | +| HG aux_state | `t2b(#mg_stateproc_Content{})` | живёт на двух catch-ловушках | да: явная клауза | +| HG call-args задач | `{thrift_call, Service, FunRef, EncodedArgs}` в старой обёртке | новая форма `{FunRef, Args}` | да: compat-клауза чтения | + +Принцип этапа 1: **писать в старом формате, читать оба**. Тогда и rollback +безопасен (старый код читает всё, что записал новый). Унификация конверта +HG+FF («дальше одинаково для обоих») — отдельный финальный этап с bump'ом +версии формата, когда reader уже повсеместно выкачен. + +--- + +## Этап 1. Совместимость данных (блокер) + +### 1.1 Метаданные событий: оба ключа +`apps/prg_machine/src/prg_machine.erl` + +Важно: «рабочий» ключ у стеков разный. Старый HG (`hg_progressor`) писал +`<<"format_version">>`, старый FF (`machinery_prg_backend`) — `<<"format">>` +(и читает только его, с дефолтом 0). Возврат к одному из них ломает rollback +второго стека, поэтому: + +- Запись: `event_metadata/1` → `#{<<"format_version">> => V, <<"format">> => V}` + — оба старых ключа. Это разом чинит чтение старых HG-событий, event sink + (`prg_notifier` читает `format_version`) и rollback обоих стеков + (старый HG-reader найдёт `format_version`, старый FF-reader — `format`). +- Чтение: `unmarshal_event/2` — порядок `<<"format_version">>` → `<<"format">>` + → `format` → `undefined`. + +### 1.2 FF события: старый конверт на запись, sniff на чтение +`apps/ff_transfer/src/ff_machine_codec.erl` + +- `payload_to_binary`: вернуть старый конверт — `term_to_binary({bin, ThriftBin})` + (как писал `machinery_prg_backend` через `machinery_utils:encode(term, ...)`). +- `unmarshal_thrift_event` → принимать оба варианта: + - первый байт `131` → `binary_to_term` → `{bin, Bin}` → thrift из `Bin`; + любое другое msgpack-значение → понятная ошибка `{legacy_msgpack_event, ...}` + (по данным таких быть не должно — проверить на стейдже); + - иначе → сырой thrift (события, записанные текущей веткой в dev/test). +- То же чтение используется `ff_machine_trace` — автоматически чинится. + +Примечание: sniff по первому байту безопасен только в эту сторону +(thrift-струkt не начинается с `131`); для msgpack наоборот — fixmap(3) тоже +`0x83`, поэтому порядок проверки именно такой. + +### 1.3 FF aux_state: запись t2b, чтение try-оба +`apps/ff_transfer/src/ff_machine_codec.erl` + +- `marshal_aux_state` → `term_to_binary(AuxSt)` (старое поведение). +- `unmarshal_aux_state` → `try binary_to_term` (старый формат), на ошибке — + msgpack-путь (`binary_to_payload` + `ff_machine_schema:unmarshal`) для + записанного веткой. + +### 1.4 HG aux_state: явная клауза вместо catch-ловушек +`apps/hellgate/src/hg_invoice.erl` (`unmarshal_aux_state/1`) + +- Явно матчить `#mg_stateproc_Content{format_version = _, data = Data}` → + `mg_msgpack_marshalling:unmarshal(Data)`; убрать слепые `catch _:_`. +- Проверить ветку `dispatch(call, remove)` в `prg_machine`: туда уходит уже + размаршалленный aux_state — `marshal_aux_state` должен его переживать. + +### 1.5 Pending-задачи: compat-чтение args (решение — доработать, не откатывать) + +Заключение: формат менялся **только у HG** (FF всегда писал plain +`term_to_binary(Args)` — там регрессии нет). Новая форма `{FunRef, Args}` +проще и не тянет thrift-сериализацию в слой `prg_machine` — откатывать на +`{thrift_call, Service, FunRef, EncodedArgs}` не стоит. Достаточно +compat-чтения, окно риска — только незавершённые call/init задачи в момент +деплоя (timeout-задачи с пустыми args не затронуты): + +- `prg_machine:decode_term/1`: результат `{bin, Bin}` → `binary_to_term(Bin)` + (старая двойная обёртка). +- `apps/hellgate/src/hg_invoice.erl` (`process_call`): клауза для старой формы + `{thrift_call, Service, FunRef, EncodedArgs}` → `hg_proto_utils`-десериализация + args → дальше обычный путь `{FunRef, Args}`. Перед реализацией сверить точную + старую форму по `e035d3c1:apps/hellgate/src/hg_machine.erl` (строки ~137–143) + и старому клиентскому пути `hg_progressor:call`. + +### 1.6 Golden-тесты на старые форматы (критерий приёмки этапа) + +- Снять реальные бинари (payload/aux_state/metadata/args), сгенерированные кодом + базового коммита (или со стейджа), положить фикстурами. +- CT/eunit: чтение старого события, старого aux_state, старого call-арга — для + hg_invoice и каждого из 5 ff-неймспейсов; плюс симметричный тест «новая запись + читается старым форматом конверта» (rollback-инвариант). + +--- + +## Этап 2. Таймстемпы событий — вернуть микросекунды (мажор) + +PG-бэкенд progressor хранит `timestamptz` с микросекундами и сам детектит +юниты (`prg_utils:split_timestamp/to_microseconds`) — секунды сейчас режет +только `prg_machine`. + +- `prg_machine:marshal_new_events`: писать `timestamp => erlang:system_time(microsecond)` + (без `div 1000000`), один таймстемп на весь батч (как старый `emit_events`). +- Чтение: `event_timestamp_to_datetime` → возвращать `{calendar:datetime(), Micro}` + (machinery-формат); обновить тип `machine_event()` и спеки. +- HG: `hg_invoice:event_timestamp_to_binary` — форматировать с микро + (`hg_datetime`, как старый MG RFC3339). +- FF: `marshal_event_body` — в `{ev, {Dt, USec}, Body}` класть реальные микро + вместо захардкоженного `0`; «недостижимая» клауза `codec_timestamp({Dt, USec})` + в 5 `*_machine` модулях становится рабочей; `events/2` (GetEvents) наружу + отдаёт микро. +- В progressor при его ревью: поправить спеку `event() :: timestamp := timestamp_sec()` + → допускать `timestamp_us()` (фактически уже работает). + +--- + +## Этап 3. Ошибки и устойчивость (мажоры) + +### 3.1 `notify` / `remove` +- `prg_machine:notify/3`, `remove/2`: обработать все исходы + (`{error, failed}`, `{error, {exception, _, _}}`, прочие guard-ошибки) — + без `case_clause`. +- `ff_withdrawal_session:process_session`: восстановить старую семантику — + notify в сломанный withdrawal глотается с warning-логом (`{error, failed}` → + `ok`), сессия не заражается. Остальные ошибки — как сейчас, в error. + +### 3.2 `env_enter`/`env_leave` +- `prg_machine:process/3`: флаг «enter выполнен»; `after` зовёт Leave только + если был Enter. Ошибки до Enter возвращаются progressor'у как `{error, _}` + без маскировки исключением из `after`. + +### 3.3 Дефолтный woody deadline +- В `process/3` после восстановления контекста — `ensure_deadline_set` + (дефолт 30 с, конфигурируемо через опции неймспейса), как делал старый + `hg_progressor` через `hg_woody_service_wrapper:ensure_woody_deadline_set/2`. + +### 3.4 Repair-путь +- `prg_action:marshal_timer`: клауза `{deadline, {{_,_}=Dt, USec}}` (machinery-формат + из `ff_codec:unmarshal(timer, ...)`) — срезать USec, как в + `ff_adapter_withdrawal_codec:unmarshal_provider_timer/1`. Тест на deadline-таймер + в `ff_withdrawal_codec` (сейчас покрыт только `{timeout, 0}`). +- `prg_machine:repair/3`: декодировать `Reason` (`decode_term`) в ветке + `{error, {repair, {failed, Reason}}}` — наружу term, а не t2b-бинарь. +- Выправить спеки `ff_*_machine:repair/2` и хендлеры (`ff_withdrawal_repair` + и др.) под фактическую форму ошибки; убрать недостижимую ветку + `{error, failed} -> {failed, {invalid_result, unexpected_failure}}`. + +### 3.5 `hg_invoice_handler` +- `get_state/1`: `throw(#payproc_InvoiceNotFound{})` — голый рекорд, как в + `map_history_error`. +- `ensure_started/2`: вернуть ветку `{error, Reason} -> erlang:error(Reason)`. + +### 3.6 Контракт исключений процессора (решение) +- Форму `{error, {exception, Class, Reason}}` оставляем как есть — и на проводе + к progressor (его контракт: `is_retryable/5` по 3-tuple решает «не ретраить»), + и в клиентском API как pass-through. Переименование маркера (`failed` и т.п.) + — косметика, не делаем. +- Чиним только реальные баги: + - `prg_machine:get/3`: убрать `raise_exception` — возвращать + `{error, {exception, Class, Reason}}` как обычную ошибку, а не + re-raise с пустым stacktrace; + - убедиться, что потребители (`hg_invoicing_machine_client`, `ff_*_machine`, + repair-хендлеры) матчат эту форму: детали — в лог / маппинг в + thrift-ошибки, без `case_clause`. +- Голый `{error, failed}` остаётся для статусной ошибки + `<<"process is error">>` (процесс уже в error; деталей в ответе progressor + нет — при необходимости их даёт отдельный `get` с `detail` процесса). +- `docs/prg-machine.md`: убрать упоминание 4-tuple со stacktrace, описать + фактический контракт. + +--- + +## Этап 4. Контекст и конфиг + +### 4.1 Убрать глобальный `woody_context_loader` +- Удалить `application:set_env(prg_machine, woody_context_loader, fun ...)` из + `hellgate.erl` и `ff_server.erl` (анонимный fun в app env + общий ключ — + ломается на hot upgrade и при совместном старте в одном узле). +- `prg_machine:encode_rpc_context/0` → `op_context:current_woody_context()`: + пробует hg-binding, затем ff-binding (gproc-ключи текущего процесса различны, + коллизий нет), fallback `woody_context:new()` с warning-логом. +- Добавить `op_context` в `applications` у `prg_machine.app.src` + (зависимость уже фактическая — `resolve_env_enter`). + +### 4.2 `binary_to_term` без `[safe]` +- Старый стек (`machinery_utils:decode`, `hg_progressor`) работал без `[safe]` — + возвращаем как было: убрать `[safe]` во всех decode собственных данных + (`prg_machine:decode_term`, `unmarshal_event_body` fallback, + `unmarshal_aux_state`, `ff_machine_trace:decode_term`, `hg_invoice`). + +--- + +## Этап 5. Гигиена HG/FF + +- `hg_invoice`: `log_changes` для signal/repair (как старый `handle_result`); + убрать двойной `validate_changes` в `process_call` (заодно закрывает пункт + «двойной collapse» из техдолга в `docs/prg-machine.md`). +- FF копипаста ×5: вынести `to_repair_machine/1`, `from_repair_result/2`, + `repair_events_to_domain/1`, `event_body_from_timestamped/1`, + `history_times/1`, `history_to_events/1`, `codec_timestamp/1` в общий модуль + (`ff_machine_codec` или новый `ff_machine_lib`); удалить мёртвое поле `times` + из `st()` пяти `*_machine`, либо начать использовать. +- FF: вернуть no-op `process_notification` (`#{}`) у session/source/destination + вместо принудительного `action => timeout`; убрать лишний `action => timeout` + из `ff_destination:init`. +- FF `machine_to_st`: явная ветка для `aux_state = undefined` (сейчас дефолт + `maps:get(ctx, AuxState, #{})` мёртвый, падает `badmap`). +- `docs/prg-machine.md`: обновить разделы про ошибки/форматы по итогам этапов 1–4. + +Вне скоупа (решено): `hg_hybrid` не возвращаем; trace (`ff_machine_trace`, +дефолт формата, `<<>>`-args) — отдельный ПР с переездом на thrift; тег +progressor в `rebar.config` — после ревью progressor. + +--- + +## Этап 6 (опционально, отдельный ПР). Единый конверт HG+FF + +После выкатки этапов 1–5 и стабилизации: + +- format 2 = сырой thrift-binary payload для **обоих** стеков (HG уходит от + `t2b(msgpack)`, FF — от `t2b({bin, ...})`). +- Reader к этому моменту уже умеет оба формата (этап 1), поэтому включение + записи format 2 — отдельный маленький коммит; rollback-политика: откат только + на версии, содержащие reader этапа 1. +- Сюда же — переезд trace на thrift (`docs/trace-api-thrift.md`). + +## Порядок и критерии + +1 → 2 → 3 → 4 → 5 — каждый этап самостоятелен и мержибелен отдельно; 1 и 2 +трогают одни и те же функции маршалинга, их удобно делать подряд. Критерий +этапа 1 — golden-тесты (1.6) зелёные; критерий общий — CT + dialyzer + compose +зелёные, grep-инварианты из `docs/prg-machine.md` соблюдены. diff --git a/docs/prg-machine.md b/docs/prg-machine.md index 55fa3394..3e2ef578 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -2,7 +2,7 @@ Единый runtime поверх progressor для HG и FF. Контракт `action()` в progressor: `progressor/docs/step-effect-migration.md`. -*Обновлено: 2026-06-16. CI (compile, dialyzer, CT + compose) — см. текущий PR.* +*Обновлено: 2026-06-13. CI (compile, dialyzer, CT + compose) — green локально.* --- @@ -42,13 +42,8 @@ sequenceDiagram | Модуль | Роль | |--------|------| -| `prg_machine` | публичный фасад: behaviour, client API, `process/3`, `collapse` / `emit_events` | -| `prg_machine_client` | progressor client API: `start` / `call` / `get` / `repair` / history | -| `prg_machine_processor` | progressor `process/3`: dispatch доменного callback и сбор intent | -| `prg_machine_events` | event fold, event payload/metadata, aux_state codec defaults | -| `prg_machine_env` | woody/otel context, deadline, `env_enter` / `env_leave` | -| `prg_machine_codec` | term envelope и legacy double-envelope decode | -| `prg_machine_registry` | ETS `{Namespace, Handler}` под простым owner-процессом | +| `prg_machine` | behaviour, client API, `process/3`, `collapse` / `emit_events` | +| `prg_machine_registry` | ETS `{Namespace, Handler}`; `{unknown_namespace, NS}` | | `prg_action` | `{timeout, Sec}` / `{deadline, Dt}` → wire `action()` | | `ff_machine_lib` | общие FF-хелперы: repair/history/timestamp для `*_machine` | @@ -66,14 +61,6 @@ sequenceDiagram `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 @@ -180,15 +167,15 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa ### 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`. +Реплей: `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 на `apply_event/2`. ### Прочее (низкий приоритет) -- Golden-fixtures со стейджа для legacy payload/aux_state -- Registry без ETS `heir` — краткое окно при рестарте owner-процесса +- Golden-fixtures со стейджа для legacy payload/aux_state (см. `docs/prg-machine-fix-plan.md` §1.6) +- Registry без ETS `heir` — краткое окно при рестарте - Фиктивная обёртка `{ev, Ts, Body}` в event payload - Trace: сейчас HTTP JSON (`ff_machine_trace`); Thrift — `docs/trace-api-thrift.md` -- Единый конверт HG+FF (format 2) +- Единый конверт HG+FF (format 2) — этап 6 fix-plan --- @@ -196,7 +183,7 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa 1. `sys.config` — `client => prg_machine` 2. `-behaviour(prg_machine)` + callbacks -3. `apply_event/4` для `collapse/2` +3. `apply_event/2` (FF) или `apply_event/4` (HG) для `collapse/2` 4. `*_machine.erl` — только `prg_machine:*` 5. Handler в `get_child_spec` (`hellgate.erl` / `ff_server.erl`) 6. CT suite From 44d55443a7bb80b73c8b4310469fce6d428f5316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 17 Jun 2026 13:03:32 +0300 Subject: [PATCH 48/62] Revert "Refactor unmarshal_event_body function across multiple modules to simplify its signature by removing the format parameter. Update specifications in ff_machine_lib, ff_deposit_machine, ff_destination_machine, ff_source_machine, ff_withdrawal_machine, and hg_invoice modules for consistency. This change enhances code clarity and prepares for improved event handling." This reverts commit 1b4f07a9464c79dda439de2c8166139ddbcbd0d6. --- docs/prg-machine-fix-plan.md | 237 ----------------------------------- docs/prg-machine.md | 29 +++-- 2 files changed, 21 insertions(+), 245 deletions(-) delete mode 100644 docs/prg-machine-fix-plan.md diff --git a/docs/prg-machine-fix-plan.md b/docs/prg-machine-fix-plan.md deleted file mode 100644 index 3d068c36..00000000 --- a/docs/prg-machine-fix-plan.md +++ /dev/null @@ -1,237 +0,0 @@ -# План правок по ревью `add_prg_layer` - -Статус: согласован по итогам ревью (2026-06-12). База диффа — `e035d3c1` (epic/monorepo). - -## Главный вывод: миграция данных НЕ нужна - -Все найденные несовместимости живут в нашем промежуточном слое (`prg_machine`, -`ff_machine_codec`, `hg_invoice`), а не в thrift-схемах и не в progressor: - -| Данные | Старый формат в БД | Что сломали | Лечится в слое | -|---|---|---|---| -| HG события | payload `t2b(msgpack {bin, Thrift})`, metadata `format_version` | читаем только ключ `format` | да: payload и так бинарно совместим, чинится только ключ метаданных | -| FF события | payload `t2b({bin, Thrift})`, metadata `format` | новый код ждёт сырой thrift | да: compat-чтение + запись в старом конверте | -| FF aux_state | `t2b(map)` | новый код ждёт msgpack-thrift | да: try-чтение обоих | -| HG aux_state | `t2b(#mg_stateproc_Content{})` | живёт на двух catch-ловушках | да: явная клауза | -| HG call-args задач | `{thrift_call, Service, FunRef, EncodedArgs}` в старой обёртке | новая форма `{FunRef, Args}` | да: compat-клауза чтения | - -Принцип этапа 1: **писать в старом формате, читать оба**. Тогда и rollback -безопасен (старый код читает всё, что записал новый). Унификация конверта -HG+FF («дальше одинаково для обоих») — отдельный финальный этап с bump'ом -версии формата, когда reader уже повсеместно выкачен. - ---- - -## Этап 1. Совместимость данных (блокер) - -### 1.1 Метаданные событий: оба ключа -`apps/prg_machine/src/prg_machine.erl` - -Важно: «рабочий» ключ у стеков разный. Старый HG (`hg_progressor`) писал -`<<"format_version">>`, старый FF (`machinery_prg_backend`) — `<<"format">>` -(и читает только его, с дефолтом 0). Возврат к одному из них ломает rollback -второго стека, поэтому: - -- Запись: `event_metadata/1` → `#{<<"format_version">> => V, <<"format">> => V}` - — оба старых ключа. Это разом чинит чтение старых HG-событий, event sink - (`prg_notifier` читает `format_version`) и rollback обоих стеков - (старый HG-reader найдёт `format_version`, старый FF-reader — `format`). -- Чтение: `unmarshal_event/2` — порядок `<<"format_version">>` → `<<"format">>` - → `format` → `undefined`. - -### 1.2 FF события: старый конверт на запись, sniff на чтение -`apps/ff_transfer/src/ff_machine_codec.erl` - -- `payload_to_binary`: вернуть старый конверт — `term_to_binary({bin, ThriftBin})` - (как писал `machinery_prg_backend` через `machinery_utils:encode(term, ...)`). -- `unmarshal_thrift_event` → принимать оба варианта: - - первый байт `131` → `binary_to_term` → `{bin, Bin}` → thrift из `Bin`; - любое другое msgpack-значение → понятная ошибка `{legacy_msgpack_event, ...}` - (по данным таких быть не должно — проверить на стейдже); - - иначе → сырой thrift (события, записанные текущей веткой в dev/test). -- То же чтение используется `ff_machine_trace` — автоматически чинится. - -Примечание: sniff по первому байту безопасен только в эту сторону -(thrift-струkt не начинается с `131`); для msgpack наоборот — fixmap(3) тоже -`0x83`, поэтому порядок проверки именно такой. - -### 1.3 FF aux_state: запись t2b, чтение try-оба -`apps/ff_transfer/src/ff_machine_codec.erl` - -- `marshal_aux_state` → `term_to_binary(AuxSt)` (старое поведение). -- `unmarshal_aux_state` → `try binary_to_term` (старый формат), на ошибке — - msgpack-путь (`binary_to_payload` + `ff_machine_schema:unmarshal`) для - записанного веткой. - -### 1.4 HG aux_state: явная клауза вместо catch-ловушек -`apps/hellgate/src/hg_invoice.erl` (`unmarshal_aux_state/1`) - -- Явно матчить `#mg_stateproc_Content{format_version = _, data = Data}` → - `mg_msgpack_marshalling:unmarshal(Data)`; убрать слепые `catch _:_`. -- Проверить ветку `dispatch(call, remove)` в `prg_machine`: туда уходит уже - размаршалленный aux_state — `marshal_aux_state` должен его переживать. - -### 1.5 Pending-задачи: compat-чтение args (решение — доработать, не откатывать) - -Заключение: формат менялся **только у HG** (FF всегда писал plain -`term_to_binary(Args)` — там регрессии нет). Новая форма `{FunRef, Args}` -проще и не тянет thrift-сериализацию в слой `prg_machine` — откатывать на -`{thrift_call, Service, FunRef, EncodedArgs}` не стоит. Достаточно -compat-чтения, окно риска — только незавершённые call/init задачи в момент -деплоя (timeout-задачи с пустыми args не затронуты): - -- `prg_machine:decode_term/1`: результат `{bin, Bin}` → `binary_to_term(Bin)` - (старая двойная обёртка). -- `apps/hellgate/src/hg_invoice.erl` (`process_call`): клауза для старой формы - `{thrift_call, Service, FunRef, EncodedArgs}` → `hg_proto_utils`-десериализация - args → дальше обычный путь `{FunRef, Args}`. Перед реализацией сверить точную - старую форму по `e035d3c1:apps/hellgate/src/hg_machine.erl` (строки ~137–143) - и старому клиентскому пути `hg_progressor:call`. - -### 1.6 Golden-тесты на старые форматы (критерий приёмки этапа) - -- Снять реальные бинари (payload/aux_state/metadata/args), сгенерированные кодом - базового коммита (или со стейджа), положить фикстурами. -- CT/eunit: чтение старого события, старого aux_state, старого call-арга — для - hg_invoice и каждого из 5 ff-неймспейсов; плюс симметричный тест «новая запись - читается старым форматом конверта» (rollback-инвариант). - ---- - -## Этап 2. Таймстемпы событий — вернуть микросекунды (мажор) - -PG-бэкенд progressor хранит `timestamptz` с микросекундами и сам детектит -юниты (`prg_utils:split_timestamp/to_microseconds`) — секунды сейчас режет -только `prg_machine`. - -- `prg_machine:marshal_new_events`: писать `timestamp => erlang:system_time(microsecond)` - (без `div 1000000`), один таймстемп на весь батч (как старый `emit_events`). -- Чтение: `event_timestamp_to_datetime` → возвращать `{calendar:datetime(), Micro}` - (machinery-формат); обновить тип `machine_event()` и спеки. -- HG: `hg_invoice:event_timestamp_to_binary` — форматировать с микро - (`hg_datetime`, как старый MG RFC3339). -- FF: `marshal_event_body` — в `{ev, {Dt, USec}, Body}` класть реальные микро - вместо захардкоженного `0`; «недостижимая» клауза `codec_timestamp({Dt, USec})` - в 5 `*_machine` модулях становится рабочей; `events/2` (GetEvents) наружу - отдаёт микро. -- В progressor при его ревью: поправить спеку `event() :: timestamp := timestamp_sec()` - → допускать `timestamp_us()` (фактически уже работает). - ---- - -## Этап 3. Ошибки и устойчивость (мажоры) - -### 3.1 `notify` / `remove` -- `prg_machine:notify/3`, `remove/2`: обработать все исходы - (`{error, failed}`, `{error, {exception, _, _}}`, прочие guard-ошибки) — - без `case_clause`. -- `ff_withdrawal_session:process_session`: восстановить старую семантику — - notify в сломанный withdrawal глотается с warning-логом (`{error, failed}` → - `ok`), сессия не заражается. Остальные ошибки — как сейчас, в error. - -### 3.2 `env_enter`/`env_leave` -- `prg_machine:process/3`: флаг «enter выполнен»; `after` зовёт Leave только - если был Enter. Ошибки до Enter возвращаются progressor'у как `{error, _}` - без маскировки исключением из `after`. - -### 3.3 Дефолтный woody deadline -- В `process/3` после восстановления контекста — `ensure_deadline_set` - (дефолт 30 с, конфигурируемо через опции неймспейса), как делал старый - `hg_progressor` через `hg_woody_service_wrapper:ensure_woody_deadline_set/2`. - -### 3.4 Repair-путь -- `prg_action:marshal_timer`: клауза `{deadline, {{_,_}=Dt, USec}}` (machinery-формат - из `ff_codec:unmarshal(timer, ...)`) — срезать USec, как в - `ff_adapter_withdrawal_codec:unmarshal_provider_timer/1`. Тест на deadline-таймер - в `ff_withdrawal_codec` (сейчас покрыт только `{timeout, 0}`). -- `prg_machine:repair/3`: декодировать `Reason` (`decode_term`) в ветке - `{error, {repair, {failed, Reason}}}` — наружу term, а не t2b-бинарь. -- Выправить спеки `ff_*_machine:repair/2` и хендлеры (`ff_withdrawal_repair` - и др.) под фактическую форму ошибки; убрать недостижимую ветку - `{error, failed} -> {failed, {invalid_result, unexpected_failure}}`. - -### 3.5 `hg_invoice_handler` -- `get_state/1`: `throw(#payproc_InvoiceNotFound{})` — голый рекорд, как в - `map_history_error`. -- `ensure_started/2`: вернуть ветку `{error, Reason} -> erlang:error(Reason)`. - -### 3.6 Контракт исключений процессора (решение) -- Форму `{error, {exception, Class, Reason}}` оставляем как есть — и на проводе - к progressor (его контракт: `is_retryable/5` по 3-tuple решает «не ретраить»), - и в клиентском API как pass-through. Переименование маркера (`failed` и т.п.) - — косметика, не делаем. -- Чиним только реальные баги: - - `prg_machine:get/3`: убрать `raise_exception` — возвращать - `{error, {exception, Class, Reason}}` как обычную ошибку, а не - re-raise с пустым stacktrace; - - убедиться, что потребители (`hg_invoicing_machine_client`, `ff_*_machine`, - repair-хендлеры) матчат эту форму: детали — в лог / маппинг в - thrift-ошибки, без `case_clause`. -- Голый `{error, failed}` остаётся для статусной ошибки - `<<"process is error">>` (процесс уже в error; деталей в ответе progressor - нет — при необходимости их даёт отдельный `get` с `detail` процесса). -- `docs/prg-machine.md`: убрать упоминание 4-tuple со stacktrace, описать - фактический контракт. - ---- - -## Этап 4. Контекст и конфиг - -### 4.1 Убрать глобальный `woody_context_loader` -- Удалить `application:set_env(prg_machine, woody_context_loader, fun ...)` из - `hellgate.erl` и `ff_server.erl` (анонимный fun в app env + общий ключ — - ломается на hot upgrade и при совместном старте в одном узле). -- `prg_machine:encode_rpc_context/0` → `op_context:current_woody_context()`: - пробует hg-binding, затем ff-binding (gproc-ключи текущего процесса различны, - коллизий нет), fallback `woody_context:new()` с warning-логом. -- Добавить `op_context` в `applications` у `prg_machine.app.src` - (зависимость уже фактическая — `resolve_env_enter`). - -### 4.2 `binary_to_term` без `[safe]` -- Старый стек (`machinery_utils:decode`, `hg_progressor`) работал без `[safe]` — - возвращаем как было: убрать `[safe]` во всех decode собственных данных - (`prg_machine:decode_term`, `unmarshal_event_body` fallback, - `unmarshal_aux_state`, `ff_machine_trace:decode_term`, `hg_invoice`). - ---- - -## Этап 5. Гигиена HG/FF - -- `hg_invoice`: `log_changes` для signal/repair (как старый `handle_result`); - убрать двойной `validate_changes` в `process_call` (заодно закрывает пункт - «двойной collapse» из техдолга в `docs/prg-machine.md`). -- FF копипаста ×5: вынести `to_repair_machine/1`, `from_repair_result/2`, - `repair_events_to_domain/1`, `event_body_from_timestamped/1`, - `history_times/1`, `history_to_events/1`, `codec_timestamp/1` в общий модуль - (`ff_machine_codec` или новый `ff_machine_lib`); удалить мёртвое поле `times` - из `st()` пяти `*_machine`, либо начать использовать. -- FF: вернуть no-op `process_notification` (`#{}`) у session/source/destination - вместо принудительного `action => timeout`; убрать лишний `action => timeout` - из `ff_destination:init`. -- FF `machine_to_st`: явная ветка для `aux_state = undefined` (сейчас дефолт - `maps:get(ctx, AuxState, #{})` мёртвый, падает `badmap`). -- `docs/prg-machine.md`: обновить разделы про ошибки/форматы по итогам этапов 1–4. - -Вне скоупа (решено): `hg_hybrid` не возвращаем; trace (`ff_machine_trace`, -дефолт формата, `<<>>`-args) — отдельный ПР с переездом на thrift; тег -progressor в `rebar.config` — после ревью progressor. - ---- - -## Этап 6 (опционально, отдельный ПР). Единый конверт HG+FF - -После выкатки этапов 1–5 и стабилизации: - -- format 2 = сырой thrift-binary payload для **обоих** стеков (HG уходит от - `t2b(msgpack)`, FF — от `t2b({bin, ...})`). -- Reader к этому моменту уже умеет оба формата (этап 1), поэтому включение - записи format 2 — отдельный маленький коммит; rollback-политика: откат только - на версии, содержащие reader этапа 1. -- Сюда же — переезд trace на thrift (`docs/trace-api-thrift.md`). - -## Порядок и критерии - -1 → 2 → 3 → 4 → 5 — каждый этап самостоятелен и мержибелен отдельно; 1 и 2 -трогают одни и те же функции маршалинга, их удобно делать подряд. Критерий -этапа 1 — golden-тесты (1.6) зелёные; критерий общий — CT + dialyzer + compose -зелёные, grep-инварианты из `docs/prg-machine.md` соблюдены. diff --git a/docs/prg-machine.md b/docs/prg-machine.md index 3e2ef578..55fa3394 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -2,7 +2,7 @@ Единый runtime поверх progressor для HG и FF. Контракт `action()` в progressor: `progressor/docs/step-effect-migration.md`. -*Обновлено: 2026-06-13. CI (compile, dialyzer, CT + compose) — green локально.* +*Обновлено: 2026-06-16. CI (compile, dialyzer, CT + compose) — см. текущий PR.* --- @@ -42,8 +42,13 @@ sequenceDiagram | Модуль | Роль | |--------|------| -| `prg_machine` | behaviour, client API, `process/3`, `collapse` / `emit_events` | -| `prg_machine_registry` | ETS `{Namespace, Handler}`; `{unknown_namespace, NS}` | +| `prg_machine` | публичный фасад: behaviour, client API, `process/3`, `collapse` / `emit_events` | +| `prg_machine_client` | progressor client API: `start` / `call` / `get` / `repair` / history | +| `prg_machine_processor` | progressor `process/3`: dispatch доменного callback и сбор intent | +| `prg_machine_events` | event fold, event payload/metadata, aux_state codec defaults | +| `prg_machine_env` | woody/otel context, deadline, `env_enter` / `env_leave` | +| `prg_machine_codec` | term envelope и legacy double-envelope decode | +| `prg_machine_registry` | ETS `{Namespace, Handler}` под простым owner-процессом | | `prg_action` | `{timeout, Sec}` / `{deadline, Dt}` → wire `action()` | | `ff_machine_lib` | общие FF-хелперы: repair/history/timestamp для `*_machine` | @@ -61,6 +66,14 @@ sequenceDiagram `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 @@ -167,15 +180,15 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa ### 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 на `apply_event/2`. +Реплей: `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`. ### Прочее (низкий приоритет) -- Golden-fixtures со стейджа для legacy payload/aux_state (см. `docs/prg-machine-fix-plan.md` §1.6) -- Registry без ETS `heir` — краткое окно при рестарте +- Golden-fixtures со стейджа для legacy payload/aux_state +- 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 fix-plan +- Единый конверт HG+FF (format 2) --- @@ -183,7 +196,7 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa 1. `sys.config` — `client => prg_machine` 2. `-behaviour(prg_machine)` + callbacks -3. `apply_event/2` (FF) или `apply_event/4` (HG) для `collapse/2` +3. `apply_event/4` для `collapse/2` 4. `*_machine.erl` — только `prg_machine:*` 5. Handler в `get_child_spec` (`hellgate.erl` / `ff_server.erl`) 6. CT suite From 6c13c315b7313c212126d53a264e93bf8b531c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 17 Jun 2026 13:15:52 +0300 Subject: [PATCH 49/62] Refactor unmarshal_event function in ff_machine_codec and ff_machine_lib to remove the format_version parameter, simplifying the function signature. Update related specifications and adjust calls in ff_machine_lib for consistency. This change enhances code clarity and prepares for improved event handling. --- apps/ff_transfer/src/ff_machine_codec.erl | 18 +-- apps/ff_transfer/src/ff_machine_lib.erl | 2 +- .../test/legacy_fixture_golden_test.erl | 144 ------------------ apps/prg_machine/test/legacy_fixture_lib.erl | 72 --------- docs/prg-machine.md | 1 - .../legacy/ff_deposit_v1/latest/aux_state.bin | Bin 16 -> 0 bytes .../latest/events/0001.event_summary.term | 3 - .../latest/events/0001.metadata.term | 1 - .../latest/events/0001.payload.bin | Bin 310 -> 0 bytes .../latest/events/0002.event_summary.term | 3 - .../latest/events/0002.metadata.term | 1 - .../latest/events/0002.payload.bin | Bin 64 -> 0 bytes .../ff_deposit_v1/latest/process_id.txt | 1 - .../ff_deposit_v1/latest/process_summary.term | 4 - .../ff_destination_v2/latest/aux_state.bin | Bin 28 -> 0 bytes .../latest/events/0001.event_summary.term | 3 - .../latest/events/0001.metadata.term | 1 - .../latest/events/0001.payload.bin | Bin 443 -> 0 bytes .../latest/events/0002.event_summary.term | 3 - .../latest/events/0002.metadata.term | 1 - .../latest/events/0002.payload.bin | Bin 135 -> 0 bytes .../ff_destination_v2/latest/process_id.txt | 1 - .../latest/process_summary.term | 4 - .../legacy/ff_source_v1/latest/aux_state.bin | Bin 28 -> 0 bytes .../latest/events/0001.event_summary.term | 3 - .../latest/events/0001.metadata.term | 1 - .../latest/events/0001.payload.bin | Bin 302 -> 0 bytes .../latest/events/0002.event_summary.term | 3 - .../latest/events/0002.metadata.term | 1 - .../latest/events/0002.payload.bin | Bin 135 -> 0 bytes .../legacy/ff_source_v1/latest/process_id.txt | 1 - .../ff_source_v1/latest/process_summary.term | 4 - .../latest/aux_state.bin | Bin 16 -> 0 bytes .../latest/events/0001.event_summary.term | 3 - .../latest/events/0001.metadata.term | 1 - .../latest/events/0001.payload.bin | Bin 537 -> 0 bytes .../latest/events/0002.event_summary.term | 3 - .../latest/events/0002.metadata.term | 1 - .../latest/events/0002.payload.bin | Bin 60 -> 0 bytes .../latest/events/0003.event_summary.term | 3 - .../latest/events/0003.metadata.term | 1 - .../latest/events/0003.payload.bin | Bin 290 -> 0 bytes .../latest/events/0004.event_summary.term | 3 - .../latest/events/0004.metadata.term | 1 - .../latest/events/0004.payload.bin | Bin 60 -> 0 bytes .../latest/process_id.txt | 1 - .../latest/process_summary.term | 6 - .../ff_withdrawal_v2/latest/aux_state.bin | Bin 16 -> 0 bytes .../latest/events/0001.event_summary.term | 3 - .../latest/events/0001.metadata.term | 1 - .../latest/events/0001.payload.bin | Bin 328 -> 0 bytes .../latest/events/0002.event_summary.term | 3 - .../latest/events/0002.metadata.term | 1 - .../latest/events/0002.payload.bin | Bin 64 -> 0 bytes .../latest/events/0003.event_summary.term | 3 - .../latest/events/0003.metadata.term | 1 - .../latest/events/0003.payload.bin | Bin 217 -> 0 bytes .../ff_withdrawal_v2/latest/process_id.txt | 1 - .../latest/process_summary.term | 4 - .../legacy/hg_invoice/latest/aux_state.bin | Bin 48 -> 0 bytes .../latest/call_args_thrift_get.bin | Bin 86 -> 0 bytes .../latest/call_args_thrift_get.expected.term | 8 - .../latest/events/0001.event_summary.term | 3 - .../latest/events/0001.metadata.term | 1 - .../hg_invoice/latest/events/0001.payload.bin | Bin 307 -> 0 bytes .../legacy/hg_invoice/latest/process_id.txt | 1 - .../hg_invoice/latest/process_summary.term | 3 - 67 files changed, 9 insertions(+), 323 deletions(-) delete mode 100644 apps/prg_machine/test/legacy_fixture_golden_test.erl delete mode 100644 apps/prg_machine/test/legacy_fixture_lib.erl delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/aux_state.bin delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0001.event_summary.term delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0001.metadata.term delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0001.payload.bin delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0002.event_summary.term delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0002.metadata.term delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/events/0002.payload.bin delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/process_id.txt delete mode 100644 test/fixtures/legacy/ff_deposit_v1/latest/process_summary.term delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/aux_state.bin delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0001.event_summary.term delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0001.metadata.term delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0001.payload.bin delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0002.event_summary.term delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0002.metadata.term delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/events/0002.payload.bin delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/process_id.txt delete mode 100644 test/fixtures/legacy/ff_destination_v2/latest/process_summary.term delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/aux_state.bin delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0001.event_summary.term delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0001.metadata.term delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0001.payload.bin delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0002.event_summary.term delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0002.metadata.term delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/events/0002.payload.bin delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/process_id.txt delete mode 100644 test/fixtures/legacy/ff_source_v1/latest/process_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/aux_state.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.event_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.metadata.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.payload.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.event_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.metadata.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.payload.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.event_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.metadata.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.payload.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.event_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.metadata.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.payload.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_id.txt delete mode 100644 test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/aux_state.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.event_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.metadata.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.payload.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.event_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.metadata.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.payload.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.event_summary.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.metadata.term delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.payload.bin delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/process_id.txt delete mode 100644 test/fixtures/legacy/ff_withdrawal_v2/latest/process_summary.term delete mode 100644 test/fixtures/legacy/hg_invoice/latest/aux_state.bin delete mode 100644 test/fixtures/legacy/hg_invoice/latest/call_args_thrift_get.bin delete mode 100644 test/fixtures/legacy/hg_invoice/latest/call_args_thrift_get.expected.term delete mode 100644 test/fixtures/legacy/hg_invoice/latest/events/0001.event_summary.term delete mode 100644 test/fixtures/legacy/hg_invoice/latest/events/0001.metadata.term delete mode 100644 test/fixtures/legacy/hg_invoice/latest/events/0001.payload.bin delete mode 100644 test/fixtures/legacy/hg_invoice/latest/process_id.txt delete mode 100644 test/fixtures/legacy/hg_invoice/latest/process_summary.term diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl index de57fca8..87277992 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -1,7 +1,7 @@ -module(ff_machine_codec). -export([marshal_event/3]). --export([unmarshal_event/3]). +-export([unmarshal_event/2]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). -export([payload_to_binary/1]). @@ -51,44 +51,42 @@ marshal_event(withdrawal_session, 1, Timestamped) -> marshal_event(Domain, Format, _Timestamped) -> erlang:error({unknown_event_format, Domain, Format}). --spec unmarshal_event(domain(), format_version(), binary()) -> timestamped_event(). -unmarshal_event(deposit, 1, Payload) -> +-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, 1, Payload) -> +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, 1, Payload) -> +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, 1, Payload) -> +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, 1, Payload) -> +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' - ); -unmarshal_event(Domain, Format, _Payload) -> - erlang:error({unknown_event_format, Domain, Format}). + ). %% aux_state: legacy machinery_prg_backend wrote plain term_to_binary(AuxSt). -spec marshal_aux_state(term()) -> binary(). diff --git a/apps/ff_transfer/src/ff_machine_lib.erl b/apps/ff_transfer/src/ff_machine_lib.erl index b1a21b14..d0b65de9 100644 --- a/apps/ff_transfer/src/ff_machine_lib.erl +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -188,7 +188,7 @@ marshal_event_body(Domain, Format, Body) -> -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, 1, Payload), + Timestamped = ff_machine_codec:unmarshal_event(Domain, Payload), event_body_from_timestamped(Timestamped). -spec marshal_aux_state(term()) -> binary(). diff --git a/apps/prg_machine/test/legacy_fixture_golden_test.erl b/apps/prg_machine/test/legacy_fixture_golden_test.erl deleted file mode 100644 index 6f3271ef..00000000 --- a/apps/prg_machine/test/legacy_fixture_golden_test.erl +++ /dev/null @@ -1,144 +0,0 @@ --module(legacy_fixture_golden_test). - --compile(nowarn_unused_function). --compile(nowarn_missing_spec). - --export([test/0]). - --include_lib("eunit/include/eunit.hrl"). --include_lib("hellgate/include/domain.hrl"). --include_lib("damsel/include/dmsl_payproc_thrift.hrl"). - --spec test() -> _. -test() -> - eunit:test(?MODULE, [verbose]). - -legacy_ff_event_test_() -> - [ - {fixture_id(Dir, "_event"), fun() -> legacy_ff_event_test(Domain, Dir) end} - || {Domain, Dir} <- legacy_fixture_lib:ff_fixtures() - ]. - -legacy_ff_metadata_test_() -> - [ - {fixture_id(Dir, "_metadata"), fun() -> legacy_ff_metadata_test(Dir) end} - || {_Domain, Dir} <- legacy_fixture_lib:ff_fixtures() - ]. - -legacy_ff_aux_state_test_() -> - [ - {fixture_id(Dir, "_aux_state"), fun() -> legacy_ff_aux_state_test(Dir) end} - || {_Domain, Dir} <- legacy_fixture_lib:ff_fixtures() - ]. - -legacy_ff_rollback_test_() -> - [ - {fixture_id(Dir, "_rollback"), fun() -> legacy_ff_rollback_roundtrip_test(Domain, Dir) end} - || {Domain, Dir} <- legacy_fixture_lib:ff_fixtures() - ]. - -legacy_hg_invoice_event_test_() -> - {"hg_invoice_event", fun legacy_hg_invoice_event_test/0}. - -legacy_hg_invoice_metadata_test_() -> - {"hg_invoice_metadata", fun legacy_hg_invoice_metadata_test/0}. - -legacy_hg_invoice_aux_state_test_() -> - {"hg_invoice_aux_state", fun legacy_hg_invoice_aux_state_test/0}. - -legacy_hg_aux_state_rollback_test_() -> - {"hg_invoice_aux_state_rollback", fun legacy_hg_aux_state_rollback_test/0}. - -legacy_hg_call_args_test_() -> - {"hg_call_args", fun legacy_hg_call_args_test/0}. - -legacy_hg_event_rollback_test_() -> - {"hg_invoice_event_rollback", fun legacy_hg_event_rollback_test/0}. - -%% - -legacy_ff_event_test(Domain, Dir) -> - Payload = legacy_fixture_lib:read_event_payload(Dir, 1), - Meta = legacy_fixture_lib:read_event_metadata(Dir, 1), - ?assertEqual(1, maps:get(<<"format">>, Meta)), - ?assertNot(maps:is_key(<<"format_version">>, Meta)), - ?assertMatch(<<131, _/binary>>, Payload), - {bin, _} = binary_to_term(Payload), - Timestamped = ff_machine_codec:unmarshal_event(Domain, 1, Payload), - Change = ff_machine_lib:event_body_from_timestamped(Timestamped), - ?assertMatch({created, _}, Change). - -legacy_ff_metadata_test(Dir) -> - Meta = legacy_fixture_lib:read_event_metadata(Dir, 1), - ?assertEqual(#{<<"format">> => 1}, Meta). - -legacy_ff_aux_state_test(Dir) -> - Aux = legacy_fixture_lib:read_aux_state(Dir), - ?assertMatch(#{ctx := _}, ff_machine_codec:unmarshal_aux_state(Aux)). - -legacy_ff_rollback_roundtrip_test(Domain, Dir) -> - LegacyPayload = legacy_fixture_lib:read_event_payload(Dir, 1), - Timestamped = ff_machine_codec:unmarshal_event(Domain, 1, LegacyPayload), - Encoded = ff_machine_codec:marshal_event(Domain, 1, Timestamped), - NewPayload = ff_machine_codec:payload_to_binary(Encoded), - ?assertEqual(LegacyPayload, NewPayload). - -legacy_hg_invoice_event_test() -> - Dir = legacy_fixture_lib:hg_invoice_dir(), - Payload = legacy_fixture_lib:read_event_payload(Dir, 1), - Meta = legacy_fixture_lib:read_event_metadata(Dir, 1), - InvoiceID = trim_binary(legacy_fixture_lib:read_bin(Dir, "process_id.txt")), - ?assertEqual(#{<<"format_version">> => 1}, Meta), - ?assertNot(maps:is_key(<<"format">>, Meta)), - Changes = hg_invoice:unmarshal_event_body(Payload), - ?assertMatch([{invoice_created, _}], Changes), - [{invoice_created, {payproc_InvoiceCreated, Invoice}}] = Changes, - ?assertEqual(InvoiceID, Invoice#domain_Invoice.id). - -legacy_hg_invoice_metadata_test() -> - Meta = legacy_fixture_lib:read_event_metadata(legacy_fixture_lib:hg_invoice_dir(), 1), - ?assertEqual(#{<<"format_version">> => 1}, Meta). - -legacy_hg_invoice_aux_state_test() -> - Dir = legacy_fixture_lib:hg_invoice_dir(), - Aux = legacy_fixture_lib:read_aux_state(Dir), - ?assertEqual(#{}, hg_invoice:unmarshal_aux_state(Aux)). - -legacy_hg_aux_state_rollback_test() -> - Dir = legacy_fixture_lib:hg_invoice_dir(), - LegacyAux = legacy_fixture_lib:read_aux_state(Dir), - ?assertEqual(LegacyAux, hg_invoice:marshal_aux_state(#{})). - -legacy_hg_call_args_test() -> - Dir = legacy_fixture_lib:hg_invoice_dir(), - Bin = legacy_fixture_lib:read_bin(Dir, "call_args_thrift_get.bin"), - Expected = legacy_fixture_lib:read_term(Dir, "call_args_thrift_get.expected.term"), - Inner = decode_legacy_call_args(Bin), - ?assertEqual(maps:get(inner_call, Expected), Inner), - {thrift_call, invoicing, FunRef, EncodedArgs} = Inner, - {Module, _Service} = hg_proto:get_service(invoicing), - FullFunctionRef = {Module, FunRef}, - Args = hg_proto_utils:deserialize_function_args(FullFunctionRef, EncodedArgs), - ?assertEqual(maps:get(normalized_call, Expected), {FunRef, Args}). - -legacy_hg_event_rollback_test() -> - Dir = legacy_fixture_lib:hg_invoice_dir(), - LegacyPayload = legacy_fixture_lib:read_event_payload(Dir, 1), - Changes = hg_invoice:unmarshal_event_body(LegacyPayload), - {Format, NewPayload} = hg_invoice:marshal_event_body(Changes), - ?assertEqual(1, Format), - ?assertEqual(LegacyPayload, NewPayload). - -fixture_id(Dir, Suffix) -> - Dir ++ Suffix. - -decode_legacy_call_args(Bin) when is_binary(Bin) -> - case binary_to_term(Bin) of - {bin, Inner} when is_binary(Inner) -> - binary_to_term(Inner); - Term -> - Term - end. - -trim_binary(Bin) -> - re:replace(Bin, "[\\s]+$", "", [{return, binary}, global]). diff --git a/apps/prg_machine/test/legacy_fixture_lib.erl b/apps/prg_machine/test/legacy_fixture_lib.erl deleted file mode 100644 index 7779ba91..00000000 --- a/apps/prg_machine/test/legacy_fixture_lib.erl +++ /dev/null @@ -1,72 +0,0 @@ --module(legacy_fixture_lib). - --compile(nowarn_missing_spec). - -%%% Load CT-captured legacy progressor bytes from test/fixtures/legacy/. - --export([root/0]). --export([read_bin/2]). --export([read_term/2]). --export([read_event_payload/2]). --export([read_event_metadata/2]). --export([read_aux_state/1]). --export([ff_fixtures/0]). --export([hg_invoice_dir/0]). - --type fixture_dir() :: string(). --type domain() :: deposit | source | destination | withdrawal | withdrawal_session. - --spec root() -> file:filename(). -root() -> - filename:absname( - filename:join([ - filename:dirname(?FILE), - "..", - "..", - "..", - "test", - "fixtures", - "legacy" - ]) - ). - --spec ff_fixtures() -> [{domain(), fixture_dir()}]. -ff_fixtures() -> - [ - {deposit, "ff_deposit_v1"}, - {source, "ff_source_v1"}, - {destination, "ff_destination_v2"}, - {withdrawal, "ff_withdrawal_v2"}, - {withdrawal_session, "ff_withdrawal_session_v2"} - ]. - --spec hg_invoice_dir() -> fixture_dir(). -hg_invoice_dir() -> - "hg_invoice". - --spec read_bin(fixture_dir(), file:filename()) -> binary(). -read_bin(Dir, Name) -> - Path = filename:join([root(), Dir, "latest", Name]), - {ok, Bin} = file:read_file(Path), - Bin. - --spec read_term(fixture_dir(), file:filename()) -> term(). -read_term(Dir, Name) -> - Path = filename:join([root(), Dir, "latest", Name]), - {ok, [Term]} = file:consult(Path), - Term. - --spec read_event_payload(fixture_dir(), pos_integer()) -> binary(). -read_event_payload(Dir, Index) -> - read_bin(Dir, event_name(Index, "payload.bin")). - --spec read_event_metadata(fixture_dir(), pos_integer()) -> map(). -read_event_metadata(Dir, Index) -> - read_term(Dir, event_name(Index, "metadata.term")). - --spec read_aux_state(fixture_dir()) -> binary(). -read_aux_state(Dir) -> - read_bin(Dir, "aux_state.bin"). - -event_name(Index, Suffix) -> - filename:join(["events", lists:flatten(io_lib:format("~4..0w.", [Index])) ++ Suffix]). diff --git a/docs/prg-machine.md b/docs/prg-machine.md index 55fa3394..d65400e9 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -184,7 +184,6 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa ### Прочее (низкий приоритет) -- Golden-fixtures со стейджа для legacy payload/aux_state - Registry без ETS `heir` — краткое окно при рестарте owner-процесса - Фиктивная обёртка `{ev, Ts, Body}` в event payload - Trace: сейчас HTTP JSON (`ff_machine_trace`); Thrift — `docs/trace-api-thrift.md` diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/aux_state.bin b/test/fixtures/legacy/ff_deposit_v1/latest/aux_state.bin deleted file mode 100644 index 79b493f59a13fc97177d60f3622e93c4428410ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16 UcmZoJVPIfjEN4zGsQ|GU03R>|HUIzs diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.event_summary.term b/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.event_summary.term deleted file mode 100644 index 3ea7b5d6..00000000 --- a/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.event_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{index => 1,timestamp => 1781366441967720,event_id => 1, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 310}. diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_deposit_v1/latest/events/0001.payload.bin deleted file mode 100644 index adcef61f0f37979d913ec245a8cf00958ffac081..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 310 zcmaKnJ5R$f5P&ZUFV)#tkXTt5jQoh5q#L0{tq7`2iDL>FBF+iLMOg_P#I(W@FQ2hwm7eKP%0dty4m;oaX5jSG hc>WlA4_N|$;K*M3*Y?;DXMesfMBafD)3!lu`2zLiKC%D+ diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.event_summary.term b/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.event_summary.term deleted file mode 100644 index e794c3a6..00000000 --- a/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.event_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{index => 2,timestamp => 1781366441967740,event_id => 2, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 64}. diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_deposit_v1/latest/events/0002.payload.bin deleted file mode 100644 index 939787a8e925f6ea49246ba0b07c760bac4f8ec7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64 zcmZq9U@B)$%FN4UU|=xjW?%$T(nbbGX1WGux`xIfhGtd<23979dX{DuMuz55JPb@= L#K;3y%D?~scrpn| diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/process_id.txt b/test/fixtures/legacy/ff_deposit_v1/latest/process_id.txt deleted file mode 100644 index e6bfcff0..00000000 --- a/test/fixtures/legacy/ff_deposit_v1/latest/process_id.txt +++ /dev/null @@ -1 +0,0 @@ -4yX9AB6Zel5jIe7L3ijkQj6y2SH \ No newline at end of file diff --git a/test/fixtures/legacy/ff_deposit_v1/latest/process_summary.term b/test/fixtures/legacy/ff_deposit_v1/latest/process_summary.term deleted file mode 100644 index 941290d6..00000000 --- a/test/fixtures/legacy/ff_deposit_v1/latest/process_summary.term +++ /dev/null @@ -1,4 +0,0 @@ -#{extra => #{domain => deposit}, - status => <<"running">>,namespace => 'ff/deposit_v1', - process_id => <<"4yX9AB6Zel5jIe7L3ijkQj6y2SH">>,last_event_id => 2, - history_len => 2}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/aux_state.bin b/test/fixtures/legacy/ff_destination_v2/latest/aux_state.bin deleted file mode 100644 index 7a2c44d49c2f1f64713e7eb51fefc49e6129fa41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28 ccmZoJVPIfjEN4zGsQ|Nbfm|lPU=W)D08vQ 1,timestamp => 1781366442089605,event_id => 1, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 443}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_destination_v2/latest/events/0001.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_destination_v2/latest/events/0001.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_destination_v2/latest/events/0001.payload.bin deleted file mode 100644 index dcbdcc1705f501f4254f68034bf42c6485c0501e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 443 zcmZut!A`#MB@svr(Q=Us*>+dJ(k&K*aQ1)vFaN?R z5Dv!KWGDOf<-IraH8*y}aG?``!Z(2e05lxxcsBKH)*3UfLurRQceG6z^(Ph>78Fjr zQ8xHke>&lCwI2yJ>&_?fAc%W?ne9w4Pzi}eY&Wbeg=qP%Et%`vT=0Q?CICw{UOnB zx&|h(kdv>_Ow>k5u@N<>SO@UvXs**lHDk3uC8!?WIT6!9^*lWZl*YK@G*+4%R#|&d Mky>m|7+L`Q0;TR%3IG5A diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0002.event_summary.term b/test/fixtures/legacy/ff_destination_v2/latest/events/0002.event_summary.term deleted file mode 100644 index 434c7567..00000000 --- a/test/fixtures/legacy/ff_destination_v2/latest/events/0002.event_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{index => 2,timestamp => 1781366442089619,event_id => 2, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 135}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_destination_v2/latest/events/0002.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_destination_v2/latest/events/0002.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_destination_v2/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_destination_v2/latest/events/0002.payload.bin deleted file mode 100644 index 9067a79e2678ac99561161ef784be38bf32021e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135 zcmZq9U@B)$%FN4UU|^`?W?%$T(nbbGX1WGux`xIfhGtd<2396UdIpvTh6d(QJPb@= z#0XKUVrZF?m}qQns+(eHYOZUNXr8Q_WRhZ_o04c^U~Xn;YG#sZ#KFJ>G?kHuff=HS SIVjYLfs26!C #{domain => destination}, - status => <<"running">>,namespace => 'ff/destination_v2', - process_id => <<"OrIqsu2bJpyOaegAhZkISkDHdrw">>,last_event_id => 2, - history_len => 2}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/aux_state.bin b/test/fixtures/legacy/ff_source_v1/latest/aux_state.bin deleted file mode 100644 index 7a2c44d49c2f1f64713e7eb51fefc49e6129fa41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28 ccmZoJVPIfjEN4zGsQ|Nbfm|lPU=W)D08vQ 1,timestamp => 1781366442029049,event_id => 1, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 302}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_source_v1/latest/events/0001.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_source_v1/latest/events/0001.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_source_v1/latest/events/0001.payload.bin deleted file mode 100644 index 52d0bcc3231c534c52cbbcb93f01b47bb5c84999..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 302 zcmZvX%}c~E5XGn6)mGBI3tkkwMv`n2n!Sm-2o*(?ReGDG%b)(u2sr~ zLS#x3IB@8+!`2cw^yeS%tBw4+`xGDL<&YZlwtQ%^E7OVPia_Lu 2,timestamp => 1781366442029064,event_id => 2, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 135}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_source_v1/latest/events/0002.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_source_v1/latest/events/0002.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_source_v1/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_source_v1/latest/events/0002.payload.bin deleted file mode 100644 index de0883ee47b144654243978a0ec2e80ae1ec55e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135 zcmZq9U@B)$%FN4UU|^`?W?%$T(nbbGX1WGux`xIfhGtd<2396UdIm<8rpA_0JPb@= z#0XKUVwq%`VwPrPsGDkOYN>0InwY3-k&X!ok1 #{domain => source}, - status => <<"running">>,namespace => 'ff/source_v1', - process_id => <<"EtH1KKKcf4giWhZX4ogD6Vlo69S">>,last_event_id => 2, - history_len => 2}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/aux_state.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/aux_state.bin deleted file mode 100644 index 79b493f59a13fc97177d60f3622e93c4428410ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16 UcmZoJVPIfjEN4zGsQ|GU03R>|HUIzs diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.event_summary.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.event_summary.term deleted file mode 100644 index 5f0e4fad..00000000 --- a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.event_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{index => 1,timestamp => 1781366446616059,event_id => 1, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 537}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0001.payload.bin deleted file mode 100644 index a1187903fd21c1591999f674c98e9a0bcbd1a08d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537 zcma)2Jx{|h5PkVj32~4rB*eg4F(4W2#BtJYu(7s zMwfz@P8zOCQ_xLFZbY%P$_ZmlaUxa`q9c4YZkHeq;3Wh2M?YY+fK&Unu+^?!ZC`4)TQt?iB{+*ac`D-*3C{bE zhmZHO+ZIa@#NCM6q~03~rVn=@(TLEIuH;guWy*`Owp^)P@lutN+stILu$gmp%5Z!* vwe@oFsN? 2,timestamp => 1781366446622962,event_id => 2, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 60}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0002.payload.bin deleted file mode 100644 index eaa28fbe049525b1e2804f6f493f263b513c2646..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60 zcmZq9U@B)$%FN4UU|`VaW?%$T(nbbGX1WGux`xIfhGtd<2396!dS*t(rUn*KJPb@= I1X9NU08!crGynhq diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.event_summary.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.event_summary.term deleted file mode 100644 index 94afa299..00000000 --- a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.event_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{index => 3,timestamp => 1781366446622982,event_id => 3, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 290}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.metadata.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.payload.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0003.payload.bin deleted file mode 100644 index 72ece02e0db07e1390f4bd916a82fb0e0e60313f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 290 zcmZvWI}XAy5Jbn$hk#8<#TguJoM2hL0HUEFsZvrw0WAmM;>5%vTQJo=X=ZlbH~Gr0 z_vZr;I|Tt?Z^=4C&NS`NxIh#-S3BDt$d4LQqtFnuwd{w>>3md>Dc2cljP(QgU}LnV uuf{Dhr6y18Pod?~$p6&+y0gWpkaH+FlpIzZDh_K78xC<_dRjqGFzW;DUn% 4,timestamp => 1781366446622988,event_id => 4, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 60}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.metadata.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.payload.bin b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/events/0004.payload.bin deleted file mode 100644 index 8adee7732120ba88830804d5b306701a4618a2a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60 zcmZq9U@B)$%FN4UU|`VaW?%$T(nbbGX1WGux`xIfhGtd<2396!dS*t(rUn*KJPb@c K49q-WbqoMe;t4eX diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_id.txt b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_id.txt deleted file mode 100644 index c88b9875..00000000 --- a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_id.txt +++ /dev/null @@ -1 +0,0 @@ -5b8ab680-8e1b-48b7-8e01-b07fc4e0bcb7/1 \ No newline at end of file diff --git a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_summary.term b/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_summary.term deleted file mode 100644 index c8228c0e..00000000 --- a/test/fixtures/legacy/ff_withdrawal_session_v2/latest/process_summary.term +++ /dev/null @@ -1,6 +0,0 @@ -#{extra => - #{domain => withdrawal_session, - withdrawal_id => <<"5b8ab680-8e1b-48b7-8e01-b07fc4e0bcb7">>}, - status => <<"running">>,namespace => 'ff/withdrawal/session_v2', - process_id => <<"5b8ab680-8e1b-48b7-8e01-b07fc4e0bcb7/1">>, - last_event_id => 4,history_len => 4}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/aux_state.bin b/test/fixtures/legacy/ff_withdrawal_v2/latest/aux_state.bin deleted file mode 100644 index 79b493f59a13fc97177d60f3622e93c4428410ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16 UcmZoJVPIfjEN4zGsQ|GU03R>|HUIzs diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.event_summary.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.event_summary.term deleted file mode 100644 index ec60515c..00000000 --- a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.event_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{index => 1,timestamp => 1781366444329720,event_id => 1, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 328}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.metadata.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.payload.bin b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0001.payload.bin deleted file mode 100644 index 4889ba7668c77b35a56ad1c442d7bb057d9793f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 328 zcma)%F;BxV5QQ%dh^p=nKw|A`baouaE>n 2,timestamp => 1781366444329752,event_id => 2, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 64}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.metadata.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.payload.bin b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0002.payload.bin deleted file mode 100644 index e32165b3c068bd565f8ebe89c14e61c68d4e7969..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64 zcmZq9U@B)$%FN4UU|=xjW?%$T(nbbGX1WGux`xIfhGtd<2395}dd9{EhGr&FJPb@= L#K;3y%D?~scM%CZ diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.event_summary.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.event_summary.term deleted file mode 100644 index 86e17791..00000000 --- a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.event_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{index => 3,timestamp => 1781366444329796,event_id => 3, - metadata_keys => [<<"format">>], - payload_first_byte => 131,payload_size => 217}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.metadata.term b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.metadata.term deleted file mode 100644 index a034155d..00000000 --- a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format">> => 1}. diff --git a/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.payload.bin b/test/fixtures/legacy/ff_withdrawal_v2/latest/events/0003.payload.bin deleted file mode 100644 index 69f1e26accadd5a7f894856a0494adde65bd1ab8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217 zcmZq9U@B)$%FN4UU|=}I&AT)W%ybRRbPbI|49%*? z7}$6i7~u$_N5I6;)WFyh1PlxefS8+s8E6z6M3|d_1;`Q5LIWHOY(U{v+zjkMilsOy zwTOp-iy3G(8v_&jO9pNrWMB|VE{RW0EK149&q+xwiqA{TP32(_ #{domain => withdrawal}, - status => <<"running">>,namespace => 'ff/withdrawal_v2', - process_id => <<"4fb902cf-e5e0-400e-871d-6587492acd01">>,last_event_id => 3, - history_len => 3}. diff --git a/test/fixtures/legacy/hg_invoice/latest/aux_state.bin b/test/fixtures/legacy/hg_invoice/latest/aux_state.bin deleted file mode 100644 index 8064dcf68016b3a3e4937d99eca792147bcb0df4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48 zcmZq9U@jNQO^+`wNi0b%D9TTcch1i%NzE%M=Pb=jNlnYlOHIjODrZj0%*$l}0sy#L B5O@Fp diff --git a/test/fixtures/legacy/hg_invoice/latest/call_args_thrift_get.bin b/test/fixtures/legacy/hg_invoice/latest/call_args_thrift_get.bin deleted file mode 100644 index deeab2cd9c308b7f1e6129669c51df8ad766ecec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86 zcmZq9U@B)$%FN4UU|{fU&R{9$F3Bj$Oe={`PRz+E=giD2%g;>C%u5F< - {thrift_call,invoicing, - {'Invoicing','Get'}, - <<11,0,2,0,0,0,11,50,72,72,89,90,88,57,102,122,107,48,12,0, - 3,0,0>>}, - normalized_call => - {{'Invoicing','Get'}, - {<<"2HHYZX9fzk0">>,{payproc_EventRange,undefined,undefined}}}}. diff --git a/test/fixtures/legacy/hg_invoice/latest/events/0001.event_summary.term b/test/fixtures/legacy/hg_invoice/latest/events/0001.event_summary.term deleted file mode 100644 index 86d4834c..00000000 --- a/test/fixtures/legacy/hg_invoice/latest/events/0001.event_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{index => 1,timestamp => 1781366490607970,event_id => 1, - metadata_keys => [<<"format_version">>], - payload_first_byte => 131,payload_size => 307}. diff --git a/test/fixtures/legacy/hg_invoice/latest/events/0001.metadata.term b/test/fixtures/legacy/hg_invoice/latest/events/0001.metadata.term deleted file mode 100644 index f057ad2c..00000000 --- a/test/fixtures/legacy/hg_invoice/latest/events/0001.metadata.term +++ /dev/null @@ -1 +0,0 @@ -#{<<"format_version">> => 1}. diff --git a/test/fixtures/legacy/hg_invoice/latest/events/0001.payload.bin b/test/fixtures/legacy/hg_invoice/latest/events/0001.payload.bin deleted file mode 100644 index 4b8f8d0240a9547f4f4429cd4e041f027c1d73c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 307 zcmZXQO>crg5QYb6)xy?#=)uIhH`)aW29L%jo{dK9g>1KrZghdbHd?R#mHto%xR`XZ zhxvM+*_rF!JXv2dRRG5D1q=e{koq}bVBq-k`R8^MC6@!W!Ng2u5drJIzMic<0tEyf z{$*2e;1MMQpZcLoL)RNEy>LRk$%wuM<0$fiErAx!1k|@1efKDKjQ_fcJnNrAAfkha z*L%dcL7?`@u78RRe?0Ku+Q@RC^^$IOW!lDg?39_2&SBRr$L!15i3#f ZJxg_5=7P&a^7xx+hLGn@J)yb>zzu67HCO-u diff --git a/test/fixtures/legacy/hg_invoice/latest/process_id.txt b/test/fixtures/legacy/hg_invoice/latest/process_id.txt deleted file mode 100644 index 5a4359ba..00000000 --- a/test/fixtures/legacy/hg_invoice/latest/process_id.txt +++ /dev/null @@ -1 +0,0 @@ -2HHYZX9fzk0 \ No newline at end of file diff --git a/test/fixtures/legacy/hg_invoice/latest/process_summary.term b/test/fixtures/legacy/hg_invoice/latest/process_summary.term deleted file mode 100644 index 9bcfdcba..00000000 --- a/test/fixtures/legacy/hg_invoice/latest/process_summary.term +++ /dev/null @@ -1,3 +0,0 @@ -#{extra => #{domain => hg_invoice}, - status => <<"running">>,namespace => invoice, - process_id => <<"2HHYZX9fzk0">>,last_event_id => 1,history_len => 1}. From caf839e551bad46c06315e2060668255e590da47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 17 Jun 2026 13:55:21 +0300 Subject: [PATCH 50/62] Refactor lookup function in prg_machine_registry to remove unnecessary ETS info check, simplifying the logic. Ensure consistent handling of unknown namespaces. Update TABLE definition location in prg_machine for clarity. --- apps/prg_machine/src/prg_machine.erl | 3 +-- apps/prg_machine/src/prg_machine_registry.erl | 15 +++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index ee64cf7e..cdc632eb 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -5,8 +5,6 @@ -include_lib("progressor/include/progressor.hrl"). --define(TABLE, prg_machine_dispatch). - %% Types -type namespace() :: namespace_id(). @@ -660,6 +658,7 @@ range_from_process(_) -> registry_key => ?TEST_REGISTRY_KEY, cleanup_mode => lenient }). +-define(TABLE, prg_machine_dispatch). -spec test() -> _. diff --git a/apps/prg_machine/src/prg_machine_registry.erl b/apps/prg_machine/src/prg_machine_registry.erl index 92f03659..09e3548c 100644 --- a/apps/prg_machine/src/prg_machine_registry.erl +++ b/apps/prg_machine/src/prg_machine_registry.erl @@ -37,16 +37,11 @@ start_link(Handlers) -> -spec lookup(prg_machine:namespace()) -> {ok, module()} | {error, {unknown_namespace, prg_machine:namespace()}}. lookup(NS) -> - case ets:info(?TABLE) of - undefined -> - {error, {unknown_namespace, NS}}; - _ -> - case ets:lookup(?TABLE, NS) of - [{NS, Handler}] -> - {ok, Handler}; - [] -> - {error, {unknown_namespace, NS}} - end + case ets:lookup(?TABLE, NS) of + [{NS, Handler}] -> + {ok, Handler}; + [] -> + {error, {unknown_namespace, NS}} end. -spec ensure_table() -> ok. From dfed455f9fbf1ec87c9bc705a61f8385a7a5d62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 17 Jun 2026 14:50:20 +0300 Subject: [PATCH 51/62] Enhance event handling across multiple modules by introducing process_notification and apply_event functions. Update specifications in ff_deposit_machine, ff_destination_machine, ff_source_machine, ff_withdrawal_machine, and ff_withdrawal_session_machine for consistency. Refactor get function calls to utilize the module name for improved clarity. This change improves event processing and prepares for future enhancements. --- apps/ff_server/src/ff_server.erl | 2 +- apps/ff_transfer/src/ff_deposit_machine.erl | 21 +- apps/ff_transfer/src/ff_destination.erl | 13 +- .../src/ff_destination_machine.erl | 19 +- apps/ff_transfer/src/ff_machine_lib.erl | 16 +- apps/ff_transfer/src/ff_source.erl | 11 +- apps/ff_transfer/src/ff_source_machine.erl | 19 +- .../ff_transfer/src/ff_withdrawal_machine.erl | 20 +- .../ff_transfer/src/ff_withdrawal_session.erl | 17 +- .../src/ff_withdrawal_session_machine.erl | 25 +- .../test/ff_withdrawal_limits_SUITE.erl | 2 +- apps/hellgate/src/hellgate.erl | 2 +- apps/hellgate/src/hg_invoice.erl | 5 + apps/hellgate/src/hg_invoice_template.erl | 5 + apps/prg_machine/src/prg_machine.erl | 215 +++++------------- .../prg_machine_aux_state_test_handler.erl | 27 ++- .../test/prg_machine_env_mock_context.erl | 15 +- .../test/prg_machine_env_mock_handler.erl | 52 ++++- 18 files changed, 265 insertions(+), 221 deletions(-) diff --git a/apps/ff_server/src/ff_server.erl b/apps/ff_server/src/ff_server.erl index 8cb12afd..a92fe25d 100644 --- a/apps/ff_server/src/ff_server.erl +++ b/apps/ff_server/src/ff_server.erl @@ -98,7 +98,7 @@ init([]) -> ) ), PartyClientSpec = party_client:child_spec(party_client, PartyClient), - PrgMachineSpec = prg_machine:get_child_spec([ + PrgMachineSpec = prg_machine_registry:get_child_spec([ ff_deposit_machine, ff_source_machine, ff_destination_machine, diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index 2f950813..ac08f4d1 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -67,10 +67,12 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% Internal types @@ -98,7 +100,7 @@ get(ID) -> {ok, st()} | {error, unknown_deposit_error()}. get(ID, {After, Limit}) -> - ff_machine_lib:get(?NS, ID, {After, Limit}, ff_deposit, {unknown_deposit, ID}). + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, {unknown_deposit, ID}). -spec events(id(), event_range()) -> {ok, [event()]} @@ -137,7 +139,7 @@ init({Events, Ctx}, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, Machine) -> - Deposit = prg_machine:collapse(ff_deposit, Machine), + Deposit = prg_machine:collapse(?MODULE, Machine), ff_machine_lib:to_prg_result(ff_deposit:process_transfer(Deposit)). -spec process_call(term(), machine()) -> no_return(). @@ -146,7 +148,20 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - ff_machine_lib:process_repair(ff_deposit, Machine, Scenario). + ff_machine_lib:process_repair(?MODULE, Machine, Scenario). + +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-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:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> 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 48b1152b..9b95795e 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -56,10 +56,12 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). +-export([apply_event/4]). -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). @@ -84,7 +86,7 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - ff_machine_lib:get(?NS, ID, {After, Limit}, ff_destination, notfound). + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, notfound). -spec events(id(), event_range()) -> {ok, events()} @@ -122,7 +124,20 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - ff_machine_lib:process_repair(ff_destination, Machine, Scenario). + ff_machine_lib:process_repair(?MODULE, Machine, Scenario). + +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-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:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> diff --git a/apps/ff_transfer/src/ff_machine_lib.erl b/apps/ff_transfer/src/ff_machine_lib.erl index d0b65de9..8e793126 100644 --- a/apps/ff_transfer/src/ff_machine_lib.erl +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -53,10 +53,10 @@ create(NS, CreateFun, Params, Ctx) -> -spec get(prg_machine:namespace(), prg_machine:id(), event_range(), module(), term()) -> {ok, map()} | {error, term()}. -get(NS, ID, {After, Limit}, Domain, NotFoundError) -> +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(Domain, Machine)}; + {ok, machine_to_st(Handler, Machine)}; {error, notfound} -> {error, NotFoundError}; {error, {exception, Class, Reason}} -> @@ -105,9 +105,9 @@ init_result(Events, Ctx, Action) -> (init_result(Events, Ctx))#{action => Action}. -spec machine_to_st(module(), prg_machine:machine()) -> map(). -machine_to_st(Domain, #{aux_state := AuxState} = Machine) -> +machine_to_st(Handler, #{aux_state := AuxState} = Machine) -> #{ - model => prg_machine:collapse(Domain, Machine), + model => prg_machine:collapse(Handler, Machine), ctx => ctx(AuxState) }. @@ -120,8 +120,8 @@ to_prg_result({Action, Events}) -> -spec process_repair(module(), prg_machine:machine(), ff_repair:scenario()) -> prg_machine:result() | {error, term()}. -process_repair(Domain, Machine, Scenario) -> - case ff_repair:apply_scenario(Domain, to_repair_machine(Machine), Scenario) of +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} -> @@ -130,8 +130,8 @@ process_repair(Domain, Machine, Scenario) -> -spec process_repair(module(), prg_machine:machine(), ff_repair:scenario(), ff_repair:processors()) -> prg_machine:result() | {error, term()}. -process_repair(Domain, Machine, Scenario, ScenarioProcessors) -> - case ff_repair:apply_scenario(Domain, to_repair_machine(Machine), Scenario, ScenarioProcessors) of +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} -> 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 df7296e5..601e2427 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -56,10 +56,12 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). +-export([apply_event/4]). -type machine() :: prg_machine:machine(). -type prg_result() :: prg_machine:result(). @@ -84,7 +86,7 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - ff_machine_lib:get(?NS, ID, {After, Limit}, ff_source, notfound). + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, notfound). -spec events(id(), event_range()) -> {ok, events()} @@ -122,7 +124,20 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - ff_machine_lib:process_repair(ff_source, Machine, Scenario). + ff_machine_lib:process_repair(?MODULE, Machine, Scenario). + +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-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:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 79dd5bb9..3865bb13 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -90,6 +90,7 @@ -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% Internal types @@ -117,7 +118,7 @@ get(ID) -> {ok, st()} | {error, unknown_withdrawal_error()}. get(ID, {After, Limit}) -> - ff_machine_lib:get(?NS, ID, {After, Limit}, ff_withdrawal, {unknown_withdrawal, ID}). + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, {unknown_withdrawal, ID}). -spec events(id(), event_range()) -> {ok, [event()]} @@ -167,13 +168,13 @@ init({Events, Ctx}, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, Machine) -> - Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), + Withdrawal = prg_machine:collapse(?MODULE, Machine), ff_machine_lib:to_prg_result(ff_withdrawal:process_transfer(Withdrawal)). -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(ff_withdrawal, 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)}; @@ -185,11 +186,11 @@ process_call(CallArgs, _Machine) -> -spec process_repair(ff_repair:scenario(), machine()) -> prg_result() | {error, term()}. process_repair(Scenario, Machine) -> - ff_machine_lib:process_repair(ff_withdrawal, Machine, Scenario). + ff_machine_lib:process_repair(?MODULE, Machine, Scenario). -spec process_notification(notify_args(), machine()) -> prg_result(). process_notification({session_finished, SessionID, SessionResult}, Machine) -> - Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), + Withdrawal = prg_machine:collapse(?MODULE, Machine), case ff_withdrawal:finalize_session(SessionID, SessionResult, Withdrawal) of {ok, Result} -> ff_machine_lib:to_prg_result(Result); @@ -197,6 +198,15 @@ process_notification({session_finished, SessionID, SessionResult}, Machine) -> erlang:error({unable_to_finalize_session, Reason}) 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:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> ff_machine_lib:marshal_event_body(withdrawal, ?EVENT_FORMAT_VERSION, Body). diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index a35c63db..c2ff25b1 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -177,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). diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 9a3bf7c3..1f3b3182 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -28,10 +28,12 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). +-export([apply_event/4]). %% %% Types @@ -108,7 +110,7 @@ get(ID) -> {ok, st()} | {error, notfound}. get(ID, {After, Limit}) -> - ff_machine_lib:get(?NS, ID, {After, Limit}, ff_withdrawal_session, notfound). + ff_machine_lib:get(?NS, ID, {After, Limit}, ?MODULE, notfound). -spec events(id(), event_range()) -> {ok, [event()]} @@ -144,13 +146,13 @@ init(Events, _Machine) -> -spec process_signal(prg_machine:signal(), machine()) -> prg_result(). process_signal(timeout, Machine) -> - Session = prg_machine:collapse(ff_withdrawal_session, 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(ff_withdrawal_session, 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)}; @@ -164,12 +166,25 @@ process_call(CallArgs, _Machine) -> process_repair(Scenario, Machine) -> ScenarioProcessors = #{ set_session_result => fun(Args, RMachine) -> - Session = prg_machine:collapse(ff_withdrawal_session, ff_repair:to_prg_machine(RMachine)), + 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_machine_lib:process_repair(ff_withdrawal_session, Machine, Scenario, ScenarioProcessors). + ff_machine_lib:process_repair(?MODULE, Machine, Scenario, ScenarioProcessors). + +-spec process_notification(prg_machine:args(), machine()) -> prg_result(). +process_notification(_Args, _Machine) -> + #{}. + +-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 marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Body) -> diff --git a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl index ec32f4ee..4ec2c5e9 100644 --- a/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_limits_SUITE.erl @@ -548,7 +548,7 @@ await_provider_retry(FirstAmount, SecondAmount, TotalAmount, C) -> timeout, fun (Machine, ff_withdrawal_machine, _Args) -> - Withdrawal = prg_machine:collapse(ff_withdrawal, Machine), + 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); diff --git a/apps/hellgate/src/hellgate.erl b/apps/hellgate/src/hellgate.erl index b1243fbe..64d934da 100644 --- a/apps/hellgate/src/hellgate.erl +++ b/apps/hellgate/src/hellgate.erl @@ -48,7 +48,7 @@ init([]) -> %% for debugging only %% hg_profiler:get_child_spec(), party_client:child_spec(party_client, PartyClient), - prg_machine:get_child_spec([hg_invoice, hg_invoice_template]), + prg_machine_registry:get_child_spec([hg_invoice, hg_invoice_template]), get_api_child_spec(Opts) ] }}. diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index c7031c95..099628d3 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -54,6 +54,7 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). @@ -317,6 +318,10 @@ 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 = case Changes of diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index 95a90773..5848aa13 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -24,6 +24,7 @@ -export([process_signal/2]). -export([process_call/2]). -export([process_repair/2]). +-export([process_notification/2]). -export([marshal_event_body/1]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). @@ -260,6 +261,10 @@ create_invoice_template(ID, P) -> process_repair(_Args, _Machine) -> erlang:error({not_implemented, repair}). +-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) -> #{}. diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index cdc632eb..395b8f7b 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -47,14 +47,10 @@ auxst => term() }. --type env_enter_fun() :: fun(() -> ok) | fun((woody_context:ctx()) -> ok). - -type context_binding() :: op_context:binding(). -type process_options() :: #{ ns := namespace(), - env_enter => env_enter_fun(), - env_leave => fun(() -> ok), context_binding => context_binding(), default_handling_timeout => timeout() }. @@ -102,18 +98,8 @@ -callback unmarshal_aux_state(binary()) -> term(). -%% Optional: collapse passes event_id and timestamp (HG invoice). Default: apply_event/2. -callback apply_event(event_id(), timestamp(), event_body(), term()) -> term(). --optional_callbacks([ - process_notification/2, - marshal_event_body/1, - unmarshal_event_body/1, - marshal_aux_state/1, - unmarshal_aux_state/1, - apply_event/4 -]). - %% Client API -export([start/3]). @@ -135,9 +121,7 @@ %% Registry (namespace -> handler module) --export([get_child_spec/1]). -export([handler_namespace/1]). --export([unmarshal_event_body/2]). %% Event-sourcing helpers (replaces ff_machine) @@ -146,8 +130,16 @@ -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 | term()}. start(NS, ID, Args) -> Req = #{ @@ -220,12 +212,16 @@ repair(NS, ID, Args) -> {error, {repair, {failed, decode_term(Reason)}}} 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) -> Req = request(NS, ID, undefined, Range), case progressor:get(Req) of {ok, Process} -> - case get_handler_module(NS) of + case prg_machine_registry:lookup(NS) of {ok, Handler} -> {ok, unmarshal_machine(Handler, NS, Process)}; {error, _} = Error -> @@ -239,10 +235,6 @@ get(NS, ID, Range) -> {error, {exception, Class, Reason}} end. --spec get(namespace(), id()) -> {ok, machine()} | {error, get_error()}. -get(NS, ID) -> - get(NS, ID, #{direction => forward}). - -spec get_history(namespace(), id()) -> {ok, history()} | {error, get_error()}. get_history(NS, ID) -> get_history(NS, ID, undefined, undefined, forward). @@ -289,21 +281,15 @@ history_range(Offset, Limit, Direction) -> -spec process({init | call | repair | notify | timeout, binary(), map()}, process_options(), binary()) -> {ok, map()} | {error, term()}. process({CallType, BinArgs, Process}, #{ns := NS} = Opts, BinCtx) -> - Enter = resolve_env_enter(Opts), - Leave = resolve_env_leave(Opts), try - case get_handler_module(NS) of + 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), - ok = run_env_enter(Enter, WoodyCtx), - %% Enter succeeded: from here Leave must run exactly once. Errors - %% raised before this point fall through to the outer catch and are - %% returned as {error, _} without being masked by a Leave exception. - run_with_env_leave(Leave, fun() -> + run_scoped(Opts, WoodyCtx, fun() -> LastEventID = maps:get(last_event_id, Process), Machine = unmarshal_machine(Handler, NS, Process), Result = dispatch(Handler, CallType, BinArgs, Machine), @@ -334,17 +320,7 @@ ensure_deadline_set(WoodyCtx, Opts) -> WoodyCtx end. -%% Registry - --spec get_child_spec([module()]) -> supervisor:child_spec(). -get_child_spec(Handlers) -> - prg_machine_registry:get_child_spec(Handlers). - --spec handler_namespace(module()) -> namespace(). -handler_namespace(Handler) -> - Handler:namespace(). - -%% Event-sourcing (replaces ff_machine collapse/emit) +%% Event-sourcing -spec collapse(module(), machine()) -> term(). collapse(Handler, #{history := History, aux_state := AuxState}) -> @@ -380,11 +356,11 @@ dispatch(Handler, timeout, _BinArgs, Machine) -> Handler:process_signal(timeout, Machine); dispatch(Handler, notify, BinArgs, Machine) -> Args = decode_term(BinArgs), - dispatch_notification(Handler, Args, Machine); + Handler:process_notification(Args, Machine); dispatch(Handler, call, BinArgs, Machine) -> case decode_term(BinArgs) of {notify, Args} -> - dispatch_notification(Handler, Args, Machine); + Handler:process_notification(Args, Machine); remove -> #{events => [], action => remove, auxst => maps:get(aux_state, Machine)}; Call -> @@ -399,14 +375,6 @@ dispatch(Handler, repair, BinArgs, Machine) -> Result end. -dispatch_notification(Handler, Args, Machine) -> - case erlang:function_exported(Handler, process_notification, 2) of - true -> - Handler:process_notification(Args, Machine); - false -> - #{} - end. - marshal_process_result(Handler, LastEventID, {Response, Result}) when is_map(Result) -> Intent = marshal_intent(Handler, LastEventID, Result), {ok, Intent#{response => encode_term(Response)}}; @@ -474,39 +442,19 @@ marshal_new_events(Handler, LastEventID, Bodies) -> ). marshal_event_body(Handler, Body) -> - case erlang:function_exported(Handler, marshal_event_body, 1) of - true -> - Handler:marshal_event_body(Body); - false -> - {undefined, term_to_binary(Body)} - end. + Handler:marshal_event_body(Body). -spec unmarshal_event_body(module(), binary()) -> event_body(). unmarshal_event_body(Handler, Payload) -> - case erlang:function_exported(Handler, unmarshal_event_body, 1) of - true -> - Handler:unmarshal_event_body(Payload); - false -> - binary_to_term(Payload) - end. + Handler:unmarshal_event_body(Payload). marshal_aux_state(Handler, AuxSt) -> - case erlang:function_exported(Handler, marshal_aux_state, 1) of - true -> - Handler:marshal_aux_state(AuxSt); - false -> - term_to_binary(AuxSt) - end. + Handler:marshal_aux_state(AuxSt). unmarshal_aux_state(_Handler, undefined) -> undefined; unmarshal_aux_state(Handler, Bin) when is_binary(Bin) -> - case erlang:function_exported(Handler, unmarshal_aux_state, 1) of - true -> - Handler:unmarshal_aux_state(Bin); - false -> - binary_to_term(Bin) - end. + 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 @@ -528,26 +476,13 @@ event_timestamp_to_datetime(Ts) when is_integer(Ts) -> {calendar:system_time_to_universal_time(Seconds, second), Micro}. dispatch_apply_event(Handler, EventID, Ts, Body, Model) -> - case erlang:function_exported(Handler, apply_event, 4) of - true -> - Handler:apply_event(EventID, Ts, Body, Model); - false -> - case erlang:function_exported(Handler, apply_event, 2) of - true -> - Handler:apply_event(Body, Model); - false -> - erlang:error({apply_event_not_defined, Handler}) - end - end. + 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. -get_handler_module(NS) -> - prg_machine_registry:lookup(NS). - %% RPC / terms request(NS, ID, Args, Range) -> @@ -568,51 +503,22 @@ decode_rpc_context(<<>>) -> decode_rpc_context(Bin) -> woody_rpc_helper:decode_rpc_context(decode_term(Bin)). -resolve_env_enter(Opts) -> - case maps:is_key(env_enter, Opts) of - true -> - maps:get(env_enter, Opts); - false -> - case maps:get(context_binding, Opts, undefined) of - Binding when is_map(Binding) -> - fun(WoodyCtx) -> op_context:env_enter(WoodyCtx, Binding) end; - _ -> - fun(_) -> ok end - end - end. - -resolve_env_leave(Opts) -> - case maps:is_key(env_leave, Opts) of - true -> - maps:get(env_leave, Opts); - false -> - case maps:get(context_binding, Opts, undefined) of - Binding when is_map(Binding) -> - fun() -> op_context:env_leave(Binding) end; - _ -> - fun() -> ok end - end - end. - -run_env_enter(Enter, WoodyCtx) when is_function(Enter, 1) -> - Enter(WoodyCtx); -run_env_enter(Enter, _WoodyCtx) when is_function(Enter, 0) -> - Enter(). - -run_with_env_leave(Leave, Fun) when is_function(Leave, 0), is_function(Fun, 0) -> - try Fun() of - Result -> - safe_env_leave(Leave), - Result - catch - Class:Reason:Stacktrace -> - safe_env_leave(Leave), - erlang:raise(Class, Reason, Stacktrace) +run_scoped(Opts, WoodyCtx, Fun) when is_function(Fun, 0) -> + case maps:get(context_binding, Opts, undefined) of + Binding when is_map(Binding) -> + ok = op_context:env_enter(WoodyCtx, Binding), + try + Fun() + after + safe_env_leave(Binding) + end; + _ -> + Fun() end. -safe_env_leave(Leave) -> +safe_env_leave(Binding) -> try - Leave() + op_context:env_leave(Binding) catch Class:Reason:Stacktrace -> logger:error( @@ -662,16 +568,16 @@ range_from_process(_) -> -spec test() -> _. --spec noop_when_hooks_absent_test_() -> _. -noop_when_hooks_absent_test_() -> +-spec process_without_context_binding_test_() -> _. +process_without_context_binding_test_() -> {setup, fun setup_env_hook_test/0, fun cleanup_env_hook_test/1, [ - ?_test(noop_when_hooks_absent()) + ?_test(process_without_context_binding()) ]}. --spec explicit_fun_overrides_context_binding_test_() -> _. -explicit_fun_overrides_context_binding_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(explicit_fun_overrides_context_binding()) + ?_test(context_binding_scopes_process()) ]}. -spec aux_state_runtime_test_() -> _. @@ -680,7 +586,7 @@ aux_state_runtime_test_() -> ?_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_without_handler_omits_aux_state()) + ?_test(notify_noop_omits_aux_state()) ]}. -spec registry_runtime_test_() -> _. @@ -696,33 +602,18 @@ process_exception_test_() -> ?_test(process_crash_conforms_progressor_exception()) ]}. --spec noop_when_hooks_absent() -> _. -noop_when_hooks_absent() -> +-spec process_without_context_binding() -> _. +process_without_context_binding() -> ok = ensure_woody_available(), - ok = prg_machine_env_mock_context:reset(), - _ = run_env_hook_process(#{ns => ?TEST_NS}), - ?assertEqual([], prg_machine_env_mock_context:events()). + ?assertMatch({ok, _}, run_env_hook_process(#{ns => ?TEST_NS})). --spec explicit_fun_overrides_context_binding() -> _. -explicit_fun_overrides_context_binding() -> +-spec context_binding_scopes_process() -> _. +context_binding_scopes_process() -> ok = ensure_woody_available(), ok = prg_machine_env_mock_context:reset(), - Enter = fun(_) -> - prg_machine_env_mock_context:record(explicit_enter), - ok - end, - Leave = fun() -> - prg_machine_env_mock_context:record(explicit_leave), - ok - end, - Opts = #{ - ns => ?TEST_NS, - env_enter => Enter, - env_leave => Leave, - context_binding => ?TEST_BINDING - }, - _ = run_env_hook_process(Opts), - ?assertEqual([explicit_enter, explicit_leave], prg_machine_env_mock_context:events()). + Opts = #{ns => ?TEST_NS, context_binding => ?TEST_BINDING}, + ?assertMatch({ok, _}, run_env_hook_process(Opts)), + ?assertEqual([context_bound], prg_machine_env_mock_context:events()). -spec setup_env_hook_test() -> ok. setup_env_hook_test() -> @@ -835,8 +726,8 @@ business_exception_then_signal_does_not_corrupt_aux_state() -> Rechecked = binary_to_term(maps:get(aux_state, RecheckIntent), [safe]), ?assertEqual(#{model => initialized}, Rechecked). --spec notify_without_handler_omits_aux_state() -> _. -notify_without_handler_omits_aux_state() -> +-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 = #{ 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 index e8b586b9..5958d1d6 100644 --- a/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl +++ b/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl @@ -10,8 +10,12 @@ process_signal/2, process_call/2, process_repair/2, + process_notification/2, + marshal_event_body/1, + unmarshal_event_body/1, marshal_aux_state/1, - unmarshal_aux_state/1 + unmarshal_aux_state/1, + apply_event/4 ]). -define(NS, aux_state_test_ns). @@ -47,6 +51,18 @@ process_call(recheck, Machine) -> 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:event_body()) -> {undefined, binary()}. +marshal_event_body(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. @@ -59,3 +75,12 @@ 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 index f3841c94..beb62648 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_context.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_context.erl @@ -1,28 +1,17 @@ -module(prg_machine_env_mock_context). --export([env_enter/1, env_leave/0]). -export([reset/0, events/0, record/1]). --spec env_enter(woody_context:ctx()) -> ok. -env_enter(WoodyCtx) -> - record({enter, WoodyCtx}), - ok. - --spec env_leave() -> ok. -env_leave() -> - record(leave), - ok. - -spec reset() -> ok. reset() -> persistent_term:put({?MODULE, events}, []), ok. --spec events() -> [enter | leave | {enter, woody_context:ctx()} | explicit_enter | explicit_leave]. +-spec events() -> [context_bound]. events() -> persistent_term:get({?MODULE, events}, []). --spec record(enter | leave | {enter, woody_context:ctx()} | explicit_enter | explicit_leave) -> ok. +-spec record(context_bound) -> 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 index 46660b62..7860ea11 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_handler.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -2,7 +2,19 @@ -behaviour(prg_machine). --export([namespace/0, init/2, process_signal/2, process_call/2, process_repair/2]). +-export([ + namespace/0, + init/2, + process_signal/2, + process_call/2, + process_repair/2, + process_notification/2, + marshal_event_body/1, + unmarshal_event_body/1, + marshal_aux_state/1, + unmarshal_aux_state/1, + apply_event/4 +]). -spec namespace() -> prg_machine:namespace(). namespace() -> @@ -10,6 +22,13 @@ namespace() -> -spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). init(_Args, _Machine) -> + try + _ = op_context:load({p, l, prg_machine_env_test_context}), + prg_machine_env_mock_context:record(context_bound) + catch + _:_ -> + ok + end, #{events => [], action => idle}. -spec process_signal(prg_machine:signal(), prg_machine:machine()) -> prg_machine:result(). @@ -23,3 +42,34 @@ process_call(_Call, _Machine) -> -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:event_body()) -> {undefined, binary()}. +marshal_event_body(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. From 03583d337e48299b644705b67bea83856d532dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 17 Jun 2026 19:26:30 +0300 Subject: [PATCH 52/62] Update rebar.config and documentation for prg_machine changes. Remove outdated comments and clarify module roles in the documentation. Adjust configuration for Docker-sidecar tests in bender and party-management to reflect the removal of machinery_prg_backend. This enhances clarity and prepares for future development. --- docs/prg-machine.md | 107 ++++++++++++++++++++----------- rebar.config | 1 - test/bender/sys.config | 2 +- test/party-management/sys.config | 2 +- 4 files changed, 71 insertions(+), 41 deletions(-) diff --git a/docs/prg-machine.md b/docs/prg-machine.md index d65400e9..4512b4dc 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -2,7 +2,7 @@ Единый runtime поверх progressor для HG и FF. Контракт `action()` в progressor: `progressor/docs/step-effect-migration.md`. -*Обновлено: 2026-06-16. CI (compile, dialyzer, CT + compose) — см. текущий PR.* +*Обновлено: 2026-06-17.* --- @@ -10,10 +10,10 @@ ``` woody handler (hg_*_handler, ff_*_handler) - → prg_machine:start | call | get | repair | notify | remove | trace + → prg_machine:start | call | get | repair | notify | remove → progressor → prg_machine:process/3 - → domain module (-behaviour(prg_machine)) + → domain handler (-behaviour(prg_machine)) ``` ```mermaid @@ -21,7 +21,7 @@ sequenceDiagram participant WH as woody handler participant PM as prg_machine participant PR as progressor - participant DM as domain + participant DM as domain handler WH->>PM: call(NS, ID, Args) PM->>PR: progressor:call @@ -32,9 +32,11 @@ sequenceDiagram PR-->>WH: decode_term ``` -**Убрано из prod:** `hg_machine`, `ff_machine`, `hg_progressor_handler`, `machinery_prg_backend` в `config/sys.config`, `progressor_action`, legacy map в intent. +**Убрано из 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`). -**Вне scope:** Trace на Thrift → `docs/trace-api-thrift.md`. `{machinery, …}` в `rebar.config` — только docker-sidecar тесты (`test/bender`, `test/party-management`). +**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`). --- @@ -42,21 +44,34 @@ sequenceDiagram | Модуль | Роль | |--------|------| -| `prg_machine` | публичный фасад: behaviour, client API, `process/3`, `collapse` / `emit_events` | -| `prg_machine_client` | progressor client API: `start` / `call` / `get` / `repair` / history | -| `prg_machine_processor` | progressor `process/3`: dispatch доменного callback и сбор intent | -| `prg_machine_events` | event fold, event payload/metadata, aux_state codec defaults | -| `prg_machine_env` | woody/otel context, deadline, `env_enter` / `env_leave` | -| `prg_machine_codec` | term envelope и legacy double-envelope decode | -| `prg_machine_registry` | ETS `{Namespace, Handler}` под простым owner-процессом | -| `prg_action` | `{timeout, Sec}` / `{deadline, Dt}` → wire `action()` | -| `ff_machine_lib` | общие FF-хелперы: repair/history/timestamp для `*_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()`; MG/damsel repair на границе | +| `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 +``` -**`process/3`:** `env_enter` → `unmarshal_machine` → `dispatch` → `marshal_process_result` → `env_leave` (Leave только после успешного Enter). Исключение в домене → `{error, {exception, Class, Reason}}` + log (stacktrace только в логах, не на проводе). +- Неизвестный namespace → `{error, {unknown_namespace, NS}}`. +- Исключение в домене → `{error, {exception, Class, Reason}}` + log (stacktrace только в логах). +- `env_leave` в `after`, ошибки leave логируются, не маскируют результат dispatch. -**Контекст RPC:** `op_context:current_woody_context/0` (hg-binding → ff-binding → fresh ctx + warning). В `process/3` — `env_enter`/`env_leave` по `context_binding` из `sys.config` (HG strict / FF lenient). +**Контекст 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 в microsecond (`timestamp_us()`); metadata пишет оба ключа `<<"format_version">>` и `<<"format">>`. FF payload — legacy `term_to_binary({bin, ThriftBin})`; HG payload — `term_to_binary(msgpack)`. +**События:** 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). --- @@ -100,19 +115,21 @@ idle | suspend | timeout | remove `prg_action:from_mg/1`, `from_repair/1` — MG/damsel repair на границе (HG/FF handler → wire). `prg_action:marshal_timer/1` принимает `{deadline, {calendar:datetime(), Micro}}` (machinery-формат из `ff_codec:unmarshal(timer, ...)`). -FF доменный `continue` / `sleep` / `{setup_timer, T}` → wire через `map_action/1` в каждом модуле. +FF домен возвращает `prg_action:t()` напрямую; `*_machine` оборачивает через `ff_machine_lib:to_prg_result/1`. ### Prod namespaces (7) -| NS | Модуль | Registry | -|----|--------|----------| -| `invoice` | `hg_invoice` | `hellgate.erl` | -| `invoice_template` | `hg_invoice_template` | `hellgate.erl` | -| `ff/deposit_v1` | `ff_deposit` | `ff_server.erl` | -| `ff/source_v1` | `ff_source` | `ff_server.erl` | -| `ff/destination_v2` | `ff_destination` | `ff_server.erl` | -| `ff/withdrawal_v2` | `ff_withdrawal` | `ff_server.erl` | -| `ff/withdrawal/session_v2` | `ff_withdrawal_session` | `ff_server.erl` | +| 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. @@ -147,11 +164,13 @@ processor => #{ |------------|------------------| | `<<"process not found">>` / `<<"process is init">>` | `{error, notfound}` | | `<<"process is error">>` | `{error, failed}` | -| `{exception, Class, Reason}` (3-tuple) | **pass-through** `{error, {exception, Class, Reason}}` | +| `{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}` | -`repair`: + `{error, working}` для `<<"process is running">>`; `{error, {repair, {failed, Reason}}}` → `{error, {failed, Reason}}` в `ff_*_machine` / `erlang:error(Reason)` в woody repair handler. +`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`). @@ -167,7 +186,14 @@ meck:expect(prg_machine, process, fun process/3). %% внутри: 'prg_machine_meck_original':process(Call, Opts, BinCtx). ``` -Processor crash в тестах: `{error, {exception, _, _}}`, не атом `failed`. +Хелпер: `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 --- @@ -193,10 +219,10 @@ Processor crash в тестах: `{error, {exception, _, _}}`, не атом `fa ## 6. Новый namespace -1. `sys.config` — `client => prg_machine` -2. `-behaviour(prg_machine)` + callbacks -3. `apply_event/4` для `collapse/2` -4. `*_machine.erl` — только `prg_machine:*` +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 @@ -210,6 +236,7 @@ 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 ``` --- @@ -218,13 +245,17 @@ rg 'woody_context_loader' apps/hellgate apps/ff_server # 0 | Путь | Зачем | |------|-------| -| `apps/prg_machine/src/prg_machine.erl` | behaviour, errors, marshal_intent | +| `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/ff_transfer/src/ff_machine_lib.erl` | FF repair/history helpers | +| `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.erl` | FF behaviour | +| `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/rebar.config b/rebar.config index 669d504b..38e6a10d 100644 --- a/rebar.config +++ b/rebar.config @@ -36,7 +36,6 @@ {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.2"}}}, {scoper, {git, "https://github.com/valitydev/scoper.git", {tag, "v1.1.0"}}}, {thrift, {git, "https://github.com/valitydev/thrift_erlang.git", {tag, "v1.0.0"}}}, - %% Trace API goal: bump damsel after progressor_trace.thrift release — docs/trace-api-thrift.md {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.33"}}}, {payproc_errors, {git, "https://github.com/valitydev/payproc-errors-erlang.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, diff --git a/test/bender/sys.config b/test/bender/sys.config index 6c2d1c15..332696ec 100644 --- a/test/bender/sys.config +++ b/test/bender/sys.config @@ -119,7 +119,7 @@ worker_pool_size => 100, process_step_timeout => 30 }}, - %% Docker-sidecar bender (вне HG/FF): machinery_prg_backend — намеренно, не prg_machine. + {namespaces, #{ 'bender_generator' => #{ processor => #{ diff --git a/test/party-management/sys.config b/test/party-management/sys.config index 13310aa5..b3c514ab 100644 --- a/test/party-management/sys.config +++ b/test/party-management/sys.config @@ -79,7 +79,7 @@ worker_pool_size => 100, process_step_timeout => 30 }}, - %% Docker-sidecar party-management (вне HG/FF): machinery_prg_backend — намеренно, не prg_machine. + {namespaces, #{ 'party' => #{ processor => #{ From b79fe642b7c01e88f6c043975059f1420c0aff0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 18 Jun 2026 01:02:40 +0300 Subject: [PATCH 53/62] Refactor unmarshal functions in ff_codec and prg_action to streamline complex action handling. Consolidate timer and remove field processing into a single unmarshal_repairer_complex_action function, enhancing clarity and reducing redundancy. Update related specifications and documentation for improved consistency across modules. --- apps/ff_server/src/ff_codec.erl | 24 +- apps/prg_machine/src/prg_action.erl | 57 ++--- docs/prg-machine.md | 6 +- docs/refactor-architecture.md | 356 ++++++++++++++++++++++++++++ docs/refactor-local.md | 271 +++++++++++++++++++++ 5 files changed, 654 insertions(+), 60 deletions(-) create mode 100644 docs/refactor-architecture.md create mode 100644 docs/refactor-local.md diff --git a/apps/ff_server/src/ff_codec.erl b/apps/ff_server/src/ff_codec.erl index 94365b6d..2fc078c4 100644 --- a/apps/ff_server/src/ff_codec.erl +++ b/apps/ff_server/src/ff_codec.erl @@ -369,10 +369,7 @@ unmarshal(complex_action, #repairer_ComplexAction{ timer = TimerAction, remove = RemoveAction }) -> - prg_action:from_timer_remove( - unmarshal_repairer_timer_field(TimerAction), - unmarshal_repairer_remove_field(RemoveAction) - ); + unmarshal_repairer_complex_action(TimerAction, RemoveAction); unmarshal(timer, {timeout, Timeout}) -> {timeout, unmarshal(integer, Timeout)}; unmarshal(timer, {deadline, Deadline}) -> @@ -570,17 +567,14 @@ unmarshal(range, #'fistful_base_EventRange'{ unmarshal(bool, V) when is_boolean(V) -> V. -unmarshal_repairer_timer_field(undefined) -> - undefined; -unmarshal_repairer_timer_field({set_timer, #repairer_SetTimerAction{timer = Timer}}) -> - {set_timer, unmarshal(timer, Timer)}; -unmarshal_repairer_timer_field({unset_timer, _}) -> - unset_timer. - -unmarshal_repairer_remove_field(undefined) -> - undefined; -unmarshal_repairer_remove_field(#repairer_RemoveAction{}) -> - remove. +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; diff --git a/apps/prg_machine/src/prg_action.erl b/apps/prg_machine/src/prg_action.erl index c47e13a9..92e8031d 100644 --- a/apps/prg_machine/src/prg_action.erl +++ b/apps/prg_machine/src/prg_action.erl @@ -1,15 +1,14 @@ -module(prg_action). -%%% Wire `action()` helpers and thrift/MG → wire conversion at API boundaries. +%%% Wire `action()` helpers and damsel repair → wire conversion at HG API boundaries. -include_lib("progressor/include/progressor.hrl"). -include_lib("damsel/include/dmsl_repair_thrift.hrl"). --include_lib("mg_proto/include/mg_proto_state_processing_thrift.hrl"). --export([marshal_timer/1, schedule_timer/1, schedule_after/1, schedule_deadline/1]). --export([from_timer_remove/2, from_mg/1, from_repair/1]). +-export([marshal_timer/1, schedule_timer/1, schedule_deadline/1]). +-export([from_repair/1]). --export_type([t/0, timer/0, seconds/0, timer_field/0, remove_field/0]). +-export_type([t/0, timer/0, seconds/0]). -type seconds() :: timeout_sec(). -type datetime() :: calendar:datetime() | {calendar:datetime(), non_neg_integer()} | binary(). @@ -25,12 +24,6 @@ schedule_timer({timeout, 0}) -> schedule_timer(Timer) -> {schedule, #{at => marshal_timer(Timer), action => timeout}}. --spec schedule_after(seconds()) -> t(). -schedule_after(0) -> - timeout; -schedule_after(Seconds) when is_integer(Seconds), Seconds > 0 -> - {schedule, #{at => erlang:system_time(microsecond) + Seconds * 1000000, action => timeout}}. - -spec schedule_deadline(datetime()) -> t(). schedule_deadline(Deadline) -> {schedule, #{at => marshal_timer({deadline, Deadline}), action => timeout}}. @@ -49,27 +42,7 @@ marshal_timer({deadline, Bin}) when is_binary(Bin) -> marshal_timer(Other) -> error({invalid_timer, Other}). -%% Thrift / MG → wire (HG policy: remove beats timer) - --spec from_timer_remove(timer_field(), remove_field()) -> t(). -from_timer_remove(_, remove) -> - remove; -from_timer_remove(undefined, undefined) -> - idle; -from_timer_remove({set_timer, Timer}, undefined) -> - schedule_timer(Timer); -from_timer_remove(unset_timer, undefined) -> - suspend. - --spec from_mg(undefined | mg_proto_state_processing_thrift:'ComplexAction'() | t()) -> t(). -from_mg(undefined) -> - idle; -from_mg(#mg_stateproc_ComplexAction{timer = Timer, remove = Remove}) -> - from_timer_remove(mg_timer_field(Timer), mg_remove_field(Remove)); -from_mg(Wire) when Wire =:= idle; Wire =:= suspend; Wire =:= timeout; Wire =:= remove -> - Wire; -from_mg({schedule, _} = Wire) -> - Wire. +%% damsel repair → wire (remove beats timer) -spec from_repair(undefined | dmsl_repair_thrift:'ComplexAction'() | t()) -> t(). from_repair(undefined) -> @@ -81,17 +54,15 @@ from_repair(Wire) when Wire =:= idle; Wire =:= suspend; Wire =:= timeout; Wire = from_repair({schedule, _} = Wire) -> Wire. -mg_timer_field(undefined) -> - undefined; -mg_timer_field({set_timer, #mg_stateproc_SetTimerAction{timer = Timer}}) -> - {set_timer, Timer}; -mg_timer_field({unset_timer, _}) -> - unset_timer. - -mg_remove_field(undefined) -> - undefined; -mg_remove_field(#mg_stateproc_RemoveAction{}) -> - remove. +-spec from_timer_remove(timer_field(), remove_field()) -> t(). +from_timer_remove(_, remove) -> + remove; +from_timer_remove(undefined, undefined) -> + idle; +from_timer_remove({set_timer, Timer}, undefined) -> + schedule_timer(Timer); +from_timer_remove(unset_timer, undefined) -> + suspend. repair_timer_field(undefined) -> undefined; diff --git a/docs/prg-machine.md b/docs/prg-machine.md index 4512b4dc..734f11d3 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -2,6 +2,8 @@ Единый 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.* --- @@ -46,7 +48,7 @@ sequenceDiagram |--------|------| | `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()`; MG/damsel repair на границе | +| `prg_action` | `{timeout, Sec}` / `{deadline, Dt}` → wire `action()`; damsel repair на границе HG | | `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). @@ -113,7 +115,7 @@ idle | suspend | timeout | remove | `remove()` | `remove` | | `set_timeout(N, _)` / deadline | `prg_action:schedule_timer/1`, `schedule_deadline/1` | -`prg_action:from_mg/1`, `from_repair/1` — MG/damsel repair на границе (HG/FF handler → wire). `prg_action:marshal_timer/1` принимает `{deadline, {calendar:datetime(), Micro}}` (machinery-формат из `ff_codec:unmarshal(timer, ...)`). +`prg_action:from_repair/1` — damsel repair на границе HG → wire. FF repairer `#repairer_ComplexAction{}` → wire в `ff_codec:repairer_complex_action_to_wire/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`. diff --git a/docs/refactor-architecture.md b/docs/refactor-architecture.md new file mode 100644 index 00000000..d66c8e19 --- /dev/null +++ b/docs/refactor-architecture.md @@ -0,0 +1,356 @@ +# Архитектурный рефакторинг `prg_machine` + +Крупные задачи с изменением контрактов, несколькими apps или migration policy. Понятные локальные правки — в [refactor-local.md](refactor-local.md). + +*Обновлено: 2026-06-17.* + +--- + +## Принцип + +| Критерий | Local file | Architecture file | +|----------|------------|-------------------| +| 1–2 файла, без смены контракта | ✓ | | +| Несколько apps / HTTP + worker | | ✓ | +| Migration / rollback policy | | ✓ | +| Нужен отдельный PR / итерация | | ✓ | + +--- + +## 1. Env-scoping pipeline + +**Приоритет:** HIGH + +### Проблема + +Woody-контекст проходит через gproc **дважды** на одном RPC: + +```mermaid +sequenceDiagram + participant WH as woody_wrapper + participant GP as gproc + participant PM as prg_machine + participant OC as op_context + + WH->>OC: save woody ctx + OC->>GP: registry_key bind + WH->>PM: call with encode_rpc_context + PM->>PM: decode_rpc_context + PM->>OC: env_enter binding + OC->>GP: bind again + PM->>PM: dispatch handler + PM->>OC: env_leave +``` + +### Файлы + +- [`apps/op_context/src/op_context.erl`](../apps/op_context/src/op_context.erl) — `save/load`, `env_enter/env_leave`, `current_woody_context/0` +- [`apps/prg_machine/src/prg_machine.erl`](../apps/prg_machine/src/prg_machine.erl) — `encode_rpc_context/0`, `run_scoped/3` +- [`apps/hg_proto/src/hg_woody_service_wrapper.erl`](../apps/hg_proto/src/hg_woody_service_wrapper.erl), [`apps/ff_server/src/ff_woody_wrapper.erl`](../apps/ff_server/src/ff_woody_wrapper.erl) +- [`config/sys.config`](../config/sys.config) — `context_binding` + +### Цель + +Один источник truth для woody/party ctx в progressor worker: либо передавать decoded ctx явно в handler, либо `env_enter` только там, где handler читает `op_context:load/1`. + +### Критерий готовности + +- Нет двойного gproc bind на одном RPC +- HG CT + FF handler CT проходят с party client в repair/call + +### Риски + +- Регрессии в FF lenient vs HG strict `cleanup_mode` +- `current_woody_context/0` перебирает HG, потом FF — неявная связность + +### Тесты + +- `prg_machine` env mock tests +- `hg_invoice_tests_SUITE`, FF handler suites + +--- + +## 2. Duplicate deadline + +**Приоритет:** MEDIUM + +### Проблема + +Default handling timeout (30s) выставляется **дважды**: + +1. HTTP-граница: `ensure_woody_deadline_set/2` в woody wrappers +2. Worker: `ensure_deadline_set/2` в `prg_machine:process/3` + +### Файлы + +- [`apps/prg_machine/src/prg_machine.erl`](../apps/prg_machine/src/prg_machine.erl) — `ensure_deadline_set/2`, opt `default_handling_timeout` +- [`apps/hg_proto/src/hg_woody_service_wrapper.erl`](../apps/hg_proto/src/hg_woody_service_wrapper.erl) +- [`apps/ff_server/src/ff_woody_wrapper.erl`](../apps/ff_server/src/ff_woody_wrapper.erl) + +### Цель + +Deadline только на HTTP-границе до `encode_rpc_context`; worker не переопределяет woody deadline без явной причины. + +### Критерий готовности + +- Единый helper или одна точка установки deadline +- CT с коротким/отсутствующим deadline не ломаются + +### Риски + +- Notify/async paths без woody wrapper + +--- + +## 3. `context_binding` DRY + +**Приоритет:** MEDIUM + +### Проблема + +Ключи gproc заданы в **двух местах**: + +- [`config/sys.config`](../config/sys.config): `registry_key => {p, l, stored_hg_context}` (HG), FF-аналог +- [`apps/op_context/src/op_context.erl`](../apps/op_context/src/op_context.erl): `key/1`, `binding/1` + +Расхождение сломает worker scoping незаметно. + +### Цель + +При старте HG/FF собирать progressor `options.context_binding` через `op_context:binding(hellgate | fistful)` — без hardcode literals в sys.config. + +### Файлы + +- `config/sys.config`, `test/*/sys.config` +- `apps/hellgate/src/hellgate.erl`, `apps/ff_server/src/ff_server.erl` — registry init + +### Критерий готовности + +- Один источник ключей gproc +- CT configs используют тот же механизм + +--- + +## 4. Legacy migration из core runtime + +**Приоритет:** MEDIUM + +### Проблема + +Политика совместимости с `hg_machine` / `ff_machine` зашита в permanent runtime: + +| Механизм | Где | Суть | +|----------|-----|------| +| `{bin, Bin}` unwrap | `prg_machine:decode_term/1` | Legacy double envelope call args | +| Dual metadata keys | `event_metadata/1` | `format` + `format_version` | +| `initial_model/2` | `prg_machine:collapse/2` | HG aux_state `#{model => ...}` | + +### Цель + +- Migration adapter или behaviour callback (`initial_model/1`, `decode_call_args/1`) +- Core runtime без HG/FF-specific rollback + +### Файлы + +- [`apps/prg_machine/src/prg_machine.erl`](../apps/prg_machine/src/prg_machine.erl) +- Handler codecs: [`hg_invoice.erl`](../apps/hellgate/src/hg_invoice.erl), [`ff_machine_codec.erl`](../apps/ff_transfer/src/ff_machine_codec.erl) + +### Критерий готовности + +- Новые events/calls не пишут legacy формы +- Чтение старых данных покрыто тестами migration adapter + +### Риски + +- Prod history с legacy envelope — нельзя удалить unwrap без migration window + +--- + +## 5. FF codec chain + +**Приоритет:** MEDIUM (зависит от [refactor-local.md §D2](refactor-local.md#d2-ff-codec-chain)) + +### Проблема + +```mermaid +flowchart LR + M["5x *_machine"] + L[ff_machine_lib] + C[ff_machine_codec] + DC["domain codec"] + M -->|"1 line delegate"| L + L -->|"passthrough"| C + C --> DC +``` + +- `ff_machine_codec` — **единственный** caller `ff_machine_lib` для marshal/unmarshal +- Behaviour callbacks в machines — 1 строка delegate + +### Варианты + +| ID | Стратегия | +|----|-----------| +| A | Merge `ff_machine_codec` → `ff_machine_lib` | +| B | Machines → `ff_machine_codec` напрямую, lib без passthrough | +| C | Status quo до отдельного PR | + +### Цель + +Убрать лишний hop без потери shared create/get/repair в lib. + +### Файлы + +- [`apps/ff_transfer/src/ff_machine_lib.erl`](../apps/ff_transfer/src/ff_machine_lib.erl) +- [`apps/ff_transfer/src/ff_machine_codec.erl`](../apps/ff_transfer/src/ff_machine_codec.erl) +- `apps/ff_transfer/src/ff_*_machine.erl` (5 modules) + +### Критерий готовности + +- Цепочка marshal event: machine → codec → domain (max 2 hops) +- Eunit codec tests green + +--- + +## 6. FF repair glue + +**Приоритет:** MEDIUM + +### Проблема + +Repair path: `*_machine:process_repair` → `ff_machine_lib:process_repair` → `ff_repair:apply_scenario`. + +- `ff_repair:apply_scenario/3,4` — **единственный** внешний caller `ff_machine_lib` +- Конвертация `prg_machine:machine()` ↔ `ff_repair:machine()` split между lib и repair + +### Цель + +`ff_repair` принимает `prg_machine:machine()` (или один converter module); machines зовут `ff_repair` без lib-glue. + +### Файлы + +- [`apps/fistful/src/ff_repair.erl`](../apps/fistful/src/ff_repair.erl) +- [`apps/ff_transfer/src/ff_machine_lib.erl`](../apps/ff_transfer/src/ff_machine_lib.erl) +- 5 `*_machine.erl` + +### Критерий готовности + +- `process_repair/2` в machine — ≤5 строк +- FF repair CT (`ff_withdrawal_session_repair_SUITE`, `force_status_change_test`) green + +### Связь с local + +- [refactor-local.md §C1](refactor-local.md#c1-ff_repairto_prg_machine1) — inline `to_prg_machine/1` (первый шаг) + +--- + +## 7. `prg_machine_registry` gen_server shell + +**Приоритет:** LOW + +### Проблема + +[`prg_machine_registry.erl`](../apps/prg_machine/src/prg_machine_registry.erl) — gen_server с пустыми `handle_call/cast/info`; `lookup/1` — прямой ETS; поле `state.handlers` не читается. + +### Цель + +ETS init в `prg_machine` application start / supervisor; registry list handlers один раз при старте HG/FF. + +### Callers `lookup/1` + +- `prg_machine.erl` (get/process) +- `ff_machine_trace.erl` (если не перенесён — см. local §D3) + +### Критерий готовности + +- Нет gen_server без state semantics +- Registry lookup behaviour unchanged + +--- + +## 8. `hg_context` facade + +**Приоритет:** LOW–MEDIUM + +### Проблема + +После удаления [`hg_context.erl`](../apps/hellgate/src/hg_context.erl) HG модули зовут `op_context:key(hellgate)` напрямую. Блок **идентичный** `get_party_client/0` скопирован в: + +- [`hg_party.erl`](../apps/hellgate/src/hg_party.erl) +- [`hg_payment_institution.erl`](../apps/hellgate/src/hg_payment_institution.erl) +- [`hg_invoice_payment.erl`](../apps/hellgate/src/hg_invoice_payment.erl) + +### Цель + +Тонкий `hg_context` facade: + +```erlang +load/0, cleanup/0, get_party_client/0 +``` + +поверх `op_context` — **не** дублировать runtime scoping из §1. + +### Критерий готовности + +- Один `get_party_client/0` в HG +- `op_context` остаётся shared для FF + `prg_machine` env binding + +--- + +## 9. FF `init_result` / `to_prg_result` унификация + +**Приоритет:** LOW + +### Проблема + +Несогласованный стиль в 5 `*_machine`: + +- source/destination/session: `ff_machine_lib:init_result/3` +- withdrawal/deposit: inline `#{events, action, auxst}` в `init/2` +- `to_prg_result/1` — trivial map, 8 callers + +### Цель + +Единый стиль init/process_result (все через lib **или** все inline). + +### Связь + +Не блокирует §5–§6; косметика после codec/repair glue. + +--- + +## Порядок итераций + +```mermaid +flowchart TD + L[refactor-local A-C] + D[refactor-local D choices] + E1["§1 env pipeline"] + E2["§2 deadline"] + E3["§3 context_binding"] + E4["§4 legacy core"] + E5["§5-6 FF glue"] + E7["§7-9 polish"] + + L --> D + D --> E5 + E1 --> E2 + E2 --> E3 + E3 --> E4 + E5 --> E7 +``` + +**Рекомендация:** + +1. Закрыть [refactor-local.md](refactor-local.md) блоки A–C +2. Решить развилки D → отдельные PR +3. §1 env — отдельная большая итерация (HG+FF) +4. §5–§6 FF — после выбора D2 +5. §7–§9 — по желанию + +--- + +## Что не включаем + +- Изменения progressor upstream / wire `action()` contract +- Массовый рефактор FF domain (`ff_withdrawal`, routing, limiter) +- Routing app split (`hg_route_collector` и т.д.) — отдельная история ветки diff --git a/docs/refactor-local.md b/docs/refactor-local.md new file mode 100644 index 00000000..9d5fd34a --- /dev/null +++ b/docs/refactor-local.md @@ -0,0 +1,271 @@ +# Локальные упрощения после миграции на `prg_machine` + +Понятные задачи с пошаговыми инструкциями. Крупные архитектурные изменения — в [refactor-architecture.md](refactor-architecture.md). + +*Обновлено: 2026-06-17.* + +--- + +## Уже сделано (не дублировать) + +| Изменение | Где | +|-----------|-----| +| FF repairer `ComplexAction` → wire `action()` локально в codec | [`apps/ff_server/src/ff_codec.erl`](../apps/ff_server/src/ff_codec.erl) — `unmarshal_repairer_complex_action/2`, вызов из `unmarshal(complex_action, ...)` | +| Убраны мёртвые `prg_action:from_mg/1`, `schedule_after/1`, MG-хелперы | [`apps/prg_machine/src/prg_action.erl`](../apps/prg_machine/src/prg_action.erl) | +| Документация HG vs FF границ action | [`docs/prg-machine.md`](prg-machine.md) | + +Policy для FF repairer (remove бьёт timer, unset → suspend) живёт в `ff_codec`, не в `prg_action`. + +--- + +## Рекомендуемый порядок + +1. [Блок A](#блок-a-мёртвый-код--dead-exports) — ~30 мин, низкий риск +2. [Блок B](#блок-b-hg--from_repair-локально-в-hg_invoice) — ~1 ч +3. [Блок C](#блок-c-ff--мелкий-inline-и-export-cleanup) — ~30 мин +4. [Блок D](#блок-d-развилки--не-делать-без-выбора) — выбрать стратегию, затем выполнить или перенести в architecture +5. [Блок E](#блок-e-проверка) — после каждого блока + +--- + +## Блок A: мёртвый код / dead exports + +**Риск:** низкий. **Развилок нет** (кроме последнего пункта — см. [Блок D](#блок-d-развилки--не-делать-без-выбора)). + +### A1. `hg_invoice_payment:set_timer/2` + +**Файл:** [`apps/hellgate/src/hg_invoice_payment.erl`](../apps/hellgate/src/hg_invoice_payment.erl) + +**Проблема:** `set_timer/2` — обёртка с 1 caller; второй аргумент `_Action` не используется. + +**Шаги:** + +1. В `retry_session/3` (~2597) заменить: + ```erlang + NewAction = set_timer({timeout, Timeout}, Action), + ``` + на: + ```erlang + NewAction = prg_action:schedule_timer({timeout, Timeout}), + ``` +2. Удалить `-spec set_timer/2` и функцию `set_timer/2` (~2655). + +**Проверка:** `rebar3 compile`, HG CT по payment retry при необходимости. + +--- + +### A2. `prg_machine:remove/2`, `emit_event/1` + +**Файл:** [`apps/prg_machine/src/prg_machine.erl`](../apps/prg_machine/src/prg_machine.erl) + +**Проблема:** 0 внешних callers. `remove` обрабатывается внутри `dispatch/4`; `emit_event/1` — alias к `emit_events/1`. + +**Шаги:** + +1. Убрать `remove/2` и `emit_event/1` из `-export`. +2. Функции оставить **internal** (используются внутри модуля / `ff_repair` зовёт `emit_events/1`). + +**Не трогать:** `emit_events/1` — caller [`apps/fistful/src/ff_repair.erl`](../apps/fistful/src/ff_repair.erl). + +--- + +### A3. `op_context:set_woody_context/2` + +**Файл:** [`apps/op_context/src/op_context.erl`](../apps/op_context/src/op_context.erl) + +**Проблема:** 0 callers. + +**Шаги:** + +1. Удалить из `-export`. +2. Удалить `-spec` и тело функции (~159–161). + +--- + +### A4. `hg_invoicing_machine_client:thrift_call/8` + +**Файл:** [`apps/hellgate/src/hg_invoicing_machine_client.erl`](../apps/hellgate/src/hg_invoicing_machine_client.erl) + +**Проблема:** 8-arg версия с range не вызывается снаружи; 5-arg делегирует в 8-arg с `undefined, undefined, forward`. + +**Минимальный fix (без развилки D1):** + +1. Убрать `-export([thrift_call/8])`. +2. Оставить одну `thrift_call/5`, inline логику без делегирования в 8-arg. + +**Полное решение:** см. [развилку D1](#d1-hg_invoicing_machine_client). + +--- + +## Блок B: HG — `from_repair` локально в `hg_invoice` + +**Риск:** средний. **Развилок нет.** + +**Проблема:** `prg_action:from_repair/1` имеет **1 caller** — [`hg_invoice.erl:333`](../apps/hellgate/src/hg_invoice.erl). FF repair идёт через `ff_codec`, не через `prg_action`. + +### B1. Перенести repair → wire в `hg_invoice` + +**Источник логики:** [`apps/prg_machine/src/prg_action.erl`](../apps/prg_machine/src/prg_action.erl) — `from_repair/1`, `from_timer_remove/2`, `repair_timer_field/1`, `repair_remove_field/1`. + +**Шаги:** + +1. В [`hg_invoice.erl`](../apps/hellgate/src/hg_invoice.erl) добавить include `dmsl_repair_thrift.hrl` (если ещё нет). +2. Восстановить приватные функции (имена как на master до prg-миграции): + - `construct_repair_action/1` — `#repair_ComplexAction{}` → wire `action()` + - `merge_repair_action/2` — merge с текущим action при repair (если используется в ~333) +3. Policy (как сейчас в `prg_action`): + - `remove` beats timer + - `undefined + undefined` → `idle` + - `{set_timer, Timer}` → `prg_action:schedule_timer(Timer)` + - `unset_timer` → `suspend` +4. Заменить `prg_action:from_repair(RepairAction)` на локальный вызов. + +### B2. Упростить `prg_action` + +**Шаги:** + +1. Удалить `-export([from_repair/1])`. +2. Удалить `from_repair/1`, `from_timer_remove/2`, `repair_timer_field/1`, `repair_remove_field/1`, типы `timer_field`/`remove_field`. +3. Убрать `-include_lib("damsel/include/dmsl_repair_thrift.hrl")`. +4. Обновить комментарий модуля: только scheduling helpers + `marshal_timer/1`. + +**Оставить в `prg_action`:** `schedule_timer/1`, `schedule_deadline/1`, `marshal_timer/1`, типы `t/0`, `timer/0`, `seconds/0`. + +### B3. Документация + +В [`docs/prg-machine.md`](prg-machine.md) (~116) заменить упоминание `from_repair/1` на «damsel repair → wire в `hg_invoice`». + +--- + +## Блок C: FF — мелкий inline и export cleanup + +**Риск:** низкий. **Развилок нет.** + +### C1. `ff_repair:to_prg_machine/1` + +**Файл:** [`apps/fistful/src/ff_repair.erl`](../apps/fistful/src/ff_repair.erl) + +**Проблема:** 1 внешний caller — [`ff_withdrawal_session_machine.erl`](../apps/ff_transfer/src/ff_withdrawal_session_machine.erl) (~169), processor `set_session_result`. + +**Шаги (вариант inline в machine):** + +1. В processor заменить `ff_repair:to_prg_machine(RMachine)` на inline map: + ```erlang + #{namespace := NS, id := ID, history := History, aux_state := AuxSt} = RMachine, + Machine = #{namespace => NS, id => ID, history => ..., aux_state => AuxSt}, + ``` + (history уже в формате prg — см. `repair_history_to_prg/1` в `ff_repair`). +2. Удалить `-export([to_prg_machine/1])` и функцию из `ff_repair`. +3. Оставить **internal** `to_prg_machine/1` в `ff_repair`, если нужен для `validate_result/3`. + +### C2. `ff_machine_lib` — убрать лишние exports + +**Файл:** [`apps/ff_transfer/src/ff_machine_lib.erl`](../apps/ff_transfer/src/ff_machine_lib.erl) + +**Проблема:** функции экспортированы, но **0 внешних callers** (только internal lib): + +| Функция | Internal usage | +|---------|----------------| +| `to_repair_machine/1` | `process_repair/*` | +| `from_repair_result/2` | `process_repair/*` | +| `repair_events_to_domain/1` | `from_repair_result/2` | +| `event_body_from_timestamped/1` | `repair_events_to_domain/1`, `unmarshal_event_body/2` | +| `history_to_events/1` | `get/5` | +| `codec_timestamp/1` | `history_to_events/1` | + +**Шаги:** + +1. Убрать перечисленные функции из `-export`. +2. Функции и `-spec` оставить в модуле (private по умолчанию в Erlang). + +**Не убирать из export:** `create/4`, `get/5`, `events/4`, `repair/3`, `process_repair/*`, `to_prg_result/1`, `init_result/*`, `machine_to_st/2`, `marshal_event_body/3`, `unmarshal_event_body/2`, `marshal_aux_state/1`, `unmarshal_aux_state/1`. + +--- + +## Блок D: развилки — не делать без выбора + +После выбора — дописать решение в этот файл или перенести в [refactor-architecture.md](refactor-architecture.md). + +### D1. `hg_invoicing_machine_client` + +**Файл:** [`apps/hellgate/src/hg_invoicing_machine_client.erl`](../apps/hellgate/src/hg_invoicing_machine_client.erl) + +**Callers:** [`hg_invoice_handler.erl`](../apps/hellgate/src/hg_invoice_handler.erl), [`hg_invoice_template.erl`](../apps/hellgate/src/hg_invoice_template.erl). + +| Вариант | Действие | Плюсы | Минусы | +|---------|----------|-------|--------| +| **A (рекомендуется)** | Удалить модуль; 5–10 строк `prg_machine:call` + `normalize_response/1` в handlers | Меньше hop, видно контракт call | Небольшое дублирование между handler и template | +| **B** | Оставить модуль, только A4 (убрать `thrift_call/8`) | Минимальный diff | Модуль остаётся thin pass-through | + +**Snippet для варианта A (handler):** + +```erlang +case prg_machine:call(NS, ID, {FunRef, Args}) of + {ok, Response} -> normalize_call_response(Response); + {error, notfound} -> {error, notfound}; + {error, failed} -> {error, failed}; + {error, _} = Error -> Error +end. +``` + +--- + +### D2. FF codec chain + +**Цепочка:** + +```mermaid +flowchart LR + M["*_machine callbacks"] + L[ff_machine_lib] + C[ff_machine_codec] + DC["ff_*_codec"] + M --> L --> C --> DC +``` + +**Проблема:** `ff_machine_codec` вызывается **только** из `ff_machine_lib`; machines — только lib. + +| Вариант | Действие | Когда выбирать | +|---------|----------|----------------| +| **A** | Слить `ff_machine_codec.erl` в `ff_machine_lib.erl` | Хочется один FF codec entry point | +| **B** | Machines зовут `ff_machine_codec` напрямую; lib теряет passthrough `marshal_*`/`unmarshal_*` | Хочется убрать lib hop, codec остаётся отдельным модулем | +| **C (рекомендуется при сомнении)** | Отложить | → [refactor-architecture.md §5](refactor-architecture.md#5-ff-codec-chain) | + +--- + +### D3. `ff_machine_trace` + +**Файлы:** [`apps/ff_transfer/src/ff_machine_trace.erl`](../apps/ff_transfer/src/ff_machine_trace.erl) — **1 caller** [`ff_machine_handler.erl`](../apps/ff_server/src/ff_machine_handler.erl). + +| Вариант | Действие | +|---------|----------| +| **A (рекомендуется)** | Перенести модуль в `apps/ff_server/src/` (debug HTTP рядом с handler) | +| **B** | Оставить в `ff_transfer` | + +**Замечание:** trace дублирует `decode_term/1` из `prg_machine` — при переносе рассмотреть экспорт decode helper из `prg_machine` (см. architecture §5). + +--- + +## Блок E: проверка + +После каждого блока: + +```bash +rebar3 compile +rebar3 eunit --module=prg_action,ff_withdrawal_codec # если трогали action/codec +``` + +При изменении HG invoice / client: + +```bash +# smoke — конкретный suite по изменённой области +rebar3 ct --suite=apps/hellgate/test/hg_invoice_tests_SUITE +``` + +--- + +## Что сознательно не включено + +- Массовый перенос FF domain logic — вне scope +- Изменения progressor dependency / wire `action()` contract +- Архитектурные задачи (env pipeline, deadline ×2, legacy в core) — [refactor-architecture.md](refactor-architecture.md) From 8e1eec662d1d48ac4acd16e7f4ed6147414ee3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 18 Jun 2026 01:05:18 +0300 Subject: [PATCH 54/62] Refactor hg_invoice_payment and hg_invoicing_machine_client modules to streamline session handling and remove unused functions. Update timer scheduling in retry_session and simplify thrift_call by eliminating redundant parameters. This enhances code clarity and prepares for future improvements. --- apps/hellgate/src/hg_invoice_payment.erl | 7 ++----- .../src/hg_invoicing_machine_client.erl | 19 ++----------------- apps/op_context/src/op_context.erl | 5 ----- apps/prg_machine/src/prg_machine.erl | 14 -------------- 4 files changed, 4 insertions(+), 41 deletions(-) diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index e940eaa0..b14baa4d 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -2592,9 +2592,9 @@ process_fatal_payment_failure(?processed(), Events, _Action, Failure, _St) -> RollbackStarted = [?payment_rollback_started(Failure)], {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}) -> @@ -2652,9 +2652,6 @@ get_action(?processed(), _Action, St) -> get_action(_Target, Action, _St) -> Action. -set_timer(Timer, _Action) -> - prg_action:schedule_timer(Timer). - get_provider_payment_terms(St, Revision) -> Opts = get_opts(St), Route = get_route(St), diff --git a/apps/hellgate/src/hg_invoicing_machine_client.erl b/apps/hellgate/src/hg_invoicing_machine_client.erl index 2b0a1154..84ed5118 100644 --- a/apps/hellgate/src/hg_invoicing_machine_client.erl +++ b/apps/hellgate/src/hg_invoicing_machine_client.erl @@ -5,33 +5,18 @@ %%% hg_proto stays in apps/hellgate (not in prg_machine). -export([thrift_call/5]). --export([thrift_call/8]). -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 event_id() :: prg_machine:event_id(). -type response() :: prg_machine:response(). -spec thrift_call(namespace(), 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( - namespace(), - id(), - service_name(), - function_ref(), - args(), - event_id() | undefined, - non_neg_integer() | undefined, - forward | backward -) -> response() | {error, notfound | failed}. -thrift_call(NS, ID, _ServiceName, FunRef, Args, After, Limit, Direction) -> - case prg_machine:call(NS, ID, {FunRef, Args}, After, Limit, Direction) of +thrift_call(NS, ID, _ServiceName, FunRef, Args) -> + case prg_machine:call(NS, ID, {FunRef, Args}) of {ok, Response} -> normalize_response(Response); {error, notfound} -> diff --git a/apps/op_context/src/op_context.erl b/apps/op_context/src/op_context.erl index 99b1cff4..63f54aeb 100644 --- a/apps/op_context/src/op_context.erl +++ b/apps/op_context/src/op_context.erl @@ -11,7 +11,6 @@ -export([binding/1]). -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]). @@ -156,10 +155,6 @@ try_load_woody_context([Key | Rest]) -> 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; diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 395b8f7b..ad393f1f 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -112,7 +112,6 @@ -export([get_history/4]). -export([get_history/5]). -export([notify/3]). --export([remove/2]). -export([history_range/3]). %% Progressor processor @@ -126,7 +125,6 @@ %% Event-sourcing helpers (replaces ff_machine) -export([collapse/2]). --export([emit_event/1]). -export([emit_events/1]). -export([timestamp/0]). @@ -262,14 +260,6 @@ notify(NS, ID, Args) -> {error, _} = Error -> Error end. --spec remove(namespace(), id()) -> - ok | {error, notfound | failed | processor_error() | term()}. -remove(NS, ID) -> - case call(NS, ID, remove) 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) -> @@ -332,10 +322,6 @@ collapse(Handler, #{history := History, aux_state := AuxState}) -> History ). --spec emit_event(term()) -> [{ev, timestamp(), term()}]. -emit_event(Event) -> - emit_events([Event]). - -spec emit_events([term()]) -> [{ev, timestamp(), term()}]. emit_events(Events) -> Ts = timestamp(), From e2815a9ff3d0863f4175339ee34e4c2cbec6c6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 18 Jun 2026 01:06:51 +0300 Subject: [PATCH 55/62] Refactor repair action handling in hg_invoice and prg_action modules. Replace from_repair function with construct_repair_action for improved clarity and maintainability. Update related documentation to reflect changes in action scheduling and processing. This enhances code organization and prepares for future enhancements. --- apps/hellgate/src/hg_invoice.erl | 20 +++++++++++++- apps/prg_machine/src/prg_action.erl | 41 +---------------------------- docs/prg-machine.md | 4 +-- 3 files changed, 22 insertions(+), 43 deletions(-) diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index 099628d3..db66ac68 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -330,7 +330,7 @@ handle_repair({changes, Changes, RepairAction, Params}, St) -> [] -> #{} end, - Action = prg_action:from_repair(RepairAction), + Action = construct_repair_action(RepairAction), Result#{ state => St, action => Action, @@ -349,6 +349,24 @@ handle_repair({scenario, Scenario}, #st{activity = {payment, PaymentID}} = St) - try_to_get_repair_state(Scenario, St) end. +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), diff --git a/apps/prg_machine/src/prg_action.erl b/apps/prg_machine/src/prg_action.erl index 92e8031d..6708f739 100644 --- a/apps/prg_machine/src/prg_action.erl +++ b/apps/prg_machine/src/prg_action.erl @@ -1,12 +1,10 @@ -module(prg_action). -%%% Wire `action()` helpers and damsel repair → wire conversion at HG API boundaries. +%%% Wire `action()` scheduling helpers for domain handlers. -include_lib("progressor/include/progressor.hrl"). --include_lib("damsel/include/dmsl_repair_thrift.hrl"). -export([marshal_timer/1, schedule_timer/1, schedule_deadline/1]). --export([from_repair/1]). -export_type([t/0, timer/0, seconds/0]). @@ -15,9 +13,6 @@ -type timer() :: {timeout, seconds()} | {deadline, datetime()}. -type t() :: action(). --type timer_field() :: undefined | {set_timer, timer()} | unset_timer. --type remove_field() :: undefined | remove. - -spec schedule_timer(timer()) -> t(). schedule_timer({timeout, 0}) -> timeout; @@ -42,40 +37,6 @@ marshal_timer({deadline, Bin}) when is_binary(Bin) -> marshal_timer(Other) -> error({invalid_timer, Other}). -%% damsel repair → wire (remove beats timer) - --spec from_repair(undefined | dmsl_repair_thrift:'ComplexAction'() | t()) -> t(). -from_repair(undefined) -> - idle; -from_repair(#repair_ComplexAction{timer = Timer, remove = Remove}) -> - from_timer_remove(repair_timer_field(Timer), repair_remove_field(Remove)); -from_repair(Wire) when Wire =:= idle; Wire =:= suspend; Wire =:= timeout; Wire =:= remove -> - Wire; -from_repair({schedule, _} = Wire) -> - Wire. - --spec from_timer_remove(timer_field(), remove_field()) -> t(). -from_timer_remove(_, remove) -> - remove; -from_timer_remove(undefined, undefined) -> - idle; -from_timer_remove({set_timer, Timer}, undefined) -> - schedule_timer(Timer); -from_timer_remove(unset_timer, undefined) -> - suspend. - -repair_timer_field(undefined) -> - undefined; -repair_timer_field({set_timer, #repair_SetTimerAction{timer = Timer}}) -> - {set_timer, Timer}; -repair_timer_field({unset_timer, _}) -> - unset_timer. - -repair_remove_field(undefined) -> - undefined; -repair_remove_field(#repair_RemoveAction{}) -> - remove. - %% datetime_to_microseconds(Dt, USec) -> diff --git a/docs/prg-machine.md b/docs/prg-machine.md index 734f11d3..5203b3f2 100644 --- a/docs/prg-machine.md +++ b/docs/prg-machine.md @@ -48,7 +48,7 @@ sequenceDiagram |--------|------| | `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()`; damsel repair на границе HG | +| `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). @@ -115,7 +115,7 @@ idle | suspend | timeout | remove | `remove()` | `remove` | | `set_timeout(N, _)` / deadline | `prg_action:schedule_timer/1`, `schedule_deadline/1` | -`prg_action:from_repair/1` — damsel repair на границе HG → wire. FF repairer `#repairer_ComplexAction{}` → wire в `ff_codec:repairer_complex_action_to_wire/2`. `prg_action:marshal_timer/1` принимает `{deadline, {calendar:datetime(), Micro}}` (machinery-формат из `ff_codec:unmarshal(timer, ...)`). +`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`. From 0321603d1a1eb0d7c62927c4ead421055c791d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 18 Jun 2026 01:11:15 +0300 Subject: [PATCH 56/62] Remove the refactor-local.md file, which contained outdated instructions and documentation related to local simplifications after migrating to prg_machine. This cleanup enhances project clarity and eliminates redundancy in documentation. --- docs/refactor-local.md | 271 ----------------------------------------- 1 file changed, 271 deletions(-) delete mode 100644 docs/refactor-local.md diff --git a/docs/refactor-local.md b/docs/refactor-local.md deleted file mode 100644 index 9d5fd34a..00000000 --- a/docs/refactor-local.md +++ /dev/null @@ -1,271 +0,0 @@ -# Локальные упрощения после миграции на `prg_machine` - -Понятные задачи с пошаговыми инструкциями. Крупные архитектурные изменения — в [refactor-architecture.md](refactor-architecture.md). - -*Обновлено: 2026-06-17.* - ---- - -## Уже сделано (не дублировать) - -| Изменение | Где | -|-----------|-----| -| FF repairer `ComplexAction` → wire `action()` локально в codec | [`apps/ff_server/src/ff_codec.erl`](../apps/ff_server/src/ff_codec.erl) — `unmarshal_repairer_complex_action/2`, вызов из `unmarshal(complex_action, ...)` | -| Убраны мёртвые `prg_action:from_mg/1`, `schedule_after/1`, MG-хелперы | [`apps/prg_machine/src/prg_action.erl`](../apps/prg_machine/src/prg_action.erl) | -| Документация HG vs FF границ action | [`docs/prg-machine.md`](prg-machine.md) | - -Policy для FF repairer (remove бьёт timer, unset → suspend) живёт в `ff_codec`, не в `prg_action`. - ---- - -## Рекомендуемый порядок - -1. [Блок A](#блок-a-мёртвый-код--dead-exports) — ~30 мин, низкий риск -2. [Блок B](#блок-b-hg--from_repair-локально-в-hg_invoice) — ~1 ч -3. [Блок C](#блок-c-ff--мелкий-inline-и-export-cleanup) — ~30 мин -4. [Блок D](#блок-d-развилки--не-делать-без-выбора) — выбрать стратегию, затем выполнить или перенести в architecture -5. [Блок E](#блок-e-проверка) — после каждого блока - ---- - -## Блок A: мёртвый код / dead exports - -**Риск:** низкий. **Развилок нет** (кроме последнего пункта — см. [Блок D](#блок-d-развилки--не-делать-без-выбора)). - -### A1. `hg_invoice_payment:set_timer/2` - -**Файл:** [`apps/hellgate/src/hg_invoice_payment.erl`](../apps/hellgate/src/hg_invoice_payment.erl) - -**Проблема:** `set_timer/2` — обёртка с 1 caller; второй аргумент `_Action` не используется. - -**Шаги:** - -1. В `retry_session/3` (~2597) заменить: - ```erlang - NewAction = set_timer({timeout, Timeout}, Action), - ``` - на: - ```erlang - NewAction = prg_action:schedule_timer({timeout, Timeout}), - ``` -2. Удалить `-spec set_timer/2` и функцию `set_timer/2` (~2655). - -**Проверка:** `rebar3 compile`, HG CT по payment retry при необходимости. - ---- - -### A2. `prg_machine:remove/2`, `emit_event/1` - -**Файл:** [`apps/prg_machine/src/prg_machine.erl`](../apps/prg_machine/src/prg_machine.erl) - -**Проблема:** 0 внешних callers. `remove` обрабатывается внутри `dispatch/4`; `emit_event/1` — alias к `emit_events/1`. - -**Шаги:** - -1. Убрать `remove/2` и `emit_event/1` из `-export`. -2. Функции оставить **internal** (используются внутри модуля / `ff_repair` зовёт `emit_events/1`). - -**Не трогать:** `emit_events/1` — caller [`apps/fistful/src/ff_repair.erl`](../apps/fistful/src/ff_repair.erl). - ---- - -### A3. `op_context:set_woody_context/2` - -**Файл:** [`apps/op_context/src/op_context.erl`](../apps/op_context/src/op_context.erl) - -**Проблема:** 0 callers. - -**Шаги:** - -1. Удалить из `-export`. -2. Удалить `-spec` и тело функции (~159–161). - ---- - -### A4. `hg_invoicing_machine_client:thrift_call/8` - -**Файл:** [`apps/hellgate/src/hg_invoicing_machine_client.erl`](../apps/hellgate/src/hg_invoicing_machine_client.erl) - -**Проблема:** 8-arg версия с range не вызывается снаружи; 5-arg делегирует в 8-arg с `undefined, undefined, forward`. - -**Минимальный fix (без развилки D1):** - -1. Убрать `-export([thrift_call/8])`. -2. Оставить одну `thrift_call/5`, inline логику без делегирования в 8-arg. - -**Полное решение:** см. [развилку D1](#d1-hg_invoicing_machine_client). - ---- - -## Блок B: HG — `from_repair` локально в `hg_invoice` - -**Риск:** средний. **Развилок нет.** - -**Проблема:** `prg_action:from_repair/1` имеет **1 caller** — [`hg_invoice.erl:333`](../apps/hellgate/src/hg_invoice.erl). FF repair идёт через `ff_codec`, не через `prg_action`. - -### B1. Перенести repair → wire в `hg_invoice` - -**Источник логики:** [`apps/prg_machine/src/prg_action.erl`](../apps/prg_machine/src/prg_action.erl) — `from_repair/1`, `from_timer_remove/2`, `repair_timer_field/1`, `repair_remove_field/1`. - -**Шаги:** - -1. В [`hg_invoice.erl`](../apps/hellgate/src/hg_invoice.erl) добавить include `dmsl_repair_thrift.hrl` (если ещё нет). -2. Восстановить приватные функции (имена как на master до prg-миграции): - - `construct_repair_action/1` — `#repair_ComplexAction{}` → wire `action()` - - `merge_repair_action/2` — merge с текущим action при repair (если используется в ~333) -3. Policy (как сейчас в `prg_action`): - - `remove` beats timer - - `undefined + undefined` → `idle` - - `{set_timer, Timer}` → `prg_action:schedule_timer(Timer)` - - `unset_timer` → `suspend` -4. Заменить `prg_action:from_repair(RepairAction)` на локальный вызов. - -### B2. Упростить `prg_action` - -**Шаги:** - -1. Удалить `-export([from_repair/1])`. -2. Удалить `from_repair/1`, `from_timer_remove/2`, `repair_timer_field/1`, `repair_remove_field/1`, типы `timer_field`/`remove_field`. -3. Убрать `-include_lib("damsel/include/dmsl_repair_thrift.hrl")`. -4. Обновить комментарий модуля: только scheduling helpers + `marshal_timer/1`. - -**Оставить в `prg_action`:** `schedule_timer/1`, `schedule_deadline/1`, `marshal_timer/1`, типы `t/0`, `timer/0`, `seconds/0`. - -### B3. Документация - -В [`docs/prg-machine.md`](prg-machine.md) (~116) заменить упоминание `from_repair/1` на «damsel repair → wire в `hg_invoice`». - ---- - -## Блок C: FF — мелкий inline и export cleanup - -**Риск:** низкий. **Развилок нет.** - -### C1. `ff_repair:to_prg_machine/1` - -**Файл:** [`apps/fistful/src/ff_repair.erl`](../apps/fistful/src/ff_repair.erl) - -**Проблема:** 1 внешний caller — [`ff_withdrawal_session_machine.erl`](../apps/ff_transfer/src/ff_withdrawal_session_machine.erl) (~169), processor `set_session_result`. - -**Шаги (вариант inline в machine):** - -1. В processor заменить `ff_repair:to_prg_machine(RMachine)` на inline map: - ```erlang - #{namespace := NS, id := ID, history := History, aux_state := AuxSt} = RMachine, - Machine = #{namespace => NS, id => ID, history => ..., aux_state => AuxSt}, - ``` - (history уже в формате prg — см. `repair_history_to_prg/1` в `ff_repair`). -2. Удалить `-export([to_prg_machine/1])` и функцию из `ff_repair`. -3. Оставить **internal** `to_prg_machine/1` в `ff_repair`, если нужен для `validate_result/3`. - -### C2. `ff_machine_lib` — убрать лишние exports - -**Файл:** [`apps/ff_transfer/src/ff_machine_lib.erl`](../apps/ff_transfer/src/ff_machine_lib.erl) - -**Проблема:** функции экспортированы, но **0 внешних callers** (только internal lib): - -| Функция | Internal usage | -|---------|----------------| -| `to_repair_machine/1` | `process_repair/*` | -| `from_repair_result/2` | `process_repair/*` | -| `repair_events_to_domain/1` | `from_repair_result/2` | -| `event_body_from_timestamped/1` | `repair_events_to_domain/1`, `unmarshal_event_body/2` | -| `history_to_events/1` | `get/5` | -| `codec_timestamp/1` | `history_to_events/1` | - -**Шаги:** - -1. Убрать перечисленные функции из `-export`. -2. Функции и `-spec` оставить в модуле (private по умолчанию в Erlang). - -**Не убирать из export:** `create/4`, `get/5`, `events/4`, `repair/3`, `process_repair/*`, `to_prg_result/1`, `init_result/*`, `machine_to_st/2`, `marshal_event_body/3`, `unmarshal_event_body/2`, `marshal_aux_state/1`, `unmarshal_aux_state/1`. - ---- - -## Блок D: развилки — не делать без выбора - -После выбора — дописать решение в этот файл или перенести в [refactor-architecture.md](refactor-architecture.md). - -### D1. `hg_invoicing_machine_client` - -**Файл:** [`apps/hellgate/src/hg_invoicing_machine_client.erl`](../apps/hellgate/src/hg_invoicing_machine_client.erl) - -**Callers:** [`hg_invoice_handler.erl`](../apps/hellgate/src/hg_invoice_handler.erl), [`hg_invoice_template.erl`](../apps/hellgate/src/hg_invoice_template.erl). - -| Вариант | Действие | Плюсы | Минусы | -|---------|----------|-------|--------| -| **A (рекомендуется)** | Удалить модуль; 5–10 строк `prg_machine:call` + `normalize_response/1` в handlers | Меньше hop, видно контракт call | Небольшое дублирование между handler и template | -| **B** | Оставить модуль, только A4 (убрать `thrift_call/8`) | Минимальный diff | Модуль остаётся thin pass-through | - -**Snippet для варианта A (handler):** - -```erlang -case prg_machine:call(NS, ID, {FunRef, Args}) of - {ok, Response} -> normalize_call_response(Response); - {error, notfound} -> {error, notfound}; - {error, failed} -> {error, failed}; - {error, _} = Error -> Error -end. -``` - ---- - -### D2. FF codec chain - -**Цепочка:** - -```mermaid -flowchart LR - M["*_machine callbacks"] - L[ff_machine_lib] - C[ff_machine_codec] - DC["ff_*_codec"] - M --> L --> C --> DC -``` - -**Проблема:** `ff_machine_codec` вызывается **только** из `ff_machine_lib`; machines — только lib. - -| Вариант | Действие | Когда выбирать | -|---------|----------|----------------| -| **A** | Слить `ff_machine_codec.erl` в `ff_machine_lib.erl` | Хочется один FF codec entry point | -| **B** | Machines зовут `ff_machine_codec` напрямую; lib теряет passthrough `marshal_*`/`unmarshal_*` | Хочется убрать lib hop, codec остаётся отдельным модулем | -| **C (рекомендуется при сомнении)** | Отложить | → [refactor-architecture.md §5](refactor-architecture.md#5-ff-codec-chain) | - ---- - -### D3. `ff_machine_trace` - -**Файлы:** [`apps/ff_transfer/src/ff_machine_trace.erl`](../apps/ff_transfer/src/ff_machine_trace.erl) — **1 caller** [`ff_machine_handler.erl`](../apps/ff_server/src/ff_machine_handler.erl). - -| Вариант | Действие | -|---------|----------| -| **A (рекомендуется)** | Перенести модуль в `apps/ff_server/src/` (debug HTTP рядом с handler) | -| **B** | Оставить в `ff_transfer` | - -**Замечание:** trace дублирует `decode_term/1` из `prg_machine` — при переносе рассмотреть экспорт decode helper из `prg_machine` (см. architecture §5). - ---- - -## Блок E: проверка - -После каждого блока: - -```bash -rebar3 compile -rebar3 eunit --module=prg_action,ff_withdrawal_codec # если трогали action/codec -``` - -При изменении HG invoice / client: - -```bash -# smoke — конкретный suite по изменённой области -rebar3 ct --suite=apps/hellgate/test/hg_invoice_tests_SUITE -``` - ---- - -## Что сознательно не включено - -- Массовый перенос FF domain logic — вне scope -- Изменения progressor dependency / wire `action()` contract -- Архитектурные задачи (env pipeline, deadline ×2, legacy в core) — [refactor-architecture.md](refactor-architecture.md) From 2daf021e92f6bb2ce71fbabb1de6853560bcf39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 18 Jun 2026 01:39:26 +0300 Subject: [PATCH 57/62] Refactor context binding handling across multiple modules by removing unnecessary context_binding options from processor configurations. Introduce new functions in op_context for determining scope and binding based on namespaces. Update related tests and configurations to reflect these changes, enhancing clarity and maintainability. --- apps/ff_cth/src/ct_payment_system.erl | 15 ++--- apps/hellgate/test/hg_ct_helper.erl | 6 +- apps/op_context/src/op_context.erl | 16 +++++ apps/prg_machine/src/prg_machine.erl | 64 ++++++++----------- .../test/prg_machine_env_mock_context.erl | 4 +- .../test/prg_machine_env_mock_handler.erl | 7 +- config/sys.config | 42 ++---------- 7 files changed, 62 insertions(+), 92 deletions(-) diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index 65cacbe9..4191a6d4 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -252,8 +252,7 @@ progressor_namespaces() -> processor => #{ client => prg_machine, options => #{ - ns => 'ff/source_v1', - context_binding => op_context:binding(fistful) + ns => 'ff/source_v1' } } }, @@ -261,8 +260,7 @@ progressor_namespaces() -> processor => #{ client => prg_machine, options => #{ - ns => 'ff/destination_v2', - context_binding => op_context:binding(fistful) + ns => 'ff/destination_v2' } } }, @@ -270,8 +268,7 @@ progressor_namespaces() -> processor => #{ client => prg_machine, options => #{ - ns => 'ff/deposit_v1', - context_binding => op_context:binding(fistful) + ns => 'ff/deposit_v1' } } }, @@ -279,8 +276,7 @@ progressor_namespaces() -> processor => #{ client => prg_machine, options => #{ - ns => 'ff/withdrawal_v2', - context_binding => op_context:binding(fistful) + ns => 'ff/withdrawal_v2' } } }, @@ -288,8 +284,7 @@ progressor_namespaces() -> processor => #{ client => prg_machine, options => #{ - ns => 'ff/withdrawal/session_v2', - context_binding => op_context:binding(fistful) + ns => 'ff/withdrawal/session_v2' } } } diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index 61d30cf4..9f182316 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -323,8 +323,7 @@ start_app(progressor = AppName) -> processor => #{ client => prg_machine, options => #{ - ns => invoice, - context_binding => op_context:binding(hellgate) + ns => invoice } }, worker_pool_size => 150 @@ -333,8 +332,7 @@ start_app(progressor = AppName) -> processor => #{ client => prg_machine, options => #{ - ns => invoice_template, - context_binding => op_context:binding(hellgate) + ns => invoice_template } } } diff --git a/apps/op_context/src/op_context.erl b/apps/op_context/src/op_context.erl index 63f54aeb..bef1b74c 100644 --- a/apps/op_context/src/op_context.erl +++ b/apps/op_context/src/op_context.erl @@ -9,6 +9,8 @@ -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]). @@ -111,6 +113,20 @@ binding(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( diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index ad393f1f..839377db 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -47,11 +47,8 @@ auxst => term() }. --type context_binding() :: op_context:binding(). - -type process_options() :: #{ ns := namespace(), - context_binding => context_binding(), default_handling_timeout => timeout() }. @@ -489,17 +486,13 @@ decode_rpc_context(<<>>) -> decode_rpc_context(Bin) -> woody_rpc_helper:decode_rpc_context(decode_term(Bin)). -run_scoped(Opts, WoodyCtx, Fun) when is_function(Fun, 0) -> - case maps:get(context_binding, Opts, undefined) of - Binding when is_map(Binding) -> - ok = op_context:env_enter(WoodyCtx, Binding), - try - Fun() - after - safe_env_leave(Binding) - end; - _ -> - Fun() +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) -> @@ -545,25 +538,16 @@ range_from_process(_) -> -include_lib("eunit/include/eunit.hrl"). -define(TEST_NS, env_test_ns). --define(TEST_REGISTRY_KEY, {p, l, prg_machine_env_test_context}). --define(TEST_BINDING, #{ - registry_key => ?TEST_REGISTRY_KEY, - cleanup_mode => lenient -}). +-define(TEST_FF_NS, 'ff/env_test_ns'). -define(TABLE, prg_machine_dispatch). -spec test() -> _. --spec process_without_context_binding_test_() -> _. -process_without_context_binding_test_() -> - {setup, fun setup_env_hook_test/0, fun cleanup_env_hook_test/1, [ - ?_test(process_without_context_binding()) - ]}. - -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()) + ?_test(context_binding_scopes_process(hellgate)), + ?_test(context_binding_scopes_process(fistful)) ]}. -spec aux_state_runtime_test_() -> _. @@ -588,18 +572,17 @@ process_exception_test_() -> ?_test(process_crash_conforms_progressor_exception()) ]}. --spec process_without_context_binding() -> _. -process_without_context_binding() -> - ok = ensure_woody_available(), - ?assertMatch({ok, _}, run_env_hook_process(#{ns => ?TEST_NS})). - --spec context_binding_scopes_process() -> _. -context_binding_scopes_process() -> +-spec context_binding_scopes_process(hellgate | fistful) -> _. +context_binding_scopes_process(Scope) -> ok = ensure_woody_available(), ok = prg_machine_env_mock_context:reset(), - Opts = #{ns => ?TEST_NS, context_binding => ?TEST_BINDING}, - ?assertMatch({ok, _}, run_env_hook_process(Opts)), - ?assertEqual([context_bound], prg_machine_env_mock_context:events()). + 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() -> @@ -613,14 +596,19 @@ setup_env_hook_test() -> {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}), + 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), - op_context:cleanup(?TEST_REGISTRY_KEY, lenient), + _ = 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. diff --git a/apps/prg_machine/test/prg_machine_env_mock_context.erl b/apps/prg_machine/test/prg_machine_env_mock_context.erl index beb62648..7b8d5fb7 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_context.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_context.erl @@ -7,11 +7,11 @@ reset() -> persistent_term:put({?MODULE, events}, []), ok. --spec events() -> [context_bound]. +-spec events() -> [{context_bound, op_context:scope()}]. events() -> persistent_term:get({?MODULE, events}, []). --spec record(context_bound) -> ok. +-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 index 7860ea11..2c301c5f 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_handler.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -21,10 +21,11 @@ namespace() -> env_test_ns. -spec init(prg_machine:args(), prg_machine:machine()) -> prg_machine:result(). -init(_Args, _Machine) -> +init(_Args, #{namespace := NS}) -> + Scope = op_context:scope_for_namespace(NS), try - _ = op_context:load({p, l, prg_machine_env_test_context}), - prg_machine_env_mock_context:record(context_bound) + _ = op_context:load(op_context:key(Scope)), + prg_machine_env_mock_context:record({context_bound, Scope}) catch _:_ -> ok diff --git a/config/sys.config b/config/sys.config index 82a83eea..03feb49a 100644 --- a/config/sys.config +++ b/config/sys.config @@ -363,11 +363,7 @@ processor => #{ client => prg_machine, options => #{ - ns => invoice, - context_binding => #{ - registry_key => {p, l, stored_hg_context}, - cleanup_mode => strict - } + ns => invoice } }, storage => #{ @@ -384,11 +380,7 @@ processor => #{ client => prg_machine, options => #{ - ns => invoice_template, - context_binding => #{ - registry_key => {p, l, stored_hg_context}, - cleanup_mode => strict - } + ns => invoice_template } }, worker_pool_size => 5 @@ -397,11 +389,7 @@ processor => #{ client => prg_machine, options => #{ - ns => 'ff/source_v1', - context_binding => #{ - registry_key => {p, l, {ff_context, stored_context}}, - cleanup_mode => lenient - } + ns => 'ff/source_v1' } } }, @@ -409,11 +397,7 @@ processor => #{ client => prg_machine, options => #{ - ns => 'ff/destination_v2', - context_binding => #{ - registry_key => {p, l, {ff_context, stored_context}}, - cleanup_mode => lenient - } + ns => 'ff/destination_v2' } } }, @@ -421,11 +405,7 @@ processor => #{ client => prg_machine, options => #{ - ns => 'ff/deposit_v1', - context_binding => #{ - registry_key => {p, l, {ff_context, stored_context}}, - cleanup_mode => lenient - } + ns => 'ff/deposit_v1' } } }, @@ -433,11 +413,7 @@ processor => #{ client => prg_machine, options => #{ - ns => 'ff/withdrawal_v2', - context_binding => #{ - registry_key => {p, l, {ff_context, stored_context}}, - cleanup_mode => lenient - } + ns => 'ff/withdrawal_v2' } } }, @@ -445,11 +421,7 @@ processor => #{ client => prg_machine, options => #{ - ns => 'ff/withdrawal/session_v2', - context_binding => #{ - registry_key => {p, l, {ff_context, stored_context}}, - cleanup_mode => lenient - } + ns => 'ff/withdrawal/session_v2' } } } From ff75246963f6992dab1c428fdc0bbb08dd7e3524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 18 Jun 2026 01:39:49 +0300 Subject: [PATCH 58/62] Remove the architectural refactoring documentation file, `refactor-architecture.md`, which contained extensive but outdated information on architectural changes and principles. This cleanup improves project organization and eliminates redundancy in documentation. --- docs/refactor-architecture.md | 356 ---------------------------------- 1 file changed, 356 deletions(-) delete mode 100644 docs/refactor-architecture.md diff --git a/docs/refactor-architecture.md b/docs/refactor-architecture.md deleted file mode 100644 index d66c8e19..00000000 --- a/docs/refactor-architecture.md +++ /dev/null @@ -1,356 +0,0 @@ -# Архитектурный рефакторинг `prg_machine` - -Крупные задачи с изменением контрактов, несколькими apps или migration policy. Понятные локальные правки — в [refactor-local.md](refactor-local.md). - -*Обновлено: 2026-06-17.* - ---- - -## Принцип - -| Критерий | Local file | Architecture file | -|----------|------------|-------------------| -| 1–2 файла, без смены контракта | ✓ | | -| Несколько apps / HTTP + worker | | ✓ | -| Migration / rollback policy | | ✓ | -| Нужен отдельный PR / итерация | | ✓ | - ---- - -## 1. Env-scoping pipeline - -**Приоритет:** HIGH - -### Проблема - -Woody-контекст проходит через gproc **дважды** на одном RPC: - -```mermaid -sequenceDiagram - participant WH as woody_wrapper - participant GP as gproc - participant PM as prg_machine - participant OC as op_context - - WH->>OC: save woody ctx - OC->>GP: registry_key bind - WH->>PM: call with encode_rpc_context - PM->>PM: decode_rpc_context - PM->>OC: env_enter binding - OC->>GP: bind again - PM->>PM: dispatch handler - PM->>OC: env_leave -``` - -### Файлы - -- [`apps/op_context/src/op_context.erl`](../apps/op_context/src/op_context.erl) — `save/load`, `env_enter/env_leave`, `current_woody_context/0` -- [`apps/prg_machine/src/prg_machine.erl`](../apps/prg_machine/src/prg_machine.erl) — `encode_rpc_context/0`, `run_scoped/3` -- [`apps/hg_proto/src/hg_woody_service_wrapper.erl`](../apps/hg_proto/src/hg_woody_service_wrapper.erl), [`apps/ff_server/src/ff_woody_wrapper.erl`](../apps/ff_server/src/ff_woody_wrapper.erl) -- [`config/sys.config`](../config/sys.config) — `context_binding` - -### Цель - -Один источник truth для woody/party ctx в progressor worker: либо передавать decoded ctx явно в handler, либо `env_enter` только там, где handler читает `op_context:load/1`. - -### Критерий готовности - -- Нет двойного gproc bind на одном RPC -- HG CT + FF handler CT проходят с party client в repair/call - -### Риски - -- Регрессии в FF lenient vs HG strict `cleanup_mode` -- `current_woody_context/0` перебирает HG, потом FF — неявная связность - -### Тесты - -- `prg_machine` env mock tests -- `hg_invoice_tests_SUITE`, FF handler suites - ---- - -## 2. Duplicate deadline - -**Приоритет:** MEDIUM - -### Проблема - -Default handling timeout (30s) выставляется **дважды**: - -1. HTTP-граница: `ensure_woody_deadline_set/2` в woody wrappers -2. Worker: `ensure_deadline_set/2` в `prg_machine:process/3` - -### Файлы - -- [`apps/prg_machine/src/prg_machine.erl`](../apps/prg_machine/src/prg_machine.erl) — `ensure_deadline_set/2`, opt `default_handling_timeout` -- [`apps/hg_proto/src/hg_woody_service_wrapper.erl`](../apps/hg_proto/src/hg_woody_service_wrapper.erl) -- [`apps/ff_server/src/ff_woody_wrapper.erl`](../apps/ff_server/src/ff_woody_wrapper.erl) - -### Цель - -Deadline только на HTTP-границе до `encode_rpc_context`; worker не переопределяет woody deadline без явной причины. - -### Критерий готовности - -- Единый helper или одна точка установки deadline -- CT с коротким/отсутствующим deadline не ломаются - -### Риски - -- Notify/async paths без woody wrapper - ---- - -## 3. `context_binding` DRY - -**Приоритет:** MEDIUM - -### Проблема - -Ключи gproc заданы в **двух местах**: - -- [`config/sys.config`](../config/sys.config): `registry_key => {p, l, stored_hg_context}` (HG), FF-аналог -- [`apps/op_context/src/op_context.erl`](../apps/op_context/src/op_context.erl): `key/1`, `binding/1` - -Расхождение сломает worker scoping незаметно. - -### Цель - -При старте HG/FF собирать progressor `options.context_binding` через `op_context:binding(hellgate | fistful)` — без hardcode literals в sys.config. - -### Файлы - -- `config/sys.config`, `test/*/sys.config` -- `apps/hellgate/src/hellgate.erl`, `apps/ff_server/src/ff_server.erl` — registry init - -### Критерий готовности - -- Один источник ключей gproc -- CT configs используют тот же механизм - ---- - -## 4. Legacy migration из core runtime - -**Приоритет:** MEDIUM - -### Проблема - -Политика совместимости с `hg_machine` / `ff_machine` зашита в permanent runtime: - -| Механизм | Где | Суть | -|----------|-----|------| -| `{bin, Bin}` unwrap | `prg_machine:decode_term/1` | Legacy double envelope call args | -| Dual metadata keys | `event_metadata/1` | `format` + `format_version` | -| `initial_model/2` | `prg_machine:collapse/2` | HG aux_state `#{model => ...}` | - -### Цель - -- Migration adapter или behaviour callback (`initial_model/1`, `decode_call_args/1`) -- Core runtime без HG/FF-specific rollback - -### Файлы - -- [`apps/prg_machine/src/prg_machine.erl`](../apps/prg_machine/src/prg_machine.erl) -- Handler codecs: [`hg_invoice.erl`](../apps/hellgate/src/hg_invoice.erl), [`ff_machine_codec.erl`](../apps/ff_transfer/src/ff_machine_codec.erl) - -### Критерий готовности - -- Новые events/calls не пишут legacy формы -- Чтение старых данных покрыто тестами migration adapter - -### Риски - -- Prod history с legacy envelope — нельзя удалить unwrap без migration window - ---- - -## 5. FF codec chain - -**Приоритет:** MEDIUM (зависит от [refactor-local.md §D2](refactor-local.md#d2-ff-codec-chain)) - -### Проблема - -```mermaid -flowchart LR - M["5x *_machine"] - L[ff_machine_lib] - C[ff_machine_codec] - DC["domain codec"] - M -->|"1 line delegate"| L - L -->|"passthrough"| C - C --> DC -``` - -- `ff_machine_codec` — **единственный** caller `ff_machine_lib` для marshal/unmarshal -- Behaviour callbacks в machines — 1 строка delegate - -### Варианты - -| ID | Стратегия | -|----|-----------| -| A | Merge `ff_machine_codec` → `ff_machine_lib` | -| B | Machines → `ff_machine_codec` напрямую, lib без passthrough | -| C | Status quo до отдельного PR | - -### Цель - -Убрать лишний hop без потери shared create/get/repair в lib. - -### Файлы - -- [`apps/ff_transfer/src/ff_machine_lib.erl`](../apps/ff_transfer/src/ff_machine_lib.erl) -- [`apps/ff_transfer/src/ff_machine_codec.erl`](../apps/ff_transfer/src/ff_machine_codec.erl) -- `apps/ff_transfer/src/ff_*_machine.erl` (5 modules) - -### Критерий готовности - -- Цепочка marshal event: machine → codec → domain (max 2 hops) -- Eunit codec tests green - ---- - -## 6. FF repair glue - -**Приоритет:** MEDIUM - -### Проблема - -Repair path: `*_machine:process_repair` → `ff_machine_lib:process_repair` → `ff_repair:apply_scenario`. - -- `ff_repair:apply_scenario/3,4` — **единственный** внешний caller `ff_machine_lib` -- Конвертация `prg_machine:machine()` ↔ `ff_repair:machine()` split между lib и repair - -### Цель - -`ff_repair` принимает `prg_machine:machine()` (или один converter module); machines зовут `ff_repair` без lib-glue. - -### Файлы - -- [`apps/fistful/src/ff_repair.erl`](../apps/fistful/src/ff_repair.erl) -- [`apps/ff_transfer/src/ff_machine_lib.erl`](../apps/ff_transfer/src/ff_machine_lib.erl) -- 5 `*_machine.erl` - -### Критерий готовности - -- `process_repair/2` в machine — ≤5 строк -- FF repair CT (`ff_withdrawal_session_repair_SUITE`, `force_status_change_test`) green - -### Связь с local - -- [refactor-local.md §C1](refactor-local.md#c1-ff_repairto_prg_machine1) — inline `to_prg_machine/1` (первый шаг) - ---- - -## 7. `prg_machine_registry` gen_server shell - -**Приоритет:** LOW - -### Проблема - -[`prg_machine_registry.erl`](../apps/prg_machine/src/prg_machine_registry.erl) — gen_server с пустыми `handle_call/cast/info`; `lookup/1` — прямой ETS; поле `state.handlers` не читается. - -### Цель - -ETS init в `prg_machine` application start / supervisor; registry list handlers один раз при старте HG/FF. - -### Callers `lookup/1` - -- `prg_machine.erl` (get/process) -- `ff_machine_trace.erl` (если не перенесён — см. local §D3) - -### Критерий готовности - -- Нет gen_server без state semantics -- Registry lookup behaviour unchanged - ---- - -## 8. `hg_context` facade - -**Приоритет:** LOW–MEDIUM - -### Проблема - -После удаления [`hg_context.erl`](../apps/hellgate/src/hg_context.erl) HG модули зовут `op_context:key(hellgate)` напрямую. Блок **идентичный** `get_party_client/0` скопирован в: - -- [`hg_party.erl`](../apps/hellgate/src/hg_party.erl) -- [`hg_payment_institution.erl`](../apps/hellgate/src/hg_payment_institution.erl) -- [`hg_invoice_payment.erl`](../apps/hellgate/src/hg_invoice_payment.erl) - -### Цель - -Тонкий `hg_context` facade: - -```erlang -load/0, cleanup/0, get_party_client/0 -``` - -поверх `op_context` — **не** дублировать runtime scoping из §1. - -### Критерий готовности - -- Один `get_party_client/0` в HG -- `op_context` остаётся shared для FF + `prg_machine` env binding - ---- - -## 9. FF `init_result` / `to_prg_result` унификация - -**Приоритет:** LOW - -### Проблема - -Несогласованный стиль в 5 `*_machine`: - -- source/destination/session: `ff_machine_lib:init_result/3` -- withdrawal/deposit: inline `#{events, action, auxst}` в `init/2` -- `to_prg_result/1` — trivial map, 8 callers - -### Цель - -Единый стиль init/process_result (все через lib **или** все inline). - -### Связь - -Не блокирует §5–§6; косметика после codec/repair glue. - ---- - -## Порядок итераций - -```mermaid -flowchart TD - L[refactor-local A-C] - D[refactor-local D choices] - E1["§1 env pipeline"] - E2["§2 deadline"] - E3["§3 context_binding"] - E4["§4 legacy core"] - E5["§5-6 FF glue"] - E7["§7-9 polish"] - - L --> D - D --> E5 - E1 --> E2 - E2 --> E3 - E3 --> E4 - E5 --> E7 -``` - -**Рекомендация:** - -1. Закрыть [refactor-local.md](refactor-local.md) блоки A–C -2. Решить развилки D → отдельные PR -3. §1 env — отдельная большая итерация (HG+FF) -4. §5–§6 FF — после выбора D2 -5. §7–§9 — по желанию - ---- - -## Что не включаем - -- Изменения progressor upstream / wire `action()` contract -- Массовый рефактор FF domain (`ff_withdrawal`, routing, limiter) -- Routing app split (`hg_route_collector` и т.д.) — отдельная история ветки From 6e60db19fe41f2cf4329854f4fa110fed902b9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 22 Jun 2026 19:17:45 +0300 Subject: [PATCH 59/62] Refactor event body marshaling across multiple modules to include timestamp as a parameter. Update specifications in ff_deposit_machine, ff_destination_machine, ff_source_machine, ff_withdrawal_machine, and ff_withdrawal_session_machine for consistency. This change enhances event processing and prepares for future improvements. --- apps/ff_core/src/ff_machine_schema.erl | 69 ------------ apps/ff_core/src/ff_msgpack.erl | 106 ------------------ apps/ff_transfer/src/ff_deposit_machine.erl | 8 +- .../src/ff_destination_machine.erl | 8 +- apps/ff_transfer/src/ff_machine_codec.erl | 7 +- apps/ff_transfer/src/ff_machine_lib.erl | 10 +- apps/ff_transfer/src/ff_source_machine.erl | 8 +- .../ff_transfer/src/ff_withdrawal_machine.erl | 18 ++- .../ff_transfer/src/ff_withdrawal_session.erl | 6 + .../src/ff_withdrawal_session_machine.erl | 8 +- apps/hellgate/src/hg_invoice.erl | 10 +- apps/hellgate/src/hg_invoice_template.erl | 6 +- apps/prg_machine/src/prg_action.erl | 2 + apps/prg_machine/src/prg_machine.erl | 80 ++++++++----- .../prg_machine_aux_state_test_handler.erl | 6 +- .../test/prg_machine_env_mock_handler.erl | 6 +- 16 files changed, 111 insertions(+), 247 deletions(-) delete mode 100644 apps/ff_core/src/ff_machine_schema.erl delete mode 100644 apps/ff_core/src/ff_msgpack.erl diff --git a/apps/ff_core/src/ff_machine_schema.erl b/apps/ff_core/src/ff_machine_schema.erl deleted file mode 100644 index bb526532..00000000 --- a/apps/ff_core/src/ff_machine_schema.erl +++ /dev/null @@ -1,69 +0,0 @@ -%%% -%%% Storage schema for arbitrary persistent Erlang terms (aux_state wire format). - --module(ff_machine_schema). - --import(ff_msgpack, [ - nil/0, - wrap/1, - unwrap/1 -]). - --export([marshal/1]). --export([unmarshal/1]). - --type eterm() :: - atom() - | number() - | tuple() - | binary() - | list() - | map(). - --spec marshal(eterm()) -> ff_msgpack:t(). -marshal(undefined) -> - nil(); -marshal(V) when is_boolean(V) -> - wrap(V); -marshal(V) when is_atom(V) -> - wrap(atom_to_binary(V, utf8)); -marshal(V) when is_number(V) -> - wrap(V); -marshal(V) when is_binary(V) -> - wrap({binary, V}); -marshal([]) -> - wrap([]); -marshal(V) when is_list(V) -> - wrap([marshal(lst) | lists:map(fun marshal/1, V)]); -marshal(V) when is_tuple(V) -> - wrap([marshal(tup) | lists:map(fun marshal/1, tuple_to_list(V))]); -marshal(V) when is_map(V) -> - wrap([marshal(map), wrap(genlib_map:truemap(fun(Ke, Ve) -> {marshal(Ke), marshal(Ve)} end, V))]); -marshal(V) -> - erlang:error(badarg, [V]). - --spec unmarshal(ff_msgpack:t()) -> eterm(). -unmarshal(M) -> - unmarshal_v(unwrap(M)). - -unmarshal_v(nil) -> - undefined; -unmarshal_v(V) when is_boolean(V) -> - V; -unmarshal_v(V) when is_binary(V) -> - binary_to_existing_atom(V, utf8); -unmarshal_v(V) when is_number(V) -> - V; -unmarshal_v({binary, V}) -> - V; -unmarshal_v([]) -> - []; -unmarshal_v([Ty | Vs]) -> - unmarshal_v(unmarshal(Ty), Vs). - -unmarshal_v(lst, Vs) -> - lists:map(fun unmarshal/1, Vs); -unmarshal_v(tup, Es) -> - list_to_tuple(unmarshal_v(lst, Es)); -unmarshal_v(map, [V]) -> - genlib_map:truemap(fun(Ke, Ve) -> {unmarshal(Ke), unmarshal(Ve)} end, unwrap(V)). diff --git a/apps/ff_core/src/ff_msgpack.erl b/apps/ff_core/src/ff_msgpack.erl deleted file mode 100644 index 51048907..00000000 --- a/apps/ff_core/src/ff_msgpack.erl +++ /dev/null @@ -1,106 +0,0 @@ -%%% -%%% Msgpack wire encoding for machine aux-state and legacy machinegun payloads. - --module(ff_msgpack). - --include_lib("mg_proto/include/mg_proto_msgpack_thrift.hrl"). - --export([wrap/1]). --export([unwrap/1]). --export([nil/0]). --export([pack/1]). --export([unpack/1]). - --type t() :: mg_proto_msgpack_thrift:'Value'(). - --export_type([t/0]). - --spec wrap - (nil) -> t(); - (boolean()) -> t(); - (integer()) -> t(); - (float()) -> t(); - (binary()) -> t(); - ({binary, binary()}) -> t(); - ([t()]) -> t(); - (#{t() => t()}) -> t(). -wrap(nil) -> - {nl, #mg_msgpack_Nil{}}; -wrap(V) when is_boolean(V) -> - {b, V}; -wrap(V) when is_integer(V) -> - {i, V}; -wrap(V) when is_float(V) -> - V; -wrap(V) when is_binary(V) -> - {str, V}; -wrap({binary, V}) when is_binary(V) -> - {bin, V}; -wrap(V) when is_list(V) -> - {arr, V}; -wrap(V) when is_map(V) -> - {obj, V}. - --spec unwrap(t()) -> - nil - | boolean() - | integer() - | float() - | binary() - | {binary, binary()} - | [t()] - | #{t() => t()}. -unwrap({nl, #mg_msgpack_Nil{}}) -> - nil; -unwrap({b, V}) when is_boolean(V) -> - V; -unwrap({i, V}) when is_integer(V) -> - V; -unwrap({flt, V}) when is_float(V) -> - V; -unwrap({str, V}) when is_binary(V) -> - V; -unwrap({bin, V}) when is_binary(V) -> - {binary, V}; -unwrap({arr, V}) when is_list(V) -> - V; -unwrap({obj, V}) when is_map(V) -> - V. - --spec nil() -> t(). -nil() -> - wrap(nil). - --spec pack(t()) -> {ok, binary()}. -pack(Value) -> - Type = value_type(), - {ok, serialize(Type, Value)}. - --spec unpack(binary()) -> {ok, t()}. -unpack(Bin) when is_binary(Bin) -> - Type = value_type(), - {ok, deserialize(Type, Bin)}. - -value_type() -> - {struct, union, {mg_proto_msgpack_thrift, 'Value'}}. - -serialize(Type, Data) -> - {ok, Trans} = thrift_membuffer_transport:new(), - {ok, Proto} = thrift_binary_protocol:new(Trans, [{strict_read, true}, {strict_write, true}]), - case thrift_protocol:write(Proto, {Type, Data}) of - {NewProto, ok} -> - {_, {ok, Result}} = thrift_protocol:close_transport(NewProto), - Result; - {_NewProto, {error, Reason}} -> - erlang:error({thrift, {protocol, Reason}}) - end. - -deserialize(Type, Data) -> - {ok, Trans} = thrift_membuffer_transport:new(Data), - {ok, Proto} = thrift_binary_protocol:new(Trans, [{strict_read, true}, {strict_write, true}]), - case thrift_protocol:read(Proto, Type) of - {_NewProto, {ok, Result}} -> - Result; - {_NewProto, {error, Reason}} -> - erlang:error({thrift, {protocol, Reason}}) - end. diff --git a/apps/ff_transfer/src/ff_deposit_machine.erl b/apps/ff_transfer/src/ff_deposit_machine.erl index ac08f4d1..6fadf27f 100644 --- a/apps/ff_transfer/src/ff_deposit_machine.erl +++ b/apps/ff_transfer/src/ff_deposit_machine.erl @@ -68,7 +68,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([process_notification/2]). --export([marshal_event_body/1]). +-export([marshal_event_body/2]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -163,9 +163,9 @@ process_notification(_Args, _Machine) -> apply_event(_EventID, _Ts, Body, Model) -> ff_deposit:apply_event(Body, Model). --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - ff_machine_lib:marshal_event_body(deposit, ?EVENT_FORMAT_VERSION, Body). +-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) -> diff --git a/apps/ff_transfer/src/ff_destination_machine.erl b/apps/ff_transfer/src/ff_destination_machine.erl index 9b95795e..f824f22a 100644 --- a/apps/ff_transfer/src/ff_destination_machine.erl +++ b/apps/ff_transfer/src/ff_destination_machine.erl @@ -57,7 +57,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([process_notification/2]). --export([marshal_event_body/1]). +-export([marshal_event_body/2]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -139,9 +139,9 @@ process_notification(_Args, _Machine) -> apply_event(_EventID, _Ts, Body, Model) -> ff_destination:apply_event(Body, Model). --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - ff_machine_lib:marshal_event_body(destination, ?EVENT_FORMAT_VERSION, Body). +-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) -> diff --git a/apps/ff_transfer/src/ff_machine_codec.erl b/apps/ff_transfer/src/ff_machine_codec.erl index 87277992..4d4476d8 100644 --- a/apps/ff_transfer/src/ff_machine_codec.erl +++ b/apps/ff_transfer/src/ff_machine_codec.erl @@ -11,8 +11,9 @@ -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()) -> ff_msgpack:t(). +-spec marshal_event(domain(), format_version(), timestamped_event()) -> event_payload(). marshal_event(deposit, 1, Timestamped) -> marshal_thrift_event( Timestamped, @@ -101,7 +102,7 @@ unmarshal_aux_state(Payload) when is_binary(Payload) -> %% Event payload: write the legacy envelope term_to_binary({bin, ThriftBin}) %% (machinery_prg_backend used machinery_utils:encode(term, ...)). --spec payload_to_binary(ff_msgpack:t()) -> binary(). +-spec payload_to_binary(event_payload()) -> binary(). payload_to_binary(Payload) -> term_to_binary(Payload). @@ -110,7 +111,7 @@ payload_to_binary(Payload) -> fun((timestamped_event()) -> term()), atom(), atom() -) -> ff_msgpack:t(). +) -> event_payload(). marshal_thrift_event(Timestamped, MarshalFun, ThriftModule, ThriftStruct) -> ThriftChange = MarshalFun(Timestamped), Type = {struct, struct, {ThriftModule, ThriftStruct}}, diff --git a/apps/ff_transfer/src/ff_machine_lib.erl b/apps/ff_transfer/src/ff_machine_lib.erl index 8e793126..78417046 100644 --- a/apps/ff_transfer/src/ff_machine_lib.erl +++ b/apps/ff_transfer/src/ff_machine_lib.erl @@ -20,6 +20,7 @@ -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]). @@ -181,7 +182,14 @@ codec_timestamp(DateTime) -> -spec marshal_event_body(ff_machine_codec:domain(), pos_integer(), prg_machine:event_body()) -> {pos_integer(), binary()}. marshal_event_body(Domain, Format, Body) -> - Timestamped = {ev, prg_machine:timestamp(), 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)}. diff --git a/apps/ff_transfer/src/ff_source_machine.erl b/apps/ff_transfer/src/ff_source_machine.erl index 601e2427..6b8107d1 100644 --- a/apps/ff_transfer/src/ff_source_machine.erl +++ b/apps/ff_transfer/src/ff_source_machine.erl @@ -57,7 +57,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([process_notification/2]). --export([marshal_event_body/1]). +-export([marshal_event_body/2]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -139,9 +139,9 @@ process_notification(_Args, _Machine) -> apply_event(_EventID, _Ts, Body, Model) -> ff_source:apply_event(Body, Model). --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - ff_machine_lib:marshal_event_body(source, ?EVENT_FORMAT_VERSION, Body). +-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) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index 3865bb13..ef069a72 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -44,6 +44,13 @@ | 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(). @@ -86,7 +93,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([process_notification/2]). --export([marshal_event_body/1]). +-export([marshal_event_body/2]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -137,8 +144,7 @@ repair(ID, Scenario) -> start_adjustment(WithdrawalID, Params) -> call(WithdrawalID, {start_adjustment, Params}). --spec notify(id(), notify_args()) -> - ok | {error, notfound | failed} | no_return(). +-spec notify(id(), notify_args()) -> ok | {error, notify_error()} | no_return(). notify(ID, Args) -> prg_machine:notify(?NS, ID, Args). @@ -207,9 +213,9 @@ process_notification({session_finished, SessionID, SessionResult}, Machine) -> apply_event(_EventID, _Ts, Body, Model) -> ff_withdrawal:apply_event(Body, Model). --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - ff_machine_lib:marshal_event_body(withdrawal, ?EVENT_FORMAT_VERSION, Body). +-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). -spec unmarshal_event_body(binary()) -> prg_machine:event_body(). unmarshal_event_body(Payload) -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index c2ff25b1..035e4380 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -215,6 +215,12 @@ process_session(#{status := {finished, _}, id := ID, result := Result, withdrawa [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; diff --git a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl index 1f3b3182..d81ad28b 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session_machine.erl @@ -29,7 +29,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([process_notification/2]). --export([marshal_event_body/1]). +-export([marshal_event_body/2]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -186,9 +186,9 @@ process_notification(_Args, _Machine) -> apply_event(_EventID, _Ts, Body, Model) -> ff_withdrawal_session:apply_event(Body, Model). --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Body) -> - ff_machine_lib:marshal_event_body(withdrawal_session, ?EVENT_FORMAT_VERSION, Body). +-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) -> diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index db66ac68..0ef6f3fc 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -55,7 +55,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([process_notification/2]). --export([marshal_event_body/1]). +-export([marshal_event_body/2]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -245,8 +245,6 @@ process_callback(Tag, Callback) -> ok; {ok, {exception, invalid_callback}} -> {error, invalid_callback}; - {ok, {error, invalid_callback}} -> - {error, invalid_callback}; {error, _} = Error -> Error end @@ -263,7 +261,7 @@ process_session_change_by_tag(Tag, SessionChange) -> ok; {ok, {exception, invalid_callback}} -> {error, notfound}; - {ok, {error, _}} -> + {ok, {exception, _}} -> {error, failed}; {error, _} = Error -> Error @@ -1059,8 +1057,8 @@ apply_event_changes(Changes, St0, Dt) -> end, collapse_changes(Changes, St, #{timestamp => Dt}). --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Changes) when is_list(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_changes, Changes}), Msgp = mg_msgpack_marshalling:marshal(Data), {?EVENT_FORMAT_VERSION, msgpack_payload_to_binary(Msgp)}. diff --git a/apps/hellgate/src/hg_invoice_template.erl b/apps/hellgate/src/hg_invoice_template.erl index 5848aa13..da688eee 100644 --- a/apps/hellgate/src/hg_invoice_template.erl +++ b/apps/hellgate/src/hg_invoice_template.erl @@ -25,7 +25,7 @@ -export([process_call/2]). -export([process_repair/2]). -export([process_notification/2]). --export([marshal_event_body/1]). +-export([marshal_event_body/2]). -export([unmarshal_event_body/1]). -export([marshal_aux_state/1]). -export([unmarshal_aux_state/1]). @@ -348,8 +348,8 @@ marshal_invoice_template_params(Params) -> Type = {struct, struct, {dmsl_payproc_thrift, 'InvoiceTemplateCreateParams'}}, hg_proto_utils:serialize(Type, Params). --spec marshal_event_body(prg_machine:event_body()) -> {pos_integer(), binary()}. -marshal_event_body(Changes) when is_list(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)}. diff --git a/apps/prg_machine/src/prg_action.erl b/apps/prg_machine/src/prg_action.erl index 6708f739..bf44d151 100644 --- a/apps/prg_machine/src/prg_action.erl +++ b/apps/prg_machine/src/prg_action.erl @@ -26,6 +26,8 @@ schedule_deadline(Deadline) -> -spec marshal_timer(timer()) -> timestamp_us(). marshal_timer({timeout, 0}) -> erlang:system_time(microsecond); +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) -> diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index 839377db..ecff5817 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -22,13 +22,16 @@ -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()}}. @@ -87,7 +90,7 @@ -callback process_notification(args(), machine()) -> result(). --callback marshal_event_body(event_body()) -> {undefined | pos_integer(), binary()}. +-callback marshal_event_body(timestamp(), event_body()) -> {undefined | pos_integer(), binary()}. -callback unmarshal_event_body(binary()) -> event_body(). @@ -135,7 +138,8 @@ handler_namespace(Handler) -> %% Client API --spec start(namespace(), id(), args()) -> {ok, ok} | {error, exists | term()}. +-spec start(namespace(), id(), args()) -> + {ok, ok} | {error, exists | timeout | {unknown_namespace, namespace()} | term()}. start(NS, ID, Args) -> Req = #{ ns => NS, @@ -148,16 +152,17 @@ start(NS, ID, Args) -> Ok; {error, <<"process already exists">>} -> {error, exists}; - {error, _} = Error -> - Error + Error -> + map_client_error(NS, Error) end. --spec call(namespace(), id(), call()) -> {ok, response()} | {error, notfound | failed | term()}. +-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 | term()}. + {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 @@ -173,8 +178,8 @@ call(NS, ID, CallArgs, After, Limit, Direction) -> {error, Exception}; {error, {exception, Class, Reason, _Stacktrace}} -> {error, {exception, Class, Reason}}; - {error, _} = Error -> - Error + Error -> + map_client_error(NS, Error) end. -spec repair(namespace(), id(), args()) -> @@ -201,6 +206,10 @@ repair(NS, ID, Args) -> {error, Exception}; {error, {exception, Class, Reason, _Stacktrace}} -> {error, {exception, Class, Reason}}; + {error, <<"namespace not found">>} -> + {error, {unknown_namespace, NS}}; + {error, <<"timeout">>} -> + {error, timeout}; {error, Reason} -> %% The repair-failed reason is our own term encoded by process/3 %% (marshal_process_result -> encode_term); hand it back as a term. @@ -213,21 +222,23 @@ get(NS, ID) -> -spec get(namespace(), id(), history_range()) -> {ok, machine()} | {error, get_error()}. get(NS, ID, Range) -> - Req = request(NS, ID, undefined, Range), - case progressor:get(Req) of - {ok, Process} -> - case prg_machine_registry:lookup(NS) of - {ok, Handler} -> + 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, _} = Error -> - Error + {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, <<"process not found">>} -> - {error, notfound}; - {error, {exception, _Class, _Reason} = Exception} -> - {error, Exception}; - {error, {exception, Class, Reason, _Stacktrace}} -> - {error, {exception, Class, Reason}} + {error, _} = Error -> + Error end. -spec get_history(namespace(), id()) -> {ok, history()} | {error, get_error()}. @@ -250,7 +261,7 @@ get_history(NS, ID, After, Limit, Direction) -> end. -spec notify(namespace(), id(), args()) -> - ok | {error, notfound | failed | processor_error() | term()}. + ok | {error, notfound | failed | timeout | {unknown_namespace, namespace()} | processor_error() | term()}. notify(NS, ID, Args) -> case call(NS, ID, {notify, Args}) of {ok, _} -> ok; @@ -398,24 +409,24 @@ unmarshal_machine(Handler, NS, #{process_id := ID, history := RawHistory} = Proc unmarshal_event(Handler, #{ event_id := EventID, - timestamp := TsSec, + timestamp := TsMicroSec, payload := Payload }) -> Body = unmarshal_event_body(Handler, Payload), - {EventID, event_timestamp_to_datetime(TsSec), Body}; + {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 microsecond timestamp for the whole batch (as the old emit_events did). - %% The PG backend stores timestamptz with microseconds and auto-detects units. - Ts = erlang:system_time(microsecond), + %% 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, Body), + {Format, Bin} = marshal_event_body(Handler, BatchTs, Body), #{ event_id => EventID, - timestamp => Ts, + timestamp => StorageTs, metadata => event_metadata(Format), payload => Bin } @@ -424,8 +435,8 @@ marshal_new_events(Handler, LastEventID, Bodies) -> Bodies ). -marshal_event_body(Handler, Body) -> - Handler:marshal_event_body(Body). +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) -> @@ -534,6 +545,13 @@ range_from_process(#{range := Range = #{}}) -> range_from_process(_) -> #{direction => forward}. +map_client_error(NS, {error, <<"namespace not found">>}) -> + {error, {unknown_namespace, NS}}; +map_client_error(_NS, {error, <<"timeout">>}) -> + {error, timeout}; +map_client_error(_NS, Error) -> + Error. + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). 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 index 5958d1d6..b2789eb5 100644 --- a/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl +++ b/apps/prg_machine/test/prg_machine_aux_state_test_handler.erl @@ -11,7 +11,7 @@ process_call/2, process_repair/2, process_notification/2, - marshal_event_body/1, + marshal_event_body/2, unmarshal_event_body/1, marshal_aux_state/1, unmarshal_aux_state/1, @@ -55,8 +55,8 @@ process_repair(_Args, _Machine) -> process_notification(_Args, _Machine) -> #{}. --spec marshal_event_body(prg_machine:event_body()) -> {undefined, binary()}. -marshal_event_body(Body) -> +-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(). diff --git a/apps/prg_machine/test/prg_machine_env_mock_handler.erl b/apps/prg_machine/test/prg_machine_env_mock_handler.erl index 2c301c5f..f67c338f 100644 --- a/apps/prg_machine/test/prg_machine_env_mock_handler.erl +++ b/apps/prg_machine/test/prg_machine_env_mock_handler.erl @@ -9,7 +9,7 @@ process_call/2, process_repair/2, process_notification/2, - marshal_event_body/1, + marshal_event_body/2, unmarshal_event_body/1, marshal_aux_state/1, unmarshal_aux_state/1, @@ -48,8 +48,8 @@ process_repair(_Args, _Machine) -> process_notification(_Args, _Machine) -> #{}. --spec marshal_event_body(prg_machine:event_body()) -> {undefined, binary()}. -marshal_event_body(Body) -> +-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(). From 2e33755add3830cb35c2f10022c83953b1d72062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Mon, 22 Jun 2026 20:11:48 +0300 Subject: [PATCH 60/62] Update error handling in ff_withdrawal_session_repair_SUITE to set error to null and change task status to 'finished'. This improves clarity in session repair tests and aligns with expected outcomes. --- apps/ff_server/test/ff_withdrawal_session_repair_SUITE.erl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 0d082838..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), From 447d0185848f8ec149709ef0adeae63440d51e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 23 Jun 2026 17:05:30 +0300 Subject: [PATCH 61/62] fixed --- apps/prg_machine/src/prg_action.erl | 6 ++---- apps/prg_machine/src/prg_machine.erl | 21 ++++++++++----------- rebar.config | 2 +- rebar.lock | 2 +- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/prg_machine/src/prg_action.erl b/apps/prg_machine/src/prg_action.erl index bf44d151..720f77f0 100644 --- a/apps/prg_machine/src/prg_action.erl +++ b/apps/prg_machine/src/prg_action.erl @@ -24,11 +24,9 @@ schedule_deadline(Deadline) -> {schedule, #{at => marshal_timer({deadline, Deadline}), action => timeout}}. -spec marshal_timer(timer()) -> timestamp_us(). -marshal_timer({timeout, 0}) -> +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); -marshal_timer({timeout, Seconds}) when is_integer(Seconds), Seconds >= 0 -> +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); diff --git a/apps/prg_machine/src/prg_machine.erl b/apps/prg_machine/src/prg_machine.erl index ecff5817..7a6137c3 100644 --- a/apps/prg_machine/src/prg_machine.erl +++ b/apps/prg_machine/src/prg_machine.erl @@ -206,14 +206,8 @@ repair(NS, ID, Args) -> {error, Exception}; {error, {exception, Class, Reason, _Stacktrace}} -> {error, {exception, Class, Reason}}; - {error, <<"namespace not found">>} -> - {error, {unknown_namespace, NS}}; - {error, <<"timeout">>} -> - {error, timeout}; - {error, Reason} -> - %% The repair-failed reason is our own term encoded by process/3 - %% (marshal_process_result -> encode_term); hand it back as a term. - {error, {repair, {failed, decode_term(Reason)}}} + Error -> + map_client_error(repair, NS, Error) end. -spec get(namespace(), id()) -> {ok, machine()} | {error, get_error()}. @@ -545,11 +539,16 @@ range_from_process(#{range := Range = #{}}) -> range_from_process(_) -> #{direction => forward}. -map_client_error(NS, {error, <<"namespace not found">>}) -> +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(_NS, {error, <<"timeout">>}) -> +map_client_error(_Type, _NS, {error, <<"timeout">>}) -> {error, timeout}; -map_client_error(_NS, Error) -> +map_client_error(repair, _NS, {error, Reason}) -> + {error, {repair, {failed, decode_term(Reason)}}}; +map_client_error(_Type, _NS, Error) -> Error. -ifdef(TEST). diff --git a/rebar.config b/rebar.config index 38e6a10d..31976ed1 100644 --- a/rebar.config +++ b/rebar.config @@ -46,7 +46,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", {branch, "add_action_module"}}}, + {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"}}}, diff --git a/rebar.lock b/rebar.lock index 85b19b83..d15d1f76 100644 --- a/rebar.lock +++ b/rebar.lock @@ -117,7 +117,7 @@ 0}, {<<"progressor">>, {git,"https://github.com/valitydev/progressor.git", - {ref,"8f18b309279f0401283e8a18a0166825a8717980"}}, + {ref,"8624e58627e33633e6eb72c5b9defb208b06a169"}}, 0}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.11.0">>},0}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.9">>},0}, From 1e33ac00535099e45319ac5b2e22a395b5759584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 1 Jul 2026 14:39:26 +0300 Subject: [PATCH 62/62] fixed after merge --- apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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