You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@seata.apache.org by zf...@apache.org on 2025/12/18 06:47:12 UTC
(incubator-seata-go) branch master updated: Feature/saga (#1030)
This is an automated email from the ASF dual-hosted git repository.
zfeng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git
The following commit(s) were added to refs/heads/master by this push:
new c39fcc63 Feature/saga (#1030)
c39fcc63 is described below
commit c39fcc63f7eb0b19c8b2d98f5ec668066ae3a676
Author: FengZhang <zf...@qq.com>
AuthorDate: Thu Dec 18 14:47:07 2025 +0800
Feature/saga (#1030)
* feature: init saga framework (#635)
init saga framework
* refactor: refactor saga scaffold to break import cycles (#647)
* feature: add serverice task parse in statelang (#650)
* feature: add saga persistence layer (#649)
* feat: add grpc invoker in saga mode (#668)
* feat: add grpc invoker in saga mode
* fix: fix some chores
* feature: add default implementation for StateMachineConfig (#669)
* feature saga :support generate id by Snowflake (#670)
* feature: sequential execution of state machine in Saga (#681)
* feat: add func invoker (#744)
* add func task state
* [to-reply] feature: support saga multi type config (#741)
* added yaml format of statelang related test data
* feature: implement multi type config parser
* adjust statemachine_json_parser to suit config parser
* bugfix: fix the problem of failure to parse "ServiceName"
* adjust statemachine_parser to be compatible with yaml
* add apache License, add some comments, and make some logical adjustments
* update state_machine_new_designer.yaml
* optimize and improve the unit test of config parser
* refactor config parser
* add apache License
* update
* [Refactor] Migrate StateMachineObject to client/config and unify config parsing with koanf (#785)
* optimize saga config reuse
* upd-test
* Modularize SagaConfig into independent configuration structure
* feat: HttpServiceTaskState Support (#769)
* support http invoker for sage engine
* fix comment language
* Feature: Database persistence for seata-go Saga state machine (#794)
* implement state machine repository
* temporary recording
* temporary recording
* temporary storage
* Supplementary test
* feat: Supplement the statelog_repository section in Database persistence for Saga state machine (#800)
* fix: Wrong commit
* feat:add state_log_repository
* fix: conflicts and wrong commit
* fix: delete Remove redundant singleton patterns
* Feature/saga interface optimization (#778)
feature: saga Interface optimization
* refactor: implement pending TODOs in machine status_decision logic (#808)
* refactor: implement pending TODOs in machine status_decision logic
* feat(status-decision): add detailed logs to methods for better debugging
* Feat cel expression (#788)
* implement cel expression
* move sequence expression into a new file
* mv expression factory into a new file
* move expression factory manager into a new file
* implement cel expression factory and add test
* move expression resolver into a new file
* add parse expression struct test
* update
* update go.mod
---------
Co-authored-by: FengZhang <zf...@qq.com>
* change the StateLangStore type (#812)
* Revert "bugfix:Remove issue translation workflow as usthe/issues-translate-action@v2.7 does not allow issues in apache/incubator-seata-go" (#822)
Revert "bugfix:Remove issue translation workflow as usthe/issues-translate-ac…"
This reverts commit 018486951c9031caacf151c018f647e81b2f1c11.
* refactor-engine/core (#838)
* refactor-engine/core
* Decouple the core package
* Feature: finish implementation for StateMachineConfig (#805)
* finish implementation for StateMachineConfig
* reuse the exiting logic from statemachine_config_parser.go
* add_license&change_cn_to_en&fix_logic
* sort imports & rename struct & change the visbility
* Merge feature/saga into feature/saga-config-selection-and-loading
* fix_conflict_from_expression-pr
* fix_test&Lock-logic_from_invoker.go
* change_method_name
* add-seqGenerator_init
* add-event_bus&event_publiser-init
* add-status_desicion-init & enhence-validate_method
* refactor: move error_expression-about to expr
* fix: stateMachineResources-load
* fix - test
* Optimize the judgment of persistence-related components.
* Optimize the logic of loadconfig and statemachineresource.
* add-store-init
* refactor-engine/core
* Decouple the core package
* Update default_statemachine_config_test.go
---------
Co-authored-by: FengZhang <zf...@qq.com>
* Decouple the transaction from the statelog (#841)
* Decouple the transaction from the statelog
* Decouple the transaction from the statelog
* Decouple the transaction from the statelog
* Resolve Merge Conflicts, Add Persistence Initialization, and Optimize Tests (#847)
* fix-conflict&add-store-component-init&fix-test
* Update process_ctrl_statemachine_engine.go
* Feature/add script manager&actuator (#868)
* add script manager & actuator
* Otto package replaces Goja package
* Resolve the issues raised by Copilot
* add test for vm-pool
* update GetServiceInvoker method
* Implement SagaResource and SagaResourceManager (#855)
* feat : impl SagaResource #843
* feat : impl SagaResource #843
* feat : impl SagaResourceManager #843
* feat : impl SagaResourceManager BranchRollback #843
* feat : impl handler_saga #843
* feat : impl invalid_exception_test #843
* update #843
* update:elegant code #843
* abstracting tm branch registration out to rm and add test #846
* test #846
* add licensed #846
* replace panic to return #843
* update #843
* update init and handler_saga #843
---------
Co-authored-by: FengZhang <zf...@qq.com>
* Support Saga mode config initialization (#871)
* remove redundant logic and methods
Make the loadconfig method only responsible for loading static parameters and remove redundant memory storage
* add the init method and refine other logic
* remove redundant logic and methods
Make the loadconfig method only responsible for loading static parameters and remove redundant memory storage
* add the init method and refine other logic
* update
* upd
---------
Co-authored-by: FengZhang <zf...@qq.com>
* Saga end-to-end verification (#897)
* saga support
* fix bug
* merge code
* format
* fix vet
* fix vet
* fix vet and lincese checker
* fix lincese chekcer
---------
Co-authored-by: wt_better <m1...@163.com>
Co-authored-by: Xiangkun Yin <32...@users.noreply.github.com>
Co-authored-by: Jingliu Xiong <92...@qq.com>
Co-authored-by: FanOne <co...@outlook.com>
Co-authored-by: marsevilspirit <ma...@gmail.com>
Co-authored-by: FinnTew <na...@gmail.com>
Co-authored-by: flypiggy <15...@users.noreply.github.com>
Co-authored-by: lxfeng1997 <33...@users.noreply.github.com>
Co-authored-by: 1kasa <13...@users.noreply.github.com>
Co-authored-by: 深几许 <14...@users.noreply.github.com>
Co-authored-by: Wiggins <12...@users.noreply.github.com>
---
.github/workflows/issue-robot.yml | 33 +
.licenserc.yaml | 1 +
go.mod | 24 +-
go.sum | 35 +-
pkg/client/client.go | 4 +-
pkg/client/client_test.go | 9 +-
pkg/client/config.go | 5 +
pkg/datasource/sql/conn_xa.go | 1 +
.../sql/datasource/base/meta_cache_test.go | 1 +
pkg/datasource/sql/datasource/mysql/trigger.go | 10 +
.../sql/datasource/mysql/trigger_test.go | 1 +
pkg/datasource/sql/exec/at/base_executor_test.go | 4 +-
pkg/datasource/sql/exec/at/update_executor.go | 4 +-
.../sql/exec/at/update_join_executor_test.go | 3 +-
pkg/datasource/sql/undo/base/undo_test.go | 6 +-
.../sql/undo/builder/basic_undo_log_builder.go | 6 +-
.../undo/builder/basic_undo_log_builder_test.go | 6 +-
.../sql/undo/parser/branch_undo_log.pb.go | 5 +-
.../sql/undo/parser/parser_protobuf_test.go | 1 +
pkg/datasource/sql/util/ctxutil_test.go | 3 +-
pkg/datasource/sql/util/lockkey.go | 3 +-
pkg/datasource/sql/util/lockkey_test.go | 1 +
pkg/discovery/etcd3_test.go | 10 +-
pkg/discovery/mock/test_etcd_client.go | 2 +-
pkg/protocol/codec/codec.go | 1 +
pkg/protocol/codec/global_report_request_codec.go | 3 +-
pkg/remoting/config/config_test.go | 1 +
.../loadbalance/least_active_loadbalance_test.go | 1 +
pkg/remoting/loadbalance/loadbalance.go | 4 -
pkg/rm/init.go | 5 +
pkg/rm/tcc/fence/fence.go | 3 +-
.../tcc/fence/handler/tcc_fence_wrapper_handler.go | 6 +-
pkg/rm/tcc/fence/store/db/dao/tcc_fence_db.go | 3 +-
pkg/rm/tcc/tcc_resource.go | 3 +-
pkg/{rm/init.go => saga/config.go} | 20 +-
pkg/saga/readme.md | 47 +
pkg/{rm/init.go => saga/rm/handler_saga.go} | 21 +-
pkg/{rm/init.go => saga/rm/saga_resource.go} | 37 +-
pkg/saga/rm/saga_resource_manager.go | 159 +++
.../rm/saga_resource_manager_test.go} | 31 +-
.../rm/state_machine_engine_holder.go} | 23 +-
.../runtime/engine_registry.go} | 61 +-
pkg/saga/statemachine/constant/constant.go | 104 ++
pkg/saga/statemachine/engine/config/bootstrap.go | 62 +
.../engine/config/default_statemachine_config.go | 824 ++++++++++++
.../config/default_statemachine_config_test.go | 325 +++++
pkg/saga/statemachine/engine/config/noop_store.go | 87 ++
.../statemachine/engine/config/store_bootstrap.go | 71 +
.../engine/config/testdata/invalid.json | 3 +
.../engine/config/testdata/invalid.yaml | 19 +
.../engine/config/testdata/order_saga.json | 274 ++++
.../engine/config/testdata/order_saga.yaml | 204 +++
.../engine/config/testdata/order_saga_v2.json | 322 +++++
.../engine/config/testdata/order_saga_v2.yaml | 228 ++++
.../engine/config/testdata/saga_config.json | 13 +
.../engine/config/testdata/saga_config.yaml | 25 +
.../engine/config/testdata/saga_config_v2.json | 13 +
.../engine/config/testdata/saga_config_v2.yaml | 25 +
.../core/process_ctrl_statemachine_engine.go | 747 +++++++++++
.../process_ctrl_statemachine_engine_async_test.go | 345 +++++
.../statemachine/engine/exception/exception.go | 84 ++
.../engine/exception/exception_test.go | 68 +
.../engine/exception/forward_invalid_exception.go} | 20 +-
.../exception/forward_invalid_exception_test.go | 68 +
.../statemachine/engine/expr/cel_expression.go | 96 ++
.../engine/expr/cel_expression_factory.go} | 22 +-
.../engine/expr/cel_expression_factory_test.go} | 21 +-
.../engine/expr/cel_expression_test.go} | 28 +-
.../statemachine/engine/expr/error_expression.go} | 24 +-
.../statemachine/engine/expr/expression.go} | 20 +-
.../engine/expr/expression_factory.go} | 9 +-
.../engine/expr/expression_factory_manager.go | 51 +
.../engine/expr/expression_resolver.go | 105 ++
.../engine/expr/expression_resolver_test.go | 70 +
.../engine/expr/sequence_expression.go | 64 +
.../engine/expr/sequence_expression_factory.go | 56 +
.../statemachine/engine/invoker/func_invoker.go | 225 ++++
.../engine/invoker/func_invoker_test.go | 168 +++
.../statemachine/engine/invoker/grpc_invoker.go | 263 ++++
.../engine/invoker/grpc_invoker_test.go | 187 +++
.../statemachine/engine/invoker/http_invoker.go | 220 ++++
.../engine/invoker/http_invoker_test.go | 179 +++
pkg/saga/statemachine/engine/invoker/invoker.go | 101 ++
.../engine/invoker/javascript_script_invoker.go | 162 +++
.../invoker/javascript_script_invoker_test.go | 262 ++++
.../statemachine/engine/invoker/json_parser.go} | 23 +-
.../statemachine/engine/invoker/local_invoker.go | 200 +++
.../engine/invoker/local_invoker_test.go | 212 +++
.../engine/pcext/compensation_holder.go | 92 ++
pkg/saga/statemachine/engine/pcext/engine_utils.go | 236 ++++
pkg/saga/statemachine/engine/pcext/instruction.go | 113 ++
.../engine/pcext/loop_context_holder.go | 111 ++
.../statemachine/engine/pcext/loop_task_utils.go | 78 ++
.../statemachine/engine/pcext/parameter_utils.go | 143 ++
.../statemachine/engine/pcext/process_handler.go | 197 +++
.../statemachine/engine/pcext/process_router.go | 139 ++
.../statemachine/engine/pcext/state_router_impl.go | 355 +++++
.../engine/pcext/state_router_impl_test.go | 152 +++
.../engine/repo/repository/state_log_repository.go | 139 ++
.../repo/repository/state_machine_repository.go | 255 ++++
.../repository/state_machine_repository_test.go | 162 +++
.../statemachine/engine/repo/statemachine_store.go | 48 +
.../statemachine/engine/sequence/sequence.go} | 9 +-
pkg/saga/statemachine/engine/sequence/snowflake.go | 133 ++
.../engine/sequence/snowflake_test.go} | 33 +-
.../statemachine/engine/sequence/uuid.go} | 17 +-
.../statemachine/engine/serializer/serializer.go | 60 +
.../engine/serializer/serializer_test.go} | 25 +-
.../statemachine/engine/statemachine_config.go | 81 ++
.../statemachine/engine/statemachine_engine.go | 60 +
.../engine/statemachine_engine_test.go | 49 +
pkg/saga/statemachine/engine/strategy.go | 37 +
.../engine/strategy/status_decision.go | 274 ++++
.../engine/utils/process_context_utils.go | 113 ++
.../process_ctrl/bussiness_processor.go | 111 ++
.../process_ctrl/default_process_handler.go} | 9 +-
.../statemachine/process_ctrl/event.go} | 8 +-
pkg/saga/statemachine/process_ctrl/event_bus.go | 157 +++
.../statemachine/process_ctrl/event_consumer.go | 54 +
.../statemachine/process_ctrl/event_publisher.go} | 22 +-
.../handlers/service_task_state_handler.go | 266 ++++
.../statemachine/process_ctrl/instruction.go} | 8 +-
.../process_ctrl/process/process_type.go} | 11 +-
.../statemachine/process_ctrl/process_context.go | 225 ++++
.../process_ctrl/process_controller.go} | 57 +-
.../statemachine/process_ctrl/process_router.go | 112 ++
.../statelang/parser/choice_state_json_parser.go | 93 ++
.../parser/compensation_trigger_state_parser.go | 48 +
.../statelang/parser/end_state_parser.go | 83 ++
.../statelang/parser/statemachine_config_parser.go | 140 ++
.../parser/statemachine_config_parser_test.go | 884 +++++++++++++
.../statelang/parser/statemachine_json_parser.go | 131 ++
.../parser/statemachine_json_parser_test.go | 127 ++
.../statelang/parser/statemachine_parser.go | 262 ++++
.../statelang/parser/sub_state_machine_parser.go | 103 ++
.../statelang/parser/task_state_json_parser.go | 351 +++++
pkg/saga/statemachine/statelang/state.go | 92 ++
.../statemachine/statelang/state/choice_state.go | 92 ++
.../statelang/state/compensation_trigger_state.go} | 25 +-
pkg/saga/statemachine/statelang/state/end_state.go | 81 ++
.../statelang/state/loop_start_state.go} | 25 +-
.../statelang/state/sub_state_machine.go | 87 ++
.../statemachine/statelang/state/task_state.go | 478 +++++++
pkg/saga/statemachine/statelang/state_instance.go | 360 ++++++
pkg/saga/statemachine/statelang/statemachine.go | 278 ++++
.../statelang/statemachine_instance.go | 351 +++++
pkg/saga/statemachine/statemachine.go | 47 +
pkg/saga/statemachine/store/db/db.go | 121 ++
pkg/saga/statemachine/store/db/db_test.go | 64 +
pkg/saga/statemachine/store/db/statelang.go | 133 ++
pkg/saga/statemachine/store/db/statelang_test.go | 92 ++
pkg/saga/statemachine/store/db/statelog.go | 1360 ++++++++++++++++++++
pkg/saga/statemachine/store/db/statelog_test.go | 823 ++++++++++++
pkg/saga/statemachine/store/store.go | 57 +
pkg/saga/tm/default_saga_transactional_template.go | 175 +++
pkg/saga/tm/saga_transactional_template.go | 44 +
pkg/tm/context.go | 3 +-
pkg/tm/global_transaction.go | 25 +
pkg/tm/transaction_executor_test.go | 2 +-
pkg/{rm/init.go => tm/transaction_hook.go} | 22 +-
pkg/tm/transaction_hook_manager.go | 61 +
pkg/util/collection/collection.go | 44 +-
pkg/util/collection/collection_test.go | 23 +
pkg/util/errors/code.go | 15 +
pkg/util/reflectx/unmarkshaler.go | 165 +++
.../saga/engine/invoker/grpc/product.go | 61 +-
testdata/saga/engine/invoker/grpc/product.pb.go | 237 ++++
testdata/saga/engine/invoker/grpc/product.proto | 35 +
.../saga/engine/invoker/grpc/product_grpc.pb.go | 168 +++
.../statelang/simple_statelang_with_choice.json | 38 +
.../statelang/simple_statelang_with_choice.yaml | 41 +
testdata/saga/statelang/simple_statemachine.json | 138 ++
testdata/saga/statelang/simple_statemachine.yaml | 105 ++
.../saga/statelang/state_machine_new_designer.json | 424 ++++++
.../saga/statelang/state_machine_new_designer.yaml | 301 +++++
testdata/sql/saga/sqlite_init.sql | 81 ++
176 files changed, 20086 insertions(+), 367 deletions(-)
diff --git a/.github/workflows/issue-robot.yml b/.github/workflows/issue-robot.yml
new file mode 100644
index 00000000..3052ce99
--- /dev/null
+++ b/.github/workflows/issue-robot.yml
@@ -0,0 +1,33 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+name: 'issue translator'
+on:
+ issue_comment:
+ types: [created]
+ issues:
+ types: [opened]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: tomsun28/issues-translate-action@v2.6
+ with:
+ # it is not necessary to decide whether you need to modify the issue header content
+ IS_MODIFY_TITLE: false
+ CUSTOM_BOT_NOTE: RoBot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑🤝🧑👫🧑🏿🤝🧑🏻👩🏾🤝👨🏿👬🏿
diff --git a/.licenserc.yaml b/.licenserc.yaml
index c42dae30..ad64d6e1 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -61,6 +61,7 @@ header: # `header` section is configurations for source codes license header.
- 'DISCLAIMER'
- 'NOTICE'
- '.github'
+ - '**/*.json'
comment: on-failure # on what condition license-eye will comment on the pull request, `on-failure`, `always`, `never`.
language:
diff --git a/go.mod b/go.mod
index 3c55e2bc..8665db27 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,9 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-json v0.10.2
github.com/golang/mock v1.6.0
+ github.com/google/cel-go v0.18.0
github.com/google/uuid v1.3.0
+ github.com/mattn/go-sqlite3 v1.14.19
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/pierrec/lz4/v4 v4.1.17
github.com/pkg/errors v0.9.1
@@ -24,8 +26,10 @@ require (
github.com/stretchr/testify v1.8.3
go.uber.org/atomic v1.9.0
go.uber.org/zap v1.27.0
- google.golang.org/grpc v1.56.3
+ golang.org/x/sync v0.11.0
+ google.golang.org/grpc v1.57.0
gopkg.in/yaml.v2 v2.4.0
+ gopkg.in/yaml.v3 v3.0.1
vimagination.zapto.org/byteio v0.0.0-20200222190125-d27cba0f0b10
)
@@ -34,14 +38,18 @@ require (
github.com/golang/protobuf v1.5.3
go.etcd.io/etcd/api/v3 v3.5.6
go.etcd.io/etcd/client/v3 v3.5.6
- google.golang.org/protobuf v1.30.0
+ google.golang.org/protobuf v1.31.0
)
-require github.com/knadh/koanf v1.5.0
+require (
+ github.com/knadh/koanf v1.5.0
+ github.com/robertkrimen/otto v0.4.0
+)
require (
github.com/RoaringBitmap/roaring v1.2.0 // indirect
github.com/Workiva/go-datastructures v1.0.52 // indirect
+ github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/apache/dubbo-go-hessian2 v1.11.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.2.0 // indirect
@@ -78,10 +86,11 @@ require (
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
- github.com/prometheus/client_model v0.2.0 // indirect
+ github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
github.com/shirou/gopsutil/v3 v3.22.2 // indirect
+ github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
@@ -91,9 +100,12 @@ require (
go.etcd.io/etcd/client/pkg/v3 v3.5.6 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.3.0 // indirect
+ golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect
golang.org/x/text v0.14.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
+ gopkg.in/sourcemap.v1 v1.0.5 // indirect
)
require (
@@ -107,7 +119,7 @@ require (
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.18.0 // indirect
- google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+ google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e // indirect
vimagination.zapto.org/memio v0.0.0-20200222190306-588ebc67b97d // indirect
)
diff --git a/go.sum b/go.sum
index e76a9f09..aa97433e 100644
--- a/go.sum
+++ b/go.sum
@@ -65,6 +65,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
github.com/alibaba/sentinel-golang v1.0.4/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18=
+github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM=
github.com/apache/dubbo-getty v1.4.9-0.20221022181821-4dc6252ce98c/go.mod h1:6qmrqBSPGs3B35zwEuGhEYNVsx1nfGT/xzV2yOt2amM=
github.com/apache/dubbo-getty v1.5.0 h1:40RMjEoSfTuoG5EwKbGgfhjd7m6Zc7qBfOFgQv1jHCI=
github.com/apache/dubbo-getty v1.5.0/go.mod h1:V64WqLIxksEgNu5aBJBOxNIvpOZyfUJ7J/DXBlKSUoA=
@@ -314,6 +316,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/cel-go v0.18.0 h1:u74MPiEC8mejBrkXqrTWT102g5IFEUjxOngzQIijMzU=
+github.com/google/cel-go v0.18.0/go.mod h1:PVAybmSnWkNMUZR/tEWFUiJ1Np4Hz0MHsZJcgC4zln4=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -518,6 +522,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
+github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -638,8 +644,9 @@ github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@@ -667,6 +674,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
+github.com/robertkrimen/otto v0.4.0 h1:/c0GRrK1XDPcgIasAsnlpBT5DelIeB9U/Z/JCQsgr7E=
+github.com/robertkrimen/otto v0.4.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -709,6 +718,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@@ -855,6 +866,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
+golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
+golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -949,6 +962,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1176,8 +1191,12 @@ google.golang.org/genproto v0.0.0-20210106152847-07624b53cd92/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220504150022-98cd25cafc72/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
-google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
+google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
+google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
+google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 h1:nIgk/EEq3/YlnmVVXVnm14rC2oxgs1o0ong4sD/rd44=
+google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 h1:eSaPbMR4T7WfH9FvABk36NBMacoTUKdWCvV0dx+KfOg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I=
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
@@ -1208,8 +1227,8 @@ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzI
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
-google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
-google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
+google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1225,8 +1244,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1244,6 +1263,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
+gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
diff --git a/pkg/client/client.go b/pkg/client/client.go
index 53ab7a00..9d9bea4d 100644
--- a/pkg/client/client.go
+++ b/pkg/client/client.go
@@ -30,6 +30,7 @@ import (
"seata.apache.org/seata-go/pkg/remoting/processor/client"
"seata.apache.org/seata-go/pkg/rm"
"seata.apache.org/seata-go/pkg/rm/tcc"
+ saga "seata.apache.org/seata-go/pkg/saga/rm"
"seata.apache.org/seata-go/pkg/tm"
"seata.apache.org/seata-go/pkg/util/log"
)
@@ -62,7 +63,7 @@ func initTmClient(cfg *Config) {
})
}
-// initRemoting init remoting
+// initRemoting init rpc client
func initRemoting(cfg *Config) {
seataConfig := remoteConfig.SeataConfig{
ApplicationID: cfg.ApplicationID,
@@ -89,6 +90,7 @@ func initRmClient(cfg *Config) {
client.RegisterProcessor()
integration.Init()
tcc.InitTCC(cfg.TCCConfig.FenceConfig)
+ saga.InitSaga()
at.InitAT(cfg.ClientConfig.UndoConfig, cfg.AsyncWorkerConfig)
at.InitXA(cfg.ClientConfig.XaConfig)
})
diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go
index 8af3cd14..c92746da 100644
--- a/pkg/client/client_test.go
+++ b/pkg/client/client_test.go
@@ -22,6 +22,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+
"seata.apache.org/seata-go/pkg/tm"
)
@@ -125,10 +126,10 @@ func TestInitRemotingFunction(t *testing.T) {
func TestSyncOnceTypes(t *testing.T) {
// Test that all once variables are of type sync.Once
- assert.IsType(t, sync.Once{}, onceInitTmClient)
- assert.IsType(t, sync.Once{}, onceInitRmClient)
- assert.IsType(t, sync.Once{}, onceInitDatasource)
- assert.IsType(t, sync.Once{}, onceInitRegistry)
+ assert.IsType(t, &sync.Once{}, &onceInitTmClient)
+ assert.IsType(t, &sync.Once{}, &onceInitRmClient)
+ assert.IsType(t, &sync.Once{}, &onceInitDatasource)
+ assert.IsType(t, &sync.Once{}, &onceInitRegistry)
}
func TestConfigStruct(t *testing.T) {
diff --git a/pkg/client/config.go b/pkg/client/config.go
index 2e15c45b..9b2c8ea7 100644
--- a/pkg/client/config.go
+++ b/pkg/client/config.go
@@ -26,6 +26,8 @@ import (
"runtime"
"strings"
+ "seata.apache.org/seata-go/pkg/saga"
+
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/toml"
@@ -85,6 +87,8 @@ type Config struct {
TransportConfig remoteConfig.TransportConfig `yaml:"transport" json:"transport" koanf:"transport"`
ServiceConfig discovery.ServiceConfig `yaml:"service" json:"service" koanf:"service"`
RegistryConfig discovery.RegistryConfig `yaml:"registry" json:"registry" koanf:"registry"`
+
+ SagaConfig saga.Config `yaml:"saga" json:"saga" koanf:"saga"`
}
func (c *Config) RegisterFlags(f *flag.FlagSet) {
@@ -103,6 +107,7 @@ func (c *Config) RegisterFlags(f *flag.FlagSet) {
c.TransportConfig.RegisterFlagsWithPrefix("transport", f)
c.RegistryConfig.RegisterFlagsWithPrefix("registry", f)
c.ServiceConfig.RegisterFlagsWithPrefix("service", f)
+ c.SagaConfig.RegisterFlagsWithPrefix("saga", f)
}
type loaderConf struct {
diff --git a/pkg/datasource/sql/conn_xa.go b/pkg/datasource/sql/conn_xa.go
index 9dc00bda..14984a95 100644
--- a/pkg/datasource/sql/conn_xa.go
+++ b/pkg/datasource/sql/conn_xa.go
@@ -27,6 +27,7 @@ import (
"time"
"github.com/go-sql-driver/mysql"
+
"seata.apache.org/seata-go/pkg/datasource/sql/types"
"seata.apache.org/seata-go/pkg/datasource/sql/xa"
"seata.apache.org/seata-go/pkg/tm"
diff --git a/pkg/datasource/sql/datasource/base/meta_cache_test.go b/pkg/datasource/sql/datasource/base/meta_cache_test.go
index 7ebbc03c..e0a5ed61 100644
--- a/pkg/datasource/sql/datasource/base/meta_cache_test.go
+++ b/pkg/datasource/sql/datasource/base/meta_cache_test.go
@@ -28,6 +28,7 @@ import (
"github.com/agiledragon/gomonkey/v2"
"github.com/go-sql-driver/mysql"
"github.com/stretchr/testify/assert"
+
"seata.apache.org/seata-go/pkg/datasource/sql/types"
"seata.apache.org/seata-go/testdata"
)
diff --git a/pkg/datasource/sql/datasource/mysql/trigger.go b/pkg/datasource/sql/datasource/mysql/trigger.go
index adbbb2f7..fa1ca922 100644
--- a/pkg/datasource/sql/datasource/mysql/trigger.go
+++ b/pkg/datasource/sql/datasource/mysql/trigger.go
@@ -30,6 +30,8 @@ import (
)
type mysqlTrigger struct {
+ getColumnMetasFn func(ctx context.Context, dbName string, table string, conn *sql.Conn) ([]types.ColumnMeta, error)
+ getIndexesFn func(ctx context.Context, dbName string, tableName string, conn *sql.Conn) ([]types.IndexMeta, error)
}
func NewMysqlTrigger() *mysqlTrigger {
@@ -93,6 +95,10 @@ func (m *mysqlTrigger) LoadAll(ctx context.Context, dbName string, conn *sql.Con
// getColumnMetas get tableMeta column
func (m *mysqlTrigger) getColumnMetas(ctx context.Context, dbName string, table string, conn *sql.Conn) ([]types.ColumnMeta, error) {
+ if m.getColumnMetasFn != nil {
+ return m.getColumnMetasFn(ctx, dbName, table, conn)
+ }
+
table = executor.DelEscape(table, types.DBTypeMySQL)
var columnMetas []types.ColumnMeta
@@ -165,6 +171,10 @@ func (m *mysqlTrigger) getColumnMetas(ctx context.Context, dbName string, table
// getIndex get tableMetaIndex
func (m *mysqlTrigger) getIndexes(ctx context.Context, dbName string, tableName string, conn *sql.Conn) ([]types.IndexMeta, error) {
+ if m.getIndexesFn != nil {
+ return m.getIndexesFn(ctx, dbName, tableName, conn)
+ }
+
tableName = executor.DelEscape(tableName, types.DBTypeMySQL)
result := make([]types.IndexMeta, 0)
diff --git a/pkg/datasource/sql/datasource/mysql/trigger_test.go b/pkg/datasource/sql/datasource/mysql/trigger_test.go
index 16b15399..98aece14 100644
--- a/pkg/datasource/sql/datasource/mysql/trigger_test.go
+++ b/pkg/datasource/sql/datasource/mysql/trigger_test.go
@@ -27,6 +27,7 @@ import (
"github.com/agiledragon/gomonkey/v2"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
+
"seata.apache.org/seata-go/testdata"
"seata.apache.org/seata-go/pkg/datasource/sql/mock"
diff --git a/pkg/datasource/sql/exec/at/base_executor_test.go b/pkg/datasource/sql/exec/at/base_executor_test.go
index 432ca0aa..82062ba3 100644
--- a/pkg/datasource/sql/exec/at/base_executor_test.go
+++ b/pkg/datasource/sql/exec/at/base_executor_test.go
@@ -18,9 +18,11 @@
package at
import (
+ "testing"
+
"github.com/stretchr/testify/assert"
+
"seata.apache.org/seata-go/pkg/datasource/sql/types"
- "testing"
)
func TestBaseExecBuildLockKey(t *testing.T) {
diff --git a/pkg/datasource/sql/exec/at/update_executor.go b/pkg/datasource/sql/exec/at/update_executor.go
index 0f14e97b..023331f3 100644
--- a/pkg/datasource/sql/exec/at/update_executor.go
+++ b/pkg/datasource/sql/exec/at/update_executor.go
@@ -21,9 +21,11 @@ import (
"context"
"database/sql/driver"
"fmt"
+ "strings"
+
"github.com/arana-db/parser/model"
+
"seata.apache.org/seata-go/pkg/datasource/sql/util"
- "strings"
"github.com/arana-db/parser/ast"
"github.com/arana-db/parser/format"
diff --git a/pkg/datasource/sql/exec/at/update_join_executor_test.go b/pkg/datasource/sql/exec/at/update_join_executor_test.go
index 401abd1a..23bf19a4 100644
--- a/pkg/datasource/sql/exec/at/update_join_executor_test.go
+++ b/pkg/datasource/sql/exec/at/update_join_executor_test.go
@@ -20,9 +20,10 @@ package at
import (
"context"
"database/sql/driver"
- "seata.apache.org/seata-go/pkg/datasource/sql/undo"
"testing"
+ "seata.apache.org/seata-go/pkg/datasource/sql/undo"
+
"github.com/stretchr/testify/assert"
"seata.apache.org/seata-go/pkg/datasource/sql/exec"
diff --git a/pkg/datasource/sql/undo/base/undo_test.go b/pkg/datasource/sql/undo/base/undo_test.go
index 10b5115d..dcda2904 100644
--- a/pkg/datasource/sql/undo/base/undo_test.go
+++ b/pkg/datasource/sql/undo/base/undo_test.go
@@ -22,11 +22,13 @@ import (
"database/sql/driver"
"encoding/json"
"errors"
+ "strings"
+ "testing"
+
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/require"
+
"seata.apache.org/seata-go/pkg/datasource/sql/types"
- "strings"
- "testing"
"github.com/stretchr/testify/assert"
diff --git a/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go b/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go
index a216d813..1093b02f 100644
--- a/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go
+++ b/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go
@@ -22,12 +22,14 @@ import (
"database/sql"
"database/sql/driver"
"fmt"
+ "io"
+ "strings"
+
"github.com/arana-db/parser/ast"
"github.com/arana-db/parser/test_driver"
gxsort "github.com/dubbogo/gost/sort"
- "io"
+
"seata.apache.org/seata-go/pkg/datasource/sql/util"
- "strings"
"seata.apache.org/seata-go/pkg/datasource/sql/types"
)
diff --git a/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go b/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go
index e4360101..821d246a 100644
--- a/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go
+++ b/pkg/datasource/sql/undo/builder/basic_undo_log_builder_test.go
@@ -128,9 +128,9 @@ func TestBuildLockKey(t *testing.T) {
types.RecordImage{
TableName: "test2_name",
Rows: []types.RowImage{
- {[]types.ColumnImage{getColumnImage("id", 1), getColumnImage("userId", "one"), getColumnImage("age", "11")}},
- {[]types.ColumnImage{getColumnImage("id", 2), getColumnImage("userId", "two"), getColumnImage("age", "22")}},
- {[]types.ColumnImage{getColumnImage("id", 3), getColumnImage("userId", "three"), getColumnImage("age", "33")}},
+ {Columns: []types.ColumnImage{getColumnImage("id", 1), getColumnImage("userId", "one"), getColumnImage("age", "11")}},
+ {Columns: []types.ColumnImage{getColumnImage("id", 2), getColumnImage("userId", "two"), getColumnImage("age", "22")}},
+ {Columns: []types.ColumnImage{getColumnImage("id", 3), getColumnImage("userId", "three"), getColumnImage("age", "33")}},
},
},
"TEST2_NAME:1_one_11,2_two_22,3_three_33",
diff --git a/pkg/datasource/sql/undo/parser/branch_undo_log.pb.go b/pkg/datasource/sql/undo/parser/branch_undo_log.pb.go
index 041a8aa5..c6c5333a 100644
--- a/pkg/datasource/sql/undo/parser/branch_undo_log.pb.go
+++ b/pkg/datasource/sql/undo/parser/branch_undo_log.pb.go
@@ -24,11 +24,12 @@
package parser
import (
+ reflect "reflect"
+ sync "sync"
+
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
anypb "google.golang.org/protobuf/types/known/anypb"
- reflect "reflect"
- sync "sync"
)
const (
diff --git a/pkg/datasource/sql/undo/parser/parser_protobuf_test.go b/pkg/datasource/sql/undo/parser/parser_protobuf_test.go
index 7427d590..7ca4dfe5 100644
--- a/pkg/datasource/sql/undo/parser/parser_protobuf_test.go
+++ b/pkg/datasource/sql/undo/parser/parser_protobuf_test.go
@@ -21,6 +21,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+
"seata.apache.org/seata-go/pkg/datasource/sql/undo"
)
diff --git a/pkg/datasource/sql/util/ctxutil_test.go b/pkg/datasource/sql/util/ctxutil_test.go
index 6a5690c8..51bef2f6 100644
--- a/pkg/datasource/sql/util/ctxutil_test.go
+++ b/pkg/datasource/sql/util/ctxutil_test.go
@@ -21,8 +21,9 @@ import (
"context"
"database/sql/driver"
"errors"
- "github.com/stretchr/testify/assert"
"testing"
+
+ "github.com/stretchr/testify/assert"
)
// Mock implementations for testing
diff --git a/pkg/datasource/sql/util/lockkey.go b/pkg/datasource/sql/util/lockkey.go
index e1aaa9f5..1b5fc05a 100644
--- a/pkg/datasource/sql/util/lockkey.go
+++ b/pkg/datasource/sql/util/lockkey.go
@@ -23,8 +23,9 @@ package util
import (
"fmt"
- "seata.apache.org/seata-go/pkg/datasource/sql/types"
"strings"
+
+ "seata.apache.org/seata-go/pkg/datasource/sql/types"
)
func BuildLockKey(records *types.RecordImage, meta types.TableMeta) string {
diff --git a/pkg/datasource/sql/util/lockkey_test.go b/pkg/datasource/sql/util/lockkey_test.go
index a69cad9f..882778b5 100644
--- a/pkg/datasource/sql/util/lockkey_test.go
+++ b/pkg/datasource/sql/util/lockkey_test.go
@@ -21,6 +21,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+
"seata.apache.org/seata-go/pkg/datasource/sql/types"
)
diff --git a/pkg/discovery/etcd3_test.go b/pkg/discovery/etcd3_test.go
index 81f87c6d..c804be13 100644
--- a/pkg/discovery/etcd3_test.go
+++ b/pkg/discovery/etcd3_test.go
@@ -18,14 +18,16 @@
package discovery
import (
+ "reflect"
+ "testing"
+ "time"
+
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"go.etcd.io/etcd/api/v3/mvccpb"
- "go.etcd.io/etcd/client/v3"
- "reflect"
+ clientv3 "go.etcd.io/etcd/client/v3"
+
"seata.apache.org/seata-go/pkg/discovery/mock"
- "testing"
- "time"
)
func TestEtcd3RegistryService_Lookup(t *testing.T) {
diff --git a/pkg/discovery/mock/test_etcd_client.go b/pkg/discovery/mock/test_etcd_client.go
index e7cc5fc0..72fcd558 100644
--- a/pkg/discovery/mock/test_etcd_client.go
+++ b/pkg/discovery/mock/test_etcd_client.go
@@ -17,7 +17,7 @@
package mock
-import "go.etcd.io/etcd/client/v3"
+import clientv3 "go.etcd.io/etcd/client/v3"
type EtcdClient interface {
clientv3.KV
diff --git a/pkg/protocol/codec/codec.go b/pkg/protocol/codec/codec.go
index a7e9a863..5330d968 100644
--- a/pkg/protocol/codec/codec.go
+++ b/pkg/protocol/codec/codec.go
@@ -114,6 +114,7 @@ func (c *CodecManager) Encode(codecType CodecType, in interface{}) []byte {
func Init() {
// Global
+ GetCodecManager().RegisterCodec(CodecTypeSeata, &GlobalReportRequestCodec{})
GetCodecManager().RegisterCodec(CodecTypeSeata, &GlobalReportResponseCodec{})
GetCodecManager().RegisterCodec(CodecTypeSeata, &GlobalBeginRequestCodec{})
GetCodecManager().RegisterCodec(CodecTypeSeata, &GlobalBeginResponseCodec{})
diff --git a/pkg/protocol/codec/global_report_request_codec.go b/pkg/protocol/codec/global_report_request_codec.go
index b261d227..6065e244 100644
--- a/pkg/protocol/codec/global_report_request_codec.go
+++ b/pkg/protocol/codec/global_report_request_codec.go
@@ -52,5 +52,6 @@ func (g *GlobalReportRequestCodec) Encode(in interface{}) []byte {
// GetMessageType get global report request's message type
func (g *GlobalReportRequestCodec) GetMessageType() message.MessageType {
- return message.MessageTypeGlobalReportResult
+ // must be the request type, not the result type
+ return message.MessageTypeGlobalReport
}
diff --git a/pkg/remoting/config/config_test.go b/pkg/remoting/config/config_test.go
index 45c29b48..2ab050d0 100644
--- a/pkg/remoting/config/config_test.go
+++ b/pkg/remoting/config/config_test.go
@@ -23,6 +23,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
+
"seata.apache.org/seata-go/pkg/util/flagext"
)
diff --git a/pkg/remoting/loadbalance/least_active_loadbalance_test.go b/pkg/remoting/loadbalance/least_active_loadbalance_test.go
index b82b13df..81d5bad2 100644
--- a/pkg/remoting/loadbalance/least_active_loadbalance_test.go
+++ b/pkg/remoting/loadbalance/least_active_loadbalance_test.go
@@ -25,6 +25,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
+
"seata.apache.org/seata-go/pkg/remoting/mock"
"seata.apache.org/seata-go/pkg/remoting/rpc"
)
diff --git a/pkg/remoting/loadbalance/loadbalance.go b/pkg/remoting/loadbalance/loadbalance.go
index abfb6b6f..8451e170 100644
--- a/pkg/remoting/loadbalance/loadbalance.go
+++ b/pkg/remoting/loadbalance/loadbalance.go
@@ -37,10 +37,6 @@ func Select(loadBalanceType string, sessions *sync.Map, xid string) getty.Sessio
return RandomLoadBalance(sessions, xid)
case xidLoadBalance:
return XidLoadBalance(sessions, xid)
- case consistentHashLoadBalance:
- return ConsistentHashLoadBalance(sessions, xid)
- case leastActiveLoadBalance:
- return LeastActiveLoadBalance(sessions, xid)
case roundRobinLoadBalance:
return RoundRobinLoadBalance(sessions, xid)
default:
diff --git a/pkg/rm/init.go b/pkg/rm/init.go
index 469cdfaa..a88d36f2 100644
--- a/pkg/rm/init.go
+++ b/pkg/rm/init.go
@@ -30,3 +30,8 @@ type RmConfig struct {
func InitRm(cfg RmConfig) {
rmConfig = cfg
}
+
+// GetRmAppAndGroup returns current RM applicationId and txServiceGroup
+func GetRmAppAndGroup() (string, string) {
+ return rmConfig.ApplicationID, rmConfig.TxServiceGroup
+}
diff --git a/pkg/rm/tcc/fence/fence.go b/pkg/rm/tcc/fence/fence.go
index ce9009e5..0b85aaa5 100644
--- a/pkg/rm/tcc/fence/fence.go
+++ b/pkg/rm/tcc/fence/fence.go
@@ -19,10 +19,11 @@ package fence
import (
"flag"
+ "time"
+
"seata.apache.org/seata-go/pkg/rm/tcc/fence/config"
"seata.apache.org/seata-go/pkg/rm/tcc/fence/handler"
"seata.apache.org/seata-go/pkg/rm/tcc/fence/store/db/dao"
- "time"
)
var (
diff --git a/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler.go b/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler.go
index 1517300d..6dbf2e73 100644
--- a/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler.go
+++ b/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler.go
@@ -23,11 +23,13 @@ import (
"database/sql"
"errors"
"fmt"
+ "sync"
+ "time"
+
"github.com/go-sql-driver/mysql"
+
"seata.apache.org/seata-go/pkg/rm/tcc/fence/store/db/dao"
"seata.apache.org/seata-go/pkg/rm/tcc/fence/store/db/model"
- "sync"
- "time"
"seata.apache.org/seata-go/pkg/rm/tcc/fence/enum"
"seata.apache.org/seata-go/pkg/tm"
diff --git a/pkg/rm/tcc/fence/store/db/dao/tcc_fence_db.go b/pkg/rm/tcc/fence/store/db/dao/tcc_fence_db.go
index a2b0692c..ed3c6533 100644
--- a/pkg/rm/tcc/fence/store/db/dao/tcc_fence_db.go
+++ b/pkg/rm/tcc/fence/store/db/dao/tcc_fence_db.go
@@ -21,11 +21,12 @@ import (
"context"
"database/sql"
"fmt"
- "seata.apache.org/seata-go/pkg/util/log"
"strings"
"sync"
"time"
+ "seata.apache.org/seata-go/pkg/util/log"
+
"github.com/go-sql-driver/mysql"
"seata.apache.org/seata-go/pkg/rm/tcc/fence/enum"
diff --git a/pkg/rm/tcc/tcc_resource.go b/pkg/rm/tcc/tcc_resource.go
index fae919c1..94039e89 100644
--- a/pkg/rm/tcc/tcc_resource.go
+++ b/pkg/rm/tcc/tcc_resource.go
@@ -21,9 +21,10 @@ import (
"context"
"encoding/json"
"fmt"
- "seata.apache.org/seata-go/pkg/rm/tcc/fence"
"sync"
+ "seata.apache.org/seata-go/pkg/rm/tcc/fence"
+
"seata.apache.org/seata-go/pkg/constant"
"seata.apache.org/seata-go/pkg/protocol/branch"
"seata.apache.org/seata-go/pkg/rm"
diff --git a/pkg/rm/init.go b/pkg/saga/config.go
similarity index 67%
copy from pkg/rm/init.go
copy to pkg/saga/config.go
index 469cdfaa..93761ea2 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/config.go
@@ -15,18 +15,20 @@
* limitations under the License.
*/
-package rm
+package saga
-var rmConfig RmConfig
+import (
+ "flag"
-type RmConfig struct {
- Config
+ "seata.apache.org/seata-go/pkg/saga/statemachine"
+)
- ApplicationID string
- TxServiceGroup string
+type Config struct {
+ StateMachine *statemachine.StateMachineObject `yaml:"state-machine" json:"state-machine" koanf:"state-machine"`
}
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func (cfg *Config) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
+ if cfg.StateMachine != nil {
+ cfg.StateMachine.RegisterFlagsWithPrefix(prefix+".state-machine", f)
+ }
}
diff --git a/pkg/saga/readme.md b/pkg/saga/readme.md
new file mode 100644
index 00000000..4b59bbc7
--- /dev/null
+++ b/pkg/saga/readme.md
@@ -0,0 +1,47 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+# seata saga
+
+未来计划有三种使用方式
+
+- 基于状态机引擎的 json
+ link: statemachine_engine#Start
+- stream builder
+ stateMachine.serviceTask().build().Start
+- 二阶段方式saga,类似tcc使用
+
+上面1、2是以来[statemachine](statemachine),状态机引擎实现的,3相对比较独立。
+
+
+状态机的实现在:saga-statemachine包中
+其中[statelang](statemachine%2Fstatelang)是状态机语言的解析,目前实现的是json解析方式,状态机语言可以参考:
+https://seata.io/docs/user/mode/saga
+
+状态机json执行的入口类是:[statemachine_engine.go](statemachine%2Fengine%2Fstatemachine_engine.go)
+
+下面简单说下engine中各个包的作用:
+events:saga的是基于事件处理的,其中是event、eventBus的实现
+expr:表达式声明、解析、执行
+invoker:声明了serviceInvoker、scriptInvoker等接口、task调用管理、执行都在这个包中,例如httpInvoker
+process_ctrl:状态机处理流程:上下文、执行、事件流转
+sequence:分布式id
+store:状态机存储接口、实现
+status_decision:状态机状态决策
+
+
+
diff --git a/pkg/rm/init.go b/pkg/saga/rm/handler_saga.go
similarity index 64%
copy from pkg/rm/init.go
copy to pkg/saga/rm/handler_saga.go
index 469cdfaa..27c01fef 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/rm/handler_saga.go
@@ -17,16 +17,19 @@
package rm
-var rmConfig RmConfig
+import (
+ "seata.apache.org/seata-go/pkg/protocol/branch"
+ "seata.apache.org/seata-go/pkg/rm"
+)
-type RmConfig struct {
- Config
+type RMHandlerSaga struct{}
- ApplicationID string
- TxServiceGroup string
+func (h *RMHandlerSaga) HandleUndoLogDeleteRequest(request interface{}) {
+ // do nothing
}
-
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func (h *RMHandlerSaga) GetResourceManager() rm.ResourceManager {
+ return rm.GetRmCacheInstance().GetResourceManager(branch.BranchTypeSAGA)
+}
+func (h *RMHandlerSaga) GetBranchType() branch.BranchType {
+ return branch.BranchTypeSAGA
}
diff --git a/pkg/rm/init.go b/pkg/saga/rm/saga_resource.go
similarity index 53%
copy from pkg/rm/init.go
copy to pkg/saga/rm/saga_resource.go
index 469cdfaa..ea46c552 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/rm/saga_resource.go
@@ -17,16 +17,37 @@
package rm
-var rmConfig RmConfig
+import (
+ "fmt"
-type RmConfig struct {
- Config
+ "seata.apache.org/seata-go/pkg/protocol/branch"
+)
- ApplicationID string
- TxServiceGroup string
+type SagaResource struct {
+ resourceGroupId string
+ applicationId string
}
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func (r *SagaResource) GetResourceGroupId() string {
+ return r.resourceGroupId
+}
+
+func (r *SagaResource) SetResourceGroupId(resourceGroupId string) {
+ r.resourceGroupId = resourceGroupId
+}
+
+func (r *SagaResource) GetResourceId() string {
+ return fmt.Sprintf("%s#%s", r.applicationId, r.resourceGroupId)
+}
+
+func (r *SagaResource) GetBranchType() branch.BranchType {
+ return branch.BranchTypeSAGA
+}
+
+func (r *SagaResource) GetApplicationId() string {
+ return r.applicationId
+}
+
+func (r *SagaResource) SetApplicationId(applicationId string) {
+ r.applicationId = applicationId
}
diff --git a/pkg/saga/rm/saga_resource_manager.go b/pkg/saga/rm/saga_resource_manager.go
new file mode 100644
index 00000000..27547032
--- /dev/null
+++ b/pkg/saga/rm/saga_resource_manager.go
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package rm
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "log"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/protocol/branch"
+ "seata.apache.org/seata-go/pkg/protocol/message"
+ "seata.apache.org/seata-go/pkg/rm"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/exception"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+ seataErrors "seata.apache.org/seata-go/pkg/util/errors"
+)
+
+var (
+ sagaResourceManagerInstance *SagaResourceManager
+ once sync.Once
+)
+
+type SagaResourceManager struct {
+ rmRemoting *rm.RMRemoting
+ resourceCache sync.Map
+}
+
+func InitSaga() {
+ rm.GetRmCacheInstance().RegisterResourceManager(GetSagaResourceManager())
+}
+
+func GetSagaResourceManager() *SagaResourceManager {
+ once.Do(func() {
+ sagaResourceManagerInstance = &SagaResourceManager{
+ rmRemoting: rm.GetRMRemotingInstance(),
+ resourceCache: sync.Map{},
+ }
+ })
+ return sagaResourceManagerInstance
+}
+
+func (s *SagaResourceManager) RegisterResource(resource rm.Resource) error {
+ if _, ok := resource.(*SagaResource); !ok {
+ return fmt.Errorf("register saga resource error, SagaResource is needed, param %v", resource)
+ }
+ s.resourceCache.Store(resource.GetResourceId(), resource)
+ return s.rmRemoting.RegisterResource(resource)
+
+}
+
+func (s *SagaResourceManager) GetCachedResources() *sync.Map {
+ return &s.resourceCache
+}
+
+func (s *SagaResourceManager) GetBranchType() branch.BranchType {
+ return branch.BranchTypeSAGA
+}
+
+func (s *SagaResourceManager) BranchCommit(ctx context.Context, resource rm.BranchResource) (branch.BranchStatus, error) {
+ engine := GetStateMachineEngine()
+ stMaInst, err := engine.Forward(ctx, resource.Xid, nil)
+ if err != nil {
+ if fie, ok := exception.IsForwardInvalidException(err); ok {
+ log.Printf("StateMachine forward failed, xid: %s, err: %v", resource.Xid, err)
+ if isInstanceNotExists(fie.ErrCode) {
+ return branch.BranchStatusPhasetwoCommitted, nil
+ }
+ }
+ log.Printf("StateMachine forward failed, xid: %s, err: %v", resource.Xid, err)
+ return branch.BranchStatusPhasetwoCommitFailedRetryable, err
+ }
+
+ status := stMaInst.Status()
+ compStatus := stMaInst.CompensationStatus()
+
+ switch {
+ case status == statelang.SU && compStatus == "":
+ return branch.BranchStatusPhasetwoCommitted, nil
+ case compStatus == statelang.SU:
+ return branch.BranchStatusPhasetwoRollbacked, nil
+ case compStatus == statelang.FA || compStatus == statelang.UN:
+ return branch.BranchStatusPhasetwoRollbackFailedRetryable, nil
+ case status == statelang.FA && compStatus == "":
+ return branch.BranchStatusPhaseoneFailed, nil
+ default:
+ return branch.BranchStatusPhasetwoCommitFailedRetryable, nil
+ }
+}
+
+func (s *SagaResourceManager) BranchRollback(ctx context.Context, resource rm.BranchResource) (branch.BranchStatus, error) {
+ engine := GetStateMachineEngine()
+ stMaInst, err := engine.ReloadStateMachineInstance(ctx, resource.Xid)
+ if err != nil || stMaInst == nil {
+ return branch.BranchStatusPhasetwoRollbacked, nil
+ }
+
+ strategy := stMaInst.StateMachine().RecoverStrategy()
+ appData := resource.ApplicationData
+ isTimeoutRollback := bytes.Equal(appData, []byte{byte(message.GlobalStatusTimeoutRollbacking)}) || bytes.Equal(appData, []byte{byte(message.GlobalStatusTimeoutRollbackRetrying)})
+
+ if strategy == statelang.Forward && isTimeoutRollback {
+ log.Printf("Retry by custom recover strategy [Forward] on timeout, SAGA global[%s]", resource.Xid)
+ return branch.BranchStatusPhasetwoCommitFailedRetryable, nil
+ }
+
+ stMaInst, err = engine.Compensate(ctx, resource.Xid, nil)
+ if err == nil && stMaInst.CompensationStatus() == statelang.SU {
+ return branch.BranchStatusPhasetwoRollbacked, nil
+ }
+
+ if fie, ok := exception.IsEngineExecutionException(err); ok {
+ log.Printf("StateMachine compensate failed, xid: %s, err: %v", resource.Xid, err)
+ if isInstanceNotExists(fie.ErrCode) {
+ return branch.BranchStatusPhasetwoRollbacked, nil
+ }
+ }
+ log.Printf("StateMachine compensate failed, xid: %s, err: %v", resource.Xid, err)
+ return branch.BranchStatusPhasetwoRollbackFailedRetryable, err
+}
+
+func (s *SagaResourceManager) BranchRegister(ctx context.Context, param rm.BranchRegisterParam) (int64, error) {
+ return s.rmRemoting.BranchRegister(param)
+}
+
+func (s *SagaResourceManager) BranchReport(ctx context.Context, param rm.BranchReportParam) error {
+ return s.rmRemoting.BranchReport(param)
+}
+
+func (s *SagaResourceManager) LockQuery(ctx context.Context, param rm.LockQueryParam) (bool, error) {
+ // LockQuery is not supported for Saga resources
+ return false, fmt.Errorf("LockQuery is not supported for Saga resources")
+}
+
+func (s *SagaResourceManager) UnregisterResource(resource rm.Resource) error {
+ // UnregisterResource is not supported for SagaResourceManager
+ return fmt.Errorf("UnregisterResource is not supported for SagaResourceManager")
+}
+
+// isInstanceNotExists checks if the error code indicates StateMachineInstanceNotExists
+func isInstanceNotExists(errCode string) bool {
+ return errCode == fmt.Sprintf("%v", seataErrors.StateMachineInstanceNotExists)
+}
diff --git a/pkg/rm/init.go b/pkg/saga/rm/saga_resource_manager_test.go
similarity index 63%
copy from pkg/rm/init.go
copy to pkg/saga/rm/saga_resource_manager_test.go
index 469cdfaa..61af679f 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/rm/saga_resource_manager_test.go
@@ -17,16 +17,27 @@
package rm
-var rmConfig RmConfig
+import (
+ "sync"
+ "testing"
+)
-type RmConfig struct {
- Config
+func TestGetSagaResourceManager_Singleton(t *testing.T) {
+ var wg sync.WaitGroup
+ instances := make([]*SagaResourceManager, 10)
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func(idx int) {
+ defer wg.Done()
+ instances[idx] = GetSagaResourceManager()
+ }(i)
+ }
+ wg.Wait()
- ApplicationID string
- TxServiceGroup string
-}
-
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+ first := instances[0]
+ for i, inst := range instances {
+ if inst != first {
+ t.Errorf("Instance at index %d is not the same as the first instance", i)
+ }
+ }
}
diff --git a/pkg/rm/init.go b/pkg/saga/rm/state_machine_engine_holder.go
similarity index 67%
copy from pkg/rm/init.go
copy to pkg/saga/rm/state_machine_engine_holder.go
index 469cdfaa..e7fb9efc 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/rm/state_machine_engine_holder.go
@@ -17,16 +17,23 @@
package rm
-var rmConfig RmConfig
+import (
+ "sync"
-type RmConfig struct {
- Config
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+)
- ApplicationID string
- TxServiceGroup string
+var (
+ stateMachineEngine engine.StateMachineEngine
+ stateMachineEngineOnce sync.Once
+)
+
+func GetStateMachineEngine() engine.StateMachineEngine {
+ return stateMachineEngine
}
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func SetStateMachineEngine(smEngine engine.StateMachineEngine) {
+ stateMachineEngineOnce.Do(func() {
+ stateMachineEngine = smEngine
+ })
}
diff --git a/pkg/util/collection/collection_test.go b/pkg/saga/runtime/engine_registry.go
similarity index 51%
copy from pkg/util/collection/collection_test.go
copy to pkg/saga/runtime/engine_registry.go
index 74df03cd..bdf70255 100644
--- a/pkg/util/collection/collection_test.go
+++ b/pkg/saga/runtime/engine_registry.go
@@ -15,45 +15,36 @@
* limitations under the License.
*/
-package collection
+package runtime
import (
- "reflect"
- "testing"
+ "context"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
)
-func TestEncodeDecodeMap(t *testing.T) {
- testCases := []struct {
- name string
- dataMap map[string]string
- expected map[string]string
- }{
- {
- name: "test case 1",
- dataMap: map[string]string{
- "key1": "value1",
- "key2": "value2",
- },
- expected: map[string]string{
- "key1": "value1",
- "key2": "value2",
- },
- },
- {
- name: "test case 2",
- dataMap: nil,
- expected: nil,
- },
- }
+// Engine is a minimal facade for state machine operations needed by Saga RM
+type Engine interface {
+ Forward(ctx context.Context, stateMachineInstId string, replaceParams map[string]interface{}) (statelang.StateMachineInstance, error)
+ Compensate(ctx context.Context, stateMachineInstId string, replaceParams map[string]interface{}) (statelang.StateMachineInstance, error)
+ ReloadStateMachineInstance(ctx context.Context, instId string) (statelang.StateMachineInstance, error)
+}
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- encoded := EncodeMap(tc.dataMap)
- decoded := DecodeMap(encoded)
+var (
+ mu sync.RWMutex
+ engineRef Engine
+)
+
+func SetEngine(e Engine) {
+ mu.Lock()
+ engineRef = e
+ mu.Unlock()
+}
- if !reflect.DeepEqual(decoded, tc.expected) {
- t.Errorf("DecodeMap(%v) = %v, expected %v", encoded, decoded, tc.expected)
- }
- })
- }
+func GetEngine() Engine {
+ mu.RLock()
+ e := engineRef
+ mu.RUnlock()
+ return e
}
diff --git a/pkg/saga/statemachine/constant/constant.go b/pkg/saga/statemachine/constant/constant.go
new file mode 100644
index 00000000..3d22e386
--- /dev/null
+++ b/pkg/saga/statemachine/constant/constant.go
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package constant
+
+type NetExceptionType string
+
+const (
+ // region State Types
+ StateTypeServiceTask string = "ServiceTask"
+ StateTypeChoice string = "Choice"
+ StateTypeFail string = "Fail"
+ StateTypeSucceed string = "Succeed"
+ StateTypeCompensationTrigger string = "CompensationTrigger"
+ StateTypeSubStateMachine string = "SubStateMachine"
+ StateTypeCompensateSubMachine string = "CompensateSubMachine"
+ StateTypeScriptTask string = "ScriptTask"
+ StateTypeLoopStart string = "LoopStart"
+ // end region
+
+ // region Service Types
+ ServiceTypeGRPC string = "GRPC"
+ // end region
+
+ // region System Variables
+ VarNameOutputParams string = "outputParams"
+ VarNameProcessType string = "_ProcessType_"
+ VarNameOperationName string = "_operation_name_"
+ OperationNameStart string = "start"
+ OperationNameCompensate string = "compensate"
+ VarNameAsyncCallback string = "_async_callback_"
+ VarNameCurrentExceptionRoute string = "_current_exception_route_"
+ VarNameIsExceptionNotCatch string = "_is_exception_not_catch_"
+ VarNameSubMachineParentId string = "_sub_machine_parent_id_"
+ VarNameCurrentChoice string = "_current_choice_"
+ VarNameStateMachineInst string = "_current_statemachine_instance_"
+ VarNameStateMachine string = "_current_statemachine_"
+ VarNameStateMachineEngine string = "_current_statemachine_engine_"
+ VarNameStateMachineConfig string = "_statemachine_config_"
+ VarNameStateMachineContext string = "context"
+ VarNameIsAsyncExecution string = "_is_async_execution_"
+ VarNameStateInst string = "_current_state_instance_"
+ VarNameBusinesskey string = "_business_key_"
+ VarNameParentId string = "_parent_id_"
+ VarNameCurrentException string = "currentException"
+ CompensateSubMachineStateNamePrefix string = "_compensate_sub_machine_state_"
+ DefaultScriptType string = "groovy"
+ VarNameSyncExeStack string = "_sync_execution_stack_"
+ VarNameInputParams string = "inputParams"
+ VarNameIsLoopState string = "_is_loop_state_"
+ VarNameCurrentCompensateTriggerState string = "_is_compensating_"
+ VarNameCurrentCompensationHolder string = "_current_compensation_holder_"
+ VarNameFirstCompensationStateStarted string = "_first_compensation_state_started"
+ VarNameNoCompensation string = "_no_compensation_case_"
+ VarNameCurrentLoopContextHolder string = "_current_loop_context_holder_"
+ VarNameRetriedStateInstId string = "_retried_state_instance_id"
+ VarNameIsForSubStatMachineForward string = "_is_for_sub_statemachine_forward_"
+ // TODO: this lock in process context only has one, try to add more to add concurrent
+ VarNameProcessContextMutexLock string = "_current_context_mutex_lock"
+ VarNameFailEndStateFlag string = "_fail_end_state_flag_"
+ VarNameGlobalTx string = "_global_transaction_"
+ // end region
+
+ // region of loop
+ LoopCounter string = "loopCounter"
+ LoopSemaphore string = "loopSemaphore"
+ LoopResult string = "loopResult"
+ NumberOfInstances string = "nrOfInstances"
+ NumberOfActiveInstances string = "nrOfActiveInstances"
+ NumberOfCompletedInstances string = "nrOfCompletedInstances"
+ // end region
+
+ // region others
+ SeqEntityStateMachine string = "STATE_MACHINE"
+ SeqEntityStateMachineInst string = "STATE_MACHINE_INST"
+ SeqEntityStateInst string = "STATE_INST"
+ OperationNameForward string = "forward"
+ LoopStateNamePattern string = "-loop-"
+ SagaTransNamePrefix string = "$Saga_"
+ // end region
+
+ SeperatorParentId string = ":"
+
+ // Machine execution timeout error code
+ FrameworkErrorCodeStateMachineExecutionTimeout = "StateMachineExecutionTimeout"
+ ConnectException NetExceptionType = "CONNECT_EXCEPTION"
+ ConnectTimeoutException NetExceptionType = "CONNECT_TIMEOUT_EXCEPTION"
+ ReadTimeoutException NetExceptionType = "READ_TIMEOUT_EXCEPTION"
+ NotNetException NetExceptionType = "NOT_NET_EXCEPTION"
+)
diff --git a/pkg/saga/statemachine/engine/config/bootstrap.go b/pkg/saga/statemachine/engine/config/bootstrap.go
new file mode 100644
index 00000000..ca18b92f
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/bootstrap.go
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package config
+
+import (
+ "fmt"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/pcext"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl/handlers"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl/process"
+)
+
+// bootstrapProcessWiring wires default handlers/routers and router handler into BusinessProcessor
+func (c *DefaultStateMachineConfig) bootstrapProcessWiring() error {
+ // Process handler for state machine
+ smHandler := pcext.NewStateMachineProcessHandler()
+ // Register ServiceTask handler by default
+ smHandler.RegistryStateHandler(constant.StateTypeServiceTask, handlers.NewServiceTaskStateHandler())
+
+ // Router for state machine
+ smRouter := &pcext.StateMachineProcessRouter{}
+ smRouter.InitDefaultStateRouters()
+
+ // Default router handler binds event publisher and process routers
+ drh := &process_ctrl.DefaultRouterHandler{}
+ drh.SetEventPublisher(c.EventPublisher())
+ drh.SetProcessRouters(map[string]process_ctrl.ProcessRouter{
+ string(process.StateLang): smRouter,
+ })
+
+ // Register into BusinessProcessor
+ pcImpl, ok := c.processController.(*process_ctrl.ProcessControllerImpl)
+ if !ok {
+ return fmt.Errorf("ProcessController is not an instance of ProcessControllerImpl")
+ }
+ bp := pcImpl.BusinessProcessor()
+ // need concrete DefaultBusinessProcessor to call Registry APIs
+ dbp, ok := bp.(*process_ctrl.DefaultBusinessProcessor)
+ if !ok {
+ return fmt.Errorf("BusinessProcessor is not DefaultBusinessProcessor, got %T", bp)
+ }
+ dbp.RegistryProcessHandler(process.StateLang, smHandler)
+ dbp.RegistryRouterHandler(process.StateLang, drh)
+ return nil
+}
diff --git a/pkg/saga/statemachine/engine/config/default_statemachine_config.go b/pkg/saga/statemachine/engine/config/default_statemachine_config.go
new file mode 100644
index 00000000..2b865502
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/default_statemachine_config.go
@@ -0,0 +1,824 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package config
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "gopkg.in/yaml.v3"
+
+ "seata.apache.org/seata-go/pkg/protocol/branch"
+ baserm "seata.apache.org/seata-go/pkg/rm"
+ sagarm "seata.apache.org/seata-go/pkg/saga/rm"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/expr"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/invoker"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/repo"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/repo/repository"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/sequence"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/strategy"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/store"
+)
+
+const (
+ DefaultTransOperTimeout = 60000 * 30
+ DefaultServiceInvokeTimeout = 60000 * 5
+ DefaultClientSagaRetryPersistModeUpdate = false
+ DefaultClientSagaCompensatePersistModeUpdate = false
+ DefaultClientReportSuccessEnable = false
+ DefaultClientSagaBranchRegisterEnable = true
+)
+
+type DefaultStateMachineConfig struct {
+ // Configuration
+ transOperationTimeout int
+ serviceInvokeTimeout int
+ charset string
+ defaultTenantId string
+ sagaRetryPersistModeUpdate bool
+ sagaCompensatePersistModeUpdate bool
+ sagaBranchRegisterEnable bool
+ rmReportSuccessEnable bool
+ stateMachineResources []string
+
+ // Components
+ processController process_ctrl.ProcessController
+
+ // Event Bus
+ syncEventBus process_ctrl.EventBus
+ asyncEventBus process_ctrl.EventBus
+
+ // Event publisher
+ syncProcessCtrlEventPublisher process_ctrl.EventPublisher
+ asyncProcessCtrlEventPublisher process_ctrl.EventPublisher
+
+ // Store related components
+ stateLogRepository repo.StateLogRepository
+ stateLogStore store.StateLogStore
+ stateLangStore store.StateLangStore
+ stateMachineRepository repo.StateMachineRepository
+
+ // Expression related components
+ expressionFactoryManager *expr.ExpressionFactoryManager
+ expressionResolver expr.ExpressionResolver
+
+ // Invoker related components
+ serviceInvokerManager invoker.ServiceInvokerManager
+ scriptInvokerManager invoker.ScriptInvokerManager
+
+ // Other components
+ statusDecisionStrategy engine.StatusDecisionStrategy
+ seqGenerator sequence.SeqGenerator
+ componentLock *sync.Mutex
+
+ enableAsync bool
+
+ // runtime store & tc options
+ storeEnabled bool
+ storeType string
+ storeDSN string
+ tcEnabled bool
+}
+
+func (c *DefaultStateMachineConfig) ComponentLock() *sync.Mutex {
+ return c.componentLock
+}
+
+func (c *DefaultStateMachineConfig) SetComponentLock(componentLock *sync.Mutex) {
+ c.componentLock = componentLock
+}
+
+func (c *DefaultStateMachineConfig) SetTransOperationTimeout(transOperationTimeout int) {
+ c.transOperationTimeout = transOperationTimeout
+}
+
+func (c *DefaultStateMachineConfig) SetServiceInvokeTimeout(serviceInvokeTimeout int) {
+ c.serviceInvokeTimeout = serviceInvokeTimeout
+}
+
+func (c *DefaultStateMachineConfig) SetCharset(charset string) {
+ c.charset = charset
+}
+
+func (c *DefaultStateMachineConfig) SetDefaultTenantId(defaultTenantId string) {
+ c.defaultTenantId = defaultTenantId
+}
+
+func (c *DefaultStateMachineConfig) SetSyncEventBus(syncEventBus process_ctrl.EventBus) {
+ c.syncEventBus = syncEventBus
+}
+
+func (c *DefaultStateMachineConfig) SetAsyncEventBus(asyncEventBus process_ctrl.EventBus) {
+ c.asyncEventBus = asyncEventBus
+}
+
+func (c *DefaultStateMachineConfig) SetSyncProcessCtrlEventPublisher(syncProcessCtrlEventPublisher process_ctrl.EventPublisher) {
+ c.syncProcessCtrlEventPublisher = syncProcessCtrlEventPublisher
+}
+
+func (c *DefaultStateMachineConfig) SetAsyncProcessCtrlEventPublisher(asyncProcessCtrlEventPublisher process_ctrl.EventPublisher) {
+ c.asyncProcessCtrlEventPublisher = asyncProcessCtrlEventPublisher
+}
+
+func (c *DefaultStateMachineConfig) SetStateLogRepository(stateLogRepository repo.StateLogRepository) {
+ c.stateLogRepository = stateLogRepository
+}
+
+func (c *DefaultStateMachineConfig) SetStateLogStore(stateLogStore store.StateLogStore) {
+ c.stateLogStore = stateLogStore
+}
+
+func (c *DefaultStateMachineConfig) SetStateLangStore(stateLangStore store.StateLangStore) {
+ c.stateLangStore = stateLangStore
+}
+
+func (c *DefaultStateMachineConfig) SetStateMachineRepository(stateMachineRepository repo.StateMachineRepository) {
+ c.stateMachineRepository = stateMachineRepository
+}
+
+func (c *DefaultStateMachineConfig) SetExpressionFactoryManager(expressionFactoryManager *expr.ExpressionFactoryManager) {
+ c.expressionFactoryManager = expressionFactoryManager
+}
+
+func (c *DefaultStateMachineConfig) SetExpressionResolver(expressionResolver expr.ExpressionResolver) {
+ c.expressionResolver = expressionResolver
+}
+
+func (c *DefaultStateMachineConfig) SetServiceInvokerManager(serviceInvokerManager invoker.ServiceInvokerManager) {
+ c.serviceInvokerManager = serviceInvokerManager
+}
+
+func (c *DefaultStateMachineConfig) SetScriptInvokerManager(scriptInvokerManager invoker.ScriptInvokerManager) {
+ c.scriptInvokerManager = scriptInvokerManager
+}
+
+func (c *DefaultStateMachineConfig) SetStatusDecisionStrategy(statusDecisionStrategy engine.StatusDecisionStrategy) {
+ c.statusDecisionStrategy = statusDecisionStrategy
+}
+
+func (c *DefaultStateMachineConfig) SetSeqGenerator(seqGenerator sequence.SeqGenerator) {
+ c.seqGenerator = seqGenerator
+}
+
+func (c *DefaultStateMachineConfig) StateLogRepository() repo.StateLogRepository {
+ return c.stateLogRepository
+}
+
+func (c *DefaultStateMachineConfig) StateMachineRepository() repo.StateMachineRepository {
+ return c.stateMachineRepository
+}
+
+func (c *DefaultStateMachineConfig) StateLogStore() store.StateLogStore {
+ return c.stateLogStore
+}
+
+func (c *DefaultStateMachineConfig) StateLangStore() store.StateLangStore {
+ return c.stateLangStore
+}
+
+func (c *DefaultStateMachineConfig) ExpressionFactoryManager() *expr.ExpressionFactoryManager {
+ return c.expressionFactoryManager
+}
+
+func (c *DefaultStateMachineConfig) ExpressionResolver() expr.ExpressionResolver {
+ return c.expressionResolver
+}
+
+func (c *DefaultStateMachineConfig) SeqGenerator() sequence.SeqGenerator {
+ return c.seqGenerator
+}
+
+func (c *DefaultStateMachineConfig) StatusDecisionStrategy() engine.StatusDecisionStrategy {
+ return c.statusDecisionStrategy
+}
+
+func (c *DefaultStateMachineConfig) SyncEventBus() process_ctrl.EventBus {
+ return c.syncEventBus
+}
+
+func (c *DefaultStateMachineConfig) AsyncEventBus() process_ctrl.EventBus {
+ return c.asyncEventBus
+}
+
+func (c *DefaultStateMachineConfig) EventPublisher() process_ctrl.EventPublisher {
+ return c.syncProcessCtrlEventPublisher
+}
+
+func (c *DefaultStateMachineConfig) AsyncEventPublisher() process_ctrl.EventPublisher {
+ return c.asyncProcessCtrlEventPublisher
+}
+
+func (c *DefaultStateMachineConfig) ServiceInvokerManager() invoker.ServiceInvokerManager {
+ return c.serviceInvokerManager
+}
+
+func (c *DefaultStateMachineConfig) ScriptInvokerManager() invoker.ScriptInvokerManager {
+ return c.scriptInvokerManager
+}
+
+func (c *DefaultStateMachineConfig) CharSet() string {
+ return c.charset
+}
+
+func (c *DefaultStateMachineConfig) SetCharSet(charset string) {
+ c.charset = charset
+}
+
+func (c *DefaultStateMachineConfig) GetDefaultTenantId() string {
+ return c.defaultTenantId
+}
+
+func (c *DefaultStateMachineConfig) GetTransOperationTimeout() int {
+ return c.transOperationTimeout
+}
+
+func (c *DefaultStateMachineConfig) GetServiceInvokeTimeout() int {
+ return c.serviceInvokeTimeout
+}
+
+func (c *DefaultStateMachineConfig) EnableAsync() bool {
+ return c.enableAsync
+}
+
+func (c *DefaultStateMachineConfig) SetEnableAsync(enable bool) {
+ c.enableAsync = enable
+}
+
+func (c *DefaultStateMachineConfig) IsSagaRetryPersistModeUpdate() bool {
+ return c.sagaRetryPersistModeUpdate
+}
+
+func (c *DefaultStateMachineConfig) SetSagaRetryPersistModeUpdate(sagaRetryPersistModeUpdate bool) {
+ c.sagaRetryPersistModeUpdate = sagaRetryPersistModeUpdate
+}
+
+func (c *DefaultStateMachineConfig) IsSagaCompensatePersistModeUpdate() bool {
+ return c.sagaCompensatePersistModeUpdate
+}
+
+func (c *DefaultStateMachineConfig) SetSagaCompensatePersistModeUpdate(sagaCompensatePersistModeUpdate bool) {
+ c.sagaCompensatePersistModeUpdate = sagaCompensatePersistModeUpdate
+}
+
+func (c *DefaultStateMachineConfig) IsSagaBranchRegisterEnable() bool {
+ return c.sagaBranchRegisterEnable
+}
+
+func (c *DefaultStateMachineConfig) SetSagaBranchRegisterEnable(sagaBranchRegisterEnable bool) {
+ c.sagaBranchRegisterEnable = sagaBranchRegisterEnable
+}
+
+func (c *DefaultStateMachineConfig) IsRmReportSuccessEnable() bool {
+ return c.rmReportSuccessEnable
+}
+
+func (c *DefaultStateMachineConfig) SetRmReportSuccessEnable(rmReportSuccessEnable bool) {
+ c.rmReportSuccessEnable = rmReportSuccessEnable
+}
+
+func (c *DefaultStateMachineConfig) GetStateMachineConfig() engine.StateMachineConfig {
+ return c
+}
+
+func (c *DefaultStateMachineConfig) GetExpressionFactory(expressionType string) expr.ExpressionFactory {
+ return c.expressionFactoryManager.GetExpressionFactory(expressionType)
+}
+
+func (c *DefaultStateMachineConfig) GetServiceInvoker(serviceType string) (invoker.ServiceInvoker, error) {
+ if serviceType == "" {
+ serviceType = "local"
+ }
+
+ invoker := c.serviceInvokerManager.ServiceInvoker(serviceType)
+ if invoker == nil {
+ return nil, fmt.Errorf("service invoker not found for type: %s", serviceType)
+ }
+
+ return invoker, nil
+}
+
+func (c *DefaultStateMachineConfig) RegisterStateMachineDef(resources []string) error {
+ var allFiles []string
+
+ for _, pattern := range resources {
+ matches, err := filepath.Glob(pattern)
+ if err != nil {
+ return fmt.Errorf("failed to expand glob pattern: pattern=%s, err=%w", pattern, err)
+ }
+ if len(matches) == 0 {
+ return fmt.Errorf("open resource file failed: pattern=%s", pattern)
+ }
+ allFiles = append(allFiles, matches...)
+ }
+
+ for _, realPath := range allFiles {
+ file, err := os.Open(realPath)
+ if err != nil {
+ return fmt.Errorf("open resource file failed: path=%s, err=%w", realPath, err)
+ }
+ defer file.Close()
+
+ if err := c.stateMachineRepository.RegistryStateMachineByReader(file); err != nil {
+ return fmt.Errorf("register state machine from file failed: path=%s, err=%w", realPath, err)
+ }
+ }
+
+ return nil
+}
+
+func (c *DefaultStateMachineConfig) RegisterExpressionFactory(expressionType string, factory expr.ExpressionFactory) {
+ c.expressionFactoryManager.PutExpressionFactory(expressionType, factory)
+}
+
+func (c *DefaultStateMachineConfig) RegisterServiceInvoker(serviceType string, invoker invoker.ServiceInvoker) {
+ c.serviceInvokerManager.PutServiceInvoker(serviceType, invoker)
+}
+
+type ConfigFileParams struct {
+ TransOperationTimeout int `json:"trans_operation_timeout" yaml:"trans_operation_timeout"`
+ ServiceInvokeTimeout int `json:"service_invoke_timeout" yaml:"service_invoke_timeout"`
+ Charset string `json:"charset" yaml:"charset"`
+ DefaultTenantId string `json:"default_tenant_id" yaml:"default_tenant_id"`
+ EnableAsync bool `json:"enable_async" yaml:"enable_async"`
+ SagaRetryPersistModeUpdate bool `json:"saga_retry_persist_mode_update" yaml:"saga_retry_persist_mode_update"`
+ SagaCompensatePersistModeUpdate bool `json:"saga_compensate_persist_mode_update" yaml:"saga_compensate_persist_mode_update"`
+ SagaBranchRegisterEnable bool `json:"saga_branch_register_enable" yaml:"saga_branch_register_enable"`
+ RmReportSuccessEnable bool `json:"rm_report_success_enable" yaml:"rm_report_success_enable"`
+ StateMachineResources []string `json:"state_machine_resources" yaml:"state_machine_resources"`
+ StoreEnabled bool `json:"store_enabled" yaml:"store_enabled"`
+ StoreType string `json:"store_type" yaml:"store_type"`
+ StoreDSN string `json:"store_dsn" yaml:"store_dsn"`
+ TCEnabled bool `json:"tc_enabled" yaml:"tc_enabled"`
+}
+
+func (c *DefaultStateMachineConfig) LoadConfig(configPath string) error {
+ if c.seqGenerator == nil {
+ c.seqGenerator = sequence.NewUUIDSeqGenerator()
+ }
+
+ content, err := os.ReadFile(configPath)
+ if err != nil {
+ return fmt.Errorf("failed to read config file: path=%s, error=%w", configPath, err)
+ }
+
+ var configFileParams ConfigFileParams
+ ext := strings.ToLower(filepath.Ext(configPath))
+
+ switch ext {
+ case ".json":
+ if err := json.Unmarshal(content, &configFileParams); err != nil {
+ return fmt.Errorf("failed to unmarshal config file as JSON: %w", err)
+ }
+ case ".yaml", ".yml":
+ if err := yaml.Unmarshal(content, &configFileParams); err != nil {
+ return fmt.Errorf("failed to unmarshal config file as YAML: %w", err)
+ }
+ default:
+ return fmt.Errorf("unsupported config file type: path=%s, ext=%s (only .json/.yaml/.yml are supported)", configPath, ext)
+ }
+
+ c.applyConfigFileParams(&configFileParams)
+ return nil
+}
+
+func (c *DefaultStateMachineConfig) applyConfigFileParams(rc *ConfigFileParams) {
+ if rc.TransOperationTimeout > 0 {
+ c.transOperationTimeout = rc.TransOperationTimeout
+ }
+ if rc.ServiceInvokeTimeout > 0 {
+ c.serviceInvokeTimeout = rc.ServiceInvokeTimeout
+ }
+ if rc.Charset != "" {
+ c.charset = rc.Charset
+ }
+ if rc.DefaultTenantId != "" {
+ c.defaultTenantId = rc.DefaultTenantId
+ }
+ c.enableAsync = rc.EnableAsync
+ c.sagaRetryPersistModeUpdate = rc.SagaRetryPersistModeUpdate
+ c.sagaCompensatePersistModeUpdate = rc.SagaCompensatePersistModeUpdate
+ c.sagaBranchRegisterEnable = rc.SagaBranchRegisterEnable
+ c.rmReportSuccessEnable = rc.RmReportSuccessEnable
+ if len(rc.StateMachineResources) > 0 {
+ c.stateMachineResources = rc.StateMachineResources
+ }
+ c.storeEnabled = rc.StoreEnabled
+ if rc.StoreType != "" {
+ c.storeType = rc.StoreType
+ }
+ if rc.StoreDSN != "" {
+ c.storeDSN = rc.StoreDSN
+ }
+ c.tcEnabled = rc.TCEnabled
+}
+
+func (c *DefaultStateMachineConfig) registerEventConsumers() error {
+ if c.processController == nil {
+ return fmt.Errorf("ProcessController is not initialized")
+ }
+
+ pcImpl, ok := c.processController.(*process_ctrl.ProcessControllerImpl)
+ if !ok {
+ return fmt.Errorf("ProcessController is not an instance of ProcessControllerImpl")
+ }
+
+ if pcImpl.BusinessProcessor() == nil {
+ return fmt.Errorf("BusinessProcessor in ProcessController is not initialized")
+ }
+
+ consumer := process_ctrl.NewProcessCtrlEventConsumer(c.processController)
+
+ c.syncEventBus.RegisterEventConsumer(consumer)
+ c.asyncEventBus.RegisterEventConsumer(consumer)
+
+ return nil
+}
+
+func (c *DefaultStateMachineConfig) Init() error {
+ if err := c.initExpressionComponents(); err != nil {
+ return fmt.Errorf("initialize expression components failed: %w", err)
+ }
+
+ if err := c.initServiceInvokers(); err != nil {
+ return fmt.Errorf("initialize service invokers failed: %w", err)
+ }
+
+ if err := c.bootstrapProcessWiring(); err != nil {
+ return fmt.Errorf("bootstrap wiring failed: %w", err)
+ }
+
+ if err := c.SetupStoresFromConfig(); err != nil {
+ return fmt.Errorf("setup stores from config failed: %w", err)
+ }
+
+ if err := c.registerEventConsumers(); err != nil {
+ return fmt.Errorf("register event consumers failed: %w", err)
+ }
+
+ if c.stateMachineRepository != nil && len(c.stateMachineResources) > 0 {
+ if err := c.RegisterStateMachineDef(c.stateMachineResources); err != nil {
+ return fmt.Errorf("register state machine def failed: %w", err)
+ }
+ }
+
+ if c.tcEnabled {
+ sagarm.InitSaga()
+ app, group := baserm.GetRmAppAndGroup()
+ if app != "" && group != "" {
+ if mgr := baserm.GetRmCacheInstance().GetResourceManager(branch.BranchTypeSAGA); mgr != nil {
+ resource := &sagarm.SagaResource{}
+ resource.SetApplicationId(app)
+ resource.SetResourceGroupId(group)
+ _ = mgr.RegisterResource(resource)
+ }
+ }
+ }
+
+ if err := c.Validate(); err != nil {
+ return fmt.Errorf("configuration validation failed: %w", err)
+ }
+
+ return nil
+}
+
+func (c *DefaultStateMachineConfig) initExpressionComponents() error {
+ if c.expressionFactoryManager == nil {
+ c.expressionFactoryManager = expr.NewExpressionFactoryManager()
+ }
+
+ defaultType := expr.DefaultExpressionType
+ if defaultType == "" {
+ defaultType = "Default"
+ }
+
+ if factory := c.expressionFactoryManager.GetExpressionFactory(defaultType); factory == nil {
+ c.RegisterExpressionFactory(defaultType, expr.NewCELExpressionFactory())
+ }
+
+ if factory := c.expressionFactoryManager.GetExpressionFactory("CEL"); factory == nil {
+ c.RegisterExpressionFactory("CEL", expr.NewCELExpressionFactory())
+ }
+
+ if factory := c.expressionFactoryManager.GetExpressionFactory("el"); factory == nil {
+ c.RegisterExpressionFactory("el", expr.NewCELExpressionFactory())
+ }
+
+ if c.seqGenerator != nil {
+ sequenceFactory := expr.NewSequenceExpressionFactory(c.seqGenerator)
+ c.RegisterExpressionFactory("SEQUENCE", sequenceFactory)
+ c.RegisterExpressionFactory("SEQ", sequenceFactory)
+ }
+
+ if c.expressionResolver == nil {
+ resolver := &expr.DefaultExpressionResolver{}
+ resolver.SetExpressionFactoryManager(*c.expressionFactoryManager)
+ c.expressionResolver = resolver
+ }
+
+ return nil
+}
+
+func (c *DefaultStateMachineConfig) initServiceInvokers() error {
+ if c.serviceInvokerManager == nil {
+ c.serviceInvokerManager = invoker.NewServiceInvokerManagerImpl()
+ }
+
+ if existing := c.serviceInvokerManager.ServiceInvoker("local"); existing == nil {
+ c.RegisterServiceInvoker("local", invoker.NewLocalServiceInvoker())
+ }
+
+ if existing := c.serviceInvokerManager.ServiceInvoker("http"); existing == nil {
+ c.RegisterServiceInvoker("http", invoker.NewHTTPInvoker())
+ }
+
+ if existing := c.serviceInvokerManager.ServiceInvoker("grpc"); existing == nil {
+ c.RegisterServiceInvoker("grpc", invoker.NewGRPCInvoker())
+ }
+
+ if existing := c.serviceInvokerManager.ServiceInvoker("func"); existing == nil {
+ c.RegisterServiceInvoker("func", invoker.NewFuncInvoker())
+ }
+
+ if c.scriptInvokerManager == nil {
+ c.scriptInvokerManager = invoker.NewScriptInvokerManager()
+ }
+
+ if jsInvoker, err := c.scriptInvokerManager.GetInvoker("javascript"); err != nil || jsInvoker == nil {
+ c.scriptInvokerManager.RegisterInvoker(invoker.NewJavaScriptScriptInvoker())
+ }
+
+ return nil
+}
+
+func (c *DefaultStateMachineConfig) Validate() error {
+ var errs []error
+
+ if c.expressionFactoryManager == nil {
+ errs = append(errs, fmt.Errorf("expression factory manager is nil"))
+ }
+ if c.expressionResolver == nil {
+ errs = append(errs, fmt.Errorf("expression resolver is nil"))
+ }
+ if c.serviceInvokerManager == nil {
+ errs = append(errs, fmt.Errorf("service invoker manager is nil"))
+ }
+
+ if c.scriptInvokerManager == nil {
+ errs = append(errs, fmt.Errorf("script invoker manager is nil"))
+ }
+
+ if c.transOperationTimeout <= 0 {
+ errs = append(errs, fmt.Errorf("invalid trans operation timeout: %d", c.transOperationTimeout))
+ }
+ if c.serviceInvokeTimeout <= 0 {
+ errs = append(errs, fmt.Errorf("invalid service invoke timeout: %d", c.serviceInvokeTimeout))
+ }
+ if c.charset == "" {
+ errs = append(errs, fmt.Errorf("charset is empty"))
+ }
+
+ if c.stateMachineRepository != nil {
+ if c.stateLogStore == nil {
+ errs = append(errs, fmt.Errorf("state log store is nil"))
+ }
+ if c.stateLangStore == nil {
+ errs = append(errs, fmt.Errorf("state lang store is nil"))
+ }
+ if c.stateLogRepository == nil {
+ errs = append(errs, fmt.Errorf("state log repository is nil"))
+ }
+ }
+
+ if c.statusDecisionStrategy == nil {
+ errs = append(errs, fmt.Errorf("status decision strategy is nil"))
+ }
+ if c.syncEventBus == nil {
+ errs = append(errs, fmt.Errorf("sync event bus is nil"))
+ }
+ if c.asyncEventBus == nil {
+ errs = append(errs, fmt.Errorf("async event bus is nil"))
+ }
+
+ if len(errs) > 0 {
+ return fmt.Errorf("configuration validation failed with %d errors: %v", len(errs), errs)
+ }
+ return nil
+}
+
+func (c *DefaultStateMachineConfig) EvaluateExpression(expressionStr string, context any) (any, error) {
+ if c.expressionResolver == nil {
+ return nil, fmt.Errorf("expression resolver not initialized")
+ }
+
+ expression := c.expressionResolver.Expression(expressionStr)
+ if expression == nil {
+ return nil, fmt.Errorf("failed to parse expression: %s", expressionStr)
+ }
+
+ var result any
+ var evalErr error
+
+ func() {
+ defer func() {
+ if r := recover(); r != nil {
+ evalErr = fmt.Errorf("expression evaluation panicked: %v", r)
+ }
+ }()
+
+ result = expression.Value(context)
+ }()
+
+ if evalErr != nil {
+ return nil, evalErr
+ }
+
+ if err, ok := result.(error); ok {
+ return nil, fmt.Errorf("expression evaluation returned error: %w", err)
+ }
+
+ return result, nil
+}
+
+func NewDefaultStateMachineConfig(opts ...Option) (*DefaultStateMachineConfig, error) {
+ ctx := context.Background()
+ defaultBP := process_ctrl.NewBusinessProcessor()
+
+ c := &DefaultStateMachineConfig{
+ transOperationTimeout: DefaultTransOperTimeout,
+ serviceInvokeTimeout: DefaultServiceInvokeTimeout,
+ charset: "UTF-8",
+ defaultTenantId: "000001",
+ stateMachineResources: parseEnvResources(),
+ sagaRetryPersistModeUpdate: DefaultClientSagaRetryPersistModeUpdate,
+ sagaCompensatePersistModeUpdate: DefaultClientSagaCompensatePersistModeUpdate,
+ sagaBranchRegisterEnable: DefaultClientSagaBranchRegisterEnable,
+ rmReportSuccessEnable: DefaultClientReportSuccessEnable,
+ componentLock: &sync.Mutex{},
+ seqGenerator: sequence.NewUUIDSeqGenerator(),
+ statusDecisionStrategy: strategy.NewDefaultStatusDecisionStrategy(),
+ processController: func() process_ctrl.ProcessController {
+ pc := &process_ctrl.ProcessControllerImpl{}
+ pc.SetBusinessProcessor(defaultBP)
+ return pc
+ }(),
+ syncEventBus: process_ctrl.NewDirectEventBus(),
+ asyncEventBus: process_ctrl.NewAsyncEventBus(ctx, 1000, 5),
+
+ syncProcessCtrlEventPublisher: nil,
+ asyncProcessCtrlEventPublisher: nil,
+
+ stateLogStore: &NoopStateLogStore{},
+ stateLangStore: &NoopStateLangStore{},
+ }
+
+ c.stateMachineRepository = repository.GetStateMachineRepositoryImpl()
+ c.stateLogRepository = repository.NewStateLogRepositoryImpl()
+ repository.GetStateMachineRepositoryImpl().SetDefaultTenantId(c.defaultTenantId)
+
+ c.syncProcessCtrlEventPublisher = process_ctrl.NewProcessCtrlEventPublisher(c.syncEventBus)
+ c.asyncProcessCtrlEventPublisher = process_ctrl.NewProcessCtrlEventPublisher(c.asyncEventBus)
+
+ for _, opt := range opts {
+ opt(c)
+ }
+ repository.GetStateMachineRepositoryImpl().SetDefaultTenantId(c.defaultTenantId)
+
+ if _, statErr := os.Stat("config.yaml"); statErr == nil {
+ if err := c.LoadConfig("config.yaml"); err == nil {
+ repository.GetStateMachineRepositoryImpl().SetDefaultTenantId(c.defaultTenantId)
+ log.Printf("Successfully loaded config from config.yaml")
+ } else {
+ log.Printf("Failed to load config file (using default/env values): %v", err)
+ }
+ }
+
+ if err := c.Init(); err != nil {
+ return nil, fmt.Errorf("failed to initialize state machine config: %w", err)
+ }
+
+ return c, nil
+}
+
+func parseEnvResources() []string {
+ if env := os.Getenv("SEATA_STATE_MACHINE_RESOURCES"); env != "" {
+ parts := strings.Split(env, ",")
+ var res []string
+ for _, p := range parts {
+ if p = strings.TrimSpace(p); p != "" {
+ res = append(res, p)
+ }
+ }
+ return res
+ }
+ return nil
+}
+
+type Option func(*DefaultStateMachineConfig)
+
+func WithStatusDecisionStrategy(strategy engine.StatusDecisionStrategy) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.statusDecisionStrategy = strategy
+ }
+}
+
+func WithSeqGenerator(gen sequence.SeqGenerator) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.seqGenerator = gen
+ }
+}
+
+func WithProcessController(ctrl process_ctrl.ProcessController) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.processController = ctrl
+ }
+}
+
+func WithBusinessProcessor(bp process_ctrl.BusinessProcessor) Option {
+ return func(c *DefaultStateMachineConfig) {
+ if pc, ok := c.processController.(*process_ctrl.ProcessControllerImpl); ok {
+ pc.SetBusinessProcessor(bp)
+ } else {
+ log.Printf("ProcessController is not of type *ProcessControllerImpl, unable to set BusinessProcessor")
+ }
+ }
+}
+
+func WithStateMachineResources(paths []string) Option {
+ return func(c *DefaultStateMachineConfig) {
+ if len(paths) > 0 {
+ c.stateMachineResources = paths
+ }
+ }
+}
+
+func WithEnableAsync(enable bool) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.enableAsync = enable
+ }
+}
+
+func WithStateLogRepository(logRepo repo.StateLogRepository) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.stateLogRepository = logRepo
+ }
+}
+
+func WithStateLogStore(logStore store.StateLogStore) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.stateLogStore = logStore
+ }
+}
+
+func WithStateLangStore(langStore store.StateLangStore) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.stateLangStore = langStore
+ }
+}
+
+func WithStateMachineRepository(machineRepo repo.StateMachineRepository) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.stateMachineRepository = machineRepo
+ }
+}
+
+func WithConfigPath(path string) Option {
+ return func(c *DefaultStateMachineConfig) {
+ if path == "" {
+ return
+ }
+ if err := c.LoadConfig(path); err != nil {
+ log.Printf("Failed to load config from %s: %v", path, err)
+ } else {
+ log.Printf("Successfully loaded config from %s", path)
+ }
+ }
+}
+
+func WithScriptInvokerManager(scriptManager invoker.ScriptInvokerManager) Option {
+ return func(c *DefaultStateMachineConfig) {
+ c.scriptInvokerManager = scriptManager
+ }
+}
diff --git a/pkg/saga/statemachine/engine/config/default_statemachine_config_test.go b/pkg/saga/statemachine/engine/config/default_statemachine_config_test.go
new file mode 100644
index 00000000..653952e6
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/default_statemachine_config_test.go
@@ -0,0 +1,325 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package config
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+
+ "github.com/pkg/errors"
+
+ "github.com/stretchr/testify/assert"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+)
+
+type TestStateMachineRepositoryMock struct {
+}
+
+func (m *TestStateMachineRepositoryMock) GetStateMachineById(stateMachineId string) (statelang.StateMachine, error) {
+ return nil, errors.New("mock repository error")
+}
+
+func (m *TestStateMachineRepositoryMock) GetStateMachineByNameAndTenantId(stateMachineName string, tenantId string) (statelang.StateMachine, error) {
+ return nil, errors.New("mock repository error")
+}
+
+func (m *TestStateMachineRepositoryMock) GetLastVersionStateMachine(stateMachineName string, tenantId string) (statelang.StateMachine, error) {
+ return nil, errors.New("mock repository error")
+}
+
+func (m *TestStateMachineRepositoryMock) RegistryStateMachine(stateMachine statelang.StateMachine) error {
+ return errors.New("mock registration error")
+}
+
+func (m *TestStateMachineRepositoryMock) RegistryStateMachineByReader(reader io.Reader) error {
+ return errors.New("mock registration error")
+}
+
+func TestDefaultStateMachineConfig_LoadValidJSON(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ testFile := filepath.Join("testdata", "saga_config.json")
+ config, err := NewDefaultStateMachineConfig(WithConfigPath(testFile))
+ assert.NoError(t, err, "Failed to initialize config")
+ assert.NotNil(t, config, "config is nil")
+
+ smo, err := config.stateMachineRepository.GetStateMachineByNameAndTenantId("OrderSaga", config.GetDefaultTenantId())
+ assert.NoError(t, err)
+ assert.NotNil(t, smo, "State machine definition should not be nil")
+
+ assert.Equal(t, "CreateOrder", smo.StartState(), "The start state should be correct")
+ assert.Contains(t, smo.States(), "CreateOrder", "The state node should exist")
+ assert.Equal(t, 30000, config.transOperationTimeout, "The timeout should be read correctly")
+}
+
+func TestDefaultStateMachineConfig_LoadValidYAML(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ testFile := filepath.Join("testdata", "saga_config.yaml")
+ config, err := NewDefaultStateMachineConfig(WithConfigPath(testFile))
+ assert.NoError(t, err, "Failed to initialize config")
+ assert.NotNil(t, config, "config is nil")
+
+ smo, err := config.stateMachineRepository.GetStateMachineByNameAndTenantId("OrderSaga", config.GetDefaultTenantId())
+ assert.NoError(t, err)
+ assert.NotNil(t, smo, "State machine definition should not be nil (YAML)")
+
+ assert.Equal(t, "CreateOrder", smo.StartState(), "The start state should be correct (YAML)")
+ assert.Contains(t, smo.States(), "CreateOrder", "The state node should exist (YAML)")
+ assert.Equal(t, 30000, config.transOperationTimeout, "The timeout should be read correctly (YAML)")
+}
+
+func TestLoadNonExistentFile(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig(WithConfigPath("non_existent.json"))
+ err := config.LoadConfig("non_existent.json")
+ assert.Error(t, err, "Loading a non-existent file should report an error")
+ assert.Contains(t, err.Error(), "failed to read config file")
+}
+
+func TestGetStateMachineDefinition_Exists(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig(WithConfigPath(filepath.Join("testdata", "saga_config.json")))
+
+ smo, err := config.stateMachineRepository.GetStateMachineByNameAndTenantId("OrderSaga", config.GetDefaultTenantId())
+ assert.NoError(t, err)
+ assert.NotNil(t, smo)
+ assert.Equal(t, "1.0", smo.Version(), "The version number should be correct")
+}
+
+func TestGetNonExistentStateMachine(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ smo, err := config.stateMachineRepository.GetStateMachineByNameAndTenantId("NonExistent", config.GetDefaultTenantId())
+ assert.Error(t, err)
+ assert.True(t, smo == nil || reflect.ValueOf(smo).IsZero(), "An unloaded state machine should return nil/zero")
+}
+
+func TestLoadDuplicateStateMachine(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ testFile := filepath.Join("testdata", "saga_config.json")
+ config, err := NewDefaultStateMachineConfig(WithConfigPath(testFile))
+ assert.NoError(t, err)
+
+ err = config.LoadConfig(testFile)
+ assert.NoError(t, err, "First load failed")
+
+ err = config.LoadConfig(testFile)
+ assert.NoError(t, err, "Duplicate load of identical config should not error")
+
+ v2File := filepath.Join("testdata", "saga_config_v2.json")
+ err = config.LoadConfig(v2File)
+ assert.NoError(t, err, "Load of different version should succeed")
+}
+
+func TestRuntimeConfig_OverrideDefaults(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ assert.Equal(t, "UTF-8", config.charset, "The default character set should be UTF-8")
+
+ _, _ = NewDefaultStateMachineConfig(WithConfigPath(filepath.Join("testdata", "order_saga.json")))
+ assert.Equal(t, "UTF-8", config.charset, "If the configuration does not specify, the default value should be used")
+
+ customConfig := &ConfigFileParams{
+ Charset: "GBK",
+ }
+ config.applyConfigFileParams(customConfig)
+ assert.Equal(t, "GBK", config.charset, "Runtime parameters should be correctly overridden")
+}
+
+func TestGetDefaultExpressionFactory(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+
+ factory := config.GetExpressionFactory("el")
+ assert.NotNil(t, factory, "The default EL factory should exist")
+
+ unknownFactory := config.GetExpressionFactory("unknown")
+ assert.Nil(t, unknownFactory, "An unknown expression type should return nil")
+}
+
+func TestGetServiceInvoker(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig(WithConfigPath(filepath.Join("testdata", "saga_config.json")))
+
+ invoker, _ := config.GetServiceInvoker("local")
+ if invoker == nil {
+ t.Errorf("expected non-nil invoker, got nil")
+ }
+}
+
+func TestLoadConfig_InvalidJSON(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ testFile := filepath.Join("testdata", "invalid.json")
+
+ err := config.LoadConfig(testFile)
+ assert.Error(t, err, "Loading an invalid JSON configuration should report an error")
+ assert.Contains(t, err.Error(), "JSON", "The error message should indicate JSON parsing failure")
+}
+
+func TestLoadConfig_InvalidYAML(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ testFile := filepath.Join("testdata", "invalid.yaml")
+
+ err := config.LoadConfig(testFile)
+ assert.Error(t, err, "Loading an invalid YAML configuration should report an error")
+ assert.Contains(t, err.Error(), "yaml", "The error message should indicate YAML parsing failure")
+}
+
+func TestRegisterStateMachineDef_Fail(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ invalidResource := []string{"invalid_path.json"}
+
+ err := config.RegisterStateMachineDef(invalidResource)
+ assert.Error(t, err, "Registering an invalid resource should report an error")
+ assert.Contains(t, err.Error(), "open resource file failed", "The error message should contain file opening failure")
+}
+
+func TestInit_ExpressionFactoryManagerNil(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ config.expressionFactoryManager = nil
+
+ err := config.Init()
+ assert.NoError(t, err, "Initialization should succeed when the expression factory manager is nil")
+}
+
+func TestInit_ServiceInvokerManagerNil(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ config.serviceInvokerManager = nil
+
+ err := config.Init()
+ assert.NoError(t, err, "Initialization should succeed when the service invoker manager is nil")
+}
+
+func TestInit_StateMachineRepositoryNil(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ config.stateMachineRepository = nil
+
+ err := config.Init()
+ assert.NoError(t, err, "Initialization should succeed when the state machine repository is nil")
+}
+
+func TestApplyRuntimeConfig_BoundaryValues(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig()
+ customConfig := &ConfigFileParams{
+ TransOperationTimeout: 1,
+ ServiceInvokeTimeout: 1,
+ }
+ config.applyConfigFileParams(customConfig)
+ assert.Equal(t, 1, config.transOperationTimeout, "The minimum transaction operation timeout should be correctly applied")
+ assert.Equal(t, 1, config.serviceInvokeTimeout, "The minimum service invocation timeout should be correctly applied")
+
+ maxTimeout := int(^uint(0) >> 1)
+ customConfig = &ConfigFileParams{
+ TransOperationTimeout: maxTimeout,
+ ServiceInvokeTimeout: maxTimeout,
+ }
+ config.applyConfigFileParams(customConfig)
+ assert.Equal(t, maxTimeout, config.transOperationTimeout, "The maximum transaction operation timeout should be correctly applied")
+ assert.Equal(t, maxTimeout, config.serviceInvokeTimeout, "The maximum service invocation timeout should be correctly applied")
+}
+
+func TestRegisterStateMachineDef_RepositoryError(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, _ := NewDefaultStateMachineConfig(WithStateMachineRepository(&TestStateMachineRepositoryMock{}))
+ resource := []string{filepath.Join("testdata", "order_saga.json")}
+
+ err := config.RegisterStateMachineDef(resource)
+ assert.Error(t, err, "Registration should fail when the state machine repository reports an error")
+ assert.Contains(t, err.Error(), "register state machine from file failed", "The error message should contain registration failure")
+}
+
+func TestAllComponentsInitialization(t *testing.T) {
+ os.Unsetenv("SEATA_STATE_MACHINE_RESOURCES")
+
+ config, err := NewDefaultStateMachineConfig()
+ assert.NoError(t, err, "Failed to create config instance")
+ assert.NotNil(t, config, "Config instance is nil")
+
+ err = config.Init()
+ assert.NoError(t, err, "Failed to initialize config")
+
+ t.Run("ProcessController", func(t *testing.T) {
+ assert.NotNil(t, config.processController, "Process controller is not initialized")
+ })
+
+ t.Run("EventBusAndPublisher", func(t *testing.T) {
+ assert.NotNil(t, config.syncEventBus, "Sync event bus is not initialized")
+ assert.NotNil(t, config.asyncEventBus, "Async event bus is not initialized")
+ assert.NotNil(t, config.syncProcessCtrlEventPublisher, "Sync event publisher is not initialized")
+ assert.NotNil(t, config.asyncProcessCtrlEventPublisher, "Async event publisher is not initialized")
+ })
+
+ t.Run("StoreComponents", func(t *testing.T) {
+ assert.NotNil(t, config.stateLogRepository, "State log repository is not initialized")
+ assert.NotNil(t, config.stateLogStore, "State log store is not initialized")
+ assert.NotNil(t, config.stateLangStore, "State language store is not initialized")
+ assert.NotNil(t, config.stateMachineRepository, "State machine repository is not initialized")
+ })
+
+ t.Run("ExpressionComponents", func(t *testing.T) {
+ assert.NotNil(t, config.expressionFactoryManager, "Expression factory manager is not initialized")
+ assert.NotNil(t, config.expressionResolver, "Expression resolver is not initialized")
+
+ elFactory := config.expressionFactoryManager.GetExpressionFactory("el")
+ assert.NotNil(t, elFactory, "EL expression factory is not registered")
+ })
+
+ t.Run("InvokerComponents", func(t *testing.T) {
+ assert.NotNil(t, config.serviceInvokerManager, "Service invoker manager is not initialized")
+ assert.NotNil(t, config.scriptInvokerManager, "Script invoker manager is not initialized")
+
+ localInvoker := config.serviceInvokerManager.ServiceInvoker("local")
+ assert.NotNil(t, localInvoker, "Local service invoker is not registered")
+ })
+
+ t.Run("OtherCoreComponents", func(t *testing.T) {
+ assert.NotNil(t, config.statusDecisionStrategy, "Status decision strategy is not initialized")
+ assert.NotNil(t, config.seqGenerator, "Sequence generator is not initialized")
+ assert.NotNil(t, config.componentLock, "Component lock is not initialized")
+
+ testID := config.seqGenerator.GenerateId("test-machine", "test-tenant")
+ assert.NotEmpty(t, testID, "Sequence generator failed to generate ID")
+ })
+}
diff --git a/pkg/saga/statemachine/engine/config/noop_store.go b/pkg/saga/statemachine/engine/config/noop_store.go
new file mode 100644
index 00000000..e2fc7676
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/noop_store.go
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package config
+
+import (
+ "context"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+)
+
+// NoopStateLogStore is a no-op implementation of StateLogStore for out-of-the-box scenarios.
+// All methods perform no actual operations and return nil or zero values to ensure validation passes.
+type NoopStateLogStore struct{}
+
+func (s *NoopStateLogStore) RecordStateMachineStarted(ctx context.Context, machineInstance statelang.StateMachineInstance, pc process_ctrl.ProcessContext) error {
+ return nil
+}
+
+func (s *NoopStateLogStore) RecordStateMachineFinished(ctx context.Context, machineInstance statelang.StateMachineInstance, pc process_ctrl.ProcessContext) error {
+ return nil
+}
+
+func (s *NoopStateLogStore) RecordStateMachineRestarted(ctx context.Context, machineInstance statelang.StateMachineInstance, pc process_ctrl.ProcessContext) error {
+ return nil
+}
+
+func (s *NoopStateLogStore) RecordStateStarted(ctx context.Context, stateInstance statelang.StateInstance, pc process_ctrl.ProcessContext) error {
+ return nil
+}
+
+func (s *NoopStateLogStore) RecordStateFinished(ctx context.Context, stateInstance statelang.StateInstance, pc process_ctrl.ProcessContext) error {
+ return nil
+}
+
+func (s *NoopStateLogStore) GetStateMachineInstance(stateMachineInstanceId string) (statelang.StateMachineInstance, error) {
+ return nil, nil
+}
+
+func (s *NoopStateLogStore) GetStateMachineInstanceByBusinessKey(businessKey string, tenantId string) (statelang.StateMachineInstance, error) {
+ return nil, nil
+}
+
+func (s *NoopStateLogStore) GetStateMachineInstanceByParentId(parentId string) ([]statelang.StateMachineInstance, error) {
+ return nil, nil
+}
+
+func (s *NoopStateLogStore) GetStateInstance(stateInstanceId string, stateMachineInstanceId string) (statelang.StateInstance, error) {
+ return nil, nil
+}
+
+func (s *NoopStateLogStore) GetStateInstanceListByMachineInstanceId(stateMachineInstanceId string) ([]statelang.StateInstance, error) {
+ return nil, nil
+}
+
+func (s *NoopStateLogStore) ClearUp(pc process_ctrl.ProcessContext) {
+ // no-op
+}
+
+type NoopStateLangStore struct{}
+
+func (s *NoopStateLangStore) GetStateMachineById(stateMachineId string) (statelang.StateMachine, error) {
+ return nil, nil
+}
+
+func (s *NoopStateLangStore) GetLastVersionStateMachine(stateMachineName string, tenantId string) (statelang.StateMachine, error) {
+ return nil, nil
+}
+
+func (s *NoopStateLangStore) StoreStateMachine(stateMachine statelang.StateMachine) error {
+ return nil
+}
diff --git a/pkg/saga/statemachine/engine/config/store_bootstrap.go b/pkg/saga/statemachine/engine/config/store_bootstrap.go
new file mode 100644
index 00000000..16d6b259
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/store_bootstrap.go
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package config
+
+import (
+ "database/sql"
+ "fmt"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/repo/repository"
+ dbstore "seata.apache.org/seata-go/pkg/saga/statemachine/store/db"
+ sagaTm "seata.apache.org/seata-go/pkg/saga/tm"
+)
+
+// SetupStoresFromConfig wires DB-backed StateLogStore/StateLangStore and Saga TM template
+// according to runtime options loaded into DefaultStateMachineConfig.
+// It keeps Go style (no DI/SPI), and is safe to call multiple times.
+func (c *DefaultStateMachineConfig) SetupStoresFromConfig() error {
+ if !c.storeEnabled {
+ // keep Noop stores for in-memory usage
+ return nil
+ }
+ if c.storeType == "" || c.storeDSN == "" {
+ return fmt.Errorf("store_enabled=true but store_type/dsn not provided")
+ }
+
+ driver := c.storeType
+ if driver == "sqlite" || driver == "sqlite3" {
+ driver = "sqlite3"
+ }
+
+ db, err := sql.Open(driver, c.storeDSN)
+ if err != nil {
+ return err
+ }
+ if err := db.Ping(); err != nil {
+ return fmt.Errorf("db ping failed: %w", err)
+ }
+
+ // build stores with default table prefix `seata_`
+ lang := dbstore.NewStateLangStore(db, "seata_")
+ logStore := dbstore.NewStateLogStore(db, "seata_")
+
+ // inject transactional template only when tc is enabled
+ if c.tcEnabled {
+ // keep minimal default template; client init is handled externally by user
+ var tmpl sagaTm.SagaTransactionalTemplate = &sagaTm.DefaultSagaTransactionalTemplate{}
+ logStore.SetSagaTransactionalTemplate(tmpl)
+ }
+
+ // set into config & repository
+ c.SetStateLangStore(lang)
+ c.SetStateLogStore(logStore)
+ repository.GetStateMachineRepositoryImpl().SetStateLangStore(lang)
+
+ return nil
+}
diff --git a/pkg/saga/statemachine/engine/config/testdata/invalid.json b/pkg/saga/statemachine/engine/config/testdata/invalid.json
new file mode 100644
index 00000000..7f171d97
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/invalid.json
@@ -0,0 +1,3 @@
+{
+ "name": "John",
+ "age": 30,
\ No newline at end of file
diff --git a/pkg/saga/statemachine/engine/config/testdata/invalid.yaml b/pkg/saga/statemachine/engine/config/testdata/invalid.yaml
new file mode 100644
index 00000000..872937fa
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/invalid.yaml
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+parent:
+ child1: value1
+ child2: value2
+ child3: value3
\ No newline at end of file
diff --git a/pkg/saga/statemachine/engine/config/testdata/order_saga.json b/pkg/saga/statemachine/engine/config/testdata/order_saga.json
new file mode 100644
index 00000000..6c4eb4c8
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/order_saga.json
@@ -0,0 +1,274 @@
+{
+ "Name": "OrderSaga",
+ "Version": "1.0",
+ "StartState": "CreateOrder",
+ "trans_operation_timeout": 30000,
+ "States": {
+ "CreateOrder": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "orderService",
+ "ServiceMethod": "createOrder",
+ "CompensateState": "CancelOrder",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Retry": [
+ {
+ "Exceptions": [
+ "OrderCreationException",
+ "InventoryUnavailableException"
+ ],
+ "IntervalSeconds": 2,
+ "MaxAttempts": 3,
+ "BackoffRate": 1.5
+ }
+ ],
+ "Catches": [
+ {
+ "Exceptions": [
+ "OrderCreationException",
+ "InventoryUnavailableException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.code == 'SUCCESS'": "SUCCEEDED",
+ "return.code == 'FAIL'": "FAILED",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderInfo": "$.orderInfo"
+ }
+ ],
+ "Output": {
+ "orderId": "$.#root"
+ },
+ "Next": "CheckStock",
+ "Loop": {
+ "Parallel": 1,
+ "Collection": "$.orderItems",
+ "ElementVariableName": "item",
+ "ElementIndexName": "index",
+ "CompletionCondition": "[nrOfInstances] == [nrOfCompletedInstances]"
+ }
+ },
+ "CheckStock": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "inventoryService",
+ "ServiceMethod": "checkAvailability",
+ "CompensateState": "RollbackStock",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Retry": [
+ {
+ "Exceptions": [
+ "StockCheckException"
+ ],
+ "IntervalSeconds": 2,
+ "MaxAttempts": 2,
+ "BackoffRate": 1.2
+ }
+ ],
+ "Catches": [
+ {
+ "Exceptions": [
+ "StockCheckException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.available == true": "IN_STOCK",
+ "return.available == false": "OUT_OF_STOCK",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "itemsList": "$.orderItems"
+ }
+ ],
+ "Output": {
+ "stockAvailable": "$.#root"
+ },
+ "Next": "DecideStock"
+ },
+ "DecideStock": {
+ "Type": "Choice",
+ "Choices": [
+ {
+ "Expression": "stockAvailable == true",
+ "Next": "ReserveStock"
+ },
+ {
+ "Expression": "stockAvailable == false",
+ "Next": "CancelOrder"
+ }
+ ],
+ "Default": "ErrorHandler"
+ },
+ "ReserveStock": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "inventoryService",
+ "ServiceMethod": "reserveItems",
+ "CompensateState": "RollbackStock",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Retry": [
+ {
+ "Exceptions": [
+ "StockReservationException"
+ ],
+ "IntervalSeconds": 2,
+ "MaxAttempts": 2,
+ "BackoffRate": 1.2
+ }
+ ],
+ "Catches": [
+ {
+ "Exceptions": [
+ "StockReservationException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.code == 'RESERVED'": "STOCK_RESERVED",
+ "return.code == 'FAILED'": "FAILED",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "itemList": "$.orderItems"
+ }
+ ],
+ "Output": {
+ "stockReservationId": "$.#root"
+ },
+ "Next": "ProcessPayment"
+ },
+ "ProcessPayment": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "paymentService",
+ "ServiceMethod": "processPayment",
+ "CompensateState": "RefundPayment",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Retry": [
+ {
+ "Exceptions": [
+ "PaymentProcessingException"
+ ],
+ "IntervalSeconds": 3,
+ "MaxAttempts": 3,
+ "BackoffRate": 1.5
+ }
+ ],
+ "Catches": [
+ {
+ "Exceptions": [
+ "PaymentProcessingException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.code == 'PAID'": "PAYMENT_SUCCESS",
+ "return.code == 'DECLINED'": "PAYMENT_FAILED",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "amount": "$.orderTotal"
+ }
+ ],
+ "Output": {
+ "paymentTransactionId": "$.#root"
+ },
+ "Next": "CompleteOrder"
+ },
+ "CompleteOrder": {
+ "Type": "Succeed"
+ },
+ "CancelOrder": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "orderService",
+ "ServiceMethod": "cancelOrder",
+ "ForCompensation": true,
+ "ForUpdate": true,
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ }
+ ],
+ "Output": {
+ "cancelResult": "$.#root"
+ },
+ "Next": "RollbackStock"
+ },
+ "RollbackStock": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "inventoryService",
+ "ServiceMethod": "releaseItems",
+ "ForCompensation": true,
+ "ForUpdate": true,
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "stockReservationId": "$.stockReservationId"
+ }
+ ],
+ "Output": {
+ "rollbackResult": "$.#root"
+ },
+ "Next": "RefundPayment"
+ },
+ "RefundPayment": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "paymentService",
+ "ServiceMethod": "refundPayment",
+ "ForCompensation": true,
+ "ForUpdate": true,
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "paymentTransactionId": "$.paymentTransactionId"
+ }
+ ],
+ "Output": {
+ "refundResult": "$.#root"
+ },
+ "Next": "FailState"
+ },
+ "ErrorHandler": {
+ "Type": "Fail",
+ "ErrorCode": "ORDER_PROCESSING_ERROR",
+ "Message": "An unrecoverable error occurred during order processing."
+ },
+ "FailState": {
+ "Type": "Fail",
+ "ErrorCode": "ORDER_CANCELLED",
+ "Message": "The order has been cancelled and compensation actions have been completed."
+ }
+ }
+}
\ No newline at end of file
diff --git a/pkg/saga/statemachine/engine/config/testdata/order_saga.yaml b/pkg/saga/statemachine/engine/config/testdata/order_saga.yaml
new file mode 100644
index 00000000..6f29ec6d
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/order_saga.yaml
@@ -0,0 +1,204 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+Name: "OrderSaga"
+Version: "1.0"
+StartState: "CreateOrder"
+trans_operation_timeout: 30000
+States:
+ CreateOrder:
+ Type: "ServiceTask"
+ ServiceType: "local"
+ ServiceName: "orderService"
+ ServiceMethod: "createOrder"
+ CompensateState: "CancelOrder"
+ ForCompensation: false
+ ForUpdate: false
+ Retry:
+ - Exceptions:
+ - "OrderCreationException"
+ - "InventoryUnavailableException"
+ IntervalSeconds: 2
+ MaxAttempts: 3
+ BackoffRate: 1.5
+ Catches:
+ - Exceptions:
+ - "OrderCreationException"
+ - "InventoryUnavailableException"
+ Next: "ErrorHandler"
+ Status:
+ "return.code == 'SUCCESS'": "SUCCEEDED"
+ "return.code == 'FAIL'": "FAILED"
+ "$exception{*}": "UNKNOWN"
+ Input:
+ - orderInfo: "$.orderInfo"
+ Output:
+ orderId: "$.#root"
+ Next: "CheckStock"
+ Loop:
+ Parallel: 1
+ Collection: "$.orderItems"
+ ElementVariableName: "item"
+ ElementIndexName: "index"
+ CompletionCondition: "[nrOfInstances] == [nrOfCompletedInstances]"
+
+ CheckStock:
+ Type: "ServiceTask"
+ ServiceType: "local"
+ ServiceName: "inventoryService"
+ ServiceMethod: "checkAvailability"
+ CompensateState: "RollbackStock"
+ ForCompensation: false
+ ForUpdate: false
+ Retry:
+ - Exceptions:
+ - "StockCheckException"
+ IntervalSeconds: 2
+ MaxAttempts: 2
+ BackoffRate: 1.2
+ Catches:
+ - Exceptions:
+ - "StockCheckException"
+ Next: "ErrorHandler"
+ Status:
+ "return.available == true": "IN_STOCK"
+ "return.available == false": "OUT_OF_STOCK"
+ "$exception{*}": "UNKNOWN"
+ Input:
+ - orderId: "$.orderId"
+ - itemsList: "$.orderItems"
+ Output:
+ stockAvailable: "$.#root"
+ Next: "DecideStock"
+
+ DecideStock:
+ Type: "Choice"
+ Choices:
+ - Expression: "stockAvailable == true"
+ Next: "ReserveStock"
+ - Expression: "stockAvailable == false"
+ Next: "CancelOrder"
+ Default: "ErrorHandler"
+
+ ReserveStock:
+ Type: "ServiceTask"
+ ServiceType: "local"
+ ServiceName: "inventoryService"
+ ServiceMethod: "reserveItems"
+ CompensateState: "RollbackStock"
+ ForCompensation: false
+ ForUpdate: false
+ Retry:
+ - Exceptions:
+ - "StockReservationException"
+ IntervalSeconds: 2
+ MaxAttempts: 2
+ BackoffRate: 1.2
+ Catches:
+ - Exceptions:
+ - "StockReservationException"
+ Next: "ErrorHandler"
+ Status:
+ "return.code == 'RESERVED'": "STOCK_RESERVED"
+ "return.code == 'FAILED'": "FAILED"
+ "$exception{*}": "UNKNOWN"
+ Input:
+ - orderId: "$.orderId"
+ - itemList: "$.orderItems"
+ Output:
+ stockReservationId: "$.#root"
+ Next: "ProcessPayment"
+
+ ProcessPayment:
+ Type: "ServiceTask"
+ ServiceType: "local"
+ ServiceName: "paymentService"
+ ServiceMethod: "processPayment"
+ CompensateState: "RefundPayment"
+ ForCompensation: false
+ ForUpdate: false
+ Retry:
+ - Exceptions:
+ - "PaymentProcessingException"
+ IntervalSeconds: 3
+ MaxAttempts: 3
+ BackoffRate: 1.5
+ Catches:
+ - Exceptions:
+ - "PaymentProcessingException"
+ Next: "ErrorHandler"
+ Status:
+ "return.code == 'PAID'": "PAYMENT_SUCCESS"
+ "return.code == 'DECLINED'": "PAYMENT_FAILED"
+ "$exception{*}": "UNKNOWN"
+ Input:
+ - orderId: "$.orderId"
+ - amount: "$.orderTotal"
+ Output:
+ paymentTransactionId: "$.#root"
+ Next: "CompleteOrder"
+
+ CompleteOrder:
+ Type: "Succeed"
+
+ CancelOrder:
+ Type: "ServiceTask"
+ ServiceType: "local"
+ ServiceName: "orderService"
+ ServiceMethod: "cancelOrder"
+ ForCompensation: true
+ ForUpdate: true
+ Input:
+ - orderId: "$.orderId"
+ Output:
+ cancelResult: "$.#root"
+ Next: "RollbackStock"
+
+ RollbackStock:
+ Type: "ServiceTask"
+ ServiceType: "local"
+ ServiceName: "inventoryService"
+ ServiceMethod: "releaseItems"
+ ForCompensation: true
+ ForUpdate: true
+ Input:
+ - orderId: "$.orderId"
+ - stockReservationId: "$.stockReservationId"
+ Output:
+ rollbackResult: "$.#root"
+ Next: "RefundPayment"
+
+ RefundPayment:
+ Type: "ServiceTask"
+ ServiceType: "local"
+ ServiceName: "paymentService"
+ ServiceMethod: "refundPayment"
+ ForCompensation: true
+ ForUpdate: true
+ Input:
+ - orderId: "$.orderId"
+ - paymentTransactionId: "$.paymentTransactionId"
+ Output:
+ refundResult: "$.#root"
+ Next: "FailState"
+
+ ErrorHandler:
+ Type: "Fail"
+ ErrorCode: "ORDER_PROCESSING_ERROR"
+ Message: "An unrecoverable error occurred during order processing."
+
+ FailState:
+ Type: "Fail"
+ ErrorCode: "ORDER_CANCELLED"
+ Message: "The order has been cancelled and compensation actions have been completed."
\ No newline at end of file
diff --git a/pkg/saga/statemachine/engine/config/testdata/order_saga_v2.json b/pkg/saga/statemachine/engine/config/testdata/order_saga_v2.json
new file mode 100644
index 00000000..e6a3dd07
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/order_saga_v2.json
@@ -0,0 +1,322 @@
+{
+ "Name": "OrderSaga",
+ "Version": "2.0",
+ "StartState": "ValidateOrder",
+ "trans_operation_timeout": 45000,
+ "States": {
+ "ValidateOrder": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "validationService",
+ "ServiceMethod": "validateOrderParams",
+ "CompensateState": "SkipValidation",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Catches": [
+ {
+ "Exceptions": [
+ "InvalidOrderParamsException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.valid == true": "VALID",
+ "return.valid == false": "INVALID",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderInfo": "$.orderInfo"
+ }
+ ],
+ "Output": {
+ "validationResult": "$.#root"
+ },
+ "Next": "CreateOrder"
+ },
+ "CreateOrder": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "orderService",
+ "ServiceMethod": "createOrder",
+ "CompensateState": "CancelOrder",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Retry": [
+ {
+ "Exceptions": [
+ "OrderCreationException",
+ "InventoryUnavailableException"
+ ],
+ "IntervalSeconds": 3,
+ "MaxAttempts": 4,
+ "BackoffRate": 1.5
+ }
+ ],
+ "Catches": [
+ {
+ "Exceptions": [
+ "OrderCreationException",
+ "InventoryUnavailableException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.code == 'SUCCESS'": "SUCCEEDED",
+ "return.code == 'FAIL'": "FAILED",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderInfo": "$.orderInfo"
+ }
+ ],
+ "Output": {
+ "orderId": "$.#root"
+ },
+ "Next": "CheckStock",
+ "Loop": {
+ "Parallel": 1,
+ "Collection": "$.orderItems",
+ "ElementVariableName": "item",
+ "ElementIndexName": "index",
+ "CompletionCondition": "[nrOfInstances] == [nrOfCompletedInstances]"
+ }
+ },
+ "CheckStock": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "inventoryService",
+ "ServiceMethod": "checkAvailability",
+ "CompensateState": "RollbackStock",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Retry": [
+ {
+ "Exceptions": [
+ "StockCheckException"
+ ],
+ "IntervalSeconds": 2,
+ "MaxAttempts": 2,
+ "BackoffRate": 1.2
+ }
+ ],
+ "Catches": [
+ {
+ "Exceptions": [
+ "StockCheckException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.available == true": "IN_STOCK",
+ "return.available == false": "OUT_OF_STOCK",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "itemsList": "$.orderItems"
+ }
+ ],
+ "Output": {
+ "stockAvailable": "$.#root"
+ },
+ "Next": "DecideStock"
+ },
+ "DecideStock": {
+ "Type": "Choice",
+ "Choices": [
+ {
+ "Expression": "stockAvailable == true",
+ "Next": "ReserveStock"
+ },
+ {
+ "Expression": "stockAvailable == false",
+ "Next": "CancelOrder"
+ }
+ ],
+ "Default": "ErrorHandler"
+ },
+ "ReserveStock": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "inventoryService",
+ "ServiceMethod": "reserveItems",
+ "CompensateState": "RollbackStock",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Retry": [
+ {
+ "Exceptions": [
+ "StockReservationException"
+ ],
+ "IntervalSeconds": 2,
+ "MaxAttempts": 2,
+ "BackoffRate": 1.2
+ }
+ ],
+ "Catches": [
+ {
+ "Exceptions": [
+ "StockReservationException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.code == 'RESERVED'": "STOCK_RESERVED",
+ "return.code == 'FAILED'": "FAILED",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "itemList": "$.orderItems"
+ }
+ ],
+ "Output": {
+ "stockReservationId": "$.#root"
+ },
+ "Next": "ProcessPayment"
+ },
+ "ProcessPayment": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "paymentService",
+ "ServiceMethod": "processPayment",
+ "CompensateState": "RefundPayment",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Retry": [
+ {
+ "Exceptions": [
+ "PaymentProcessingException"
+ ],
+ "IntervalSeconds": 3,
+ "MaxAttempts": 3,
+ "BackoffRate": 1.5
+ }
+ ],
+ "Catches": [
+ {
+ "Exceptions": [
+ "PaymentProcessingException"
+ ],
+ "Next": "ErrorHandler"
+ }
+ ],
+ "Status": {
+ "return.code == 'PAID'": "PAYMENT_SUCCESS",
+ "return.code == 'DECLINED'": "PAYMENT_FAILED",
+ "$exception{*}": "UNKNOWN"
+ },
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "amount": "$.orderTotal"
+ }
+ ],
+ "Output": {
+ "paymentTransactionId": "$.#root"
+ },
+ "Next": "CompleteOrder"
+ },
+ "CompleteOrder": {
+ "Type": "Succeed"
+ },
+ "CancelOrder": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "orderService",
+ "ServiceMethod": "cancelOrder",
+ "ForCompensation": true,
+ "ForUpdate": true,
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ }
+ ],
+ "Output": {
+ "cancelResult": "$.#root"
+ },
+ "Next": "RollbackStock"
+ },
+ "RollbackStock": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "inventoryService",
+ "ServiceMethod": "releaseItems",
+ "ForCompensation": true,
+ "ForUpdate": true,
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "stockReservationId": "$.stockReservationId"
+ }
+ ],
+ "Output": {
+ "rollbackResult": "$.#root"
+ },
+ "Next": "RefundPayment"
+ },
+ "RefundPayment": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "paymentService",
+ "ServiceMethod": "refundPayment",
+ "ForCompensation": true,
+ "ForUpdate": true,
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ },
+ {
+ "paymentTransactionId": "$.paymentTransactionId"
+ }
+ ],
+ "Output": {
+ "refundResult": "$.#root"
+ },
+ "Next": "FailState"
+ },
+ "ErrorHandler": {
+ "Type": "Fail",
+ "ErrorCode": "ORDER_PROCESSING_ERROR_V2",
+ "Message": "v2版本:订单处理发生不可恢复错误"
+ },
+ "FailState": {
+ "Type": "Fail",
+ "ErrorCode": "ORDER_CANCELLED",
+ "Message": "The order has been cancelled and compensation actions have been completed."
+ },
+ "SkipValidation": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "validationService",
+ "ServiceMethod": "skipValidation",
+ "ForCompensation": true,
+ "ForUpdate": true,
+ "Input": [
+ {
+ "orderId": "$.orderId"
+ }
+ ],
+ "Output": {
+ "skipResult": "Validation compensation skipped"
+ },
+ "Next": "CancelOrder"
+ }
+ }
+}
\ No newline at end of file
diff --git a/pkg/saga/statemachine/engine/config/testdata/order_saga_v2.yaml b/pkg/saga/statemachine/engine/config/testdata/order_saga_v2.yaml
new file mode 100644
index 00000000..7ce7bc01
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/order_saga_v2.yaml
@@ -0,0 +1,228 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Name: OrderSaga
+Version: "2.0"
+StartState: ValidateOrder
+trans_operation_timeout: 45000
+States:
+ ValidateOrder:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: validationService
+ ServiceMethod: validateOrderParams
+ CompensateState: SkipValidation
+ ForCompensation: false
+ ForUpdate: false
+ Catches:
+ - Exceptions:
+ - InvalidOrderParamsException
+ Next: ErrorHandler
+ Status:
+ return.valid == true: VALID
+ return.valid == false: INVALID
+ $exception{*}: UNKNOWN
+ Input:
+ - orderInfo: $.orderInfo
+ Output:
+ validationResult: "$.#root"
+ Next: CreateOrder
+ CreateOrder:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: orderService
+ ServiceMethod: createOrder
+ CompensateState: CancelOrder
+ ForCompensation: false
+ ForUpdate: false
+ Retry:
+ - Exceptions:
+ - OrderCreationException
+ - InventoryUnavailableException
+ IntervalSeconds: 3
+ MaxAttempts: 4
+ BackoffRate: 1.5
+ Catches:
+ - Exceptions:
+ - OrderCreationException
+ - InventoryUnavailableException
+ Next: ErrorHandler
+ Status:
+ return.code == 'SUCCESS': SUCCEEDED
+ return.code == 'FAIL': FAILED
+ $exception{*}: UNKNOWN
+ Input:
+ - orderInfo: $.orderInfo
+ Output:
+ orderId: "$.#root"
+ Next: CheckStock
+ Loop:
+ Parallel: 1
+ Collection: $.orderItems
+ ElementVariableName: item
+ ElementIndexName: index
+ CompletionCondition: "[nrOfInstances] == [nrOfCompletedInstances]"
+ CheckStock:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: inventoryService
+ ServiceMethod: checkAvailability
+ CompensateState: RollbackStock
+ ForCompensation: false
+ ForUpdate: false
+ Retry:
+ - Exceptions:
+ - StockCheckException
+ IntervalSeconds: 2
+ MaxAttempts: 2
+ BackoffRate: 1.2
+ Catches:
+ - Exceptions:
+ - StockCheckException
+ Next: ErrorHandler
+ Status:
+ return.available == true: IN_STOCK
+ return.available == false: OUT_OF_STOCK
+ $exception{*}: UNKNOWN
+ Input:
+ - orderId: $.orderId
+ - itemsList: $.orderItems
+ Output:
+ stockAvailable: "$.#root"
+ Next: DecideStock
+ DecideStock:
+ Type: Choice
+ Choices:
+ - Expression: stockAvailable == true
+ Next: ReserveStock
+ - Expression: stockAvailable == false
+ Next: CancelOrder
+ Default: ErrorHandler
+ ReserveStock:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: inventoryService
+ ServiceMethod: reserveItems
+ CompensateState: RollbackStock
+ ForCompensation: false
+ ForUpdate: false
+ Retry:
+ - Exceptions:
+ - StockReservationException
+ IntervalSeconds: 2
+ MaxAttempts: 2
+ BackoffRate: 1.2
+ Catches:
+ - Exceptions:
+ - StockReservationException
+ Next: ErrorHandler
+ Status:
+ return.code == 'RESERVED': STOCK_RESERVED
+ return.code == 'FAILED': FAILED
+ $exception{*}: UNKNOWN
+ Input:
+ - orderId: $.orderId
+ - itemList: $.orderItems
+ Output:
+ stockReservationId: "$.#root"
+ Next: ProcessPayment
+ ProcessPayment:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: paymentService
+ ServiceMethod: processPayment
+ CompensateState: RefundPayment
+ ForCompensation: false
+ ForUpdate: false
+ Retry:
+ - Exceptions:
+ - PaymentProcessingException
+ IntervalSeconds: 3
+ MaxAttempts: 3
+ BackoffRate: 1.5
+ Catches:
+ - Exceptions:
+ - PaymentProcessingException
+ Next: ErrorHandler
+ Status:
+ return.code == 'PAID': PAYMENT_SUCCESS
+ return.code == 'DECLINED': PAYMENT_FAILED
+ $exception{*}: UNKNOWN
+ Input:
+ - orderId: $.orderId
+ - amount: $.orderTotal
+ Output:
+ paymentTransactionId: "$.#root"
+ Next: CompleteOrder
+ CompleteOrder:
+ Type: Succeed
+ CancelOrder:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: orderService
+ ServiceMethod: cancelOrder
+ ForCompensation: true
+ ForUpdate: true
+ Input:
+ - orderId: $.orderId
+ Output:
+ cancelResult: "$.#root"
+ Next: RollbackStock
+ RollbackStock:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: inventoryService
+ ServiceMethod: releaseItems
+ ForCompensation: true
+ ForUpdate: true
+ Input:
+ - orderId: $.orderId
+ - stockReservationId: $.stockReservationId
+ Output:
+ rollbackResult: "$.#root"
+ Next: RefundPayment
+ RefundPayment:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: paymentService
+ ServiceMethod: refundPayment
+ ForCompensation: true
+ ForUpdate: true
+ Input:
+ - orderId: $.orderId
+ - paymentTransactionId: $.paymentTransactionId
+ Output:
+ refundResult: "$.#root"
+ Next: FailState
+ ErrorHandler:
+ Type: Fail
+ ErrorCode: ORDER_PROCESSING_ERROR_V2
+ Message: v2版本:订单处理发生不可恢复错误
+ FailState:
+ Type: Fail
+ ErrorCode: ORDER_CANCELLED
+ Message: The order has been cancelled and compensation actions have been completed.
+ SkipValidation:
+ Type: ServiceTask
+ ServiceType: local
+ ServiceName: validationService
+ ServiceMethod: skipValidation
+ ForCompensation: true
+ ForUpdate: true
+ Input:
+ - orderId: $.orderId
+ Output:
+ skipResult: Validation compensation skipped
+ Next: CancelOrder
diff --git a/pkg/saga/statemachine/engine/config/testdata/saga_config.json b/pkg/saga/statemachine/engine/config/testdata/saga_config.json
new file mode 100644
index 00000000..e87a3eb6
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/saga_config.json
@@ -0,0 +1,13 @@
+{
+ "trans_operation_timeout": 30000,
+ "service_invoke_timeout": 30000,
+ "charset": "UTF-8",
+ "default_tenant_id": "tenant_1",
+ "saga_retry_persist_mode_update": true,
+ "saga_compensate_persist_mode_update": true,
+ "saga_branch_register_enable": true,
+ "rm_report_success_enable": true,
+ "state_machine_resources": [
+ "testdata/order_saga.json"
+ ]
+}
diff --git a/pkg/saga/statemachine/engine/config/testdata/saga_config.yaml b/pkg/saga/statemachine/engine/config/testdata/saga_config.yaml
new file mode 100644
index 00000000..a11ed4f6
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/saga_config.yaml
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+trans_operation_timeout: 30000
+service_invoke_timeout: 30000
+charset: "UTF-8"
+default_tenant_id: "tenant_1"
+saga_retry_persist_mode_update: true
+saga_compensate_persist_mode_update: true
+saga_branch_register_enable: true
+rm_report_success_enable: true
+state_machine_resources:
+ - "testdata/order_saga.json"
\ No newline at end of file
diff --git a/pkg/saga/statemachine/engine/config/testdata/saga_config_v2.json b/pkg/saga/statemachine/engine/config/testdata/saga_config_v2.json
new file mode 100644
index 00000000..5a5b2dd5
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/saga_config_v2.json
@@ -0,0 +1,13 @@
+{
+ "trans_operation_timeout": 30000,
+ "service_invoke_timeout": 30000,
+ "charset": "UTF-8",
+ "default_tenant_id": "tenant_1",
+ "saga_retry_persist_mode_update": true,
+ "saga_compensate_persist_mode_update": true,
+ "saga_branch_register_enable": true,
+ "rm_report_success_enable": true,
+ "state_machine_resources": [
+ "testdata/order_saga_v2.json"
+ ]
+}
\ No newline at end of file
diff --git a/pkg/saga/statemachine/engine/config/testdata/saga_config_v2.yaml b/pkg/saga/statemachine/engine/config/testdata/saga_config_v2.yaml
new file mode 100644
index 00000000..4288389a
--- /dev/null
+++ b/pkg/saga/statemachine/engine/config/testdata/saga_config_v2.yaml
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+trans_operation_timeout: 30000
+service_invoke_timeout: 30000
+charset: UTF-8
+default_tenant_id: tenant_1
+saga_retry_persist_mode_update: true
+saga_compensate_persist_mode_update: true
+saga_branch_register_enable: true
+rm_report_success_enable: true
+state_machine_resources:
+ - testdata/order_saga_v2.json
diff --git a/pkg/saga/statemachine/engine/core/process_ctrl_statemachine_engine.go b/pkg/saga/statemachine/engine/core/process_ctrl_statemachine_engine.go
new file mode 100644
index 00000000..6fc81f08
--- /dev/null
+++ b/pkg/saga/statemachine/engine/core/process_ctrl_statemachine_engine.go
@@ -0,0 +1,747 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package core
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "seata.apache.org/seata-go/pkg/saga/runtime"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/config"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/exception"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/pcext"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/utils"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl/process"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+ tmctx "seata.apache.org/seata-go/pkg/tm"
+ seataErrors "seata.apache.org/seata-go/pkg/util/errors"
+ "seata.apache.org/seata-go/pkg/util/log"
+)
+
+type ProcessCtrlStateMachineEngine struct {
+ StateMachineConfig engine.StateMachineConfig
+}
+
+func NewProcessCtrlStateMachineEngine() (*ProcessCtrlStateMachineEngine, error) {
+ cfg, err := config.NewDefaultStateMachineConfig()
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to create state machine configuration: %w", err)
+ }
+
+ engine := &ProcessCtrlStateMachineEngine{
+ StateMachineConfig: cfg,
+ }
+
+ runtime.SetEngine(engine)
+
+ return engine, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) Start(ctx context.Context, stateMachineName string, tenantId string,
+ startParams map[string]interface{}) (statelang.StateMachineInstance, error) {
+ return p.startInternal(ctx, stateMachineName, tenantId, "", startParams, false, nil)
+}
+
+func (p ProcessCtrlStateMachineEngine) StartAsync(ctx context.Context, stateMachineName string, tenantId string,
+ startParams map[string]interface{}, callback engine.CallBack) (statelang.StateMachineInstance, error) {
+ return p.startInternal(ctx, stateMachineName, tenantId, "", startParams, true, callback)
+}
+
+func (p ProcessCtrlStateMachineEngine) StartWithBusinessKey(ctx context.Context, stateMachineName string,
+ tenantId string, businessKey string, startParams map[string]interface{}) (statelang.StateMachineInstance, error) {
+ return p.startInternal(ctx, stateMachineName, tenantId, businessKey, startParams, false, nil)
+}
+
+func (p ProcessCtrlStateMachineEngine) StartWithBusinessKeyAsync(ctx context.Context, stateMachineName string,
+ tenantId string, businessKey string, startParams map[string]interface{}, callback engine.CallBack) (statelang.StateMachineInstance, error) {
+ return p.startInternal(ctx, stateMachineName, tenantId, businessKey, startParams, true, callback)
+}
+
+func (p ProcessCtrlStateMachineEngine) Forward(ctx context.Context, stateMachineInstId string,
+ replaceParams map[string]interface{}) (statelang.StateMachineInstance, error) {
+ return p.forwardInternal(ctx, stateMachineInstId, replaceParams, false, false, nil)
+}
+
+func (p ProcessCtrlStateMachineEngine) ForwardAsync(ctx context.Context, stateMachineInstId string, replaceParams map[string]interface{}, callback engine.CallBack) (statelang.StateMachineInstance, error) {
+ return p.forwardInternal(ctx, stateMachineInstId, replaceParams, false, true, callback)
+}
+
+func (p ProcessCtrlStateMachineEngine) Compensate(ctx context.Context, stateMachineInstId string,
+ replaceParams map[string]any) (statelang.StateMachineInstance, error) {
+ return p.compensateInternal(ctx, stateMachineInstId, replaceParams, false, nil)
+}
+
+func (p ProcessCtrlStateMachineEngine) CompensateAsync(ctx context.Context, stateMachineInstId string, replaceParams map[string]interface{}, callback engine.CallBack) (statelang.StateMachineInstance, error) {
+ return p.compensateInternal(ctx, stateMachineInstId, replaceParams, true, callback)
+}
+
+func (p ProcessCtrlStateMachineEngine) SkipAndForward(ctx context.Context, stateMachineInstId string, replaceParams map[string]interface{}) (statelang.StateMachineInstance, error) {
+ return p.forwardInternal(ctx, stateMachineInstId, replaceParams, true, false, nil)
+}
+
+func (p ProcessCtrlStateMachineEngine) SkipAndForwardAsync(ctx context.Context, stateMachineInstId string, callback engine.CallBack) (statelang.StateMachineInstance, error) {
+ return p.forwardInternal(ctx, stateMachineInstId, nil, true, true, callback)
+}
+
+func (p ProcessCtrlStateMachineEngine) GetStateMachineConfig() engine.StateMachineConfig {
+ return p.StateMachineConfig
+}
+
+func (p ProcessCtrlStateMachineEngine) ReloadStateMachineInstance(ctx context.Context, instId string) (statelang.StateMachineInstance, error) {
+ inst, err := p.StateMachineConfig.StateLogStore().GetStateMachineInstance(instId)
+ if err != nil {
+ return nil, err
+ }
+ if inst != nil {
+ stateMachine := inst.StateMachine()
+ if stateMachine == nil {
+ stateMachine, err = p.StateMachineConfig.StateMachineRepository().GetStateMachineById(inst.MachineID())
+ if err != nil {
+ return nil, err
+ }
+ inst.SetStateMachine(stateMachine)
+ }
+ if stateMachine == nil {
+ return nil, exception.NewEngineExecutionException(seataErrors.ObjectNotExists,
+ "StateMachine[id:"+inst.MachineID()+"] not exist.", nil)
+ }
+
+ stateList := inst.StateList()
+ if len(stateList) == 0 {
+ stateList, err = p.StateMachineConfig.StateLogStore().GetStateInstanceListByMachineInstanceId(instId)
+ if err != nil {
+ return nil, err
+ }
+ if len(stateList) > 0 {
+ for _, tmpStateInstance := range stateList {
+ inst.PutState(tmpStateInstance.ID(), tmpStateInstance)
+ }
+ }
+ }
+
+ if len(inst.EndParams()) == 0 {
+ endParams, err := p.replayContextVariables(ctx, inst)
+ if err != nil {
+ return nil, err
+ }
+ inst.SetEndParams(endParams)
+ }
+ }
+ return inst, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) startInternal(ctx context.Context, stateMachineName string, tenantId string,
+ businessKey string, startParams map[string]interface{}, async bool, callback engine.CallBack) (statelang.StateMachineInstance, error) {
+ if !tmctx.IsSeataContext(ctx) {
+ ctx = tmctx.InitSeataContext(ctx)
+ }
+
+ if async && !p.StateMachineConfig.EnableAsync() {
+ return nil, exception.NewEngineExecutionException(seataErrors.AsynchronousStartDisabled,
+ "asynchronous start is disabled by configuration (enable_async=false)", nil)
+ }
+
+ if tenantId == "" {
+ tenantId = p.StateMachineConfig.GetDefaultTenantId()
+ }
+
+ stateMachineInstance, err := p.createMachineInstance(stateMachineName, tenantId, businessKey, startParams)
+ if err != nil {
+ return nil, err
+ }
+
+ // Build the process_ctrl context.
+ processContextBuilder := utils.NewProcessContextBuilder().
+ WithProcessType(process.StateLang).
+ WithOperationName(constant.OperationNameStart).
+ WithAsyncCallback(callback).
+ WithInstruction(pcext.NewStateInstruction(stateMachineName, tenantId)).
+ WithStateMachineInstance(stateMachineInstance).
+ WithStateMachineConfig(p.StateMachineConfig).
+ WithStateMachineEngine(p).
+ WithIsAsyncExecution(async)
+
+ contextMap := p.copyMap(startParams)
+
+ stateMachineInstance.SetContext(contextMap)
+
+ processContext := processContextBuilder.WithStateMachineContextVariables(contextMap).Build()
+
+ if stateMachineInstance.StateMachine().IsPersist() && p.StateMachineConfig.StateLogStore() != nil {
+ err := p.StateMachineConfig.StateLogStore().RecordStateMachineStarted(ctx, stateMachineInstance, processContext)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if stateMachineInstance.ID() == "" {
+ stateMachineInstance.SetID(p.StateMachineConfig.SeqGenerator().GenerateId(constant.SeqEntityStateMachineInst, ""))
+ }
+
+ var eventPublisher process_ctrl.EventPublisher
+ if async {
+ eventPublisher = p.StateMachineConfig.AsyncEventPublisher()
+ } else {
+ eventPublisher = p.StateMachineConfig.EventPublisher()
+ }
+
+ _, err = eventPublisher.PushEvent(ctx, processContext)
+ if err != nil {
+ return nil, err
+ }
+
+ return stateMachineInstance, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) forwardInternal(ctx context.Context, stateMachineInstId string,
+ replaceParams map[string]interface{}, skip bool, async bool, callback engine.CallBack) (statelang.StateMachineInstance, error) {
+ stateMachineInstance, err := p.reloadStateMachineInstance(ctx, stateMachineInstId)
+ if err != nil {
+ return nil, err
+ }
+
+ if stateMachineInstance == nil {
+ return nil, exception.NewEngineExecutionException(seataErrors.StateMachineInstanceNotExists, "StateMachineInstance is not exists", nil)
+ }
+
+ if stateMachineInstance.Status() == statelang.SU && stateMachineInstance.CompensationStatus() == "" {
+ return stateMachineInstance, nil
+ }
+
+ acceptStatus := []statelang.ExecutionStatus{statelang.FA, statelang.UN, statelang.RU}
+ if _, err := p.checkStatus(ctx, stateMachineInstance, acceptStatus, nil, stateMachineInstance.Status(), "", "forward"); err != nil {
+ return nil, err
+ }
+
+ actList := stateMachineInstance.StateList()
+ if len(actList) == 0 {
+ return nil, exception.NewEngineExecutionException(seataErrors.OperationDenied,
+ fmt.Sprintf("StateMachineInstance[id:%s] has no stateInstance, please start a new StateMachine execution instead", stateMachineInstId), nil)
+ }
+
+ lastForwardState, err := p.findOutLastForwardStateInstance(actList)
+ if err != nil {
+ return nil, err
+ }
+ if lastForwardState == nil {
+ return nil, exception.NewEngineExecutionException(seataErrors.OperationDenied,
+ fmt.Sprintf("StateMachineInstance[id:%s] Cannot find last forward execution stateInstance", stateMachineInstId), nil)
+ }
+
+ contextBuilder := utils.NewProcessContextBuilder().
+ WithProcessType(process.StateLang).
+ WithOperationName(constant.OperationNameForward).
+ WithAsyncCallback(callback).
+ WithStateMachineInstance(stateMachineInstance).
+ WithStateInstance(lastForwardState).
+ WithStateMachineConfig(p.StateMachineConfig).
+ WithStateMachineEngine(p).
+ WithIsAsyncExecution(async)
+
+ context := contextBuilder.Build()
+
+ contextVariables, err := p.getStateMachineContextVariables(ctx, stateMachineInstance)
+ if err != nil {
+ return nil, err
+ }
+
+ if replaceParams != nil {
+ for k, v := range replaceParams {
+ contextVariables[k] = v
+ }
+ }
+ p.putBusinesskeyToContextariables(stateMachineInstance, contextVariables)
+
+ concurrentContextVariables := p.copyMap(contextVariables)
+
+ context.SetVariable(constant.VarNameStateMachineContext, concurrentContextVariables)
+ stateMachineInstance.SetContext(concurrentContextVariables)
+
+ originStateName := pcext.GetOriginStateName(lastForwardState)
+ lastState := stateMachineInstance.StateMachine().State(originStateName)
+ loop := pcext.GetLoopConfig(ctx, context, lastState)
+ if loop != nil && lastForwardState.Status() == statelang.SU {
+ lastForwardState = p.findOutLastNeedForwardStateInstance(ctx, context)
+ }
+
+ context.SetVariable(lastForwardState.Name()+constant.VarNameRetriedStateInstId, lastForwardState.ID())
+ if lastForwardState.Type() == constant.StateTypeSubStateMachine && lastForwardState.CompensationStatus() != statelang.SU {
+ context.SetVariable(constant.VarNameIsForSubStatMachineForward, true)
+ }
+
+ if lastForwardState.Status() != statelang.SU {
+ lastForwardState.SetIgnoreStatus(true)
+ }
+
+ inst := pcext.NewStateInstruction(stateMachineInstance.StateMachine().Name(), stateMachineInstance.TenantID())
+ if skip || lastForwardState.Status() == statelang.SU {
+ next := ""
+ curState := stateMachineInstance.StateMachine().State(pcext.GetOriginStateName(lastForwardState))
+ if taskState, ok := curState.(*state.AbstractTaskState); ok {
+ next = taskState.Next()
+ }
+ if next == "" {
+ log.Warn(fmt.Sprintf("Last Forward execution StateInstance was succeed, and it has not Next State, skip forward operation"))
+ return stateMachineInstance, nil
+ }
+ inst.SetStateName(next)
+ } else {
+ if lastForwardState.Status() == statelang.RU && !pcext.IsTimeout(lastForwardState.StartedTime(), p.StateMachineConfig.GetServiceInvokeTimeout()) {
+ return nil, exception.NewEngineExecutionException(seataErrors.OperationDenied,
+ fmt.Sprintf("State [%s] is running, operation[forward] denied", lastForwardState.Name()), nil)
+ }
+ inst.SetStateName(pcext.GetOriginStateName(lastForwardState))
+ }
+ context.SetInstruction(inst)
+
+ stateMachineInstance.SetStatus(statelang.RU)
+ stateMachineInstance.SetRunning(true)
+
+ log.Info(fmt.Sprintf("Operation [forward] started stateMachineInstance[id:%s]", stateMachineInstance.ID()))
+
+ if stateMachineInstance.StateMachine().IsPersist() {
+ if err := p.StateMachineConfig.StateLogStore().RecordStateMachineRestarted(ctx, stateMachineInstance, context); err != nil {
+ return nil, err
+ }
+ }
+
+ curState, err := inst.GetState(context)
+ if err != nil {
+ return nil, err
+ }
+ loop = pcext.GetLoopConfig(ctx, context, curState)
+ if loop != nil {
+ inst.SetTemporaryState(state.NewLoopStartStateImpl())
+ }
+
+ if async {
+ if _, err := p.StateMachineConfig.AsyncEventPublisher().PushEvent(ctx, context); err != nil {
+ return nil, err
+ }
+ } else {
+ if _, err := p.StateMachineConfig.EventPublisher().PushEvent(ctx, context); err != nil {
+ return nil, err
+ }
+ }
+
+ return stateMachineInstance, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) findOutLastForwardStateInstance(stateInstanceList []statelang.StateInstance) (statelang.StateInstance, error) {
+ var lastForwardStateInstance statelang.StateInstance
+ var err error
+ for i := len(stateInstanceList) - 1; i >= 0; i-- {
+ stateInstance := stateInstanceList[i]
+ if !stateInstance.IsForCompensation() {
+ if stateInstance.CompensationStatus() == statelang.SU {
+ continue
+ }
+
+ if stateInstance.Type() == constant.StateTypeSubStateMachine {
+ finalState := stateInstance
+ for finalState.StateIDRetriedFor() != "" {
+ if finalState, err = p.StateMachineConfig.StateLogStore().GetStateInstance(finalState.StateIDRetriedFor(),
+ finalState.MachineInstanceID()); err != nil {
+ return nil, err
+ }
+ }
+
+ subInst, _ := p.StateMachineConfig.StateLogStore().GetStateMachineInstanceByParentId(pcext.GenerateParentId(finalState))
+ if len(subInst) > 0 {
+ if subInst[0].CompensationStatus() == statelang.SU {
+ continue
+ }
+
+ if subInst[0].CompensationStatus() == statelang.UN {
+ return nil, exception.NewEngineExecutionException(seataErrors.ForwardInvalid,
+ "Last forward execution state instance is SubStateMachine and compensation status is [UN], Operation[forward] denied, stateInstanceId:"+stateInstance.ID(),
+ nil)
+ }
+ }
+ } else if stateInstance.CompensationStatus() == statelang.UN {
+ return nil, exception.NewEngineExecutionException(seataErrors.ForwardInvalid,
+ "Last forward execution state instance compensation status is [UN], Operation[forward] denied, stateInstanceId:"+stateInstance.ID(),
+ nil)
+ }
+
+ lastForwardStateInstance = stateInstance
+ break
+ }
+ }
+ return lastForwardStateInstance, nil
+}
+
+// copyMap not deep copy, so best practice: Don’t pass by reference
+func (p ProcessCtrlStateMachineEngine) copyMap(startParams map[string]interface{}) map[string]interface{} {
+ copyMap := make(map[string]interface{}, len(startParams))
+ for k, v := range startParams {
+ copyMap[k] = v
+ }
+ return copyMap
+}
+
+func (p ProcessCtrlStateMachineEngine) createMachineInstance(stateMachineName string, tenantId string, businessKey string, startParams map[string]interface{}) (statelang.StateMachineInstance, error) {
+ stateMachine, err := p.StateMachineConfig.StateMachineRepository().GetLastVersionStateMachine(stateMachineName, tenantId)
+ if err != nil {
+ return nil, err
+ }
+
+ if stateMachine == nil {
+ return nil, exception.NewEngineExecutionException(seataErrors.ObjectNotExists,
+ "StateMachine ["+stateMachineName+"] is not exists", nil)
+ }
+
+ stateMachineInstance := statelang.NewStateMachineInstanceImpl()
+ stateMachineInstance.SetStateMachine(stateMachine)
+ stateMachineInstance.SetTenantID(tenantId)
+ stateMachineInstance.SetBusinessKey(businessKey)
+ stateMachineInstance.SetStartParams(startParams)
+ if startParams != nil {
+ if businessKey != "" {
+ startParams[constant.VarNameBusinesskey] = businessKey
+ }
+
+ if parentRaw, exists := startParams[constant.VarNameParentId]; exists {
+ parentId, ok := parentRaw.(string)
+ if !ok {
+ return nil, fmt.Errorf("parentId must be string")
+ }
+ stateMachineInstance.SetParentID(parentId)
+ delete(startParams, constant.VarNameParentId)
+ }
+ }
+
+ stateMachineInstance.SetStatus(statelang.RU)
+ stateMachineInstance.SetRunning(true)
+
+ now := time.Now()
+ stateMachineInstance.SetStartedTime(now)
+ stateMachineInstance.SetUpdatedTime(now)
+ return stateMachineInstance, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) compensateInternal(ctx context.Context, stateMachineInstId string, replaceParams map[string]any,
+ async bool, callback engine.CallBack) (statelang.StateMachineInstance, error) {
+ stateMachineInstance, err := p.reloadStateMachineInstance(ctx, stateMachineInstId)
+ if err != nil {
+ return nil, err
+ }
+
+ if stateMachineInstance == nil {
+ return nil, exception.NewEngineExecutionException(seataErrors.StateMachineInstanceNotExists,
+ "StateMachineInstance is not exits", nil)
+ }
+
+ if statelang.SU == stateMachineInstance.CompensationStatus() {
+ return stateMachineInstance, nil
+ }
+
+ if stateMachineInstance.CompensationStatus() != "" {
+ denyStatus := make([]statelang.ExecutionStatus, 0)
+ denyStatus = append(denyStatus, statelang.SU)
+ p.checkStatus(ctx, stateMachineInstance, nil, denyStatus, "", stateMachineInstance.CompensationStatus(),
+ "compensate")
+ }
+
+ if replaceParams != nil {
+ for key, value := range replaceParams {
+ stateMachineInstance.EndParams()[key] = value
+ }
+ }
+
+ contextBuilder := utils.NewProcessContextBuilder().WithProcessType(process.StateLang).
+ WithOperationName(constant.OperationNameCompensate).WithAsyncCallback(callback).
+ WithStateMachineInstance(stateMachineInstance).
+ WithStateMachineConfig(p.StateMachineConfig).WithStateMachineEngine(p).WithIsAsyncExecution(async)
+
+ context := contextBuilder.Build()
+
+ contextVariables, err := p.getStateMachineContextVariables(ctx, stateMachineInstance)
+ if err != nil {
+ return nil, err
+ }
+
+ if replaceParams != nil {
+ for key, value := range replaceParams {
+ contextVariables[key] = value
+ }
+ }
+
+ p.putBusinesskeyToContextariables(stateMachineInstance, contextVariables)
+
+ // TODO: Here is not use sync.map, make sure whether to use it
+ concurrentContextVariables := make(map[string]any)
+ p.nullSafeCopy(contextVariables, concurrentContextVariables)
+
+ context.SetVariable(constant.VarNameStateMachineContext, concurrentContextVariables)
+ stateMachineInstance.SetContext(concurrentContextVariables)
+
+ tempCompensationTriggerState := state.NewCompensationTriggerStateImpl()
+ tempCompensationTriggerState.SetStateMachine(stateMachineInstance.StateMachine())
+
+ stateMachineInstance.SetRunning(true)
+
+ log.Info("Operation [compensate] start. stateMachineInstance[id:" + stateMachineInstance.ID() + "]")
+
+ if stateMachineInstance.StateMachine().IsPersist() {
+ err := p.StateMachineConfig.StateLogStore().RecordStateMachineRestarted(ctx, stateMachineInstance, context)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ inst := pcext.NewStateInstruction(stateMachineInstance.TenantID(), stateMachineInstance.StateMachine().Name())
+ inst.SetTemporaryState(tempCompensationTriggerState)
+ context.SetInstruction(inst)
+
+ if async {
+ _, err := p.StateMachineConfig.AsyncEventPublisher().PushEvent(ctx, context)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ _, err := p.StateMachineConfig.EventPublisher().PushEvent(ctx, context)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return stateMachineInstance, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) reloadStateMachineInstance(ctx context.Context, instId string) (statelang.StateMachineInstance, error) {
+ instance, err := p.StateMachineConfig.StateLogStore().GetStateMachineInstance(instId)
+ if err != nil {
+ return nil, err
+ }
+ if instance != nil {
+ stateMachine := instance.StateMachine()
+ if stateMachine == nil {
+ stateMachine, err = p.StateMachineConfig.StateMachineRepository().GetStateMachineById(instance.MachineID())
+ if err != nil {
+ return nil, err
+ }
+ instance.SetStateMachine(stateMachine)
+ }
+ if stateMachine == nil {
+ return nil, exception.NewEngineExecutionException(seataErrors.ObjectNotExists,
+ "StateMachine[id:"+instance.MachineID()+"] not exist.", nil)
+ }
+
+ stateList := instance.StateList()
+ if stateList == nil || len(stateList) == 0 {
+ stateList, err = p.StateMachineConfig.StateLogStore().GetStateInstanceListByMachineInstanceId(instId)
+ if err != nil {
+ return nil, err
+ }
+ if stateList != nil && len(stateList) > 0 {
+ for _, tmpStateInstance := range stateList {
+ instance.PutState(tmpStateInstance.ID(), tmpStateInstance)
+ }
+ }
+ }
+
+ if instance.EndParams() == nil || len(instance.EndParams()) == 0 {
+ variables, err := p.replayContextVariables(ctx, instance)
+ if err != nil {
+ return nil, err
+ }
+ instance.SetEndParams(variables)
+ }
+ }
+ return instance, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) replayContextVariables(ctx context.Context, stateMachineInstance statelang.StateMachineInstance) (map[string]any, error) {
+ contextVariables := make(map[string]any)
+ if stateMachineInstance.StartParams() != nil {
+ for key, value := range stateMachineInstance.StartParams() {
+ contextVariables[key] = value
+ }
+ }
+
+ stateInstanceList := stateMachineInstance.StateList()
+ if stateInstanceList == nil || len(stateInstanceList) == 0 {
+ return contextVariables, nil
+ }
+
+ for _, stateInstance := range stateInstanceList {
+ serviceOutputParams := stateInstance.OutputParams()
+ if serviceOutputParams != nil {
+ serviceTaskStateImpl, ok := stateMachineInstance.StateMachine().State(pcext.GetOriginStateName(stateInstance)).(*state.ServiceTaskStateImpl)
+ if !ok {
+ return nil, exception.NewEngineExecutionException(seataErrors.ObjectNotExists,
+ "Cannot find State by state name ["+stateInstance.Name()+"], may be this is a bug", nil)
+ }
+
+ if serviceTaskStateImpl.Output() != nil && len(serviceTaskStateImpl.Output()) != 0 {
+ outputVariablesToContext, err := pcext.CreateOutputParams(p.StateMachineConfig,
+ p.StateMachineConfig.ExpressionResolver(), serviceTaskStateImpl.AbstractTaskState, serviceOutputParams)
+ if err != nil {
+ return nil, exception.NewEngineExecutionException(seataErrors.ObjectNotExists,
+ "Context variable replay failed", err)
+ }
+ if outputVariablesToContext != nil && len(outputVariablesToContext) != 0 {
+ for key, value := range outputVariablesToContext {
+ contextVariables[key] = value
+ }
+ }
+ if len(stateInstance.BusinessKey()) > 0 {
+ contextVariables[serviceTaskStateImpl.Name()+constant.VarNameBusinesskey] = stateInstance.BusinessKey()
+ }
+ }
+ }
+ }
+
+ return contextVariables, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) checkStatus(ctx context.Context, stateMachineInstance statelang.StateMachineInstance,
+ acceptStatus []statelang.ExecutionStatus, denyStatus []statelang.ExecutionStatus, status statelang.ExecutionStatus,
+ compenStatus statelang.ExecutionStatus, operation string) (bool, error) {
+ if status != "" && compenStatus != "" {
+ return false, exception.NewEngineExecutionException(seataErrors.InvalidParameter,
+ "status and compensationStatus are not supported at the same time", nil)
+ }
+ if status == "" && compenStatus == "" {
+ return false, exception.NewEngineExecutionException(seataErrors.InvalidParameter,
+ "status and compensationStatus must input at least one", nil)
+ }
+ if statelang.SU == compenStatus {
+ message := p.buildExceptionMessage(stateMachineInstance, nil, nil, "", statelang.SU, operation)
+ return false, exception.NewEngineExecutionException(seataErrors.OperationDenied,
+ message, nil)
+ }
+
+ if stateMachineInstance.IsRunning() &&
+ !pcext.IsTimeout(stateMachineInstance.UpdatedTime(), p.StateMachineConfig.GetTransOperationTimeout()) {
+ return false, exception.NewEngineExecutionException(seataErrors.OperationDenied,
+ "StateMachineInstance [id:"+stateMachineInstance.ID()+"] is running, operation["+operation+
+ "] denied", nil)
+ }
+
+ if (denyStatus == nil || len(denyStatus) == 0) && (acceptStatus == nil || len(acceptStatus) == 0) {
+ return false, exception.NewEngineExecutionException(seataErrors.InvalidParameter,
+ "StateMachineInstance[id:"+stateMachineInstance.ID()+
+ "], acceptable status and deny status must input at least one", nil)
+ }
+
+ currentStatus := compenStatus
+ if status != "" {
+ currentStatus = status
+ }
+
+ if denyStatus != nil && len(denyStatus) == 0 {
+ for _, tempDenyStatus := range denyStatus {
+ if tempDenyStatus == currentStatus {
+ message := p.buildExceptionMessage(stateMachineInstance, acceptStatus, denyStatus, status,
+ compenStatus, operation)
+ return false, exception.NewEngineExecutionException(seataErrors.OperationDenied,
+ message, nil)
+ }
+ }
+ }
+
+ if acceptStatus == nil || len(acceptStatus) == 0 {
+ return true, nil
+ } else {
+ for _, tempStatus := range acceptStatus {
+ if tempStatus == currentStatus {
+ return true, nil
+ }
+ }
+ }
+
+ message := p.buildExceptionMessage(stateMachineInstance, acceptStatus, denyStatus, status, compenStatus,
+ operation)
+ return false, exception.NewEngineExecutionException(seataErrors.OperationDenied,
+ message, nil)
+}
+
+func (p ProcessCtrlStateMachineEngine) getStateMachineContextVariables(ctx context.Context,
+ stateMachineInstance statelang.StateMachineInstance) (map[string]any, error) {
+ contextVariables := stateMachineInstance.EndParams()
+ if contextVariables == nil || len(contextVariables) == 0 {
+ return p.replayContextVariables(ctx, stateMachineInstance)
+ }
+ return contextVariables, nil
+}
+
+func (p ProcessCtrlStateMachineEngine) buildExceptionMessage(instance statelang.StateMachineInstance,
+ acceptStatus []statelang.ExecutionStatus, denyStatus []statelang.ExecutionStatus, status statelang.ExecutionStatus,
+ compenStatus statelang.ExecutionStatus, operation string) string {
+ message := fmt.Sprintf("StateMachineInstance[id:%s]", instance.ID())
+ if len(acceptStatus) > 0 {
+ message += ",acceptable status :"
+ for _, tempStatus := range acceptStatus {
+ message += string(tempStatus) + " "
+ }
+ }
+
+ if len(denyStatus) > 0 {
+ message += ",deny status:"
+ for _, tempStatus := range denyStatus {
+ message += string(tempStatus) + " "
+ }
+ }
+
+ if status != "" {
+ message += ",current status:" + string(status)
+ }
+
+ if compenStatus != "" {
+ message += ",current compensation status:" + string(compenStatus)
+ }
+
+ message += fmt.Sprintf(",so operation [%s] denied", operation)
+ return message
+}
+
+func (p ProcessCtrlStateMachineEngine) putBusinesskeyToContextariables(instance statelang.StateMachineInstance, variables map[string]any) {
+ if instance.BusinessKey() != "" && variables[constant.VarNameBusinesskey] == "" {
+ variables[constant.VarNameBusinesskey] = instance.BusinessKey()
+ }
+}
+
+func (p ProcessCtrlStateMachineEngine) nullSafeCopy(srcMap map[string]any, destMap map[string]any) {
+ for key, value := range srcMap {
+ if value == nil {
+ destMap[key] = value
+ }
+ }
+}
+
+func (p ProcessCtrlStateMachineEngine) findOutLastNeedForwardStateInstance(ctx context.Context, processContext process_ctrl.ProcessContext) statelang.StateInstance {
+ stateMachineInstance := processContext.GetVariable(constant.VarNameStateMachineInst).(statelang.StateMachineInstance)
+ lastForwardState := processContext.GetVariable(constant.VarNameStateInst).(statelang.StateInstance)
+
+ actList := stateMachineInstance.StateList()
+ for i := len(actList) - 1; i >= 0; i-- {
+ stateInstance := actList[i]
+ if pcext.GetOriginStateName(stateInstance) == pcext.GetOriginStateName(lastForwardState) && stateInstance.Status() != statelang.SU {
+ return stateInstance
+ }
+ }
+ return lastForwardState
+}
diff --git a/pkg/saga/statemachine/engine/core/process_ctrl_statemachine_engine_async_test.go b/pkg/saga/statemachine/engine/core/process_ctrl_statemachine_engine_async_test.go
new file mode 100644
index 00000000..95ecd406
--- /dev/null
+++ b/pkg/saga/statemachine/engine/core/process_ctrl_statemachine_engine_async_test.go
@@ -0,0 +1,345 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package core
+
+import (
+ "context"
+ stderr "errors"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/config"
+ engExc "seata.apache.org/seata-go/pkg/saga/statemachine/engine/exception"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/invoker"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/parser"
+ "seata.apache.org/seata-go/pkg/util/errors"
+)
+
+func TestStartAsyncDisabled(t *testing.T) {
+ cfg, err := config.NewDefaultStateMachineConfig(
+ config.WithEnableAsync(false),
+ config.WithStateMachineResources(nil),
+ )
+ require.NoError(t, err)
+
+ engine := &ProcessCtrlStateMachineEngine{StateMachineConfig: cfg}
+
+ _, err = engine.StartAsync(context.Background(), "dummy", "", nil, nil)
+ require.Error(t, err)
+
+ execErr, ok := engExc.IsEngineExecutionException(err)
+ if !ok {
+ t.Fatalf("unexpected error type %T: %v", err, err)
+ }
+ require.Equal(t, errors.AsynchronousStartDisabled, execErr.Code)
+}
+
+func TestStartAsyncEnabledWithoutStateMachine(t *testing.T) {
+ cfg, err := config.NewDefaultStateMachineConfig(
+ config.WithEnableAsync(true),
+ config.WithStateMachineResources(nil),
+ )
+ require.NoError(t, err)
+
+ engine := &ProcessCtrlStateMachineEngine{StateMachineConfig: cfg}
+
+ _, err = engine.StartAsync(context.Background(), "missing", "", nil, nil)
+ require.Error(t, err)
+
+ execErr, ok := engExc.IsEngineExecutionException(err)
+ if !ok {
+ t.Fatalf("unexpected error type %T: %v", err, err)
+ }
+ require.Equal(t, errors.ObjectNotExists, execErr.Code)
+}
+
+func TestStartAsyncSuccessfulFlow(t *testing.T) {
+ cfg, err := config.NewDefaultStateMachineConfig(
+ config.WithEnableAsync(true),
+ config.WithStateMachineResources(nil),
+ )
+ require.NoError(t, err)
+
+ const simpleStateMachine = `{
+ "Name": "AsyncSimple",
+ "StartState": "Greet",
+ "States": {
+ "Greet": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "asyncTestService",
+ "ServiceMethod": "Hello",
+ "CompensateState": "",
+ "ForCompensation": false,
+ "ForUpdate": false,
+ "Next": "Success"
+ },
+ "Success": {
+ "Type": "Succeed"
+ }
+ }
+ }`
+
+ stateMachine, err := parser.NewJSONStateMachineParser().Parse(simpleStateMachine)
+ require.NoError(t, err)
+ stateMachine.SetTenantId(cfg.GetDefaultTenantId())
+ stateMachine.SetContent(simpleStateMachine)
+ require.NoError(t, cfg.StateMachineRepository().RegistryStateMachine(stateMachine))
+
+ localInvoker, ok := cfg.ServiceInvokerManager().ServiceInvoker("local").(*invoker.LocalServiceInvoker)
+ require.True(t, ok)
+ service := &asyncTestService{}
+ localInvoker.RegisterService("asyncTestService", service)
+
+ callback := newAsyncTestCallback()
+ engine := &ProcessCtrlStateMachineEngine{StateMachineConfig: cfg}
+
+ inst, err := engine.StartAsync(context.Background(), "AsyncSimple", "", nil, callback)
+ require.NoError(t, err)
+ require.NotNil(t, inst)
+ require.Equal(t, "AsyncSimple", inst.StateMachine().Name())
+
+ select {
+ case <-callback.Done():
+ case <-time.After(3 * time.Second):
+ t.Fatal("timed out waiting for async callback")
+ }
+
+ require.NoError(t, callback.Err())
+ require.NotNil(t, callback.Instance())
+ require.Equal(t, statelang.SU, callback.Instance().Status())
+ require.Equal(t, 1, service.Calls())
+}
+
+type asyncTestService struct {
+ mu sync.Mutex
+ calls int
+}
+
+func (s *asyncTestService) Hello() string {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.calls++
+ return "ok"
+}
+
+func (s *asyncTestService) Calls() int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.calls
+}
+
+type asyncTestCallback struct {
+ done chan struct{}
+ once sync.Once
+ mu sync.Mutex
+ inst statelang.StateMachineInstance
+ err error
+}
+
+func newAsyncTestCallback() *asyncTestCallback {
+ return &asyncTestCallback{done: make(chan struct{})}
+}
+
+func (c *asyncTestCallback) OnFinished(ctx context.Context, _ process_ctrl.ProcessContext, stateMachineInstance statelang.StateMachineInstance) {
+ c.mu.Lock()
+ c.inst = stateMachineInstance
+ c.err = nil
+ c.mu.Unlock()
+ c.once.Do(func() { close(c.done) })
+}
+
+func (c *asyncTestCallback) OnError(ctx context.Context, _ process_ctrl.ProcessContext, stateMachineInstance statelang.StateMachineInstance, err error) {
+ c.mu.Lock()
+ c.inst = stateMachineInstance
+ c.err = err
+ c.mu.Unlock()
+ c.once.Do(func() { close(c.done) })
+}
+
+func (c *asyncTestCallback) Done() <-chan struct{} {
+ return c.done
+}
+
+func (c *asyncTestCallback) Err() error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.err
+}
+
+func (c *asyncTestCallback) Instance() statelang.StateMachineInstance {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.inst
+}
+
+// TestStartAsyncWithCompensation tests async state machine with compensation flow
+func TestStartAsyncWithCompensation(t *testing.T) {
+ cfg, err := config.NewDefaultStateMachineConfig(
+ config.WithEnableAsync(true),
+ config.WithStateMachineResources(nil),
+ )
+ require.NoError(t, err)
+
+ const compensationStateMachine = `{
+ "Name": "AsyncCompensation",
+ "StartState": "Task1",
+ "States": {
+ "Task1": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "asyncCompensateService",
+ "ServiceMethod": "DoTask1",
+ "CompensateState": "CompensateTask1",
+ "Next": "Task2"
+ },
+ "Task2": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "asyncCompensateService",
+ "ServiceMethod": "DoTask2Fail",
+ "CompensateState": "CompensateTask2",
+ "Next": "Success",
+ "Catch": [
+ {
+ "Exceptions": ["java.lang.Exception"],
+ "Next": "CompensationTrigger"
+ }
+ ]
+ },
+ "CompensationTrigger": {
+ "Type": "CompensationTrigger",
+ "Next": "Fail"
+ },
+ "CompensateTask1": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "asyncCompensateService",
+ "ServiceMethod": "CompensateTask1",
+ "ForCompensation": true,
+ "Next": "CompensateEnd"
+ },
+ "CompensateTask2": {
+ "Type": "ServiceTask",
+ "ServiceType": "local",
+ "ServiceName": "asyncCompensateService",
+ "ServiceMethod": "CompensateTask2",
+ "ForCompensation": true,
+ "Next": "CompensateEnd"
+ },
+ "CompensateEnd": {
+ "Type": "Succeed"
+ },
+ "Success": {
+ "Type": "Succeed"
+ },
+ "Fail": {
+ "Type": "Fail"
+ }
+ }
+ }`
+
+ stateMachine, err := parser.NewJSONStateMachineParser().Parse(compensationStateMachine)
+ require.NoError(t, err)
+ stateMachine.SetTenantId(cfg.GetDefaultTenantId())
+ stateMachine.SetContent(compensationStateMachine)
+ require.NoError(t, cfg.StateMachineRepository().RegistryStateMachine(stateMachine))
+
+ localInvoker, ok := cfg.ServiceInvokerManager().ServiceInvoker("local").(*invoker.LocalServiceInvoker)
+ require.True(t, ok)
+ service := &asyncCompensateService{}
+ localInvoker.RegisterService("asyncCompensateService", service)
+
+ callback := newAsyncTestCallback()
+ engine := &ProcessCtrlStateMachineEngine{StateMachineConfig: cfg}
+
+ inst, err := engine.StartAsync(context.Background(), "AsyncCompensation", "", nil, callback)
+ require.NoError(t, err)
+ require.NotNil(t, inst)
+ require.Equal(t, "AsyncCompensation", inst.StateMachine().Name())
+
+ select {
+ case <-callback.Done():
+ case <-time.After(5 * time.Second):
+ t.Fatal("timed out waiting for async callback")
+ }
+
+ // Should finish with error due to Task2 failure
+ // Note: In test environment without DB store, compensation flow may not be fully executed
+ require.NotNil(t, callback.Instance())
+ require.NotEqual(t, statelang.SU, callback.Instance().Status(), "Status should not be SU after Task2 fails")
+ require.Equal(t, 1, service.Task1Calls(), "Task1 should be called once")
+ require.Equal(t, 1, service.Task2Calls(), "Task2 should be called once (and fail)")
+}
+
+type asyncCompensateService struct {
+ mu sync.Mutex
+ task1Calls int
+ task2Calls int
+ compensateTask1Calls int
+}
+
+func (s *asyncCompensateService) DoTask1() string {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.task1Calls++
+ return "task1_ok"
+}
+
+func (s *asyncCompensateService) DoTask2Fail() (string, error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.task2Calls++
+ return "", stderr.New("task2_failed")
+}
+
+func (s *asyncCompensateService) CompensateTask1() string {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.compensateTask1Calls++
+ return "compensate_task1_ok"
+}
+
+func (s *asyncCompensateService) CompensateTask2() string {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return "compensate_task2_ok"
+}
+
+func (s *asyncCompensateService) Task1Calls() int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.task1Calls
+}
+
+func (s *asyncCompensateService) Task2Calls() int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.task2Calls
+}
+
+func (s *asyncCompensateService) CompensateTask1Calls() int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ return s.compensateTask1Calls
+}
diff --git a/pkg/saga/statemachine/engine/exception/exception.go b/pkg/saga/statemachine/engine/exception/exception.go
new file mode 100644
index 00000000..29be1e24
--- /dev/null
+++ b/pkg/saga/statemachine/engine/exception/exception.go
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package exception
+
+import (
+ perror "errors"
+ "fmt"
+
+ "seata.apache.org/seata-go/pkg/util/errors"
+)
+
+type EngineExecutionException struct {
+ errors.SeataError
+ stateName string
+ stateMachineName string
+ stateMachineInstanceId string
+ stateInstanceId string
+ ErrCode string
+}
+
+func (e *EngineExecutionException) Error() string {
+ return fmt.Sprintf("EngineExecutionException: %s", e.ErrCode)
+}
+
+func NewEngineExecutionException(code errors.TransactionErrorCode, msg string, parent error) *EngineExecutionException {
+ seataError := errors.New(code, msg, parent)
+ return &EngineExecutionException{
+ SeataError: *seataError,
+ }
+}
+func IsEngineExecutionException(err error) (*EngineExecutionException, bool) {
+ var fie *EngineExecutionException
+ if perror.As(err, &fie) {
+ return fie, true
+ }
+ return nil, false
+}
+
+func (e *EngineExecutionException) StateName() string {
+ return e.stateName
+}
+
+func (e *EngineExecutionException) SetStateName(stateName string) {
+ e.stateName = stateName
+}
+
+func (e *EngineExecutionException) StateMachineName() string {
+ return e.stateMachineName
+}
+
+func (e *EngineExecutionException) SetStateMachineName(stateMachineName string) {
+ e.stateMachineName = stateMachineName
+}
+
+func (e *EngineExecutionException) StateMachineInstanceId() string {
+ return e.stateMachineInstanceId
+}
+
+func (e *EngineExecutionException) SetStateMachineInstanceId(stateMachineInstanceId string) {
+ e.stateMachineInstanceId = stateMachineInstanceId
+}
+
+func (e *EngineExecutionException) StateInstanceId() string {
+ return e.stateInstanceId
+}
+
+func (e *EngineExecutionException) SetStateInstanceId(stateInstanceId string) {
+ e.stateInstanceId = stateInstanceId
+}
diff --git a/pkg/saga/statemachine/engine/exception/exception_test.go b/pkg/saga/statemachine/engine/exception/exception_test.go
new file mode 100644
index 00000000..a6d78400
--- /dev/null
+++ b/pkg/saga/statemachine/engine/exception/exception_test.go
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package exception
+
+import (
+ "errors"
+ "testing"
+
+ pkgerr "seata.apache.org/seata-go/pkg/util/errors"
+)
+
+func TestIsEngineExecutionException(t *testing.T) {
+ cases := []struct {
+ name string
+ err error
+ wantOk bool
+ wantMsg string
+ }{
+ {
+ name: "EngineExecutionException",
+ err: &EngineExecutionException{SeataError: pkgerr.SeataError{Message: "engine error"}},
+ wantOk: true,
+ wantMsg: "engine error",
+ },
+ {
+ name: "Other error",
+ err: errors.New("some other error"),
+ wantOk: false,
+ wantMsg: "",
+ },
+ {
+ name: "nil error",
+ err: nil,
+ wantOk: false,
+ wantMsg: "",
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ fie, ok := IsEngineExecutionException(c.err)
+ if ok != c.wantOk {
+ t.Errorf("expected ok=%v, got %v", c.wantOk, ok)
+ }
+ if ok && fie.SeataError.Message != c.wantMsg {
+ t.Errorf("expected Message=%q, got %q", c.wantMsg, fie.SeataError.Message)
+ }
+ if !ok && fie != nil {
+ t.Errorf("expected fie=nil, got %v", fie)
+ }
+ })
+ }
+}
diff --git a/pkg/rm/init.go b/pkg/saga/statemachine/engine/exception/forward_invalid_exception.go
similarity index 74%
copy from pkg/rm/init.go
copy to pkg/saga/statemachine/engine/exception/forward_invalid_exception.go
index 469cdfaa..ac79903e 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/statemachine/engine/exception/forward_invalid_exception.go
@@ -15,18 +15,18 @@
* limitations under the License.
*/
-package rm
+package exception
-var rmConfig RmConfig
+import "errors"
-type RmConfig struct {
- Config
-
- ApplicationID string
- TxServiceGroup string
+type ForwardInvalidException struct {
+ EngineExecutionException
}
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func IsForwardInvalidException(err error) (*ForwardInvalidException, bool) {
+ var fie *ForwardInvalidException
+ if errors.As(err, &fie) {
+ return fie, true
+ }
+ return nil, false
}
diff --git a/pkg/saga/statemachine/engine/exception/forward_invalid_exception_test.go b/pkg/saga/statemachine/engine/exception/forward_invalid_exception_test.go
new file mode 100644
index 00000000..b2ef6a19
--- /dev/null
+++ b/pkg/saga/statemachine/engine/exception/forward_invalid_exception_test.go
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package exception
+
+import (
+ "errors"
+ "testing"
+
+ pkgerr "seata.apache.org/seata-go/pkg/util/errors"
+)
+
+func TestIsForwardInvalidException(t *testing.T) {
+ cases := []struct {
+ name string
+ err error
+ wantOk bool
+ wantMsg string
+ }{
+ {
+ name: "ForwardInvalidException",
+ err: &ForwardInvalidException{EngineExecutionException: EngineExecutionException{SeataError: pkgerr.SeataError{Message: "forward invalid"}}},
+ wantOk: true,
+ wantMsg: "forward invalid",
+ },
+ {
+ name: "Other error",
+ err: errors.New("some other error"),
+ wantOk: false,
+ wantMsg: "",
+ },
+ {
+ name: "nil error",
+ err: nil,
+ wantOk: false,
+ wantMsg: "",
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ fie, ok := IsForwardInvalidException(c.err)
+ if ok != c.wantOk {
+ t.Errorf("expected ok=%v, got %v", c.wantOk, ok)
+ }
+ if ok && fie.SeataError.Message != c.wantMsg {
+ t.Errorf("expected Message=%q, got %q", c.wantMsg, fie.SeataError.Message)
+ }
+ if !ok && fie != nil {
+ t.Errorf("expected fie=nil, got %v", fie)
+ }
+ })
+ }
+}
diff --git a/pkg/saga/statemachine/engine/expr/cel_expression.go b/pkg/saga/statemachine/engine/expr/cel_expression.go
new file mode 100644
index 00000000..675e0c72
--- /dev/null
+++ b/pkg/saga/statemachine/engine/expr/cel_expression.go
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package expr
+
+import (
+ "github.com/google/cel-go/cel"
+)
+
+type CELExpression struct {
+ env *cel.Env
+ program cel.Program
+ expression string
+}
+
+// This is used to make sure that the CELExpression implements the Expression interface.
+var _ Expression = (*CELExpression)(nil)
+
+// NewCELExpression creates a new CELExpression instance
+// by compiling the provided expression.
+// how to use cel: https://codelabs.developers.google.com/codelabs/cel-go
+func NewCELExpression(expression string) (*CELExpression, error) {
+ // Create the standard environment.
+ env, err := cel.NewEnv(
+ cel.Variable(
+ "elContext", cel.DynType,
+ ),
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ // Check that the expression compiles and returns a String.
+ ast, issues := env.Compile(expression)
+ // Report syntax errors, if present.
+ if issues != nil && issues.Err() != nil {
+ return nil, issues.Err()
+ }
+
+ // Type-check the expression ofr correctness.
+ checkedAst, issues := env.Check(ast)
+ if issues.Err() != nil {
+ return nil, issues.Err()
+ }
+
+ program, err := env.Program(checkedAst)
+ if err != nil {
+ return nil, err
+ }
+
+ CELExpression := &CELExpression{
+ env: env,
+ program: program,
+ expression: expression,
+ }
+
+ return CELExpression, nil
+}
+
+// Value evaluates the expression with the provided context and returns the result.
+func (c *CELExpression) Value(elContext any) any {
+ result, _, err := c.program.Eval(map[string]any{
+ "elContext": elContext,
+ })
+ if err != nil {
+ return err
+ }
+ return result.Value()
+}
+
+// TODO: I think this is not needed.
+// I see seata-java doesn't use this method.
+// Do we need to implement this?
+func (c *CELExpression) SetValue(val any, elContext any) {
+ panic("implement me")
+}
+
+// ExpressionString returns the expression string.
+func (c *CELExpression) ExpressionString() string {
+ return c.expression
+}
diff --git a/pkg/rm/init.go b/pkg/saga/statemachine/engine/expr/cel_expression_factory.go
similarity index 58%
copy from pkg/rm/init.go
copy to pkg/saga/statemachine/engine/expr/cel_expression_factory.go
index 469cdfaa..dcdc7d67 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/statemachine/engine/expr/cel_expression_factory.go
@@ -15,18 +15,22 @@
* limitations under the License.
*/
-package rm
+package expr
-var rmConfig RmConfig
+type CELExpressionFactory struct {
+ expr *CELExpression
+}
-type RmConfig struct {
- Config
+// This is used to make sure that CELExpressionFactory implements ExpressionFactory
+var _ ExpressionFactory = (*CELExpressionFactory)(nil)
- ApplicationID string
- TxServiceGroup string
+// NewCELExpressionFactory creates a new instance of CELExpressionFactory
+func NewCELExpressionFactory() *CELExpressionFactory {
+ return &CELExpressionFactory{}
}
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+// CreateExpression creates a new instance of CELExpression
+func (f *CELExpressionFactory) CreateExpression(expression string) Expression {
+ f.expr, _ = NewCELExpression(expression)
+ return f.expr
}
diff --git a/pkg/rm/init.go b/pkg/saga/statemachine/engine/expr/cel_expression_factory_test.go
similarity index 71%
copy from pkg/rm/init.go
copy to pkg/saga/statemachine/engine/expr/cel_expression_factory_test.go
index 469cdfaa..a84fe48c 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/statemachine/engine/expr/cel_expression_factory_test.go
@@ -15,18 +15,17 @@
* limitations under the License.
*/
-package rm
+package expr
-var rmConfig RmConfig
+import (
+ "testing"
-type RmConfig struct {
- Config
+ "github.com/stretchr/testify/assert"
+)
- ApplicationID string
- TxServiceGroup string
-}
-
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func TestCelExpressionFactory(t *testing.T) {
+ factory := NewCELExpressionFactory()
+ expression := factory.CreateExpression("'Hello' + ' World!'")
+ value := expression.Value(nil)
+ assert.Equal(t, "Hello World!", value, "Expected 'Hello World!'")
}
diff --git a/pkg/rm/init.go b/pkg/saga/statemachine/engine/expr/cel_expression_test.go
similarity index 57%
copy from pkg/rm/init.go
copy to pkg/saga/statemachine/engine/expr/cel_expression_test.go
index 469cdfaa..1c3f05da 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/statemachine/engine/expr/cel_expression_test.go
@@ -15,18 +15,28 @@
* limitations under the License.
*/
-package rm
+package expr
-var rmConfig RmConfig
+import (
+ "testing"
-type RmConfig struct {
- Config
+ "github.com/stretchr/testify/assert"
+)
- ApplicationID string
- TxServiceGroup string
+func TestValueWithNil(t *testing.T) {
+ expr, err := NewCELExpression("'Hello' + ' World!'")
+ assert.NoError(t, err, "Error creating expression")
+ value := expr.Value(nil)
+ assert.Equal(t, "Hello World!", value, "Expected 'Hello World!'")
}
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func TestValue(t *testing.T) {
+ expr, err := NewCELExpression("elContext['name'] + ' World!'")
+ assert.NoError(t, err, "Error creating expression")
+ elContext := map[string]any{
+ "name": "Hello",
+ }
+
+ value := expr.Value(elContext)
+ assert.Equal(t, "Hello World!", value, "Expected 'Hello World!'")
}
diff --git a/pkg/rm/init.go b/pkg/saga/statemachine/engine/expr/error_expression.go
similarity index 58%
copy from pkg/rm/init.go
copy to pkg/saga/statemachine/engine/expr/error_expression.go
index 469cdfaa..fc6a6b00 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/statemachine/engine/expr/error_expression.go
@@ -15,18 +15,24 @@
* limitations under the License.
*/
-package rm
+package expr
-var rmConfig RmConfig
+// ErrorExpression is a placeholder implementation that always reports an error.
+// When parsing/constructing an expression fails, this type is returned directly.
+// The Value() method returns the error as-is, and SetValue() is a no-op.
+type ErrorExpression struct {
+ err error
+ expressionStr string
+}
-type RmConfig struct {
- Config
+func (e *ErrorExpression) Value(elContext any) any {
+ return e.err
+}
- ApplicationID string
- TxServiceGroup string
+func (e *ErrorExpression) SetValue(value any, elContext any) {
+ // No write operation for error expressions
}
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func (e *ErrorExpression) ExpressionString() string {
+ return e.expressionStr
}
diff --git a/pkg/rm/init.go b/pkg/saga/statemachine/engine/expr/expression.go
similarity index 76%
copy from pkg/rm/init.go
copy to pkg/saga/statemachine/engine/expr/expression.go
index 469cdfaa..343ea1be 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/statemachine/engine/expr/expression.go
@@ -15,18 +15,16 @@
* limitations under the License.
*/
-package rm
+package expr
-var rmConfig RmConfig
+// expression interface
+type Expression interface {
+ // get the value of the expression
+ // elContext is the el context
+ Value(elContext any) any
-type RmConfig struct {
- Config
+ SetValue(value any, elContext any)
- ApplicationID string
- TxServiceGroup string
-}
-
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+ // return the expression string
+ ExpressionString() string
}
diff --git a/pkg/discovery/mock/test_etcd_client.go b/pkg/saga/statemachine/engine/expr/expression_factory.go
similarity index 88%
copy from pkg/discovery/mock/test_etcd_client.go
copy to pkg/saga/statemachine/engine/expr/expression_factory.go
index e7cc5fc0..b785775a 100644
--- a/pkg/discovery/mock/test_etcd_client.go
+++ b/pkg/saga/statemachine/engine/expr/expression_factory.go
@@ -15,11 +15,8 @@
* limitations under the License.
*/
-package mock
+package expr
-import "go.etcd.io/etcd/client/v3"
-
-type EtcdClient interface {
- clientv3.KV
- clientv3.Watcher
+type ExpressionFactory interface {
+ CreateExpression(expression string) Expression
}
diff --git a/pkg/saga/statemachine/engine/expr/expression_factory_manager.go b/pkg/saga/statemachine/engine/expr/expression_factory_manager.go
new file mode 100644
index 00000000..17ad4408
--- /dev/null
+++ b/pkg/saga/statemachine/engine/expr/expression_factory_manager.go
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package expr
+
+import (
+ "strings"
+)
+
+const DefaultExpressionType = "Default"
+
+type ExpressionFactoryManager struct {
+ expressionFactoryMap map[string]ExpressionFactory
+}
+
+func NewExpressionFactoryManager() *ExpressionFactoryManager {
+ return &ExpressionFactoryManager{
+ expressionFactoryMap: make(map[string]ExpressionFactory),
+ }
+}
+
+func (e *ExpressionFactoryManager) GetExpressionFactory(expressionType string) ExpressionFactory {
+ if strings.TrimSpace(expressionType) == "" {
+ expressionType = DefaultExpressionType
+ }
+ return e.expressionFactoryMap[expressionType]
+}
+
+func (e *ExpressionFactoryManager) SetExpressionFactoryMap(expressionFactoryMap map[string]ExpressionFactory) {
+ for k, v := range expressionFactoryMap {
+ e.expressionFactoryMap[k] = v
+ }
+}
+
+func (e *ExpressionFactoryManager) PutExpressionFactory(expressionType string, factory ExpressionFactory) {
+ e.expressionFactoryMap[expressionType] = factory
+}
diff --git a/pkg/saga/statemachine/engine/expr/expression_resolver.go b/pkg/saga/statemachine/engine/expr/expression_resolver.go
new file mode 100644
index 00000000..d7841021
--- /dev/null
+++ b/pkg/saga/statemachine/engine/expr/expression_resolver.go
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package expr
+
+import (
+ "errors"
+ "strings"
+)
+
+type ExpressionResolver interface {
+ Expression(expressionStr string) Expression
+ ExpressionFactoryManager() ExpressionFactoryManager
+ SetExpressionFactoryManager(expressionFactoryManager ExpressionFactoryManager)
+}
+
+type DefaultExpressionResolver struct {
+ expressionFactoryManager ExpressionFactoryManager
+}
+
+func (resolver *DefaultExpressionResolver) Expression(expressionStr string) Expression {
+ expressionStruct, err := parseExpressionStruct(expressionStr)
+ if err != nil {
+ return nil
+ }
+ expressionFactory := resolver.expressionFactoryManager.GetExpressionFactory(expressionStruct.typ)
+ if expressionFactory == nil {
+ return nil
+ }
+ return expressionFactory.CreateExpression(expressionStruct.content)
+}
+
+func (resolver *DefaultExpressionResolver) ExpressionFactoryManager() ExpressionFactoryManager {
+ return resolver.expressionFactoryManager
+}
+
+func (resolver *DefaultExpressionResolver) SetExpressionFactoryManager(expressionFactoryManager ExpressionFactoryManager) {
+ resolver.expressionFactoryManager = expressionFactoryManager
+}
+
+type ExpressionStruct struct {
+ typeStart int
+ typeEnd int
+ end int
+ typ string
+ content string
+}
+
+// old style: $type{content}
+// new style: $type.content
+func parseExpressionStruct(expressionStr string) (*ExpressionStruct, error) {
+ eStruct := &ExpressionStruct{}
+ eStruct.typeStart = strings.Index(expressionStr, "$")
+ if eStruct.typeStart == -1 {
+ return nil, errors.New("invalid expression")
+ }
+
+ dot := strings.Index(expressionStr, ".")
+ leftBracket := strings.Index(expressionStr, "{")
+
+ isOldEvaluatorStyle := false
+ if eStruct.typeStart == 0 {
+ if leftBracket < 0 && dot < 0 {
+ return nil, errors.New("invalid expression")
+ }
+ // Backward compatible for structure: $expressionType{expressionContent}
+ if leftBracket > 0 && (leftBracket < dot || dot < 0) {
+ eStruct.typeEnd = leftBracket
+ isOldEvaluatorStyle = true
+ }
+ if dot > 0 && (dot < leftBracket || leftBracket < 0) {
+ eStruct.typeEnd = dot
+ }
+ }
+
+ if eStruct.typeStart == 0 && leftBracket != -1 && leftBracket < dot {
+ // Backward compatible for structure: $expressionType{expressionContent}
+ eStruct.typeEnd = strings.Index(expressionStr, "{")
+ isOldEvaluatorStyle = true
+ }
+
+ eStruct.typ = expressionStr[eStruct.typeStart+1 : eStruct.typeEnd]
+
+ if isOldEvaluatorStyle {
+ eStruct.end = strings.Index(expressionStr, "}")
+ } else {
+ eStruct.end = len(expressionStr)
+ }
+ eStruct.content = expressionStr[eStruct.typeEnd+1 : eStruct.end]
+ return eStruct, nil
+}
diff --git a/pkg/saga/statemachine/engine/expr/expression_resolver_test.go b/pkg/saga/statemachine/engine/expr/expression_resolver_test.go
new file mode 100644
index 00000000..f65f3b6f
--- /dev/null
+++ b/pkg/saga/statemachine/engine/expr/expression_resolver_test.go
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package expr
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseExpressionStruct(t *testing.T) {
+ tests := []struct {
+ expressionStr string
+ expected *ExpressionStruct
+ expectError bool
+ }{
+ {
+ expressionStr: "$type{content}",
+ expected: &ExpressionStruct{
+ typeStart: 0,
+ typeEnd: 5,
+ typ: "type",
+ end: 13,
+ content: "content",
+ },
+ expectError: false,
+ },
+ {
+ expressionStr: "$type.content",
+ expected: &ExpressionStruct{
+ typeStart: 0,
+ typeEnd: 5,
+ typ: "type",
+ end: 13,
+ content: "content",
+ },
+ expectError: false,
+ },
+ {
+ expressionStr: "invalid expression",
+ expected: nil,
+ expectError: true,
+ },
+ }
+ for _, test := range tests {
+ result, err := parseExpressionStruct(test.expressionStr)
+ if test.expectError {
+ assert.Error(t, err, "Expected an error for input '%s'", test.expressionStr)
+ } else {
+ assert.NoError(t, err, "Did not expect an error for input '%s'", test.expressionStr)
+ assert.NotNil(t, result, "Expected a non-nil result for input '%s'", test.expressionStr)
+ assert.Equal(t, *test.expected, *result, "Expected result %+v, got %+v for input '%s'", test.expected, result, test.expressionStr)
+ }
+ }
+}
diff --git a/pkg/saga/statemachine/engine/expr/sequence_expression.go b/pkg/saga/statemachine/engine/expr/sequence_expression.go
new file mode 100644
index 00000000..61afba1c
--- /dev/null
+++ b/pkg/saga/statemachine/engine/expr/sequence_expression.go
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package expr
+
+import (
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/sequence"
+)
+
+type SequenceExpression struct {
+ seqGenerator sequence.SeqGenerator
+ entity string
+ rule string
+}
+
+func (s *SequenceExpression) SeqGenerator() sequence.SeqGenerator {
+ return s.seqGenerator
+}
+
+func (s *SequenceExpression) SetSeqGenerator(seqGenerator sequence.SeqGenerator) {
+ s.seqGenerator = seqGenerator
+}
+
+func (s *SequenceExpression) Entity() string {
+ return s.entity
+}
+
+func (s *SequenceExpression) SetEntity(entity string) {
+ s.entity = entity
+}
+
+func (s *SequenceExpression) Rule() string {
+ return s.rule
+}
+
+func (s *SequenceExpression) SetRule(rule string) {
+ s.rule = rule
+}
+
+func (s SequenceExpression) Value(elContext any) any {
+ return s.seqGenerator.GenerateId(s.entity, s.rule)
+}
+
+func (s SequenceExpression) SetValue(value any, elContext any) {
+
+}
+
+func (s SequenceExpression) ExpressionString() string {
+ return s.entity + "|" + s.rule
+}
diff --git a/pkg/saga/statemachine/engine/expr/sequence_expression_factory.go b/pkg/saga/statemachine/engine/expr/sequence_expression_factory.go
new file mode 100644
index 00000000..df3e6e76
--- /dev/null
+++ b/pkg/saga/statemachine/engine/expr/sequence_expression_factory.go
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package expr
+
+import (
+ "fmt"
+ "strings"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/sequence"
+)
+
+// SequenceExpressionFactory implements the ExpressionFactory interface,
+// designed to parse strings in the format "entity|rule" and create SequenceExpression instances.
+// If the format is invalid, it returns an *ErrorExpression containing the parsing error.
+type SequenceExpressionFactory struct {
+ seqGenerator sequence.SeqGenerator
+}
+
+func NewSequenceExpressionFactory(seqGenerator sequence.SeqGenerator) *SequenceExpressionFactory {
+ return &SequenceExpressionFactory{seqGenerator: seqGenerator}
+}
+
+// CreateExpression parses the input string into a SequenceExpression.
+// The input must be in the format "entity|rule". If the format is invalid,
+// it returns an ErrorExpression with a descriptive error message.
+func (f *SequenceExpressionFactory) CreateExpression(expression string) Expression {
+ parts := strings.Split(expression, "|")
+ if len(parts) != 2 {
+ return &ErrorExpression{
+ err: fmt.Errorf("invalid sequence expression format: %s, expected 'entity|rule'", expression),
+ expressionStr: expression,
+ }
+ }
+
+ seqExpr := &SequenceExpression{
+ seqGenerator: f.seqGenerator,
+ entity: strings.TrimSpace(parts[0]),
+ rule: strings.TrimSpace(parts[1]),
+ }
+ return seqExpr
+}
diff --git a/pkg/saga/statemachine/engine/invoker/func_invoker.go b/pkg/saga/statemachine/engine/invoker/func_invoker.go
new file mode 100644
index 00000000..6a14c7b5
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/func_invoker.go
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+ "seata.apache.org/seata-go/pkg/util/log"
+)
+
+type FuncInvoker struct {
+ ServicesMapLock sync.Mutex
+ servicesMap map[string]FuncService
+}
+
+func NewFuncInvoker() *FuncInvoker {
+ return &FuncInvoker{
+ servicesMap: make(map[string]FuncService),
+ }
+}
+
+func (f *FuncInvoker) RegisterService(serviceName string, service FuncService) {
+ f.ServicesMapLock.Lock()
+ defer f.ServicesMapLock.Unlock()
+ f.servicesMap[serviceName] = service
+}
+
+func (f *FuncInvoker) GetService(serviceName string) FuncService {
+ f.ServicesMapLock.Lock()
+ defer f.ServicesMapLock.Unlock()
+ return f.servicesMap[serviceName]
+}
+
+func (f *FuncInvoker) Invoke(ctx context.Context, input []any, service state.ServiceTaskState) (output []reflect.Value, err error) {
+ serviceTaskStateImpl := service.(*state.ServiceTaskStateImpl)
+ FuncService := f.GetService(serviceTaskStateImpl.ServiceName())
+ if FuncService == nil {
+ return nil, errors.New("no func service " + serviceTaskStateImpl.ServiceName() + " for service task state")
+ }
+
+ if serviceTaskStateImpl.IsAsync() {
+ go func() {
+ _, err := FuncService.CallMethod(serviceTaskStateImpl, input)
+ if err != nil {
+ log.Errorf("invoke Service[%s].%s failed, err is %s", serviceTaskStateImpl.ServiceName(), serviceTaskStateImpl.ServiceMethod(), err.Error())
+ }
+ }()
+ return nil, nil
+ }
+
+ return FuncService.CallMethod(serviceTaskStateImpl, input)
+}
+
+func (f *FuncInvoker) Close(ctx context.Context) error {
+ return nil
+}
+
+type FuncService interface {
+ CallMethod(ServiceTaskStateImpl *state.ServiceTaskStateImpl, input []any) ([]reflect.Value, error)
+}
+
+type FuncServiceImpl struct {
+ serviceName string
+ methodLock sync.Mutex
+ method any
+}
+
+func NewFuncService(serviceName string, method any) *FuncServiceImpl {
+ return &FuncServiceImpl{
+ serviceName: serviceName,
+ method: method,
+ }
+}
+
+func (f *FuncServiceImpl) getMethod(serviceTaskStateImpl *state.ServiceTaskStateImpl) (*reflect.Value, error) {
+ method := serviceTaskStateImpl.Method()
+ if method == nil {
+ return f.initMethod(serviceTaskStateImpl)
+ }
+ return method, nil
+}
+
+func (f *FuncServiceImpl) prepareArguments(input []any) []reflect.Value {
+ args := make([]reflect.Value, len(input))
+ for i, arg := range input {
+ args[i] = reflect.ValueOf(arg)
+ }
+ return args
+}
+
+func (f *FuncServiceImpl) CallMethod(serviceTaskStateImpl *state.ServiceTaskStateImpl, input []any) ([]reflect.Value, error) {
+ method, err := f.getMethod(serviceTaskStateImpl)
+ if err != nil {
+ return nil, err
+ }
+
+ args := f.prepareArguments(input)
+
+ retryCountMap := make(map[state.Retry]int)
+ for {
+ res, err, shouldRetry := f.invokeMethod(method, args, serviceTaskStateImpl, retryCountMap)
+
+ if !shouldRetry {
+ if err != nil {
+ return nil, errors.New("invoke service[" + serviceTaskStateImpl.ServiceName() + "]." + serviceTaskStateImpl.ServiceMethod() + " failed, err is " + err.Error())
+ }
+ return res, nil
+ }
+ }
+}
+
+func (f *FuncServiceImpl) initMethod(serviceTaskStateImpl *state.ServiceTaskStateImpl) (*reflect.Value, error) {
+ methodName := serviceTaskStateImpl.ServiceMethod()
+ f.methodLock.Lock()
+ defer f.methodLock.Unlock()
+ methodValue := reflect.ValueOf(f.method)
+ if methodValue.IsZero() {
+ return nil, errors.New("invalid method when func call, serviceName: " + f.serviceName)
+ }
+
+ if methodValue.Kind() == reflect.Func {
+ serviceTaskStateImpl.SetMethod(&methodValue)
+ return &methodValue, nil
+ }
+
+ method := methodValue.MethodByName(methodName)
+ if method.IsZero() {
+ return nil, errors.New("invalid method name when func call, serviceName: " + f.serviceName + ", methodName: " + methodName)
+ }
+ serviceTaskStateImpl.SetMethod(&method)
+ return &method, nil
+}
+
+func (f *FuncServiceImpl) invokeMethod(method *reflect.Value, args []reflect.Value, serviceTaskStateImpl *state.ServiceTaskStateImpl, retryCountMap map[state.Retry]int) ([]reflect.Value, error, bool) {
+ var res []reflect.Value
+ var resErr error
+ var shouldRetry bool
+
+ defer func() {
+ if r := recover(); r != nil {
+ errStr := fmt.Sprintf("%v", r)
+ retry := f.matchRetry(serviceTaskStateImpl, errStr)
+ resErr = errors.New(errStr)
+ if retry != nil {
+ shouldRetry = f.needRetry(serviceTaskStateImpl, retryCountMap, retry, resErr)
+ }
+ }
+ }()
+
+ outs := method.Call(args)
+ if err, ok := outs[len(outs)-1].Interface().(error); ok {
+ resErr = err
+ errStr := err.Error()
+ retry := f.matchRetry(serviceTaskStateImpl, errStr)
+ if retry != nil {
+ shouldRetry = f.needRetry(serviceTaskStateImpl, retryCountMap, retry, resErr)
+ }
+ return nil, resErr, shouldRetry
+ }
+
+ res = outs
+ return res, nil, false
+}
+
+func (f *FuncServiceImpl) matchRetry(impl *state.ServiceTaskStateImpl, str string) state.Retry {
+ if impl.Retry() != nil {
+ for _, retry := range impl.Retry() {
+ if retry.Exceptions() != nil {
+ for _, exception := range retry.Exceptions() {
+ if strings.Contains(str, exception) {
+ return retry
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (f *FuncServiceImpl) needRetry(impl *state.ServiceTaskStateImpl, countMap map[state.Retry]int, retry state.Retry, err error) bool {
+ attempt, exist := countMap[retry]
+ if !exist {
+ countMap[retry] = 0
+ }
+
+ if attempt >= retry.MaxAttempt() {
+ return false
+ }
+
+ interval := retry.IntervalSecond()
+ backoffRate := retry.BackoffRate()
+ curInterval := int64(interval * 1000)
+ if attempt != 0 {
+ curInterval = int64(interval * backoffRate * float64(attempt) * 1000)
+ }
+
+ log.Warnf("invoke service[%s.%s] failed, will retry after %s millis, current retry count: %s, current err: %s",
+ impl.ServiceName(), impl.ServiceMethod(), curInterval, attempt, err)
+
+ time.Sleep(time.Duration(curInterval) * time.Millisecond)
+ countMap[retry] = attempt + 1
+ return true
+}
diff --git a/pkg/saga/statemachine/engine/invoker/func_invoker_test.go b/pkg/saga/statemachine/engine/invoker/func_invoker_test.go
new file mode 100644
index 00000000..2f8432b8
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/func_invoker_test.go
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+)
+
+// struct's method test
+type mockFuncImpl struct {
+ invokeCount int
+}
+
+func (m *mockFuncImpl) SayHelloRight(word string) (string, error) {
+ m.invokeCount++
+ fmt.Println("invoke right")
+ return word, nil
+}
+
+func (m *mockFuncImpl) SayHelloRightLater(word string, delay int) (string, error) {
+ m.invokeCount++
+ if delay == m.invokeCount {
+ fmt.Println("invoke right")
+ return word, nil
+ }
+ fmt.Println("invoke fail")
+ return "", errors.New("invoke failed")
+}
+
+func TestFuncInvokerInvokeSucceed(t *testing.T) {
+ tests := []struct {
+ name string
+ input []any
+ taskState state.ServiceTaskState
+ expected string
+ expectErr bool
+ }{
+ {
+ name: "Invoke Struct Succeed",
+ input: []any{"hello"},
+ taskState: newFuncHelloServiceTaskState(),
+ expected: "hello",
+ expectErr: false,
+ },
+ {
+ name: "Invoke Struct In Retry",
+ input: []any{"hello", 2},
+ taskState: newFuncHelloServiceTaskStateWithRetry(),
+ expected: "hello",
+ expectErr: false,
+ },
+ }
+
+ ctx := context.Background()
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ invoker := newFuncServiceInvoker()
+ values, err := invoker.Invoke(ctx, tt.input, tt.taskState)
+
+ if (err != nil) != tt.expectErr {
+ t.Errorf("expected error: %v, got: %v", tt.expectErr, err)
+ }
+
+ if values == nil || len(values) == 0 {
+ t.Fatal("no value in values")
+ }
+
+ if resultString, ok := values[0].Interface().(string); ok {
+ if resultString != tt.expected {
+ t.Errorf("expect %s, but got %s", tt.expected, resultString)
+ }
+ } else {
+ t.Errorf("expected string, but got %v", values[0].Interface())
+ }
+
+ if resultError, ok := values[1].Interface().(error); ok {
+ if resultError != nil {
+ t.Errorf("expect nil, but got %s", resultError)
+ }
+ }
+ })
+ }
+}
+
+func TestFuncInvokerInvokeFailed(t *testing.T) {
+ tests := []struct {
+ name string
+ input []any
+ taskState state.ServiceTaskState
+ expected string
+ expectErr bool
+ }{
+ {
+ name: "Invoke Struct Failed In Retry",
+ input: []any{"hello", 5},
+ taskState: newFuncHelloServiceTaskStateWithRetry(),
+ expected: "",
+ expectErr: true,
+ },
+ }
+
+ ctx := context.Background()
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ invoker := newFuncServiceInvoker()
+ _, err := invoker.Invoke(ctx, tt.input, tt.taskState)
+
+ if (err != nil) != tt.expectErr {
+ t.Errorf("expected error: %v, got: %v", tt.expectErr, err)
+ }
+ })
+ }
+}
+
+func newFuncServiceInvoker() ServiceInvoker {
+ mockFuncInvoker := NewFuncInvoker()
+ mockFuncService := &mockFuncImpl{}
+ mockService := NewFuncService("hello", mockFuncService)
+ mockFuncInvoker.RegisterService("hello", mockService)
+ return mockFuncInvoker
+}
+
+func newFuncHelloServiceTaskState() state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName("hello")
+ serviceTaskStateImpl.SetIsAsync(false)
+ serviceTaskStateImpl.SetServiceName("hello")
+ serviceTaskStateImpl.SetServiceType("func")
+ serviceTaskStateImpl.SetServiceMethod("SayHelloRight")
+ return serviceTaskStateImpl
+}
+
+func newFuncHelloServiceTaskStateWithRetry() state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName("hello")
+ serviceTaskStateImpl.SetIsAsync(false)
+ serviceTaskStateImpl.SetServiceName("hello")
+ serviceTaskStateImpl.SetServiceType("func")
+ serviceTaskStateImpl.SetServiceMethod("SayHelloRightLater")
+
+ retryImpl := &state.RetryImpl{}
+ retryImpl.SetExceptions([]string{"fail"})
+ retryImpl.SetIntervalSecond(1)
+ retryImpl.SetMaxAttempt(3)
+ retryImpl.SetBackoffRate(0.9)
+ serviceTaskStateImpl.SetRetry([]state.Retry{retryImpl})
+ return serviceTaskStateImpl
+}
diff --git a/pkg/saga/statemachine/engine/invoker/grpc_invoker.go b/pkg/saga/statemachine/engine/invoker/grpc_invoker.go
new file mode 100644
index 00000000..dcc921f2
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/grpc_invoker.go
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "google.golang.org/grpc"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+ "seata.apache.org/seata-go/pkg/util/log"
+)
+
+type GRPCInvoker struct {
+ clients map[string]GRPCClient
+ clientsMapLock sync.Mutex
+ needClose bool
+}
+
+func NewGRPCInvoker() *GRPCInvoker {
+ return &GRPCInvoker{
+ clients: make(map[string]GRPCClient),
+ }
+}
+
+func (g *GRPCInvoker) NeedClose() bool {
+ return g.needClose
+}
+
+func (g *GRPCInvoker) SetNeedClose(needClose bool) {
+ g.needClose = needClose
+}
+
+func (g *GRPCInvoker) RegisterClient(serviceName string, client GRPCClient) {
+ g.clientsMapLock.Lock()
+ defer g.clientsMapLock.Unlock()
+
+ g.clients[serviceName] = client
+}
+
+func (g *GRPCInvoker) GetClient(serviceName string) GRPCClient {
+ g.clientsMapLock.Lock()
+ defer g.clientsMapLock.Unlock()
+
+ if client, ok := g.clients[serviceName]; ok {
+ return client
+ }
+
+ return nil
+}
+
+func (g *GRPCInvoker) Invoke(ctx context.Context, input []any, service state.ServiceTaskState) (output []reflect.Value, err error) {
+ serviceTaskStateImpl := service.(*state.ServiceTaskStateImpl)
+ client := g.GetClient(serviceTaskStateImpl.ServiceName())
+ if client == nil {
+ return nil, errors.New(fmt.Sprintf("no grpc client %s for service task state", serviceTaskStateImpl.ServiceName()))
+ }
+
+ // context is the first arg in grpc client method
+ input = append([]any{ctx}, input...)
+ if serviceTaskStateImpl.IsAsync() {
+ go func() {
+ _, err := client.CallMethod(serviceTaskStateImpl, input)
+ if err != nil {
+ log.Errorf("invoke Service[%s].%s failed, err is %s", serviceTaskStateImpl.ServiceName(),
+ serviceTaskStateImpl.ServiceMethod(), err)
+ }
+ }()
+ return nil, nil
+ } else {
+ return client.CallMethod(serviceTaskStateImpl, input)
+ }
+}
+
+func (g *GRPCInvoker) Close(ctx context.Context) error {
+ if g.needClose {
+ for _, client := range g.clients {
+ err := client.CloseConnection()
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+type GRPCClient interface {
+ CallMethod(serviceTaskStateImpl *state.ServiceTaskStateImpl, input []any) ([]reflect.Value, error)
+
+ CloseConnection() error
+}
+
+type GPRCClientImpl struct {
+ serviceName string
+ client any
+ connection *grpc.ClientConn
+ methodLock sync.Mutex
+}
+
+func NewGRPCClient(serviceName string, client any, connection *grpc.ClientConn) *GPRCClientImpl {
+ return &GPRCClientImpl{
+ serviceName: serviceName,
+ client: client,
+ connection: connection,
+ }
+}
+
+func (g *GPRCClientImpl) CallMethod(serviceTaskStateImpl *state.ServiceTaskStateImpl, input []any) ([]reflect.Value, error) {
+
+ if serviceTaskStateImpl.Method() == nil {
+ err := g.initMethod(serviceTaskStateImpl)
+ if err != nil {
+ return nil, err
+ }
+ }
+ method := serviceTaskStateImpl.Method()
+
+ args := make([]reflect.Value, 0, len(input))
+ for _, arg := range input {
+ args = append(args, reflect.ValueOf(arg))
+ }
+
+ retryCountMap := make(map[state.Retry]int)
+ for {
+ res, err, shouldRetry := func() (res []reflect.Value, resErr error, shouldRetry bool) {
+ defer func() {
+ // err may happen in the method invoke (panic) and method return, we try to find err and use it to decide retry by
+ // whether contains exception or not
+ if r := recover(); r != nil {
+ errStr := fmt.Sprintf("%v", r)
+ retry := g.matchRetry(serviceTaskStateImpl, errStr)
+ res = nil
+ resErr = errors.New(errStr)
+
+ if retry == nil {
+ return
+ }
+ shouldRetry = g.needRetry(serviceTaskStateImpl, retryCountMap, retry, resErr)
+ return
+ }
+ }()
+
+ outs := method.Call(args)
+ // err is the last arg in grpc client method
+ if err, ok := outs[len(outs)-1].Interface().(error); ok {
+ errStr := err.Error()
+ retry := g.matchRetry(serviceTaskStateImpl, errStr)
+ res = nil
+ resErr = err
+
+ if retry == nil {
+ return
+ }
+ shouldRetry = g.needRetry(serviceTaskStateImpl, retryCountMap, retry, resErr)
+ return
+ }
+
+ // invoke success
+ res = outs
+ resErr = nil
+ shouldRetry = false
+ return
+ }()
+
+ if !shouldRetry {
+ if err != nil {
+ return nil, errors.New(fmt.Sprintf("invoke Service[%s] failed, not satisfy retry config, the last err is %s",
+ serviceTaskStateImpl.ServiceName(), err))
+ }
+ return res, nil
+ }
+ }
+}
+
+func (g *GPRCClientImpl) CloseConnection() error {
+ if g.connection != nil {
+ err := g.connection.Close()
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (g *GPRCClientImpl) initMethod(serviceTaskStateImpl *state.ServiceTaskStateImpl) error {
+ methodName := serviceTaskStateImpl.ServiceMethod()
+ g.methodLock.Lock()
+ defer g.methodLock.Unlock()
+ clientValue := reflect.ValueOf(g.client)
+ if clientValue.IsZero() {
+ return errors.New(fmt.Sprintf("invalid client value when grpc client call, serviceName: %s", g.serviceName))
+ }
+ method := clientValue.MethodByName(methodName)
+ if method.IsZero() {
+ return errors.New(fmt.Sprintf("invalid client method when grpc client call, serviceName: %s, serviceMethod: %s",
+ g.serviceName, methodName))
+ }
+ serviceTaskStateImpl.SetMethod(&method)
+ return nil
+}
+
+func (g *GPRCClientImpl) matchRetry(impl *state.ServiceTaskStateImpl, str string) state.Retry {
+ if impl.Retry() != nil {
+ for _, retry := range impl.Retry() {
+ if retry.Exceptions() != nil {
+ for _, exception := range retry.Exceptions() {
+ if strings.Contains(str, exception) {
+ return retry
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (g *GPRCClientImpl) needRetry(impl *state.ServiceTaskStateImpl, countMap map[state.Retry]int, retry state.Retry, err error) bool {
+ attempt, exist := countMap[retry]
+ if !exist {
+ countMap[retry] = 0
+ }
+
+ if attempt >= retry.MaxAttempt() {
+ return false
+ }
+
+ intervalSecond := retry.IntervalSecond()
+ backoffRate := retry.BackoffRate()
+ var currentInterval int64
+ if attempt == 0 {
+ currentInterval = int64(intervalSecond * 1000)
+ } else {
+ currentInterval = int64(intervalSecond * backoffRate * float64(attempt) * 1000)
+ }
+
+ log.Warnf("invoke service[%s.%s] failed, will retry after %s millis, current retry count: %s, current err: %s",
+ impl.ServiceName(), impl.ServiceMethod(), currentInterval, attempt, err)
+
+ time.Sleep(time.Duration(currentInterval) * time.Millisecond)
+ countMap[retry] = attempt + 1
+ return true
+}
diff --git a/pkg/saga/statemachine/engine/invoker/grpc_invoker_test.go b/pkg/saga/statemachine/engine/invoker/grpc_invoker_test.go
new file mode 100644
index 00000000..fc27d5ae
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/grpc_invoker_test.go
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+
+ pb "seata.apache.org/seata-go/testdata/saga/engine/invoker/grpc"
+)
+
+type MockGPRCClientImpl struct {
+ GPRCClientImpl
+}
+
+type mockClientImpl struct {
+ invokeCount int
+}
+
+func (m *mockClientImpl) SayHelloRight(ctx context.Context, word string) (string, error) {
+ m.invokeCount++
+ fmt.Println("invoke right")
+ return word, nil
+}
+
+func (m *mockClientImpl) SayHelloRightLater(ctx context.Context, word string, delay int) (string, error) {
+ m.invokeCount++
+ if delay == m.invokeCount {
+ fmt.Println("invoke right")
+ return word, nil
+ }
+ fmt.Println("invoke fail")
+ return "", errors.New("invoke failed")
+}
+
+func TestGRPCInvokerInvokeSucceedWithOutRetry(t *testing.T) {
+ ctx := context.Background()
+ invoker := newGRPCServiceInvoker()
+ values, err := invoker.Invoke(ctx, []any{"hello"}, newHelloServiceTaskState())
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if values == nil || len(values) == 0 {
+ t.Error("no value in values")
+ return
+ }
+ if values[0].Interface().(string) != "hello" {
+ t.Errorf("expect hello, but got %v", values[0].Interface())
+ }
+ if _, ok := values[1].Interface().(error); ok {
+ t.Errorf("expect nil, but got %v", values[1].Interface())
+ }
+}
+
+func TestGRPCInvokerInvokeSucceedInRetry(t *testing.T) {
+ ctx := context.Background()
+ invoker := newGRPCServiceInvoker()
+ values, err := invoker.Invoke(ctx, []any{"hello", 2}, newHelloServiceTaskStateWithRetry())
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if values == nil || len(values) == 0 {
+ t.Error("no value in values")
+ return
+ }
+ if values[0].Interface().(string) != "hello" {
+ t.Errorf("expect hello, but got %v", values[0].Interface())
+ }
+ if _, ok := values[1].Interface().(error); ok {
+ t.Errorf("expect nil, but got %v", values[1].Interface())
+ }
+}
+
+func TestGRPCInvokerInvokeFailedInRetry(t *testing.T) {
+ ctx := context.Background()
+ invoker := newGRPCServiceInvoker()
+ _, err := invoker.Invoke(ctx, []any{"hello", 5}, newHelloServiceTaskStateWithRetry())
+ if err != nil {
+ assert.Error(t, err)
+ }
+}
+
+func TestGRPCInvokerInvokeE2E(t *testing.T) {
+ go func() {
+ pb.StartProductServer()
+ }()
+ time.Sleep(3000 * time.Millisecond)
+ conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
+ if err != nil {
+ t.Fatalf("did not connect: %v", err)
+ }
+ c := pb.NewProductInfoClient(conn)
+ grpcClient := NewGRPCClient("product", c, conn)
+
+ invoker := NewGRPCInvoker()
+ invoker.RegisterClient("product", grpcClient)
+ ctx := context.Background()
+ values, err := invoker.Invoke(ctx, []any{&pb.Product{Id: "123"}}, newProductServiceTaskState())
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ t.Log(values)
+ err = invoker.Close(ctx)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+}
+
+func newGRPCServiceInvoker() ServiceInvoker {
+ mockGRPCInvoker := NewGRPCInvoker()
+ mockGRPCClient := &mockClientImpl{}
+ mockClient := NewGRPCClient("hello", mockGRPCClient, &grpc.ClientConn{})
+ mockGRPCInvoker.RegisterClient("hello", mockClient)
+ return mockGRPCInvoker
+}
+
+func newProductServiceTaskState() state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName("product")
+ serviceTaskStateImpl.SetIsAsync(false)
+ serviceTaskStateImpl.SetServiceName("product")
+ serviceTaskStateImpl.SetServiceType("GRPC")
+ serviceTaskStateImpl.SetServiceMethod("AddProduct")
+
+ retryImpl := &state.RetryImpl{}
+ retryImpl.SetExceptions([]string{"fail"})
+ retryImpl.SetIntervalSecond(1)
+ retryImpl.SetMaxAttempt(3)
+ retryImpl.SetBackoffRate(0.9)
+ serviceTaskStateImpl.SetRetry([]state.Retry{retryImpl})
+ return serviceTaskStateImpl
+}
+
+func newHelloServiceTaskState() state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName("hello")
+ serviceTaskStateImpl.SetIsAsync(false)
+ serviceTaskStateImpl.SetServiceName("hello")
+ serviceTaskStateImpl.SetServiceType("GRPC")
+ serviceTaskStateImpl.SetServiceMethod("SayHelloRight")
+ return serviceTaskStateImpl
+}
+
+func newHelloServiceTaskStateWithRetry() state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName("hello")
+ serviceTaskStateImpl.SetIsAsync(false)
+ serviceTaskStateImpl.SetServiceName("hello")
+ serviceTaskStateImpl.SetServiceType("GRPC")
+ serviceTaskStateImpl.SetServiceMethod("SayHelloRightLater")
+
+ retryImpl := &state.RetryImpl{}
+ retryImpl.SetExceptions([]string{"fail"})
+ retryImpl.SetIntervalSecond(1)
+ retryImpl.SetMaxAttempt(3)
+ retryImpl.SetBackoffRate(0.9)
+ serviceTaskStateImpl.SetRetry([]state.Retry{retryImpl})
+ return serviceTaskStateImpl
+}
diff --git a/pkg/saga/statemachine/engine/invoker/http_invoker.go b/pkg/saga/statemachine/engine/invoker/http_invoker.go
new file mode 100644
index 00000000..c172aaa7
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/http_invoker.go
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+ "seata.apache.org/seata-go/pkg/util/log"
+)
+
+const errHttpCode = 400
+
+type HTTPInvoker struct {
+ clientsMapLock sync.Mutex
+ clients map[string]HTTPClient
+}
+
+func NewHTTPInvoker() *HTTPInvoker {
+ return &HTTPInvoker{
+ clients: make(map[string]HTTPClient),
+ }
+}
+
+func (h *HTTPInvoker) RegisterClient(serviceName string, client HTTPClient) {
+ h.clientsMapLock.Lock()
+ defer h.clientsMapLock.Unlock()
+ h.clients[serviceName] = client
+}
+
+func (h *HTTPInvoker) GetClient(serviceName string) HTTPClient {
+ h.clientsMapLock.Lock()
+ defer h.clientsMapLock.Unlock()
+ return h.clients[serviceName]
+}
+
+func (h *HTTPInvoker) Invoke(ctx context.Context, input []any, service state.ServiceTaskState) (output []reflect.Value, err error) {
+ serviceTaskStateImpl := service.(*state.ServiceTaskStateImpl)
+ client := h.GetClient(serviceTaskStateImpl.ServiceName())
+ if client == nil {
+ return nil, fmt.Errorf("no http client %s for service task state", serviceTaskStateImpl.ServiceName())
+ }
+
+ if serviceTaskStateImpl.IsAsync() {
+ go func() {
+ _, err := client.Call(ctx, serviceTaskStateImpl, input)
+ if err != nil {
+ log.Errorf("invoke Service[%s].%s failed, err is %s", serviceTaskStateImpl.ServiceName(),
+ serviceTaskStateImpl.ServiceMethod(), err)
+ }
+ }()
+ return nil, nil
+ }
+
+ return client.Call(ctx, serviceTaskStateImpl, input)
+}
+
+func (h *HTTPInvoker) Close(ctx context.Context) error {
+ return nil
+}
+
+type HTTPClient interface {
+ Call(ctx context.Context, serviceTaskStateImpl *state.ServiceTaskStateImpl, input []any) ([]reflect.Value, error)
+}
+
+type HTTPClientImpl struct {
+ serviceName string
+ baseURL string
+ client *http.Client
+}
+
+func NewHTTPClient(serviceName string, baseURL string, client *http.Client) *HTTPClientImpl {
+ if client == nil {
+ client = &http.Client{
+ Timeout: time.Second * 30,
+ }
+ }
+ return &HTTPClientImpl{
+ serviceName: serviceName,
+ baseURL: baseURL,
+ client: client,
+ }
+}
+
+func (h *HTTPClientImpl) Call(ctx context.Context, serviceTaskStateImpl *state.ServiceTaskStateImpl, input []any) ([]reflect.Value, error) {
+ retryCountMap := make(map[state.Retry]int)
+ for {
+ res, err, shouldRetry := func() (res []reflect.Value, resErr error, shouldRetry bool) {
+ defer func() {
+ if r := recover(); r != nil {
+ errStr := fmt.Sprintf("%v", r)
+ retry := h.matchRetry(serviceTaskStateImpl, errStr)
+ resErr = errors.New(errStr)
+ if retry != nil {
+ shouldRetry = h.needRetry(serviceTaskStateImpl, retryCountMap, retry, resErr)
+ }
+ }
+ }()
+
+ reqBody, err := json.Marshal(input)
+ if err != nil {
+ return nil, err, false
+ }
+
+ req, err := http.NewRequestWithContext(ctx,
+ serviceTaskStateImpl.ServiceMethod(),
+ h.baseURL+serviceTaskStateImpl.Name(),
+ bytes.NewBuffer(reqBody))
+ if err != nil {
+ return nil, err, false
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := h.client.Do(req)
+ if err != nil {
+ retry := h.matchRetry(serviceTaskStateImpl, err.Error())
+ if retry != nil {
+ return nil, err, h.needRetry(serviceTaskStateImpl, retryCountMap, retry, err)
+ }
+ return nil, err, false
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err, false
+ }
+
+ if resp.StatusCode >= errHttpCode {
+ errStr := fmt.Sprintf("HTTP error: %d - %s", resp.StatusCode, string(body))
+ retry := h.matchRetry(serviceTaskStateImpl, errStr)
+ if retry != nil {
+ return nil, errors.New(errStr), h.needRetry(serviceTaskStateImpl, retryCountMap, retry, err)
+ }
+ return nil, errors.New(errStr), false
+ }
+
+ return []reflect.Value{
+ reflect.ValueOf(string(body)),
+ reflect.Zero(reflect.TypeOf((*error)(nil)).Elem()),
+ }, nil, false
+ }()
+
+ if !shouldRetry {
+ if err != nil {
+ return nil, fmt.Errorf("invoke Service[%s] failed, not satisfy retry config, the last err is %s",
+ serviceTaskStateImpl.ServiceName(), err)
+ }
+ return res, nil
+ }
+ }
+}
+
+func (h *HTTPClientImpl) matchRetry(impl *state.ServiceTaskStateImpl, str string) state.Retry {
+ if impl.Retry() != nil {
+ for _, retry := range impl.Retry() {
+ if retry.Exceptions() != nil {
+ for _, exception := range retry.Exceptions() {
+ if strings.Contains(str, exception) {
+ return retry
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (h *HTTPClientImpl) needRetry(impl *state.ServiceTaskStateImpl, countMap map[state.Retry]int, retry state.Retry, err error) bool {
+ attempt, exist := countMap[retry]
+ if !exist {
+ countMap[retry] = 0
+ }
+
+ if attempt >= retry.MaxAttempt() {
+ return false
+ }
+
+ intervalSecond := retry.IntervalSecond()
+ backoffRate := retry.BackoffRate()
+ var currentInterval int64
+ if attempt == 0 {
+ currentInterval = int64(intervalSecond * 1000)
+ } else {
+ currentInterval = int64(intervalSecond * backoffRate * float64(attempt) * 1000)
+ }
+
+ log.Warnf("invoke service[%s.%s] failed, will retry after %s millis, current retry count: %s, current err: %s",
+ impl.ServiceName(), impl.ServiceMethod(), currentInterval, attempt, err)
+
+ time.Sleep(time.Duration(currentInterval) * time.Millisecond)
+ countMap[retry] = attempt + 1
+ return true
+}
diff --git a/pkg/saga/statemachine/engine/invoker/http_invoker_test.go b/pkg/saga/statemachine/engine/invoker/http_invoker_test.go
new file mode 100644
index 00000000..a5288cb5
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/http_invoker_test.go
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+)
+
+func TestHTTPInvokerInvokeSucceedWithOutRetry(t *testing.T) {
+ // create test server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ var input []interface{}
+ err := json.NewDecoder(r.Body).Decode(&input)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(input[0].(string)))
+ }))
+ defer server.Close()
+
+ // create HTTP Invoker
+ invoker := NewHTTPInvoker()
+ client := NewHTTPClient("test", server.URL+"/", &http.Client{})
+ invoker.RegisterClient("test", client)
+
+ // invoke
+ ctx := context.Background()
+ values, err := invoker.Invoke(ctx, []any{"hello"}, newHTTPServiceTaskState())
+
+ // verify
+ assert.NoError(t, err)
+ assert.NotNil(t, values)
+ assert.Equal(t, "hello", values[0].Interface())
+}
+
+func TestHTTPInvokerInvokeWithRetry(t *testing.T) {
+ attemptCount := 0
+ // create test server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ attemptCount++
+ if attemptCount < 2 {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("fail"))
+ return
+ }
+ var input []interface{}
+ json.NewDecoder(r.Body).Decode(&input)
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(input[0].(string)))
+ }))
+ defer server.Close()
+
+ // create HTTP Invoker
+ invoker := NewHTTPInvoker()
+ client := NewHTTPClient("test", server.URL+"/", &http.Client{})
+ invoker.RegisterClient("test", client)
+
+ // invoker
+ ctx := context.Background()
+ values, err := invoker.Invoke(ctx, []any{"hello"}, newHTTPServiceTaskStateWithRetry())
+
+ // verify
+ assert.NoError(t, err)
+ assert.NotNil(t, values)
+ assert.Equal(t, "hello", values[0].Interface())
+ assert.Equal(t, 2, attemptCount)
+}
+
+func TestHTTPInvokerInvokeFailedInRetry(t *testing.T) {
+ // create test server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte("fail"))
+ }))
+ defer server.Close()
+
+ // create HTTP Invoker
+ invoker := NewHTTPInvoker()
+ client := NewHTTPClient("test", server.URL+"/", &http.Client{})
+ invoker.RegisterClient("test", client)
+
+ // invoker
+ ctx := context.Background()
+ _, err := invoker.Invoke(ctx, []any{"hello"}, newHTTPServiceTaskStateWithRetry())
+
+ // verify
+ assert.Error(t, err)
+}
+
+func TestHTTPInvokerAsyncInvoke(t *testing.T) {
+ var called atomic.Bool
+ // create test server
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ called.Store(true)
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("success"))
+ }))
+ defer server.Close()
+
+ // create HTTP Invoker
+ invoker := NewHTTPInvoker()
+ client := NewHTTPClient("test", server.URL+"/", &http.Client{})
+ invoker.RegisterClient("test", client)
+
+ // async invoke
+ ctx := context.Background()
+ taskState := newHTTPServiceTaskStateWithAsync()
+ _, err := invoker.Invoke(ctx, []any{"hello"}, taskState)
+
+ // verify
+ assert.NoError(t, err)
+ assert.Eventually(t, func() bool {
+ return called.Load()
+ }, 500*time.Millisecond, 10*time.Millisecond)
+}
+
+func newHTTPServiceTaskState() state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName("test")
+ serviceTaskStateImpl.SetIsAsync(false)
+ serviceTaskStateImpl.SetServiceName("test")
+ serviceTaskStateImpl.SetServiceType("HTTP")
+ serviceTaskStateImpl.SetServiceMethod("POST")
+ return serviceTaskStateImpl
+}
+
+func newHTTPServiceTaskStateWithAsync() state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName("test")
+ serviceTaskStateImpl.SetIsAsync(true)
+ serviceTaskStateImpl.SetServiceName("test")
+ serviceTaskStateImpl.SetServiceType("HTTP")
+ serviceTaskStateImpl.SetServiceMethod("POST")
+ return serviceTaskStateImpl
+}
+
+func newHTTPServiceTaskStateWithRetry() state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName("test")
+ serviceTaskStateImpl.SetIsAsync(false)
+ serviceTaskStateImpl.SetServiceName("test")
+ serviceTaskStateImpl.SetServiceType("HTTP")
+ serviceTaskStateImpl.SetServiceMethod("POST")
+
+ retryImpl := &state.RetryImpl{}
+ retryImpl.SetExceptions([]string{"fail"})
+ retryImpl.SetIntervalSecond(1)
+ retryImpl.SetMaxAttempt(3)
+ retryImpl.SetBackoffRate(0.9)
+ serviceTaskStateImpl.SetRetry([]state.Retry{retryImpl})
+ return serviceTaskStateImpl
+}
diff --git a/pkg/saga/statemachine/engine/invoker/invoker.go b/pkg/saga/statemachine/engine/invoker/invoker.go
new file mode 100644
index 00000000..1f5403c0
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/invoker.go
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+)
+
+type ServiceInvokerManager interface {
+ ServiceInvoker(serviceType string) ServiceInvoker
+ PutServiceInvoker(serviceType string, invoker ServiceInvoker)
+}
+
+type ServiceInvoker interface {
+ Invoke(ctx context.Context, input []any, service state.ServiceTaskState) (output []reflect.Value, err error)
+ Close(ctx context.Context) error
+}
+
+// ScriptInvoker 脚本调用器接口
+type ScriptInvoker interface {
+ Type() string
+ Invoke(ctx context.Context, script string, params map[string]interface{}) (interface{}, error)
+ Close(ctx context.Context) error
+}
+
+// ScriptInvokerManager 脚本调用器管理器
+type ScriptInvokerManager interface {
+ GetInvoker(scriptType string) (ScriptInvoker, error)
+ RegisterInvoker(invoker ScriptInvoker) error
+}
+
+type ScriptInvokerManagerImpl struct {
+ invokers map[string]ScriptInvoker
+ mutex sync.RWMutex
+}
+
+func NewScriptInvokerManager() ScriptInvokerManager {
+ return &ScriptInvokerManagerImpl{invokers: make(map[string]ScriptInvoker)}
+}
+
+func (manager *ScriptInvokerManagerImpl) GetInvoker(scriptType string) (ScriptInvoker, error) {
+ manager.mutex.RLock()
+ defer manager.mutex.RUnlock()
+
+ invoker := manager.invokers[scriptType]
+ if invoker == nil {
+ return nil, fmt.Errorf("script invoker %s not found", scriptType)
+ }
+ return invoker, nil
+}
+
+func (manager *ScriptInvokerManagerImpl) RegisterInvoker(invoker ScriptInvoker) error {
+ if invoker == nil {
+ return fmt.Errorf("script invoker is nil")
+ }
+ manager.mutex.Lock()
+ defer manager.mutex.Unlock()
+ manager.invokers[invoker.Type()] = invoker
+ return nil
+}
+
+type ServiceInvokerManagerImpl struct {
+ invokers map[string]ServiceInvoker
+ mutex sync.Mutex
+}
+
+func NewServiceInvokerManagerImpl() *ServiceInvokerManagerImpl {
+ return &ServiceInvokerManagerImpl{
+ invokers: make(map[string]ServiceInvoker),
+ }
+}
+
+func (manager *ServiceInvokerManagerImpl) ServiceInvoker(serviceType string) ServiceInvoker {
+ return manager.invokers[serviceType]
+}
+
+func (manager *ServiceInvokerManagerImpl) PutServiceInvoker(serviceType string, invoker ServiceInvoker) {
+ manager.mutex.Lock()
+ defer manager.mutex.Unlock()
+ manager.invokers[serviceType] = invoker
+}
diff --git a/pkg/saga/statemachine/engine/invoker/javascript_script_invoker.go b/pkg/saga/statemachine/engine/invoker/javascript_script_invoker.go
new file mode 100644
index 00000000..7817494d
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/javascript_script_invoker.go
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "github.com/robertkrimen/otto"
+)
+
+const defaultPoolSize = 10
+
+type JavaScriptScriptInvoker struct {
+ mutex sync.Mutex
+ jsonParser JsonParser
+ closed bool
+ vmPool chan *otto.Otto
+ poolSize int
+}
+
+func NewJavaScriptScriptInvoker() *JavaScriptScriptInvoker {
+ return &JavaScriptScriptInvoker{
+ jsonParser: &DefaultJsonParser{},
+ closed: false,
+ poolSize: defaultPoolSize,
+ vmPool: make(chan *otto.Otto, defaultPoolSize),
+ }
+}
+
+func NewJavaScriptScriptInvokerWithPoolSize(poolSize int) *JavaScriptScriptInvoker {
+ if poolSize <= 0 {
+ poolSize = defaultPoolSize
+ }
+ return &JavaScriptScriptInvoker{
+ jsonParser: &DefaultJsonParser{},
+ closed: false,
+ poolSize: poolSize,
+ vmPool: make(chan *otto.Otto, poolSize),
+ }
+}
+
+func (j *JavaScriptScriptInvoker) Type() string {
+ return "javascript"
+}
+
+func (j *JavaScriptScriptInvoker) Invoke(ctx context.Context, script string, params map[string]interface{}) (interface{}, error) {
+ j.mutex.Lock()
+ closed := j.closed
+ j.mutex.Unlock()
+
+ if closed {
+ return nil, fmt.Errorf("javascript invoker has been closed")
+ }
+
+ var vm *otto.Otto
+ select {
+ case vm = <-j.vmPool:
+ if err := cleanVMState(vm); err != nil {
+ vm = otto.New()
+ }
+ default:
+ vm = otto.New()
+ }
+
+ defer func() {
+ j.mutex.Lock()
+ defer j.mutex.Unlock()
+ if !j.closed {
+ select {
+ case j.vmPool <- vm:
+ default:
+ // Pool full, discard current instance
+ }
+ }
+ }()
+
+ for key, value := range params {
+ if err := vm.Set(key, value); err != nil {
+ return nil, fmt.Errorf("javascript set param %s error: %w", key, err)
+ }
+ }
+
+ resultChan := make(chan struct {
+ val otto.Value
+ err error
+ }, 1)
+
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ resultChan <- struct {
+ val otto.Value
+ err error
+ }{otto.UndefinedValue(), fmt.Errorf("javascript engine panic: %v", r)}
+ }
+ }()
+
+ val, err := vm.Run(script)
+ resultChan <- struct {
+ val otto.Value
+ err error
+ }{val, err}
+ }()
+
+ select {
+ case <-ctx.Done():
+ return nil, fmt.Errorf("javascript execution timeout: %w", ctx.Err())
+ case res := <-resultChan:
+ if res.err != nil {
+ return nil, fmt.Errorf("javascript execute error: %w", res.err)
+ }
+ val, err := res.val.Export()
+ if err != nil {
+ return nil, fmt.Errorf("failed to export javascript result: %w", err)
+ }
+ return val, nil
+ }
+}
+
+func (j *JavaScriptScriptInvoker) Close(ctx context.Context) error {
+ j.mutex.Lock()
+ defer j.mutex.Unlock()
+
+ if j.closed {
+ return nil
+ }
+
+ j.closed = true
+ close(j.vmPool)
+ for range j.vmPool {
+ // Let GC recycle VM resources
+ }
+ return nil
+}
+
+func cleanVMState(vm *otto.Otto) error {
+ _, err := vm.Run(`
+ for (const prop in global) {
+ if (!['Object', 'Array', 'Function', 'String', 'Number', 'Boolean', 'JSON', 'Date', 'RegExp'].includes(prop)) {
+ delete global[prop];
+ }
+ }
+ `)
+ return err
+}
diff --git a/pkg/saga/statemachine/engine/invoker/javascript_script_invoker_test.go b/pkg/saga/statemachine/engine/invoker/javascript_script_invoker_test.go
new file mode 100644
index 00000000..e41d1e5c
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/javascript_script_invoker_test.go
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/robertkrimen/otto"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestJavaScriptScriptInvoker_Type(t *testing.T) {
+ invoker := NewJavaScriptScriptInvoker()
+ assert.Equal(t, "javascript", invoker.Type())
+}
+
+func TestJavaScriptScriptInvoker_Invoke_Basic(t *testing.T) {
+ tests := []struct {
+ name string
+ script string
+ params map[string]interface{}
+ expected interface{}
+ }{
+ {
+ name: "simple expression",
+ script: "1 + 2",
+ params: nil,
+ expected: float64(3),
+ },
+ {
+ name: "param calculation",
+ script: "a * b + c",
+ params: map[string]interface{}{"a": 2, "b": 3, "c": 4},
+ expected: float64(10),
+ },
+ {
+ name: "return string",
+ script: "['hello', name].join(' ')",
+ params: map[string]interface{}{"name": "world"},
+ expected: "hello world",
+ },
+ {
+ name: "return object",
+ script: `var obj = {id: 1, name: name}; obj;`,
+ params: map[string]interface{}{"name": "test"},
+ expected: map[string]interface{}{"id": float64(1), "name": "test"},
+ },
+ }
+
+ invoker := NewJavaScriptScriptInvoker()
+ ctx := context.Background()
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result, err := invoker.Invoke(ctx, tt.script, tt.params)
+ assert.NoError(t, err)
+
+ if resultMap, ok := result.(map[string]interface{}); ok {
+ for k, v := range resultMap {
+ if intVal, isInt := v.(int64); isInt {
+ resultMap[k] = float64(intVal)
+ }
+ }
+ }
+
+ assert.Equal(t, tt.expected, result)
+ })
+ }
+}
+
+func TestJavaScriptScriptInvoker_Invoke_Error(t *testing.T) {
+ tests := []struct {
+ name string
+ script string
+ params map[string]interface{}
+ errMsg string
+ }{
+ {
+ name: "syntax error",
+ script: "1 + ",
+ params: nil,
+ errMsg: "javascript execute error",
+ },
+ {
+ name: "reference undefined variable",
+ script: "undefinedVar",
+ params: nil,
+ errMsg: "javascript execute error",
+ },
+ }
+
+ invoker := NewJavaScriptScriptInvoker()
+ ctx := context.Background()
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := invoker.Invoke(ctx, tt.script, tt.params)
+
+ if err == nil {
+ t.Fatalf("Test case [%s] expected error but got none", tt.name)
+ }
+ assert.Contains(t, err.Error(), tt.errMsg, "Test case [%s] error message mismatch", tt.name)
+ })
+ }
+}
+
+func TestJavaScriptScriptInvoker_Invoke_Timeout(t *testing.T) {
+
+ script := `var target = 300; var start = new Date().getTime(); var elapsed = 0; while (elapsed < target) { elapsed = new Date().getTime() - start; } "done";`
+ invoker := NewJavaScriptScriptInvoker()
+
+ ctx1, cancel1 := context.WithTimeout(context.Background(), 200*time.Millisecond)
+ defer cancel1()
+ _, err := invoker.Invoke(ctx1, script, nil)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "javascript execution timeout")
+
+ ctx2, cancel2 := context.WithTimeout(context.Background(), 400*time.Millisecond)
+ defer cancel2()
+ result, err := invoker.Invoke(ctx2, script, nil)
+ assert.NoError(t, err, "Scenario 2: script execution should not return error")
+ assert.Equal(t, "done", result, "Scenario 2: should return 'done'")
+}
+
+func TestJavaScriptScriptInvoker_Invoke_Concurrent(t *testing.T) {
+ invoker := NewJavaScriptScriptInvoker()
+ ctx := context.Background()
+ var wg sync.WaitGroup
+ concurrency := 100
+ errChan := make(chan error, concurrency)
+
+ script := `a + b`
+ params := map[string]interface{}{"a": 10, "b": 20}
+
+ for i := 0; i < concurrency; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ result, err := invoker.Invoke(ctx, script, params)
+ if err != nil {
+ errChan <- err
+ return
+ }
+ if result != float64(30) {
+ errChan <- assert.AnError
+ }
+ }()
+ }
+
+ wg.Wait()
+ close(errChan)
+
+ assert.Empty(t, errChan, "Concurrent execution has errors")
+}
+
+func TestJavaScriptScriptInvoker_Close(t *testing.T) {
+ invoker := NewJavaScriptScriptInvoker()
+ ctx := context.Background()
+
+ result, err := invoker.Invoke(ctx, "1 + 1", nil)
+ assert.NoError(t, err)
+ assert.Equal(t, float64(2), result)
+
+ err = invoker.Close(ctx)
+ assert.NoError(t, err)
+
+ _, err = invoker.Invoke(ctx, "1 + 1", nil)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "javascript invoker has been closed")
+}
+
+func TestOttoScript(t *testing.T) {
+ vm := otto.New()
+ script := `var target = 300; var start = new Date().getTime(); var elapsed = 0; while (elapsed < target) { elapsed = new Date().getTime() - start; } "done";`
+ val, err := vm.Run(script)
+ if err != nil {
+ t.Fatalf("otto failed to parse script: %v", err)
+ }
+
+ result, exportErr := val.Export()
+ if exportErr != nil {
+ t.Fatalf("failed to export otto value: %v", exportErr)
+ }
+ t.Logf("Script execution result: %v", result)
+}
+
+func TestJavaScriptScriptInvoker_VMPoolReuse(t *testing.T) {
+ poolSize := 2
+ invoker := NewJavaScriptScriptInvokerWithPoolSize(poolSize)
+ ctx := context.Background()
+
+ vmIDs := make([]string, 0, 5)
+
+ script := `
+ if (!this.vmId) {
+ this.vmId = Math.random().toString(36).substr(2, 8);
+ }
+ this.vmId;
+ `
+
+ for i := 0; i < 5; i++ {
+ result, err := invoker.Invoke(ctx, script, nil)
+ assert.NoError(t, err, "Error occurred while executing script")
+
+ id, ok := result.(string)
+ assert.True(t, ok, "VM ID should be a string type")
+ vmIDs = append(vmIDs, id)
+ }
+
+ uniqueIDs := make(map[string]bool)
+ for _, id := range vmIDs {
+ uniqueIDs[id] = true
+ }
+
+ assert.True(t, len(uniqueIDs) <= 5, "Abnormal number of VM instances created")
+ assert.True(t, len(uniqueIDs) >= 1, "No VM instances reused from the pool")
+}
+
+func TestJavaScriptScriptInvoker_VMStateClean(t *testing.T) {
+ invoker := NewJavaScriptScriptInvokerWithPoolSize(1)
+ ctx := context.Background()
+
+ _, err := invoker.Invoke(ctx, `this.foo = "polluted data"`, nil)
+ assert.NoError(t, err)
+
+ result, err := invoker.Invoke(ctx, `typeof this.foo`, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, "undefined", result, "VM state not cleaned, residual global variable exists")
+
+ _, err = invoker.Invoke(ctx, `this.bar = function() { return "residual function"; }`, nil)
+ assert.NoError(t, err)
+
+ result, err = invoker.Invoke(ctx, `typeof this.bar`, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, "undefined", result, "VM state not cleaned, residual function exists")
+}
+
+func TestJavaScriptScriptInvoker_PoolSizeDefault(t *testing.T) {
+ invoker := NewJavaScriptScriptInvokerWithPoolSize(0)
+ assert.Equal(t, defaultPoolSize, invoker.poolSize, "Default pool size not used when pool size is 0")
+
+ invoker = NewJavaScriptScriptInvokerWithPoolSize(-5)
+ assert.Equal(t, defaultPoolSize, invoker.poolSize, "Default pool size not used when pool size is negative")
+}
diff --git a/pkg/rm/init.go b/pkg/saga/statemachine/engine/invoker/json_parser.go
similarity index 61%
copy from pkg/rm/init.go
copy to pkg/saga/statemachine/engine/invoker/json_parser.go
index 469cdfaa..d51bdcf2 100644
--- a/pkg/rm/init.go
+++ b/pkg/saga/statemachine/engine/invoker/json_parser.go
@@ -15,18 +15,23 @@
* limitations under the License.
*/
-package rm
+package invoker
-var rmConfig RmConfig
+import "encoding/json"
-type RmConfig struct {
- Config
+// JsonParser 抽象 JSON 序列化接口,便于后续替换实现
+type JsonParser interface {
+ Marshal(v interface{}) ([]byte, error)
+ Unmarshal(data []byte, v interface{}) error
+}
+
+// DefaultJsonParser 默认使用标准库 json
+type DefaultJsonParser struct{}
- ApplicationID string
- TxServiceGroup string
+func (p *DefaultJsonParser) Marshal(v interface{}) ([]byte, error) {
+ return json.Marshal(v)
}
-// InitRmClient init seata rm client
-func InitRm(cfg RmConfig) {
- rmConfig = cfg
+func (p *DefaultJsonParser) Unmarshal(data []byte, v interface{}) error {
+ return json.Unmarshal(data, v)
}
diff --git a/pkg/saga/statemachine/engine/invoker/local_invoker.go b/pkg/saga/statemachine/engine/invoker/local_invoker.go
new file mode 100644
index 00000000..656b2f91
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/local_invoker.go
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "fmt"
+ "reflect"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+)
+
+type LocalServiceInvoker struct {
+ serviceRegistry map[string]interface{}
+ methodCache map[string]*reflect.Method
+ jsonParser JsonParser
+ mutex sync.RWMutex
+}
+
+func NewLocalServiceInvoker() *LocalServiceInvoker {
+ return &LocalServiceInvoker{
+ serviceRegistry: make(map[string]interface{}),
+ methodCache: make(map[string]*reflect.Method),
+ jsonParser: &DefaultJsonParser{},
+ }
+}
+
+func (l *LocalServiceInvoker) RegisterService(serviceName string, instance interface{}) {
+ l.mutex.Lock()
+ defer l.mutex.Unlock()
+ l.serviceRegistry[serviceName] = instance
+}
+
+func (l *LocalServiceInvoker) Invoke(ctx context.Context, input []any, service state.ServiceTaskState) ([]reflect.Value, error) {
+ serviceName := service.ServiceName()
+ instance, exists := l.serviceRegistry[serviceName]
+ if !exists {
+ return nil, fmt.Errorf("service %s not registered", serviceName)
+ }
+
+ methodName := service.ServiceMethod()
+ method, err := l.getMethod(serviceName, methodName, service.ParameterTypes())
+ if err != nil {
+ return nil, err
+ }
+
+ params, err := l.resolveParameters(input, method.Type)
+ if err != nil {
+ return nil, err
+ }
+
+ return l.invokeMethod(instance, method, params), nil
+}
+
+func (l *LocalServiceInvoker) resolveMethod(key, serviceName, methodName string) (*reflect.Method, error) {
+ l.mutex.Lock()
+ defer l.mutex.Unlock()
+
+ if cachedMethod, ok := l.methodCache[key]; ok {
+ return cachedMethod, nil
+ }
+
+ instance, exists := l.serviceRegistry[serviceName]
+ if !exists {
+ return nil, fmt.Errorf("service %s not found", serviceName)
+ }
+
+ objType := reflect.TypeOf(instance)
+ method, ok := objType.MethodByName(methodName)
+ if !ok {
+ return nil, fmt.Errorf("method %s not found in service %s", methodName, serviceName)
+ }
+
+ l.methodCache[key] = &method
+ return &method, nil
+}
+
+func (l *LocalServiceInvoker) getMethod(serviceName, methodName string, paramTypes []string) (*reflect.Method, error) {
+ key := fmt.Sprintf("%s.%s", serviceName, methodName)
+
+ l.mutex.RLock()
+ if method, ok := l.methodCache[key]; ok {
+ l.mutex.RUnlock()
+ return method, nil
+ }
+ l.mutex.RUnlock()
+
+ return l.resolveMethod(key, serviceName, methodName)
+}
+
+func (l *LocalServiceInvoker) resolveParameters(input []any, methodType reflect.Type) ([]reflect.Value, error) {
+ argTotal := methodType.NumIn()
+ if argTotal == 0 {
+ return nil, nil
+ }
+
+ argCount := argTotal - 1 // skip receiver
+ if argCount <= 0 {
+ return nil, nil
+ }
+
+ params := make([]reflect.Value, argCount)
+ for i := 0; i < argCount; i++ {
+ paramType := methodType.In(i + 1)
+ if i >= len(input) {
+ params[i] = reflect.Zero(paramType)
+ continue
+ }
+
+ converted, err := l.convertParam(input[i], paramType)
+ if err != nil {
+ return nil, fmt.Errorf("parameter %d conversion error: %w", i, err)
+ }
+
+ val := reflect.ValueOf(converted)
+ if !val.IsValid() {
+ params[i] = reflect.Zero(paramType)
+ continue
+ }
+ if val.Type().AssignableTo(paramType) {
+ params[i] = val
+ continue
+ }
+ if val.Type().ConvertibleTo(paramType) {
+ params[i] = val.Convert(paramType)
+ continue
+ }
+ params[i] = reflect.Zero(paramType)
+ }
+
+ return params, nil
+}
+
+func (l *LocalServiceInvoker) convertParam(value any, targetType reflect.Type) (any, error) {
+ if targetType.Kind() == reflect.Ptr {
+ elemType := targetType.Elem()
+ instance := reflect.New(elemType).Interface()
+ jsonData, err := l.jsonParser.Marshal(value)
+ if err != nil {
+ return nil, err
+ }
+ if err := l.jsonParser.Unmarshal(jsonData, instance); err != nil {
+ return nil, err
+ }
+ return instance, nil
+ }
+
+ if targetType.Kind() == reflect.Struct {
+ instance := reflect.New(targetType).Interface()
+ jsonData, err := l.jsonParser.Marshal(value)
+ if err != nil {
+ return nil, err
+ }
+ if err := l.jsonParser.Unmarshal(jsonData, instance); err != nil {
+ return nil, err
+ }
+ return reflect.ValueOf(instance).Elem().Interface(), nil
+ }
+
+ if targetType.Kind() == reflect.Int && reflect.TypeOf(value).Kind() == reflect.Float64 {
+ return int(value.(float64)), nil
+ } else if targetType == reflect.TypeOf("") && reflect.TypeOf(value).Kind() == reflect.Int {
+ return fmt.Sprintf("%d", value), nil
+ }
+
+ return value, nil
+}
+
+func (l *LocalServiceInvoker) invokeMethod(instance interface{}, method *reflect.Method, params []reflect.Value) []reflect.Value {
+ instanceValue := reflect.ValueOf(instance)
+ if method.Func.IsValid() {
+ allParams := append([]reflect.Value{instanceValue}, params...)
+ return method.Func.Call(allParams)
+ }
+ return nil
+}
+
+func (l *LocalServiceInvoker) Close(ctx context.Context) error {
+ l.mutex.Lock()
+ defer l.mutex.Unlock()
+ l.serviceRegistry = nil
+ l.methodCache = nil
+ return nil
+}
diff --git a/pkg/saga/statemachine/engine/invoker/local_invoker_test.go b/pkg/saga/statemachine/engine/invoker/local_invoker_test.go
new file mode 100644
index 00000000..9cc1afd3
--- /dev/null
+++ b/pkg/saga/statemachine/engine/invoker/local_invoker_test.go
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package invoker
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+)
+
+type MockLocalService struct {
+ invokeCount int
+}
+
+func (m *MockLocalService) GetServiceName() string {
+ return "MockLocalService"
+}
+
+func (m *MockLocalService) Add(a, b int) int {
+ m.invokeCount++
+ return a + b
+}
+
+func (m *MockLocalService) Multiply(f float64, i int) float64 {
+ m.invokeCount++
+ return f * float64(i)
+}
+
+type User struct {
+ Name string `json:"name"`
+ Age int `json:"age"`
+}
+
+func (m *MockLocalService) GetUserName(user User) string {
+ m.invokeCount++
+ return user.Name
+}
+
+func (m *MockLocalService) ErrorMethod() error {
+ return errors.New("expected error")
+}
+
+func TestLocalInvoker_ServiceNotRegistered(t *testing.T) {
+ invoker := NewLocalServiceInvoker()
+ ctx := context.Background()
+ taskState := newLocalServiceTaskState("unregisteredService", "AnyMethod")
+
+ _, err := invoker.Invoke(ctx, []any{}, taskState)
+ if err == nil {
+ t.Error("expected error when service not registered, but got nil")
+ }
+ if err.Error() != "service unregisteredService not registered" {
+ t.Errorf("unexpected error message: %v", err)
+ }
+}
+
+func TestLocalInvoker_MethodNotFound(t *testing.T) {
+ invoker := NewLocalServiceInvoker()
+ service := &MockLocalService{}
+ invoker.RegisterService("mockService", service)
+
+ ctx := context.Background()
+ taskState := newLocalServiceTaskState("mockService", "NonExistentMethod")
+
+ _, err := invoker.Invoke(ctx, []any{}, taskState)
+ if err == nil {
+ t.Error("expected error when method not found, but got nil")
+ }
+ if err.Error() != "method NonExistentMethod not found in service mockService" {
+ t.Errorf("unexpected error message: %v", err)
+ }
+}
+
+func TestLocalInvoker_InvokeSuccess(t *testing.T) {
+ tests := []struct {
+ name string
+ service interface{}
+ serviceName string
+ methodName string
+ input []any
+ expected interface{}
+ }{
+ {
+ name: "test basic method call",
+ service: &MockLocalService{},
+ serviceName: "mockService",
+ methodName: "GetServiceName",
+ input: []any{},
+ expected: "MockLocalService",
+ },
+ {
+ name: "test method with parameters",
+ service: &MockLocalService{},
+ serviceName: "mockService",
+ methodName: "Add",
+ input: []any{2, 3},
+ expected: 5,
+ },
+ {
+ name: "test parameter type conversion",
+ service: &MockLocalService{},
+ serviceName: "mockService",
+ methodName: "Multiply",
+ input: []any{2.5, 4},
+ expected: 10.0,
+ },
+ }
+
+ invoker := NewLocalServiceInvoker()
+ ctx := context.Background()
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ invoker.RegisterService(tt.serviceName, tt.service)
+ taskState := newLocalServiceTaskState(tt.serviceName, tt.methodName)
+
+ results, err := invoker.Invoke(ctx, tt.input, taskState)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if len(results) == 0 {
+ t.Fatal("no results returned")
+ }
+
+ result := results[0].Interface()
+ if !reflect.DeepEqual(result, tt.expected) {
+ t.Errorf("expected %v, got %v", tt.expected, result)
+ }
+ })
+ }
+}
+
+func TestLocalInvoker_StructParameterConversion(t *testing.T) {
+ invoker := NewLocalServiceInvoker()
+ service := &MockLocalService{}
+ invoker.RegisterService("userService", service)
+
+ ctx := context.Background()
+ taskState := newLocalServiceTaskState("userService", "GetUserName")
+
+ input := []any{map[string]interface{}{"name": "Alice", "age": 30}}
+ results, err := invoker.Invoke(ctx, input, taskState)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if len(results) == 0 {
+ t.Fatal("no results returned")
+ }
+
+ result := results[0].Interface()
+ if result != "Alice" {
+ t.Errorf("expected 'Alice', got %v", result)
+ }
+}
+
+func TestLocalInvoker_MethodCaching(t *testing.T) {
+ invoker := NewLocalServiceInvoker()
+ service := &MockLocalService{}
+ invoker.RegisterService("cacheTestService", service)
+
+ ctx := context.Background()
+ taskState := newLocalServiceTaskState("cacheTestService", "Add")
+
+ _, err := invoker.Invoke(ctx, []any{1, 1}, taskState)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ results, err := invoker.Invoke(ctx, []any{2, 3}, taskState)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if results[0].Interface() != 5 {
+ t.Errorf("expected 5, got %v", results[0].Interface())
+ }
+
+ if service.invokeCount != 2 {
+ t.Errorf("expected 2 invocations, got %d", service.invokeCount)
+ }
+}
+
+func newLocalServiceTaskState(serviceName, methodName string) state.ServiceTaskState {
+ serviceTaskStateImpl := state.NewServiceTaskStateImpl()
+ serviceTaskStateImpl.SetName(fmt.Sprintf("%s_%s", serviceName, methodName))
+ serviceTaskStateImpl.SetIsAsync(false)
+ serviceTaskStateImpl.SetServiceName(serviceName)
+ serviceTaskStateImpl.SetServiceType("local")
+ serviceTaskStateImpl.SetServiceMethod(methodName)
+ return serviceTaskStateImpl
+}
diff --git a/pkg/saga/statemachine/engine/pcext/compensation_holder.go b/pkg/saga/statemachine/engine/pcext/compensation_holder.go
new file mode 100644
index 00000000..e7ecfb81
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/compensation_holder.go
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package pcext
+
+import (
+ "context"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+ "seata.apache.org/seata-go/pkg/util/collection"
+)
+
+type CompensationHolder struct {
+ statesNeedCompensation *sync.Map
+ statesForCompensation *sync.Map
+ stateStackNeedCompensation *collection.Stack
+}
+
+func (c *CompensationHolder) StatesNeedCompensation() *sync.Map {
+ return c.statesNeedCompensation
+}
+
+func (c *CompensationHolder) SetStatesNeedCompensation(statesNeedCompensation *sync.Map) {
+ c.statesNeedCompensation = statesNeedCompensation
+}
+
+func (c *CompensationHolder) StatesForCompensation() *sync.Map {
+ return c.statesForCompensation
+}
+
+func (c *CompensationHolder) SetStatesForCompensation(statesForCompensation *sync.Map) {
+ c.statesForCompensation = statesForCompensation
+}
+
+func (c *CompensationHolder) StateStackNeedCompensation() *collection.Stack {
+ return c.stateStackNeedCompensation
+}
+
+func (c *CompensationHolder) SetStateStackNeedCompensation(stateStackNeedCompensation *collection.Stack) {
+ c.stateStackNeedCompensation = stateStackNeedCompensation
+}
+
+func (c *CompensationHolder) AddToBeCompensatedState(stateName string, toBeCompensatedState statelang.StateInstance) {
+ c.statesNeedCompensation.Store(stateName, toBeCompensatedState)
+}
+
+func NewCompensationHolder() *CompensationHolder {
+ return &CompensationHolder{
+ statesNeedCompensation: &sync.Map{},
+ statesForCompensation: &sync.Map{},
+ stateStackNeedCompensation: collection.NewStack(),
+ }
+}
+
+func GetCurrentCompensationHolder(ctx context.Context, processContext process_ctrl.ProcessContext, forceCreate bool) *CompensationHolder {
+ var holder *CompensationHolder
+ if v := processContext.GetVariable(constant.VarNameCurrentCompensationHolder); v != nil {
+ if h, ok := v.(*CompensationHolder); ok {
+ holder = h
+ }
+ }
+ // ensure context mutex exists
+ lock, _ := processContext.GetVariable(constant.VarNameProcessContextMutexLock).(*sync.Mutex)
+ if lock == nil {
+ lock = &sync.Mutex{}
+ processContext.SetVariable(constant.VarNameProcessContextMutexLock, lock)
+ }
+ lock.Lock()
+ defer lock.Unlock()
+ if holder == nil && forceCreate {
+ holder = NewCompensationHolder()
+ processContext.SetVariable(constant.VarNameCurrentCompensationHolder, holder)
+ }
+ return holder
+}
diff --git a/pkg/saga/statemachine/engine/pcext/engine_utils.go b/pkg/saga/statemachine/engine/pcext/engine_utils.go
new file mode 100644
index 00000000..c3d0177e
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/engine_utils.go
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package pcext
+
+import (
+ "context"
+ "errors"
+ "reflect"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/sync/semaphore"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+ "seata.apache.org/seata-go/pkg/util/log"
+)
+
+func EndStateMachine(ctx context.Context, processContext process_ctrl.ProcessContext) error {
+ if processContext.HasVariable(constant.VarNameIsLoopState) {
+ if processContext.HasVariable(constant.LoopSemaphore) {
+ weighted, ok := processContext.GetVariable(constant.LoopSemaphore).(*semaphore.Weighted)
+ if !ok || weighted == nil {
+ return errors.New("semaphore type is not *semaphore.Weighted")
+ }
+ weighted.Release(1)
+ }
+ }
+
+ stateMachineInstance, ok := processContext.GetVariable(constant.VarNameStateMachineInst).(statelang.StateMachineInstance)
+ if !ok {
+ return errors.New("state machine instance type is not statelang.StateMachineInstance")
+ }
+
+ stateMachineInstance.SetEndTime(time.Now())
+
+ var exp error
+ if v := processContext.GetVariable(constant.VarNameCurrentException); v != nil {
+ ev, ok := v.(error)
+ if !ok {
+ return errors.New("exception type is not error")
+ }
+ exp = ev
+ }
+
+ if exp != nil {
+ stateMachineInstance.SetException(exp)
+ log.Debugf("Exception Occurred: %s", exp)
+ }
+
+ stateMachineConfig, ok := processContext.GetVariable(constant.VarNameStateMachineConfig).(engine.StateMachineConfig)
+ if !ok {
+ return errors.New("state machine config type is not engine.StateMachineConfig")
+ }
+
+ if err := stateMachineConfig.StatusDecisionStrategy().DecideOnEndState(ctx, processContext, stateMachineInstance, exp); err != nil {
+ return err
+ }
+
+ // Reconcile compensation status using executed compensation states to align with Java semantics
+ // If there are compensation states and all succeeded, override to SU. If any failed/unknown, mark UN.
+ // This guards against edge cases where holder/stack visibility causes FA.
+ compSeen := false
+ compAllSU := true
+ for _, si := range stateMachineInstance.StateList() {
+ if si.StateIDCompensatedFor() != "" {
+ compSeen = true
+ if si.Status() != statelang.SU {
+ compAllSU = false
+ break
+ }
+ }
+ }
+ if compSeen {
+ if compAllSU {
+ log.Debugf("All compensation states SU, overriding machine compensation_status to SU for [%s]", stateMachineInstance.ID())
+ stateMachineInstance.SetCompensationStatus(statelang.SU)
+ // In Java semantics, compensation success with Fail end yields final machine status FA
+ stateMachineInstance.SetStatus(statelang.FA)
+ } else if stateMachineInstance.CompensationStatus() == "" || stateMachineInstance.CompensationStatus() == statelang.RU {
+ log.Debugf("Compensation states contain non-SU, marking compensation_status UN for [%s]", stateMachineInstance.ID())
+ stateMachineInstance.SetCompensationStatus(statelang.UN)
+ }
+ } else {
+ log.Debugf("No compensation states observed for machine [%s]", stateMachineInstance.ID())
+ if v, ok := processContext.GetVariable(constant.VarNameFailEndStateFlag).(bool); ok && v {
+ // Fail end without compensation: normalize to status=FA, empty compensation_status
+ stateMachineInstance.SetCompensationStatus("")
+ stateMachineInstance.SetStatus(statelang.FA)
+ }
+ }
+
+ contextParams, ok := processContext.GetVariable(constant.VarNameStateMachineContext).(map[string]interface{})
+ if !ok {
+ return errors.New("state machine context type is not map[string]interface{}")
+ }
+ endParams := stateMachineInstance.EndParams()
+ for k, v := range contextParams {
+ endParams[k] = v
+ }
+ stateMachineInstance.SetEndParams(endParams)
+
+ switch v := processContext.GetInstruction().(type) {
+ case *StateInstruction:
+ v.SetEnd(true)
+ case StateInstruction:
+ tmp := v
+ tmp.SetEnd(true)
+ processContext.SetInstruction(&tmp)
+ default:
+ return errors.New("state instruction type is not process_ctrl.StateInstruction")
+ }
+
+ stateMachineInstance.SetRunning(false)
+ stateMachineInstance.SetEndTime(time.Now())
+
+ if stateMachineInstance.StateMachine().IsPersist() && stateMachineConfig.StateLangStore() != nil {
+ err := stateMachineConfig.StateLogStore().RecordStateMachineFinished(ctx, stateMachineInstance, processContext)
+ if err != nil {
+ return err
+ }
+ }
+
+ callBack, ok := processContext.GetVariable(constant.VarNameAsyncCallback).(engine.CallBack)
+ if ok {
+ if exp != nil {
+ callBack.OnError(ctx, processContext, stateMachineInstance, exp)
+ } else {
+ callBack.OnFinished(ctx, processContext, stateMachineInstance)
+ }
+ }
+
+ return nil
+}
+
+func HandleException(processContext process_ctrl.ProcessContext, abstractTaskState *state.AbstractTaskState, err error) {
+ catches := abstractTaskState.Catches()
+ for _, exceptionMatch := range catches {
+ exceptions := exceptionMatch.Exceptions()
+ exceptionTypes := exceptionMatch.ExceptionTypes()
+ if len(exceptions) != 0 {
+ if exceptionTypes == nil {
+ lock, _ := processContext.GetVariable(constant.VarNameProcessContextMutexLock).(*sync.Mutex)
+ if lock != nil {
+ lock.Lock()
+ defer lock.Unlock()
+ }
+ if exceptionMatch.ExceptionTypes() == nil {
+ errorType := reflect.TypeOf(errors.New(""))
+ for range exceptions {
+ exceptionTypes = append(exceptionTypes, errorType)
+ }
+ exceptionMatch.SetExceptionTypes(exceptionTypes)
+ } else {
+ exceptionTypes = exceptionMatch.ExceptionTypes()
+ }
+ }
+ }
+
+ for i := range exceptionTypes {
+ if reflect.TypeOf(err) == exceptionTypes[i] {
+ // HACK: we can not get error type in config file during runtime, so we use exception str
+ if strings.Contains(err.Error(), exceptions[i]) {
+ hierarchicalProcessContext := processContext.(process_ctrl.HierarchicalProcessContext)
+ hierarchicalProcessContext.SetVariable(constant.VarNameCurrentExceptionRoute, exceptionMatch.Next())
+ return
+ }
+ }
+ }
+ }
+
+ log.Error("Task execution failed and no catches configured")
+ hierarchicalProcessContext := processContext.(process_ctrl.HierarchicalProcessContext)
+ hierarchicalProcessContext.SetVariable(constant.VarNameIsExceptionNotCatch, true)
+}
+
+// GetOriginStateName get origin state name without suffix like fork
+func GetOriginStateName(stateInstance statelang.StateInstance) string {
+ stateName := stateInstance.Name()
+ if stateName != "" {
+ end := strings.LastIndex(stateName, constant.LoopStateNamePattern)
+ if end > -1 {
+ return stateName[:end+1]
+ }
+ }
+ return stateName
+}
+
+// IsTimeout test if is timeout
+func IsTimeout(gmtUpdated time.Time, timeoutMillis int) bool {
+ if timeoutMillis < 0 {
+ return false
+ }
+ return time.Now().Unix()-gmtUpdated.Unix() > int64(timeoutMillis)
+}
+
+func GenerateParentId(stateInstance statelang.StateInstance) string {
+ return stateInstance.MachineInstanceID() + constant.SeperatorParentId + stateInstance.ID()
+}
+
+// GetNetExceptionType Speculate what kind of network anomaly is caused by the error
+func GetNetExceptionType(err error) constant.NetExceptionType {
+ if err == nil {
+ return constant.NotNetException
+ }
+
+ // If it contains a specific error message, simply guess
+ errMsg := err.Error()
+ if strings.Contains(errMsg, "connection refused") {
+ return constant.ConnectException
+ } else if strings.Contains(errMsg, "timeout") {
+ return constant.ConnectTimeoutException
+ } else if strings.Contains(errMsg, "i/o timeout") {
+ return constant.ReadTimeoutException
+ }
+ return constant.NotNetException
+}
diff --git a/pkg/saga/statemachine/engine/pcext/instruction.go b/pkg/saga/statemachine/engine/pcext/instruction.go
new file mode 100644
index 00000000..9ff0d1c0
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/instruction.go
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package pcext
+
+import (
+ "errors"
+ "fmt"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+)
+
+type StateInstruction struct {
+ stateName string
+ stateMachineName string
+ tenantId string
+ end bool
+ temporaryState statelang.State
+}
+
+func NewStateInstruction(stateMachineName string, tenantId string) *StateInstruction {
+ return &StateInstruction{stateMachineName: stateMachineName, tenantId: tenantId}
+}
+
+func (s *StateInstruction) StateName() string {
+ return s.stateName
+}
+
+func (s *StateInstruction) SetStateName(stateName string) {
+ s.stateName = stateName
+}
+
+func (s *StateInstruction) StateMachineName() string {
+ return s.stateMachineName
+}
+
+func (s *StateInstruction) SetStateMachineName(stateMachineName string) {
+ s.stateMachineName = stateMachineName
+}
+
+func (s *StateInstruction) TenantId() string {
+ return s.tenantId
+}
+
+func (s *StateInstruction) SetTenantId(tenantId string) {
+ s.tenantId = tenantId
+}
+
+func (s *StateInstruction) End() bool {
+ return s.end
+}
+
+func (s *StateInstruction) SetEnd(end bool) {
+ s.end = end
+}
+
+func (s *StateInstruction) TemporaryState() statelang.State {
+ return s.temporaryState
+}
+
+func (s *StateInstruction) SetTemporaryState(temporaryState statelang.State) {
+ s.temporaryState = temporaryState
+}
+
+func (s *StateInstruction) GetState(context process_ctrl.ProcessContext) (statelang.State, error) {
+ if s.temporaryState != nil {
+ return s.temporaryState, nil
+ }
+
+ if s.stateMachineName == "" {
+ return nil, errors.New("stateMachineName is required")
+ }
+
+ stateMachineConfig, ok := context.GetVariable(constant.VarNameStateMachineConfig).(engine.StateMachineConfig)
+ if !ok {
+ return nil, errors.New("stateMachineConfig is required in context")
+ }
+ stateMachine, err := stateMachineConfig.StateMachineRepository().GetLastVersionStateMachine(s.stateMachineName, s.tenantId)
+ if err != nil {
+ return nil, errors.New("get stateMachine in state machine repository error")
+ }
+ if stateMachine == nil {
+ return nil, errors.New(fmt.Sprintf("stateMachine [%s] is not exist", s.stateMachineName))
+ }
+
+ if s.stateName == "" {
+ s.stateName = stateMachine.StartState()
+ }
+
+ state := stateMachine.States()[s.stateName]
+ if state == nil {
+ return nil, errors.New(fmt.Sprintf("state [%s] is not exist", s.stateName))
+ }
+
+ return state, nil
+}
diff --git a/pkg/saga/statemachine/engine/pcext/loop_context_holder.go b/pkg/saga/statemachine/engine/pcext/loop_context_holder.go
new file mode 100644
index 00000000..60928c10
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/loop_context_holder.go
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package pcext
+
+import (
+ "context"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+)
+
+type LoopContextHolder struct {
+ nrOfInstances int32
+ nrOfActiveInstances int32
+ nrOfCompletedInstances int32
+ failEnd bool
+ completionConditionSatisfied bool
+ loopCounterStack []int
+ forwardCounterStack []int
+ collection interface{}
+}
+
+func NewLoopContextHolder() *LoopContextHolder {
+ return &LoopContextHolder{
+ nrOfInstances: 0,
+ nrOfActiveInstances: 0,
+ nrOfCompletedInstances: 0,
+ failEnd: false,
+ completionConditionSatisfied: false,
+ loopCounterStack: make([]int, 0),
+ forwardCounterStack: make([]int, 0),
+ collection: nil,
+ }
+}
+
+func GetCurrentLoopContextHolder(ctx context.Context, processContext process_ctrl.ProcessContext, forceCreate bool) *LoopContextHolder {
+ mutex := processContext.GetVariable(constant.VarNameProcessContextMutexLock).(*sync.Mutex)
+ mutex.Lock()
+ defer mutex.Unlock()
+
+ loopContextHolder := processContext.GetVariable(constant.VarNameCurrentLoopContextHolder).(*LoopContextHolder)
+ if loopContextHolder == nil && forceCreate {
+ loopContextHolder = &LoopContextHolder{}
+ processContext.SetVariable(constant.VarNameCurrentLoopContextHolder, loopContextHolder)
+ }
+ return loopContextHolder
+}
+
+func ClearCurrent(ctx context.Context, processContext process_ctrl.ProcessContext) {
+ processContext.RemoveVariable(constant.VarNameCurrentLoopContextHolder)
+}
+
+func (l *LoopContextHolder) NrOfInstances() int32 {
+ return l.nrOfInstances
+}
+
+func (l *LoopContextHolder) NrOfActiveInstances() int32 {
+ return l.nrOfActiveInstances
+}
+
+func (l *LoopContextHolder) NrOfCompletedInstances() int32 {
+ return l.nrOfCompletedInstances
+}
+
+func (l *LoopContextHolder) FailEnd() bool {
+ return l.failEnd
+}
+
+func (l *LoopContextHolder) SetFailEnd(failEnd bool) {
+ l.failEnd = failEnd
+}
+
+func (l *LoopContextHolder) CompletionConditionSatisfied() bool {
+ return l.completionConditionSatisfied
+}
+
+func (l *LoopContextHolder) SetCompletionConditionSatisfied(completionConditionSatisfied bool) {
+ l.completionConditionSatisfied = completionConditionSatisfied
+}
+
+func (l *LoopContextHolder) LoopCounterStack() []int {
+ return l.loopCounterStack
+}
+
+func (l *LoopContextHolder) ForwardCounterStack() []int {
+ return l.forwardCounterStack
+}
+
+func (l *LoopContextHolder) Collection() interface{} {
+ return l.collection
+}
+
+func (l *LoopContextHolder) SetCollection(collection interface{}) {
+ l.collection = collection
+}
diff --git a/pkg/saga/statemachine/engine/pcext/loop_task_utils.go b/pkg/saga/statemachine/engine/pcext/loop_task_utils.go
new file mode 100644
index 00000000..8b0de8ae
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/loop_task_utils.go
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package pcext
+
+import (
+ "context"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+ "seata.apache.org/seata-go/pkg/util/log"
+)
+
+func GetLoopConfig(ctx context.Context, processContext process_ctrl.ProcessContext, currentState statelang.State) state.Loop {
+ if !matchLoop(currentState) {
+ return nil
+ }
+ // Extract underlying AbstractTaskState pointer safely
+ var task *state.AbstractTaskState
+ switch s := currentState.(type) {
+ case *state.ServiceTaskStateImpl:
+ task = s.AbstractTaskState
+ case *state.ScriptTaskStateImpl:
+ task = s.AbstractTaskState
+ case *state.SubStateMachineImpl:
+ if s.ServiceTaskStateImpl != nil {
+ task = s.ServiceTaskStateImpl.AbstractTaskState
+ }
+ default:
+ return nil
+ }
+ if task == nil || task.Loop() == nil {
+ return nil
+ }
+
+ loop := task.Loop()
+ collectionName := loop.Collection()
+ if collectionName == "" {
+ return nil
+ }
+
+ stateMachineInstance := processContext.GetVariable(constant.VarNameStateMachineInst).(statelang.StateMachineInstance)
+ stateMachineConfig := processContext.GetVariable(constant.VarNameStateMachineConfig).(engine.StateMachineConfig)
+ expression := CreateValueExpression(stateMachineConfig.ExpressionResolver(), collectionName)
+ collection := GetValue(expression, stateMachineInstance.Context(), nil)
+ if collection == nil {
+ log.Warn("State [{}] loop collection param [{}] invalid", currentState.Name(), collectionName)
+ return nil
+ }
+ if collectionList, ok := collection.([]any); ok && len(collectionList) > 0 {
+ current := GetCurrentLoopContextHolder(ctx, processContext, true)
+ current.SetCollection(collection)
+ return loop
+ }
+ return nil
+}
+
+func matchLoop(currentState statelang.State) bool {
+ return currentState != nil && (constant.StateTypeServiceTask == currentState.Type() ||
+ constant.StateTypeScriptTask == currentState.Type() || constant.StateTypeSubStateMachine == currentState.Type())
+}
diff --git a/pkg/saga/statemachine/engine/pcext/parameter_utils.go b/pkg/saga/statemachine/engine/pcext/parameter_utils.go
new file mode 100644
index 00000000..95dfea14
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/parameter_utils.go
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package pcext
+
+import (
+ "fmt"
+ "strings"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/expr"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+)
+
+func CreateInputParams(processContext process_ctrl.ProcessContext, expressionResolver expr.ExpressionResolver,
+ stateInstance *statelang.StateInstanceImpl, serviceTaskState *state.AbstractTaskState, variablesFrom any) []any {
+ inputAssignments := serviceTaskState.Input()
+ if len(inputAssignments) == 0 {
+ return inputAssignments
+ }
+
+ inputExpressions := serviceTaskState.InputExpressions()
+ if len(inputExpressions) == 0 {
+ lock := processContext.GetVariable(constant.VarNameProcessContextMutexLock).(*sync.Mutex)
+ lock.Lock()
+ defer lock.Unlock()
+ inputExpressions = serviceTaskState.InputExpressions()
+ if len(inputExpressions) == 0 {
+ inputExpressions = make([]any, 0, len(inputAssignments))
+
+ for _, assignment := range inputAssignments {
+ inputExpressions = append(inputExpressions, CreateValueExpression(expressionResolver, assignment))
+ }
+ }
+ serviceTaskState.SetInputExpressions(inputExpressions)
+ }
+ inputValues := make([]any, 0, len(inputExpressions))
+ for _, valueExpression := range inputExpressions {
+ value := GetValue(valueExpression, variablesFrom, stateInstance)
+ inputValues = append(inputValues, value)
+ }
+
+ return inputValues
+}
+
+func CreateOutputParams(config engine.StateMachineConfig, expressionResolver expr.ExpressionResolver,
+ serviceTaskState *state.AbstractTaskState, variablesFrom any) (map[string]any, error) {
+ outputAssignments := serviceTaskState.Output()
+ if len(outputAssignments) == 0 {
+ return make(map[string]any, 0), nil
+ }
+
+ outputExpressions := serviceTaskState.OutputExpressions()
+ if outputExpressions == nil {
+ config.ComponentLock().Lock()
+ defer config.ComponentLock().Unlock()
+ outputExpressions = serviceTaskState.OutputExpressions()
+ if outputExpressions == nil {
+ outputExpressions = make(map[string]any, len(outputAssignments))
+ for key, value := range outputAssignments {
+ outputExpressions[key] = CreateValueExpression(expressionResolver, value)
+ }
+ }
+ serviceTaskState.SetOutputExpressions(outputExpressions)
+ }
+ outputValues := make(map[string]any, len(outputExpressions))
+ for paramName := range outputExpressions {
+ outputValues[paramName] = GetValue(outputExpressions[paramName], variablesFrom, nil)
+ }
+ return outputValues, nil
+}
+
+func CreateValueExpression(expressionResolver expr.ExpressionResolver, paramAssignment any) any {
+ switch v := paramAssignment.(type) {
+ case expr.Expression:
+ return v
+ case map[string]any:
+ paramMap := make(map[string]any, len(v))
+ for key, value := range v {
+ paramMap[key] = CreateValueExpression(expressionResolver, value)
+ }
+ return paramMap
+ case []any:
+ paramList := make([]any, 0, len(v))
+ for _, value := range v {
+ paramList = append(paramList, CreateValueExpression(expressionResolver, value))
+ }
+ return paramList
+ case string:
+ if !strings.HasPrefix(v, "$") {
+ return v
+ }
+ return expressionResolver.Expression(v)
+ default:
+ return paramAssignment
+ }
+}
+
+func GetValue(valueExpression any, variablesFrom any, stateInstance statelang.StateInstance) any {
+ switch v := valueExpression.(type) {
+ case expr.Expression:
+ value := v.Value(variablesFrom)
+ if _, ok := v.(expr.SequenceExpression); value != nil && stateInstance != nil && stateInstance.BusinessKey() == "" && ok {
+ stateInstance.SetBusinessKey(fmt.Sprintf("%v", value))
+ }
+ return value
+ case map[string]any:
+ mapValue := make(map[string]any, len(v))
+ for key, value := range v {
+ resolved := GetValue(value, variablesFrom, stateInstance)
+ if resolved != nil {
+ mapValue[key] = resolved
+ }
+ }
+ return mapValue
+ case []any:
+ listValue := make([]any, 0, len(v))
+ for i := range v {
+ listValue = append(listValue, GetValue(v[i], variablesFrom, stateInstance))
+ }
+ return listValue
+ default:
+ return valueExpression
+ }
+}
diff --git a/pkg/saga/statemachine/engine/pcext/process_handler.go b/pkg/saga/statemachine/engine/pcext/process_handler.go
new file mode 100644
index 00000000..aef3aae1
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/process_handler.go
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package pcext
+
+import (
+ "context"
+ "errors"
+ "sync"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine/expr"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+ stateimpl "seata.apache.org/seata-go/pkg/saga/statemachine/statelang/state"
+)
+
+type StateHandler interface {
+ State() string
+ process_ctrl.ProcessHandler
+}
+
+type InterceptAbleStateHandler interface {
+ StateHandler
+ StateHandlerInterceptorList() []StateHandlerInterceptor
+ RegistryStateHandlerInterceptor(stateHandlerInterceptor StateHandlerInterceptor)
+}
+
+type StateHandlerInterceptor interface {
+ PreProcess(ctx context.Context, processContext process_ctrl.ProcessContext) error
+ PostProcess(ctx context.Context, processContext process_ctrl.ProcessContext) error
+ Match(stateType string) bool
+}
+
+type StateMachineProcessHandler struct {
+ mp map[string]StateHandler
+ mu sync.RWMutex
+}
+
+func NewStateMachineProcessHandler() *StateMachineProcessHandler {
+ return &StateMachineProcessHandler{
+ mp: make(map[string]StateHandler),
+ }
+}
+
+func (s *StateMachineProcessHandler) Process(ctx context.Context, processContext process_ctrl.ProcessContext) error {
+ var stateInstruction *StateInstruction
+ switch v := processContext.GetInstruction().(type) {
+ case *StateInstruction:
+ stateInstruction = v
+ case StateInstruction:
+ tmp := v
+ stateInstruction = &tmp
+ default:
+ return errors.New("invalid state instruction from processContext")
+ }
+
+ state, err := stateInstruction.GetState(processContext)
+ if err != nil {
+ return err
+ }
+
+ // mark Fail end-state to influence final machine status decision
+ if state.Type() == constant.StateTypeFail {
+ processContext.SetVariable(constant.VarNameFailEndStateFlag, true)
+ }
+
+ stateType := state.Type()
+ stateHandler := s.GetStateHandler(stateType)
+
+ interceptAbleStateHandler, ok := stateHandler.(InterceptAbleStateHandler)
+
+ var stateHandlerInterceptorList []StateHandlerInterceptor
+ if ok {
+ stateHandlerInterceptorList = interceptAbleStateHandler.StateHandlerInterceptorList()
+ }
+
+ if stateHandlerInterceptorList != nil && len(stateHandlerInterceptorList) > 0 {
+ for _, stateHandlerInterceptor := range stateHandlerInterceptorList {
+ err = stateHandlerInterceptor.PreProcess(ctx, processContext)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ // Prepare current state instance in context before processing
+ smInst, ok := processContext.GetVariable(constant.VarNameStateMachineInst).(statelang.StateMachineInstance)
+ if !ok || smInst == nil {
+ return errors.New("state machine instance not found in context")
+ }
+
+ stInst := statelang.NewStateInstanceImpl()
+ stInst.SetName(state.Name())
+ stInst.SetType(state.Type())
+ // If service task, enrich service attributes
+ if svc, ok := state.(*stateimpl.ServiceTaskStateImpl); ok {
+ stInst.SetServiceName(svc.ServiceName())
+ stInst.SetServiceMethod(svc.ServiceMethod())
+ stInst.SetServiceType(svc.ServiceType())
+ stInst.SetForUpdate(svc.ForUpdate())
+
+ // ensure mutex lock for parameter evaluation exists
+ if !processContext.HasVariable(constant.VarNameProcessContextMutexLock) {
+ processContext.SetVariable(constant.VarNameProcessContextMutexLock, &sync.Mutex{})
+ }
+ // if this is a compensation execution, mark the link to original state
+ if v := processContext.GetVariable("_compensate_for_state_id_"); v != nil {
+ if sid, ok := v.(string); ok && sid != "" {
+ stInst.SetStateIDCompensatedFor(sid)
+ // also add to holder's 'StatesForCompensation' for final status decision
+ holder := GetCurrentCompensationHolder(ctx, processContext, true)
+ holder.StatesForCompensation().Store(stInst.Name(), stInst)
+ // clear after consuming
+ processContext.RemoveVariable("_compensate_for_state_id_")
+ }
+ }
+
+ // evaluate input params from CEL expressions if any
+ if cfg, ok := processContext.GetVariable(constant.VarNameStateMachineConfig).(engine.StateMachineConfig); ok {
+ var exprResolver expr.ExpressionResolver = cfg.ExpressionResolver()
+ // use all variables in process context for expression scope
+ variables := processContext.GetVariables()
+ inputParams := CreateInputParams(processContext, exprResolver, stInst, svc.AbstractTaskState, variables)
+ processContext.SetVariable(constant.VarNameInputParams, inputParams)
+ }
+ }
+ // Assign a temporary ID key for list/map tracking
+ tmpId := state.Name()
+ if tmpId == "" {
+ tmpId = "state"
+ }
+ smInst.PutState(tmpId, stInst)
+ processContext.SetVariable(constant.VarNameStateInst, stInst)
+
+ // Execute handler when present; end states don't require a handler
+ if stateHandler != nil {
+ err = stateHandler.Process(ctx, processContext)
+ if err != nil {
+ return err
+ }
+ }
+
+ if stateHandlerInterceptorList != nil && len(stateHandlerInterceptorList) > 0 {
+ for _, stateHandlerInterceptor := range stateHandlerInterceptorList {
+ err = stateHandlerInterceptor.PostProcess(ctx, processContext)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ // Set execution result on state instance
+ if ex, _ := processContext.GetVariable(constant.VarNameCurrentException).(error); ex != nil {
+ stInst.SetStatus(statelang.FA)
+ stInst.SetError(ex)
+ } else {
+ // For Fail end state, mark FA; otherwise mark SU
+ if stateType == constant.StateTypeFail {
+ stInst.SetStatus(statelang.FA)
+ } else {
+ stInst.SetStatus(statelang.SU)
+ }
+ }
+
+ return nil
+}
+
+func (s *StateMachineProcessHandler) GetStateHandler(stateType string) StateHandler {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ return s.mp[stateType]
+}
+
+func (s *StateMachineProcessHandler) RegistryStateHandler(stateType string, stateHandler StateHandler) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if s.mp == nil {
+ s.mp = make(map[string]StateHandler)
+ }
+ s.mp[stateType] = stateHandler
+}
diff --git a/pkg/saga/statemachine/engine/pcext/process_router.go b/pkg/saga/statemachine/engine/pcext/process_router.go
new file mode 100644
index 00000000..c650645c
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/process_router.go
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package pcext
+
+import (
+ "context"
+
+ "github.com/pkg/errors"
+
+ "seata.apache.org/seata-go/pkg/saga/statemachine/constant"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/engine"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/process_ctrl"
+ "seata.apache.org/seata-go/pkg/saga/statemachine/statelang"
+)
+
+type StateMachineProcessRouter struct {
+ stateRouters map[string]process_ctrl.StateRouter
+}
+
+func (s *StateMachineProcessRouter) Route(ctx context.Context, processContext process_ctrl.ProcessContext) (process_ctrl.Instruction, error) {
+ var stateInstruction *StateInstruction
+ switch v := processContext.GetInstruction().(type) {
+ case *StateInstruction:
+ stateInstruction = v
+ case StateInstruction:
+ tmp := v
+ stateInstruction = &tmp
+ default:
+ return nil, errors.New("instruction is not a state instruction")
+ }
+
+ var state statelang.State
+ if stateInstruction.TemporaryState() != nil {
+ state = stateInstruction.TemporaryState()
+ stateInstruction.SetTemporaryState(nil)
+ } else {
+ stateMachineConfig, ok := processContext.GetVariable(constant.VarNameStateMachineConfig).(engine.StateMachineConfig)
+ if !ok {
+ return nil, errors.New("state machine config not found")
+ }
+
+ stateMachine, err := stateMachineConfig.StateMachineRepository().GetStateMachineByNameAndTenantId(stateInstruction.StateMachineName(),
+ stateInstruction.TenantId())
+ if err != nil {
+ return nil, err
+ }
+
+ state = stateMachine.States()[stateInstruction.StateName()]
+ }
+
+ stateType := state.Type()
+ router := s.stateRouters[stateType]
+
+ var interceptors []process_ctrl.StateRouterInterceptor
+ if interceptAbleStateRouter, ok := router.(process_ctrl.InterceptAbleStateRouter); ok {
+ interceptors = interceptAbleStateRouter.StateRouterInterceptor()
+ }
+
+ var executedInterceptors []process_ctrl.StateRouterInterceptor
+ var exception error
+ instruction, exception := func() (process_ctrl.Instruction, error) {
+ if interceptors == nil || len(executedInterceptors) == 0 {
+ executedInterceptors = make([]process_ctrl.StateRouterInterceptor, 0, len(interceptors))
+ for _, interceptor := range interceptors {
+ executedInterceptors = append(executedInterceptors, interceptor)
+ err := interceptor.PreRoute(ctx, processContext, state)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ instruction, err := router.Route(ctx, processContext, state)
+ if err != nil {
+ return nil, err
+ }
+ return instruction, nil
+ }()
+
+ if interceptors == nil || len(executedInterceptors) == 0 {
+ for i := len(executedInterceptors) - 1; i >= 0; i-- {
+ err := executedInterceptors[i].PostRoute(ctx, processContext, instruction, exception)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // if 'Succeed' or 'Fail' State did not configured, we must end the state machine
+ if instruction == nil && !stateInstruction.End() {
+ err := EndStateMachine(ctx, processContext)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return instruction, nil
+}
+
+func (s *StateMachineProcessRouter) InitDefaultStateRouters() {
+ if s.stateRouters == nil || len(s.stateRouters) == 0 {
+ s.stateRouters = make(map[string]process_ctrl.StateRouter)
+ taskStateRouter := &TaskStateRouter{}
+ s.stateRouters[constant.StateTypeServiceTask] = taskStateRouter
+ s.stateRouters[constant.StateTypeScriptTask] = taskStateRouter
+ s.stateRouters[constant.StateTypeChoice] = taskStateRouter
+ s.stateRouters[constant.StateTypeCompensationTrigger] = taskStateRouter
+ s.stateRouters[constant.StateTypeSubStateMachine] = taskStateRouter
+ s.stateRouters[constant.StateTypeCompensateSubMachine] = taskStateRouter
+ s.stateRouters[constant.StateTypeLoopStart] = taskStateRouter
+
+ endStateRouter := &EndStateRouter{}
+ s.stateRouters[constant.StateTypeSucceed] = endStateRouter
+ s.stateRouters[constant.StateTypeFail] = endStateRouter
+ }
+}
+
+func (s *StateMachineProcessRouter) StateRouters() map[string]process_ctrl.StateRouter {
+ return s.stateRouters
+}
+
+func (s *StateMachineProcessRouter) SetStateRouters(stateRouters map[string]process_ctrl.StateRouter) {
+ s.stateRouters = stateRouters
+}
diff --git a/pkg/saga/statemachine/engine/pcext/state_router_impl.go b/pkg/saga/statemachine/engine/pcext/state_router_impl.go
new file mode 100644
index 00000000..2fc1a1f9
--- /dev/null
+++ b/pkg/saga/statemachine/engine/pcext/state_router_impl.go
@@ -0,0 +1,355 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
... 12675 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@seata.apache.org
For additional commands, e-mail: notifications-help@seata.apache.org