This is an automated email from the ASF dual-hosted git repository.
wangjian pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new 1adb847e6f feature: add detailed saga result reporting to benchmark
cli (#8042)
1adb847e6f is described below
commit 1adb847e6f722a1fc0e0259f7f68fbb5c5aa1ea6
Author: zihenzzz <[email protected]>
AuthorDate: Wed Apr 15 18:38:17 2026 +0800
feature: add detailed saga result reporting to benchmark cli (#8042)
---
changes/en-us/2.x.md | 1 +
changes/zh-cn/2.x.md | 1 +
test-suite/seata-benchmark-cli/README.md | 190 ++++++++++++++-
.../seata/benchmark/BenchmarkApplication.java | 61 ++++-
.../apache/seata/benchmark/BenchmarkRunner.java | 2 +-
.../seata/benchmark/config/BenchmarkConfig.java | 110 +++++++++
.../benchmark/config/BenchmarkConfigLoader.java | 26 ++-
.../benchmark/constant/BenchmarkConstants.java | 3 +
.../seata/benchmark/executor/SagaModeExecutor.java | 254 +++++++++++++++++++--
.../benchmark/executor/WorkloadGenerator.java | 13 +-
.../seata/benchmark/model/BenchmarkMetrics.java | 81 ++++++-
.../seata/benchmark/monitor/MetricsCollector.java | 40 +++-
.../benchmark/saga/FailureRandomProvider.java | 51 +++++
.../benchmark/saga/InventoryDbSagaService.java | 183 +++++++++++++++
.../seata/benchmark/saga/InventorySagaService.java | 50 +++-
.../seata/benchmark/saga/OrderDbSagaService.java | 158 +++++++++++++
.../seata/benchmark/saga/OrderSagaService.java | 50 +++-
.../seata/benchmark/saga/PaymentDbSagaService.java | 179 +++++++++++++++
.../seata/benchmark/saga/PaymentSagaService.java | 50 +++-
.../seata/benchmark/saga/SagaDbEnvironment.java | 171 ++++++++++++++
20 files changed, 1621 insertions(+), 53 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 6b09014774..3fcbb47b13 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -43,6 +43,7 @@ Add changes here for all PR submitted to the 2.x branch.
### optimize:
+- [[#8042](https://github.com/apache/incubator-seata/pull/8042)] enhance the
traditional Saga benchmark with a more realistic state-machine workload,
reproducible failure modeling, richer Saga result reporting, and clearer Saga
parameter semantics
- [[#8022](https://github.com/apache/incubator-seata/pull/8022)] configure PMD
to output messages in English
- [[#7930](https://github.com/apache/incubator-seata/pull/7930)] pin the
Spring version for namingserver and console
- [[#7943](https://github.com/apache/incubator-seata/pull/7943)] update
jib-maven-plugin version and increase parallel test execution limits
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 4f3a8bf2d8..2f83fff256 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -45,6 +45,7 @@
### optimize:
+- [[#8042](https://github.com/apache/incubator-seata/pull/8042)] 增强传统 Saga
benchmark,支持更真实的状态机工作负载、可复现的失败建模、更丰富的 Saga 结果报告,以及更清晰的 Saga 参数语义
- [[#8022](https://github.com/apache/incubator-seata/pull/8022)] 配置PMD输出英文消息
- [[#7930](https://github.com/apache/incubator-seata/pull/7930)]
固定namingserver和console的Spring版本
- [[#7943](https://github.com/apache/incubator-seata/pull/7943)]
升级jib-maven-plugin版本和提高ci并行度
diff --git a/test-suite/seata-benchmark-cli/README.md
b/test-suite/seata-benchmark-cli/README.md
index 5a47136027..49ac85fe9c 100644
--- a/test-suite/seata-benchmark-cli/README.md
+++ b/test-suite/seata-benchmark-cli/README.md
@@ -27,6 +27,11 @@ A command-line benchmark tool for stress testing Seata
transaction modes.
- **Configurable TPS** (Transactions Per Second) control
- **Multi-threaded** workload generation
- **Fault injection** with configurable rollback percentage
+- **Selectable SAGA state machine shapes** such as `simple` and `order`
+- **Selectable SAGA workload implementations** such as `mock` and `db`
+- **Step-targeted SAGA failure injection** for forward steps such as
`inventory`, `payment`, and `order`
+- **Reproducible SAGA failure injection** with `--saga-random-seed`
+- **Step-targeted SAGA timeout simulation** with `--saga-timeout-step` and
`--saga-timeout-ms`
- **Window-based progress reporting** (every 10 seconds)
- Performance metrics collection (latency percentiles, success rate, TPS)
- **CSV export** for post-analysis
@@ -96,6 +101,59 @@ java -jar seata-benchmark-cli.jar \
--duration 60 \
--branches 3 \
--rollback-percentage 5
+
+# SAGA mode with explicit order state machine shape
+java -jar seata-benchmark-cli.jar \
+ --server 127.0.0.1:8091 \
+ --mode SAGA \
+ --tps 100 \
+ --duration 60 \
+ --branches 3 \
+ --saga-shape order
+
+# SAGA mode with DB-backed business actions (via Testcontainers MySQL)
+java -jar seata-benchmark-cli.jar \
+ --server 127.0.0.1:8091 \
+ --mode SAGA \
+ --tps 100 \
+ --duration 60 \
+ --branches 3 \
+ --saga-shape order \
+ --saga-workload db
+
+# SAGA mode with payment-step failure injection
+java -jar seata-benchmark-cli.jar \
+ --server 127.0.0.1:8091 \
+ --mode SAGA \
+ --tps 100 \
+ --duration 60 \
+ --branches 3 \
+ --rollback-percentage 20 \
+ --saga-fail-step payment
+
+# SAGA mode with reproducible payment-step failure injection
+java -jar seata-benchmark-cli.jar \
+ --server 127.0.0.1:8091 \
+ --mode SAGA \
+ --tps 100 \
+ --duration 60 \
+ --branches 3 \
+ --saga-shape order \
+ --rollback-percentage 20 \
+ --saga-fail-step payment \
+ --saga-random-seed 123
+
+# SAGA mode with payment-step timeout simulation
+java -jar seata-benchmark-cli.jar \
+ --server 127.0.0.1:8091 \
+ --mode SAGA \
+ --tps 10 \
+ --threads 1 \
+ --duration 10 \
+ --branches 3 \
+ --saga-shape order \
+ --saga-timeout-step payment \
+ --saga-timeout-ms 3000
```
### Performance Testing Modes
@@ -157,6 +215,12 @@ java -jar seata-benchmark-cli.jar \
Usage: seata-benchmark [-hV] [--application-id=<applicationId>]
[-d=<duration>] [--export-csv=<exportCsv>]
[-m=<mode>] [-s=<server>] [-t=<targetTps>]
+ [--saga-shape=<sagaShape>]
+ [--saga-workload=<sagaWorkload>]
+ [--saga-fail-step=<sagaFailStep>]
+ [--saga-random-seed=<sagaRandomSeed>]
+ [--saga-timeout-step=<sagaTimeoutStep>]
+ [--saga-timeout-ms=<sagaTimeoutMs>]
[--threads=<threads>]
[--tx-service-group=<txServiceGroup>]
[--warmup-duration=<warmupDuration>]
[--rollback-percentage=<rollbackPercentage>]
@@ -172,6 +236,26 @@ Options:
Warmup duration in seconds (default: 0)
--rollback-percentage=<rollbackPercentage>
Rollback percentage for fault injection
(0-100, default: 2)
+ --saga-shape=<sagaShape> Select SAGA state machine shape: simple
or order.
+ If omitted, the benchmark keeps the
existing
+ branches-based compatibility behavior.
+ --saga-workload=<sagaWorkload> Select SAGA workload implementation:
mock or db.
+ The default is mock. The db workload
uses
+ Testcontainers MySQL for DB-backed
order,
+ inventory, and payment actions.
+ --saga-fail-step=<sagaFailStep> Restrict SAGA failure injection to one
forward step:
+ inventory, payment, or order.
+ The failure ratio is still controlled by
+ --rollback-percentage.
+ --saga-random-seed=<sagaRandomSeed>
+ Optional random seed for reproducible
SAGA
+ failure injection behavior.
+ --saga-timeout-step=<sagaTimeoutStep>
+ Simulate SAGA timeout at one forward
step:
+ inventory, payment, or order.
+ --saga-timeout-ms=<sagaTimeoutMs>
+ Simulated timeout delay in milliseconds
for
+ SAGA timeout injection (default: 3000).
--branches=<branches> Number of branch transactions
0 = empty mode (protocol overhead only)
>=1 = real mode (actual execution)
@@ -232,21 +316,37 @@ When the benchmark completes, a final report is displayed:
===================================================
Seata Benchmark Final Report
===================================================
+Mode: SAGA
+Saga Workload: db
+Saga Shape: order
Total Transactions: 6,000
-Success Count: 5,940
-Failed Count: 60
-Success Rate: 99.00%
-Average TPS: 100.2
+Success Count: 5,780
+Failed Count: 220
+Success Rate: 96.33%
+Committed Count: 4,860
+Compensated Count: 920
+Execution Failed Count: 180
+Compensation Failed Count: 40
+Unknown Count: 0
+Committed Rate: 81.00%
+Compensated Rate: 15.33%
+End-State Success Rate: 96.33%
+Average TPS: 100.0
Elapsed Time: 60 seconds
Latency Statistics:
P50: 12 ms
P95: 45 ms
P99: 89 ms
+ P99.9: 120 ms
Max: 230 ms
===================================================
```
+For timeout simulation scenarios, the reported `Elapsed Time` may slightly
exceed the configured `--duration` because already-started transactions are
allowed to finish before the workload generator stops.
+
+For DB-backed SAGA scenarios, the final report and CSV output also include
`Saga Workload`, so benchmark results from `mock` and `db` workloads can be
compared explicitly.
+
### CSV Export
Use `--export-csv` to export metrics:
@@ -264,17 +364,29 @@ Output format:
```csv
Metric,Value
+Mode,SAGA
+Saga Workload,db
+Saga Shape,order
Total Transactions,6000
-Success Count,5940
-Failed Count,60
-Success Rate (%),99.00
-Average TPS,100.2
+Success Count,5780
+Failed Count,220
+Success Rate (%),96.33
+Committed Count,4860
+Compensated Count,920
+Execution Failed Count,180
+Compensation Failed Count,40
+Unknown Count,0
+Committed Rate (%),81.00
+Compensated Rate (%),15.33
+End-State Success Rate (%),96.33
+Average TPS,100.0
Elapsed Time (s),60
Latency P50 (ms),12
Latency P95 (ms),45
Latency P99 (ms),89
+Latency P99.9 (ms),120
Latency Max (ms),230
-Export Timestamp,2025-12-01 10:30:45
+Export Time,2025-12-01 10:30:45
```
## Examples
@@ -313,6 +425,48 @@ java -jar seata-benchmark-cli.jar \
--rollback-percentage 5
```
+### Test SAGA Mode with Payment-Step Failure Injection
+
+```bash
+java -jar seata-benchmark-cli.jar \
+ --server 127.0.0.1:8091 \
+ --mode SAGA \
+ --tps 100 \
+ --duration 60 \
+ --branches 3 \
+ --rollback-percentage 20 \
+ --saga-fail-step payment
+```
+
+### Test SAGA Mode with DB-Backed Business Actions
+
+```bash
+java -jar seata-benchmark-cli.jar \
+ --server 127.0.0.1:8091 \
+ --mode SAGA \
+ --tps 100 \
+ --duration 60 \
+ --branches 3 \
+ --saga-shape order \
+ --saga-workload db
+```
+
+### Test DB-Backed SAGA with Payment-Step Failure Injection
+
+```bash
+java -jar seata-benchmark-cli.jar \
+ --server 127.0.0.1:8091 \
+ --mode SAGA \
+ --tps 10 \
+ --threads 1 \
+ --duration 10 \
+ --branches 3 \
+ --saga-shape order \
+ --saga-workload db \
+ --rollback-percentage 20 \
+ --saga-fail-step payment
+```
+
### Test TCC Mode at High Load
```bash
@@ -372,10 +526,28 @@ When `--branches` is set to a value greater than 0:
- **SAGA Mode**:
- Uses Seata state machine engine
- Executes predefined state machine definitions
+ - Supports `mock` and `db` workloads
- Supports compensation on failure
- Available state machines:
- `benchmarkSimpleSaga`: For 1-2 branches
- `benchmarkOrderSaga`: For 3+ branches (order/inventory/payment)
+ - DB workload behavior:
+ - Starts MySQL via Testcontainers
+ - Creates benchmark inventory, account, and order tables
+ - Executes DB-backed inventory/payment/order actions with compensation
+
+### SAGA Workloads
+
+- `mock` workload:
+ - Keeps the lightweight benchmark-oriented implementation
+ - Uses in-memory Saga services with simulated delay, failure injection, and
timeout injection
+ - Is the default workload and preserves backward compatibility
+
+- `db` workload:
+ - Starts a MySQL container via Testcontainers
+ - Initializes benchmark tables for inventory, account, and order data
+ - Executes DB-backed order, inventory, and payment actions while still
supporting the same Saga shape, fail-step, random-seed, and timeout options
+ - Is intended for more realistic business-style Saga benchmarking
### Fault Injection
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/BenchmarkApplication.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/BenchmarkApplication.java
index 3018d7ccbe..3d6987c2c4 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/BenchmarkApplication.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/BenchmarkApplication.java
@@ -19,6 +19,7 @@ package org.apache.seata.benchmark;
import org.apache.seata.benchmark.config.BenchmarkConfig;
import org.apache.seata.benchmark.config.BenchmarkConfigLoader;
import org.apache.seata.benchmark.constant.BenchmarkConstants;
+import org.apache.seata.core.model.BranchType;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@@ -93,6 +94,36 @@ public class BenchmarkApplication implements
Callable<Integer> {
description = "Number of branch transactions (0=empty mode,
>=1=real mode with actual execution)")
private Integer branches;
+ @Option(
+ names = {"--saga-shape"},
+ description = "Select SAGA state machine shape: simple or order")
+ private String sagaShape;
+
+ @Option(
+ names = {"--saga-workload"},
+ description = "Select SAGA workload implementation: mock or db")
+ private String sagaWorkload;
+
+ @Option(
+ names = {"--saga-fail-step"},
+ description = "Force SAGA forward failure at a specific step:
inventory, payment, or order")
+ private String sagaFailStep;
+
+ @Option(
+ names = {"--saga-random-seed"},
+ description = "Seed for reproducible SAGA failure injection")
+ private Long sagaRandomSeed;
+
+ @Option(
+ names = {"--saga-timeout-step"},
+ description = "Simulate SAGA timeout at a specific forward step:
inventory, payment, or order")
+ private String sagaTimeoutStep;
+
+ @Option(
+ names = {"--saga-timeout-ms"},
+ description = "Simulated timeout delay in milliseconds for SAGA
timeout injection (default: 3000)")
+ private Integer sagaTimeoutMs;
+
public static void main(String[] args) {
// Parse server address from args before any Seata class loading
String serverAddr = BenchmarkConstants.DEFAULT_SERVER_ADDRESS;
@@ -143,7 +174,13 @@ public class BenchmarkApplication implements
Callable<Integer> {
applicationId,
txServiceGroup,
rollbackPercentage,
- branches);
+ branches,
+ sagaShape,
+ sagaWorkload,
+ sagaFailStep,
+ sagaRandomSeed,
+ sagaTimeoutStep,
+ sagaTimeoutMs);
}
private void printConfiguration(BenchmarkConfig config) {
@@ -159,6 +196,28 @@ public class BenchmarkApplication implements
Callable<Integer> {
System.out.println(" Rollback %: " + config.getRollbackPercentage()
+ "%");
System.out.println(" Branches: " + config.getBranches()
+ (config.getBranches() == 0 ? " (empty mode)" : " (real
mode)"));
+ if (config.getMode() == BranchType.SAGA
+ && config.getSagaWorkload() != null
+ && !config.getSagaWorkload().isEmpty()) {
+ System.out.println(" Saga Workload:" +
padValue(config.getSagaWorkload()));
+ }
+ if (config.getSagaShape() != null && !config.getSagaShape().isEmpty())
{
+ System.out.println(" Saga Shape: " + config.getSagaShape());
+ }
+ if (config.getSagaFailStep() != null &&
!config.getSagaFailStep().isEmpty()) {
+ System.out.println(" Saga Fail: " + config.getSagaFailStep());
+ }
+ if (config.getSagaRandomSeed() != null) {
+ System.out.println(" Saga Seed: " +
config.getSagaRandomSeed());
+ }
+ if (config.getSagaTimeoutStep() != null &&
!config.getSagaTimeoutStep().isEmpty()) {
+ System.out.println(" Saga Timeout: " +
config.getSagaTimeoutStep());
+ System.out.println(" Timeout Ms: " + config.getSagaTimeoutMs());
+ }
System.out.println();
}
+
+ private String padValue(String value) {
+ return value == null ? "" : " " + value;
+ }
}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/BenchmarkRunner.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/BenchmarkRunner.java
index 929b59ec88..da013e734f 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/BenchmarkRunner.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/BenchmarkRunner.java
@@ -69,7 +69,7 @@ public class BenchmarkRunner {
executor.init();
BenchmarkMetrics metrics = new BenchmarkMetrics();
- MetricsCollector metricsCollector = new MetricsCollector(metrics);
+ MetricsCollector metricsCollector = new MetricsCollector(config,
metrics);
workloadGenerator = new WorkloadGenerator(config, executor,
metrics);
System.out.println("Starting benchmark...\n");
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/config/BenchmarkConfig.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/config/BenchmarkConfig.java
index 8022b50c64..3966bdbb69 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/config/BenchmarkConfig.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/config/BenchmarkConfig.java
@@ -19,6 +19,8 @@ package org.apache.seata.benchmark.config;
import org.apache.seata.benchmark.constant.BenchmarkConstants;
import org.apache.seata.core.model.BranchType;
+import java.util.Locale;
+
/**
* Benchmark configuration
*/
@@ -34,6 +36,12 @@ public class BenchmarkConfig {
private String txServiceGroup = "default_tx_group";
private int rollbackPercentage = 2;
private int branches = 0;
+ private String sagaShape;
+ private String sagaWorkload = "mock";
+ private String sagaFailStep;
+ private Long sagaRandomSeed;
+ private String sagaTimeoutStep;
+ private int sagaTimeoutMs = 3000;
public BranchType getMode() {
return mode;
@@ -119,6 +127,54 @@ public class BenchmarkConfig {
this.branches = branches;
}
+ public String getSagaShape() {
+ return sagaShape;
+ }
+
+ public void setSagaShape(String sagaShape) {
+ this.sagaShape = sagaShape;
+ }
+
+ public String getSagaWorkload() {
+ return sagaWorkload;
+ }
+
+ public void setSagaWorkload(String sagaWorkload) {
+ this.sagaWorkload = sagaWorkload;
+ }
+
+ public String getSagaFailStep() {
+ return sagaFailStep;
+ }
+
+ public void setSagaFailStep(String sagaFailStep) {
+ this.sagaFailStep = sagaFailStep;
+ }
+
+ public Long getSagaRandomSeed() {
+ return sagaRandomSeed;
+ }
+
+ public void setSagaRandomSeed(Long sagaRandomSeed) {
+ this.sagaRandomSeed = sagaRandomSeed;
+ }
+
+ public String getSagaTimeoutStep() {
+ return sagaTimeoutStep;
+ }
+
+ public void setSagaTimeoutStep(String sagaTimeoutStep) {
+ this.sagaTimeoutStep = sagaTimeoutStep;
+ }
+
+ public int getSagaTimeoutMs() {
+ return sagaTimeoutMs;
+ }
+
+ public void setSagaTimeoutMs(int sagaTimeoutMs) {
+ this.sagaTimeoutMs = sagaTimeoutMs;
+ }
+
public void validate() {
validateMode();
validateNotEmpty(server, "server");
@@ -130,6 +186,11 @@ public class BenchmarkConfig {
validateNonNegative(warmupDuration, "warmupDuration");
validateNonNegative(branches, "branches");
validateRange(rollbackPercentage, 0, 100, "rollbackPercentage");
+ validateSagaShape();
+ validateSagaWorkload();
+ validateSagaFailStep();
+ validateSagaTimeoutStep();
+ validateNonNegative(sagaTimeoutMs, "sagaTimeoutMs");
validateTpsAndThreads();
}
@@ -163,6 +224,55 @@ public class BenchmarkConfig {
}
}
+ private void validateSagaShape() {
+ if (sagaShape == null || sagaShape.trim().isEmpty()) {
+ return;
+ }
+
+ String normalized = sagaShape.trim().toLowerCase(Locale.ROOT);
+ if (!"simple".equals(normalized) && !"order".equals(normalized)) {
+ throw new IllegalArgumentException("sagaShape must be one of:
simple, order");
+ }
+ sagaShape = normalized;
+ }
+
+ private void validateSagaWorkload() {
+ if (sagaWorkload == null || sagaWorkload.trim().isEmpty()) {
+ sagaWorkload = "mock";
+ return;
+ }
+
+ String normalized = sagaWorkload.trim().toLowerCase(Locale.ROOT);
+ if (!"mock".equals(normalized) && !"db".equals(normalized)) {
+ throw new IllegalArgumentException("sagaWorkload must be one of:
mock, db");
+ }
+ sagaWorkload = normalized;
+ }
+
+ private void validateSagaFailStep() {
+ if (sagaFailStep == null || sagaFailStep.trim().isEmpty()) {
+ return;
+ }
+
+ String normalized = sagaFailStep.trim().toLowerCase(Locale.ROOT);
+ if (!"inventory".equals(normalized) && !"payment".equals(normalized)
&& !"order".equals(normalized)) {
+ throw new IllegalArgumentException("sagaFailStep must be one of:
inventory, payment, order");
+ }
+ sagaFailStep = normalized;
+ }
+
+ private void validateSagaTimeoutStep() {
+ if (sagaTimeoutStep == null || sagaTimeoutStep.trim().isEmpty()) {
+ return;
+ }
+
+ String normalized = sagaTimeoutStep.trim().toLowerCase(Locale.ROOT);
+ if (!"inventory".equals(normalized) && !"payment".equals(normalized)
&& !"order".equals(normalized)) {
+ throw new IllegalArgumentException("sagaTimeoutStep must be one
of: inventory, payment, order");
+ }
+ sagaTimeoutStep = normalized;
+ }
+
private void validateTpsAndThreads() {
if (targetTps < BenchmarkConstants.UNLIMITED_TPS_THRESHOLD && threads
> 1) {
throw new IllegalArgumentException(String.format(
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/config/BenchmarkConfigLoader.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/config/BenchmarkConfigLoader.java
index 14ccff8414..de62357590 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/config/BenchmarkConfigLoader.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/config/BenchmarkConfigLoader.java
@@ -99,7 +99,13 @@ public class BenchmarkConfigLoader {
String applicationId,
String txServiceGroup,
Integer rollbackPercentage,
- Integer branches) {
+ Integer branches,
+ String sagaShape,
+ String sagaWorkload,
+ String sagaFailStep,
+ Long sagaRandomSeed,
+ String sagaTimeoutStep,
+ Integer sagaTimeoutMs) {
if (StringUtils.isNotEmpty(server)) {
config.setServer(server);
}
@@ -130,6 +136,24 @@ public class BenchmarkConfigLoader {
if (branches != null && branches >= 0) {
config.setBranches(branches);
}
+ if (StringUtils.isNotEmpty(sagaShape)) {
+ config.setSagaShape(sagaShape);
+ }
+ if (StringUtils.isNotEmpty(sagaWorkload)) {
+ config.setSagaWorkload(sagaWorkload);
+ }
+ if (StringUtils.isNotEmpty(sagaFailStep)) {
+ config.setSagaFailStep(sagaFailStep);
+ }
+ if (sagaRandomSeed != null) {
+ config.setSagaRandomSeed(sagaRandomSeed);
+ }
+ if (StringUtils.isNotEmpty(sagaTimeoutStep)) {
+ config.setSagaTimeoutStep(sagaTimeoutStep);
+ }
+ if (sagaTimeoutMs != null && sagaTimeoutMs >= 0) {
+ config.setSagaTimeoutMs(sagaTimeoutMs);
+ }
return config;
}
}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/constant/BenchmarkConstants.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/constant/BenchmarkConstants.java
index c0445b691c..f536846ff5 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/constant/BenchmarkConstants.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/constant/BenchmarkConstants.java
@@ -41,6 +41,9 @@ public final class BenchmarkConstants {
public static final int INITIAL_BALANCE = 10000;
public static final int MIN_TRANSFER_AMOUNT = 1;
public static final int MAX_TRANSFER_AMOUNT = 100;
+ public static final int SAGA_PRODUCT_COUNT = 100;
+ public static final int SAGA_INITIAL_BALANCE = 1000000;
+ public static final int SAGA_INITIAL_INVENTORY = 100000;
// Thread pool configuration
public static final int SHUTDOWN_TIMEOUT_SECONDS = 10;
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/executor/SagaModeExecutor.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/executor/SagaModeExecutor.java
index 2741d7a33c..75c962310c 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/executor/SagaModeExecutor.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/executor/SagaModeExecutor.java
@@ -19,9 +19,13 @@ package org.apache.seata.benchmark.executor;
import org.apache.seata.benchmark.config.BenchmarkConfig;
import org.apache.seata.benchmark.model.TransactionRecord;
import org.apache.seata.benchmark.saga.BenchmarkServiceInvoker;
+import org.apache.seata.benchmark.saga.InventoryDbSagaService;
import org.apache.seata.benchmark.saga.InventorySagaService;
+import org.apache.seata.benchmark.saga.OrderDbSagaService;
import org.apache.seata.benchmark.saga.OrderSagaService;
+import org.apache.seata.benchmark.saga.PaymentDbSagaService;
import org.apache.seata.benchmark.saga.PaymentSagaService;
+import org.apache.seata.benchmark.saga.SagaDbEnvironment;
import org.apache.seata.benchmark.saga.SimpleSpelExpressionFactory;
import org.apache.seata.core.exception.TransactionException;
import org.apache.seata.core.model.GlobalStatus;
@@ -65,6 +69,12 @@ public class SagaModeExecutor implements TransactionExecutor
{
private static final String SIMPLE_SAGA_NAME = "benchmarkSimpleSaga";
private static final String ORDER_SAGA_NAME = "benchmarkOrderSaga";
+ private static final String STEP_INVENTORY = "inventory";
+ private static final String STEP_PAYMENT = "payment";
+ private static final String STEP_ORDER = "order";
+ private static final String SHAPE_SIMPLE = "simple";
+ private static final String SHAPE_ORDER = "order";
+ private static final String WORKLOAD_DB = "db";
private final BenchmarkConfig config;
private StateMachineEngine stateMachineEngine;
@@ -95,6 +105,12 @@ public class SagaModeExecutor implements
TransactionExecutor {
// Create and configure state machine config
stateMachineConfig = new BenchmarkStateMachineConfig();
stateMachineConfig.setRollbackPercentage(config.getRollbackPercentage());
+ stateMachineConfig.setSagaFailStep(config.getSagaFailStep());
+ stateMachineConfig.setSagaRandomSeed(config.getSagaRandomSeed());
+ stateMachineConfig.setSagaTimeoutStep(config.getSagaTimeoutStep());
+ stateMachineConfig.setSagaTimeoutMs(config.getSagaTimeoutMs());
+ stateMachineConfig.setSagaWorkload(config.getSagaWorkload());
+ stateMachineConfig.setBenchmarkConfig(config);
stateMachineConfig.init();
// Create state machine engine
@@ -106,6 +122,9 @@ public class SagaModeExecutor implements
TransactionExecutor {
LOGGER.info("Available state machines: {}, {}", SIMPLE_SAGA_NAME,
ORDER_SAGA_NAME);
} catch (Exception e) {
+ if (stateMachineConfig != null) {
+ stateMachineConfig.destroy();
+ }
throw new RuntimeException("Failed to initialize Saga state
machine engine", e);
}
}
@@ -173,8 +192,7 @@ public class SagaModeExecutor implements
TransactionExecutor {
boolean success = false;
try {
- // Choose state machine based on branch count
- String stateMachineName = branchCount >= 3 ? ORDER_SAGA_NAME :
SIMPLE_SAGA_NAME;
+ String stateMachineName = resolveStateMachineName(branchCount);
// Prepare start parameters
Map<String, Object> startParams = createStartParams();
@@ -187,19 +205,17 @@ public class SagaModeExecutor implements
TransactionExecutor {
ExecutionStatus executionStatus = instance.getStatus();
ExecutionStatus compensationStatus =
instance.getCompensationStatus();
- if (ExecutionStatus.SU.equals(executionStatus)) {
+ if (ExecutionStatus.SU.equals(compensationStatus)) {
+ status = STATUS_COMPENSATED;
+ } else if (ExecutionStatus.FA.equals(compensationStatus)) {
+ status = STATUS_COMPENSATION_FAILED;
+ } else if (ExecutionStatus.UN.equals(compensationStatus)) {
+ status = STATUS_UNKNOWN;
+ } else if (ExecutionStatus.SU.equals(executionStatus)) {
status = STATUS_COMMITTED;
success = true;
} else if (ExecutionStatus.FA.equals(executionStatus)) {
- if (compensationStatus != null) {
- if (ExecutionStatus.SU.equals(compensationStatus)) {
- status = STATUS_COMPENSATED;
- } else {
- status = STATUS_COMPENSATION_FAILED;
- }
- } else {
- status = STATUS_FAILED;
- }
+ status = STATUS_FAILED;
} else if (ExecutionStatus.UN.equals(executionStatus)) {
status = STATUS_UNKNOWN;
} else {
@@ -215,6 +231,16 @@ public class SagaModeExecutor implements
TransactionExecutor {
return new TransactionRecord(businessKey, status, duration,
branchCount, success);
}
+ private String resolveStateMachineName(int branchCount) {
+ if (SHAPE_ORDER.equals(config.getSagaShape())) {
+ return ORDER_SAGA_NAME;
+ }
+ if (SHAPE_SIMPLE.equals(config.getSagaShape())) {
+ return SIMPLE_SAGA_NAME;
+ }
+ return branchCount >= 3 ? ORDER_SAGA_NAME : SIMPLE_SAGA_NAME;
+ }
+
private Map<String, Object> createStartParams() {
Map<String, Object> params = new HashMap<>();
params.put("userId", "user-" +
ThreadLocalRandom.current().nextInt(1000));
@@ -251,7 +277,9 @@ public class SagaModeExecutor implements
TransactionExecutor {
private void destroyRealMode() {
LOGGER.info("Destroying Real Saga mode resources");
- // StateMachineEngine doesn't have a close method
+ if (stateMachineConfig != null) {
+ stateMachineConfig.destroy();
+ }
stateMachineEngine = null;
stateMachineConfig = null;
}
@@ -262,11 +290,48 @@ public class SagaModeExecutor implements
TransactionExecutor {
private static class BenchmarkStateMachineConfig extends
AbstractStateMachineConfig {
private int rollbackPercentage = 0;
+ private String sagaFailStep;
+ private Long sagaRandomSeed;
+ private String sagaTimeoutStep;
+ private int sagaTimeoutMs = 3000;
+ private String sagaWorkload = "mock";
+ private BenchmarkConfig benchmarkConfig;
+ private SagaDbEnvironment sagaDbEnvironment;
+ private OrderSagaService orderSagaService;
+ private InventorySagaService inventorySagaService;
+ private PaymentSagaService paymentSagaService;
+ private OrderDbSagaService orderDbSagaService;
+ private InventoryDbSagaService inventoryDbSagaService;
+ private PaymentDbSagaService paymentDbSagaService;
public void setRollbackPercentage(int rollbackPercentage) {
this.rollbackPercentage = rollbackPercentage;
}
+ public void setSagaFailStep(String sagaFailStep) {
+ this.sagaFailStep = sagaFailStep;
+ }
+
+ public void setSagaRandomSeed(Long sagaRandomSeed) {
+ this.sagaRandomSeed = sagaRandomSeed;
+ }
+
+ public void setSagaTimeoutStep(String sagaTimeoutStep) {
+ this.sagaTimeoutStep = sagaTimeoutStep;
+ }
+
+ public void setSagaTimeoutMs(int sagaTimeoutMs) {
+ this.sagaTimeoutMs = sagaTimeoutMs;
+ }
+
+ public void setSagaWorkload(String sagaWorkload) {
+ this.sagaWorkload = sagaWorkload;
+ }
+
+ public void setBenchmarkConfig(BenchmarkConfig benchmarkConfig) {
+ this.benchmarkConfig = benchmarkConfig;
+ }
+
@Override
public void init() throws Exception {
// Load state machine definitions from classpath
@@ -304,13 +369,41 @@ public class SagaModeExecutor implements
TransactionExecutor {
// Register benchmark services with the service invoker manager
BenchmarkServiceInvoker serviceInvoker = new
BenchmarkServiceInvoker();
- // Register services with configured rollback percentage
- // Divide rollback percentage by 3 for each service so total
probability is approximately correct
- int serviceRollbackPct = rollbackPercentage > 0 ? Math.max(1,
rollbackPercentage / 3) : 0;
-
- serviceInvoker.registerService("orderService", new
OrderSagaService(serviceRollbackPct, 5));
- serviceInvoker.registerService("inventoryService", new
InventorySagaService(serviceRollbackPct, 5));
- serviceInvoker.registerService("paymentService", new
PaymentSagaService(serviceRollbackPct, 5));
+ boolean restrictFailureStep =
+ sagaFailStep != null && !sagaFailStep.trim().isEmpty();
+ int serviceRollbackPct = restrictFailureStep
+ ? rollbackPercentage
+ : (rollbackPercentage > 0 ? Math.max(1,
rollbackPercentage / 3) : 0);
+
+ boolean orderFailEnabled = !restrictFailureStep ||
STEP_ORDER.equals(sagaFailStep);
+ boolean inventoryFailEnabled = !restrictFailureStep ||
STEP_INVENTORY.equals(sagaFailStep);
+ boolean paymentFailEnabled = !restrictFailureStep ||
STEP_PAYMENT.equals(sagaFailStep);
+ boolean orderTimeoutEnabled =
STEP_ORDER.equals(sagaTimeoutStep);
+ boolean inventoryTimeoutEnabled =
STEP_INVENTORY.equals(sagaTimeoutStep);
+ boolean paymentTimeoutEnabled =
STEP_PAYMENT.equals(sagaTimeoutStep);
+
+ if (WORKLOAD_DB.equals(sagaWorkload)) {
+ initDbEnvironment();
+ registerDbServices(
+ serviceInvoker,
+ serviceRollbackPct,
+ orderFailEnabled,
+ inventoryFailEnabled,
+ paymentFailEnabled,
+ orderTimeoutEnabled,
+ inventoryTimeoutEnabled,
+ paymentTimeoutEnabled);
+ } else {
+ registerMockServices(
+ serviceInvoker,
+ serviceRollbackPct,
+ orderFailEnabled,
+ inventoryFailEnabled,
+ paymentFailEnabled,
+ orderTimeoutEnabled,
+ inventoryTimeoutEnabled,
+ paymentTimeoutEnabled);
+ }
// Register the service invoker for different service types
getServiceInvokerManager().putServiceInvoker(DomainConstants.SERVICE_TYPE_SPRING_BEAN,
serviceInvoker);
@@ -326,5 +419,126 @@ public class SagaModeExecutor implements
TransactionExecutor {
}
return buffer.toByteArray();
}
+
+ private ThreadLocal<java.util.Random> createFailureRandom(int salt) {
+ return
org.apache.seata.benchmark.saga.FailureRandomProvider.create(
+ sagaRandomSeed == null ? null : sagaRandomSeed + salt);
+ }
+
+ private void registerMockServices(
+ BenchmarkServiceInvoker serviceInvoker,
+ int serviceRollbackPct,
+ boolean orderFailEnabled,
+ boolean inventoryFailEnabled,
+ boolean paymentFailEnabled,
+ boolean orderTimeoutEnabled,
+ boolean inventoryTimeoutEnabled,
+ boolean paymentTimeoutEnabled) {
+ orderSagaService = new OrderSagaService(
+ serviceRollbackPct,
+ 5,
+ orderFailEnabled,
+ createFailureRandom(11),
+ orderTimeoutEnabled,
+ sagaTimeoutMs);
+ serviceInvoker.registerService("orderService", orderSagaService);
+ inventorySagaService = new InventorySagaService(
+ serviceRollbackPct,
+ 5,
+ inventoryFailEnabled,
+ createFailureRandom(17),
+ inventoryTimeoutEnabled,
+ sagaTimeoutMs);
+ serviceInvoker.registerService("inventoryService",
inventorySagaService);
+ paymentSagaService = new PaymentSagaService(
+ serviceRollbackPct,
+ 5,
+ paymentFailEnabled,
+ createFailureRandom(23),
+ paymentTimeoutEnabled,
+ sagaTimeoutMs);
+ serviceInvoker.registerService("paymentService",
paymentSagaService);
+ }
+
+ private void registerDbServices(
+ BenchmarkServiceInvoker serviceInvoker,
+ int serviceRollbackPct,
+ boolean orderFailEnabled,
+ boolean inventoryFailEnabled,
+ boolean paymentFailEnabled,
+ boolean orderTimeoutEnabled,
+ boolean inventoryTimeoutEnabled,
+ boolean paymentTimeoutEnabled) {
+ orderDbSagaService = new OrderDbSagaService(
+ sagaDbEnvironment.getDataSource(),
+ serviceRollbackPct,
+ 5,
+ orderFailEnabled,
+ createFailureRandom(11),
+ orderTimeoutEnabled,
+ sagaTimeoutMs);
+ serviceInvoker.registerService("orderService", orderDbSagaService);
+ inventoryDbSagaService = new InventoryDbSagaService(
+ sagaDbEnvironment.getDataSource(),
+ serviceRollbackPct,
+ 5,
+ inventoryFailEnabled,
+ createFailureRandom(17),
+ inventoryTimeoutEnabled,
+ sagaTimeoutMs);
+ serviceInvoker.registerService("inventoryService",
inventoryDbSagaService);
+ paymentDbSagaService = new PaymentDbSagaService(
+ sagaDbEnvironment.getDataSource(),
+ serviceRollbackPct,
+ 5,
+ paymentFailEnabled,
+ createFailureRandom(23),
+ paymentTimeoutEnabled,
+ sagaTimeoutMs);
+ serviceInvoker.registerService("paymentService",
paymentDbSagaService);
+ }
+
+ private void initDbEnvironment() {
+ if (benchmarkConfig == null) {
+ throw new IllegalStateException("BenchmarkConfig is required
for DB-backed Saga workload");
+ }
+ sagaDbEnvironment = new SagaDbEnvironment(benchmarkConfig);
+ sagaDbEnvironment.init();
+ }
+
+ public void destroy() {
+ destroyServices();
+ if (sagaDbEnvironment != null) {
+ sagaDbEnvironment.destroy();
+ sagaDbEnvironment = null;
+ }
+ }
+
+ private void destroyServices() {
+ if (orderSagaService != null) {
+ orderSagaService.destroy();
+ orderSagaService = null;
+ }
+ if (inventorySagaService != null) {
+ inventorySagaService.destroy();
+ inventorySagaService = null;
+ }
+ if (paymentSagaService != null) {
+ paymentSagaService.destroy();
+ paymentSagaService = null;
+ }
+ if (orderDbSagaService != null) {
+ orderDbSagaService.destroy();
+ orderDbSagaService = null;
+ }
+ if (inventoryDbSagaService != null) {
+ inventoryDbSagaService.destroy();
+ inventoryDbSagaService = null;
+ }
+ if (paymentDbSagaService != null) {
+ paymentDbSagaService.destroy();
+ paymentDbSagaService = null;
+ }
+ }
}
}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/executor/WorkloadGenerator.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/executor/WorkloadGenerator.java
index 25eccb0a62..d26b99ccc8 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/executor/WorkloadGenerator.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/executor/WorkloadGenerator.java
@@ -34,6 +34,8 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import static
org.apache.seata.benchmark.constant.BenchmarkConstants.STATUS_FAILED;
+
/**
* Workload generator with TPS rate limiting
*/
@@ -100,20 +102,17 @@ public class WorkloadGenerator {
}
private void executeTransaction() {
+ long startTime = System.currentTimeMillis();
try {
TransactionRecord record = executor.execute();
-
- if (record.isSuccess()) {
- metrics.recordSuccess(record.getDuration());
- } else {
- metrics.recordFailure(record.getDuration());
- }
+ metrics.recordTransaction(record.getStatus(),
record.getDuration());
addRecentRecord(record);
} catch (Exception e) {
LOGGER.error("Transaction execution error", e);
- metrics.recordFailure(0);
+ long duration = System.currentTimeMillis() - startTime;
+ metrics.recordTransaction(STATUS_FAILED, duration);
}
}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/model/BenchmarkMetrics.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/model/BenchmarkMetrics.java
index bc130823cd..58ad94a38b 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/model/BenchmarkMetrics.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/model/BenchmarkMetrics.java
@@ -32,10 +32,14 @@ public class BenchmarkMetrics {
private final AtomicLong totalCount = new AtomicLong(0);
private final AtomicLong successCount = new AtomicLong(0);
private final AtomicLong failedCount = new AtomicLong(0);
+ private final AtomicLong committedCount = new AtomicLong(0);
+ private final AtomicLong compensatedCount = new AtomicLong(0);
+ private final AtomicLong compensationFailedCount = new AtomicLong(0);
+ private final AtomicLong unknownCount = new AtomicLong(0);
private final List<Long> latencies =
Collections.synchronizedList(new
ArrayList<>(BenchmarkConstants.MAX_LATENCY_SAMPLES));
private final AtomicLong totalSamples = new AtomicLong(0);
- private final long startTime = System.currentTimeMillis();
+ private volatile long startTime = System.currentTimeMillis();
private volatile long lastCountSnapshot = 0;
private volatile long lastSnapshotTime = System.currentTimeMillis();
@@ -55,6 +59,29 @@ public class BenchmarkMetrics {
addLatencySample(latencyMs);
}
+ public void recordTransaction(String status, long latencyMs) {
+ totalCount.incrementAndGet();
+
+ if (BenchmarkConstants.STATUS_COMMITTED.equals(status)) {
+ successCount.incrementAndGet();
+ committedCount.incrementAndGet();
+ } else if (BenchmarkConstants.STATUS_COMPENSATED.equals(status)) {
+ compensatedCount.incrementAndGet();
+ successCount.incrementAndGet();
+ } else if
(BenchmarkConstants.STATUS_COMPENSATION_FAILED.equals(status)) {
+ compensationFailedCount.incrementAndGet();
+ failedCount.incrementAndGet();
+ } else if (BenchmarkConstants.STATUS_FAILED.equals(status)
+ || BenchmarkConstants.STATUS_ROLLBACKED.equals(status)) {
+ failedCount.incrementAndGet();
+ } else {
+ unknownCount.incrementAndGet();
+ failedCount.incrementAndGet();
+ }
+
+ addLatencySample(latencyMs);
+ }
+
private void addLatencySample(long latencyMs) {
totalSamples.incrementAndGet();
synchronized (latencies) {
@@ -80,6 +107,26 @@ public class BenchmarkMetrics {
return failedCount.get();
}
+ public long getCommittedCount() {
+ return committedCount.get();
+ }
+
+ public long getCompensatedCount() {
+ return compensatedCount.get();
+ }
+
+ public long getExecutionFailedCount() {
+ return failedCount.get() - compensationFailedCount.get() -
unknownCount.get();
+ }
+
+ public long getCompensationFailedCount() {
+ return compensationFailedCount.get();
+ }
+
+ public long getUnknownCount() {
+ return unknownCount.get();
+ }
+
public double getSuccessRate() {
long total = totalCount.get();
if (total == 0) {
@@ -88,6 +135,30 @@ public class BenchmarkMetrics {
return (double) successCount.get() / total * 100;
}
+ public double getCommittedRate() {
+ long total = totalCount.get();
+ if (total == 0) {
+ return 0.0;
+ }
+ return (double) committedCount.get() / total * 100;
+ }
+
+ public double getCompensatedRate() {
+ long total = totalCount.get();
+ if (total == 0) {
+ return 0.0;
+ }
+ return (double) compensatedCount.get() / total * 100;
+ }
+
+ public double getEndStateSuccessRate() {
+ long total = totalCount.get();
+ if (total == 0) {
+ return 0.0;
+ }
+ return (double) (committedCount.get() + compensatedCount.get()) /
total * 100;
+ }
+
public double getAverageTps() {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed == 0) {
@@ -149,12 +220,20 @@ public class BenchmarkMetrics {
}
public void reset() {
+ startTime = System.currentTimeMillis();
totalCount.set(0);
successCount.set(0);
failedCount.set(0);
+ committedCount.set(0);
+ compensatedCount.set(0);
+ compensationFailedCount.set(0);
+ unknownCount.set(0);
+ totalSamples.set(0);
latencies.clear();
lastCountSnapshot = 0;
lastSnapshotTime = System.currentTimeMillis();
+ cachedStats = null;
+ lastStatsUpdateTime = 0;
}
public static class LatencyStats {
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/monitor/MetricsCollector.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/monitor/MetricsCollector.java
index f784b1d884..84a4221f27 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/monitor/MetricsCollector.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/monitor/MetricsCollector.java
@@ -16,7 +16,9 @@
*/
package org.apache.seata.benchmark.monitor;
+import org.apache.seata.benchmark.config.BenchmarkConfig;
import org.apache.seata.benchmark.model.BenchmarkMetrics;
+import org.apache.seata.core.model.BranchType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,19 +35,38 @@ public class MetricsCollector {
private static final Logger LOGGER =
LoggerFactory.getLogger(MetricsCollector.class);
+ private final BenchmarkConfig config;
private final BenchmarkMetrics metrics;
- public MetricsCollector(BenchmarkMetrics metrics) {
+ public MetricsCollector(BenchmarkConfig config, BenchmarkMetrics metrics) {
+ this.config = config;
this.metrics = metrics;
}
public void exportToCsv(String filename) {
try (PrintWriter writer = new PrintWriter(new FileWriter(filename))) {
writer.println("Metric,Value");
+ writer.println("Mode," + config.getMode());
+ if (config.getMode() == BranchType.SAGA
+ && config.getSagaWorkload() != null
+ && !config.getSagaWorkload().isEmpty()) {
+ writer.println("Saga Workload," + config.getSagaWorkload());
+ }
+ if (config.getSagaShape() != null &&
!config.getSagaShape().isEmpty()) {
+ writer.println("Saga Shape," + config.getSagaShape());
+ }
writer.println("Total Transactions," + metrics.getTotalCount());
writer.println("Success Count," + metrics.getSuccessCount());
writer.println("Failed Count," + metrics.getFailedCount());
+ writer.println("Committed Count," + metrics.getCommittedCount());
+ writer.println("Compensated Count," +
metrics.getCompensatedCount());
+ writer.println("Execution Failed Count," +
metrics.getExecutionFailedCount());
+ writer.println("Compensation Failed Count," +
metrics.getCompensationFailedCount());
+ writer.println("Unknown Count," + metrics.getUnknownCount());
writer.println("Success Rate (%)," + String.format("%.2f",
metrics.getSuccessRate()));
+ writer.println("Committed Rate (%)," + String.format("%.2f",
metrics.getCommittedRate()));
+ writer.println("Compensated Rate (%)," + String.format("%.2f",
metrics.getCompensatedRate()));
+ writer.println("End-State Success Rate (%)," +
String.format("%.2f", metrics.getEndStateSuccessRate()));
writer.println("Average TPS," + String.format("%.2f",
metrics.getAverageTps()));
writer.println("Elapsed Time (s)," +
metrics.getElapsedTimeSeconds());
@@ -71,10 +92,27 @@ public class MetricsCollector {
report.append("===================================================\n");
report.append(" Seata Benchmark Final Report\n");
report.append("===================================================\n");
+ report.append(String.format("Mode: %s\n",
config.getMode()));
+ if (config.getMode() == BranchType.SAGA
+ && config.getSagaWorkload() != null
+ && !config.getSagaWorkload().isEmpty()) {
+ report.append(String.format("Saga Workload: %s\n",
config.getSagaWorkload()));
+ }
+ if (config.getSagaShape() != null && !config.getSagaShape().isEmpty())
{
+ report.append(String.format("Saga Shape: %s\n",
config.getSagaShape()));
+ }
report.append(String.format("Total Transactions: %,d\n",
metrics.getTotalCount()));
report.append(String.format("Success Count: %,d\n",
metrics.getSuccessCount()));
report.append(String.format("Failed Count: %,d\n",
metrics.getFailedCount()));
report.append(String.format("Success Rate: %.2f%%\n",
metrics.getSuccessRate()));
+ report.append(String.format("Committed Count: %,d\n",
metrics.getCommittedCount()));
+ report.append(String.format("Compensated Count: %,d\n",
metrics.getCompensatedCount()));
+ report.append(String.format("Execution Failed Count: %,d\n",
metrics.getExecutionFailedCount()));
+ report.append(String.format("Compensation Failed Count: %,d\n",
metrics.getCompensationFailedCount()));
+ report.append(String.format("Unknown Count: %,d\n",
metrics.getUnknownCount()));
+ report.append(String.format("Committed Rate: %.2f%%\n",
metrics.getCommittedRate()));
+ report.append(String.format("Compensated Rate: %.2f%%\n",
metrics.getCompensatedRate()));
+ report.append(String.format("End-State Success Rate: %.2f%%\n",
metrics.getEndStateSuccessRate()));
report.append(String.format("Average TPS: %.2f\n",
metrics.getAverageTps()));
report.append(String.format("Elapsed Time: %d seconds\n",
metrics.getElapsedTimeSeconds()));
report.append("\n");
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/FailureRandomProvider.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/FailureRandomProvider.java
new file mode 100644
index 0000000000..7375812d7c
--- /dev/null
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/FailureRandomProvider.java
@@ -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 org.apache.seata.benchmark.saga;
+
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Thread-local failure random provider to avoid cross-thread contention while
keeping seeded behavior reproducible.
+ */
+public final class FailureRandomProvider {
+
+ private FailureRandomProvider() {}
+
+ public static ThreadLocal<Random> create(Long seed) {
+ if (seed == null) {
+ return null;
+ }
+ return ThreadLocal.withInitial(
+ () -> new Random(mixSeed(seed,
Thread.currentThread().getName())));
+ }
+
+ public static int nextPercent(ThreadLocal<Random> failureRandom) {
+ if (failureRandom != null) {
+ return failureRandom.get().nextInt(100);
+ }
+ return ThreadLocalRandom.current().nextInt(100);
+ }
+
+ private static long mixSeed(long seed, String threadName) {
+ long mixed = seed ^ threadName.hashCode();
+ mixed ^= (mixed << 21);
+ mixed ^= (mixed >>> 35);
+ mixed ^= (mixed << 4);
+ return mixed;
+ }
+}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventoryDbSagaService.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventoryDbSagaService.java
new file mode 100644
index 0000000000..cbdc66d39d
--- /dev/null
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventoryDbSagaService.java
@@ -0,0 +1,183 @@
+/*
+ * 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 org.apache.seata.benchmark.saga;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Database-backed inventory service for Saga benchmark.
+ * @author zihenzzz
+ */
+public class InventoryDbSagaService {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(InventoryDbSagaService.class);
+
+ private final DataSource dataSource;
+ private final int rollbackPercentage;
+ private final int simulatedDelayMs;
+ private final boolean failInjectionEnabled;
+ private final ThreadLocal<Random> failureRandom;
+ private final boolean timeoutInjectionEnabled;
+ private final int timeoutMs;
+
+ public InventoryDbSagaService(
+ DataSource dataSource,
+ int rollbackPercentage,
+ int simulatedDelayMs,
+ boolean failInjectionEnabled,
+ ThreadLocal<Random> failureRandom,
+ boolean timeoutInjectionEnabled,
+ int timeoutMs) {
+ this.dataSource = dataSource;
+ this.rollbackPercentage = rollbackPercentage;
+ this.simulatedDelayMs = simulatedDelayMs;
+ this.failInjectionEnabled = failInjectionEnabled;
+ this.failureRandom = failureRandom;
+ this.timeoutInjectionEnabled = timeoutInjectionEnabled;
+ this.timeoutMs = timeoutMs;
+ }
+
+ public Map<String, Object> reserveInventory(Map<String, Object> params) {
+ String productId = (String) params.get("productId");
+ Integer quantity = (Integer) params.get("quantity");
+
+ simulateDelay();
+ if (timeoutInjectionEnabled) {
+ simulateTimeout("inventory reservation");
+ }
+ if (shouldFail()) {
+ throw new RuntimeException("Simulated inventory reservation
failure");
+ }
+
+ try (Connection conn = dataSource.getConnection()) {
+ boolean originalAutoCommit = conn.getAutoCommit();
+ conn.setAutoCommit(false);
+ try {
+ try (PreparedStatement pstmt = conn.prepareStatement("UPDATE
benchmark_inventory "
+ + "SET available_qty = available_qty - ?, reserved_qty
= reserved_qty + ? "
+ + "WHERE product_id = ? AND available_qty >= ?")) {
+ pstmt.setInt(1, quantity);
+ pstmt.setInt(2, quantity);
+ pstmt.setString(3, productId);
+ pstmt.setInt(4, quantity);
+ if (pstmt.executeUpdate() == 0) {
+ throw new RuntimeException("Insufficient inventory for
product " + productId);
+ }
+ }
+ conn.commit();
+ } catch (Exception e) {
+ rollbackQuietly(conn, e);
+ throw e;
+ } finally {
+ restoreAutoCommit(conn, originalAutoCommit);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to reserve inventory", e);
+ }
+
+ LOGGER.debug("Reserved inventory in DB: productId={}, quantity={}",
productId, quantity);
+ Map<String, Object> result = new HashMap<>();
+ result.put("code", "S");
+ result.put("productId", productId);
+ result.put("quantity", quantity);
+ return result;
+ }
+
+ public Map<String, Object> releaseInventory(Map<String, Object> params) {
+ String productId = (String) params.get("productId");
+ Integer quantity = (Integer) params.get("quantity");
+
+ simulateDelay();
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement("UPDATE
benchmark_inventory "
+ + "SET available_qty = available_qty + ?, reserved_qty
= GREATEST(reserved_qty - ?, 0) "
+ + "WHERE product_id = ?")) {
+ pstmt.setInt(1, quantity);
+ pstmt.setInt(2, quantity);
+ pstmt.setString(3, productId);
+ pstmt.executeUpdate();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to release inventory", e);
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("code", "S");
+ result.put("productId", productId);
+ result.put("quantity", quantity);
+ return result;
+ }
+
+ public void destroy() {
+ if (failureRandom != null) {
+ failureRandom.remove();
+ }
+ }
+
+ private void rollbackQuietly(Connection conn, Exception original) {
+ try {
+ conn.rollback();
+ } catch (SQLException rollbackException) {
+ original.addSuppressed(rollbackException);
+ }
+ }
+
+ private void restoreAutoCommit(Connection conn, boolean
originalAutoCommit) {
+ try {
+ conn.setAutoCommit(originalAutoCommit);
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to restore auto-commit for
inventory connection", e);
+ }
+ }
+
+ private void simulateDelay() {
+ if (simulatedDelayMs > 0) {
+ try {
+
Thread.sleep(ThreadLocalRandom.current().nextInt(simulatedDelayMs));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private boolean shouldFail() {
+ return failInjectionEnabled && nextFailurePercent() <
rollbackPercentage;
+ }
+
+ private void simulateTimeout(String operation) {
+ try {
+ Thread.sleep(timeoutMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ throw new RuntimeException("Simulated " + operation + " timeout");
+ }
+
+ private int nextFailurePercent() {
+ return FailureRandomProvider.nextPercent(failureRandom);
+ }
+}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventorySagaService.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventorySagaService.java
index d75bc53eca..3e330216a7 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventorySagaService.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventorySagaService.java
@@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
+import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
/**
@@ -33,10 +34,24 @@ public class InventorySagaService {
private final int rollbackPercentage;
private final int simulatedDelayMs;
-
- public InventorySagaService(int rollbackPercentage, int simulatedDelayMs) {
+ private final boolean failInjectionEnabled;
+ private final ThreadLocal<Random> failureRandom;
+ private final boolean timeoutInjectionEnabled;
+ private final int timeoutMs;
+
+ public InventorySagaService(
+ int rollbackPercentage,
+ int simulatedDelayMs,
+ boolean failInjectionEnabled,
+ ThreadLocal<Random> failureRandom,
+ boolean timeoutInjectionEnabled,
+ int timeoutMs) {
this.rollbackPercentage = rollbackPercentage;
this.simulatedDelayMs = simulatedDelayMs;
+ this.failInjectionEnabled = failInjectionEnabled;
+ this.failureRandom = failureRandom;
+ this.timeoutInjectionEnabled = timeoutInjectionEnabled;
+ this.timeoutMs = timeoutMs;
}
/**
@@ -54,7 +69,11 @@ public class InventorySagaService {
// Simulate processing time
simulateDelay();
- // Simulate random failure based on rollback percentage
+ if (timeoutInjectionEnabled) {
+ simulateTimeout("inventory reservation");
+ }
+
+ // Simulate failure injection on the selected step.
if (shouldFail()) {
LOGGER.debug("Inventory reservation failed (simulated):
productId={}", productId);
throw new RuntimeException("Simulated inventory reservation
failure");
@@ -91,6 +110,12 @@ public class InventorySagaService {
return result;
}
+ public void destroy() {
+ if (failureRandom != null) {
+ failureRandom.remove();
+ }
+ }
+
private void simulateDelay() {
if (simulatedDelayMs > 0) {
try {
@@ -102,6 +127,23 @@ public class InventorySagaService {
}
private boolean shouldFail() {
- return ThreadLocalRandom.current().nextInt(100) < rollbackPercentage;
+ if (!failInjectionEnabled) {
+ return false;
+ }
+ return nextFailurePercent() < rollbackPercentage;
+ }
+
+ private void simulateTimeout(String operation) {
+ LOGGER.debug("Simulating {} timeout: {} ms", operation, timeoutMs);
+ try {
+ Thread.sleep(timeoutMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ throw new RuntimeException("Simulated " + operation + " timeout");
+ }
+
+ private int nextFailurePercent() {
+ return FailureRandomProvider.nextPercent(failureRandom);
}
}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/OrderDbSagaService.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/OrderDbSagaService.java
new file mode 100644
index 0000000000..4d4715e695
--- /dev/null
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/OrderDbSagaService.java
@@ -0,0 +1,158 @@
+/*
+ * 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 org.apache.seata.benchmark.saga;
+
+import javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Database-backed order service for Saga benchmark.
+ * @author zihenzzz
+ */
+public class OrderDbSagaService {
+
+ private final DataSource dataSource;
+ private final int rollbackPercentage;
+ private final int simulatedDelayMs;
+ private final boolean failInjectionEnabled;
+ private final ThreadLocal<Random> failureRandom;
+ private final boolean timeoutInjectionEnabled;
+ private final int timeoutMs;
+
+ public OrderDbSagaService(
+ DataSource dataSource,
+ int rollbackPercentage,
+ int simulatedDelayMs,
+ boolean failInjectionEnabled,
+ ThreadLocal<Random> failureRandom,
+ boolean timeoutInjectionEnabled,
+ int timeoutMs) {
+ this.dataSource = dataSource;
+ this.rollbackPercentage = rollbackPercentage;
+ this.simulatedDelayMs = simulatedDelayMs;
+ this.failInjectionEnabled = failInjectionEnabled;
+ this.failureRandom = failureRandom;
+ this.timeoutInjectionEnabled = timeoutInjectionEnabled;
+ this.timeoutMs = timeoutMs;
+ }
+
+ public Map<String, Object> createOrder(Map<String, Object> params) {
+ String orderId = UUID.randomUUID().toString().substring(0, 8);
+ String userId = (String) params.get("userId");
+ String productId = (String) params.get("productId");
+ Integer quantity = (Integer) params.get("quantity");
+ BigDecimal amount = toBigDecimal(params.get("amount"));
+
+ simulateDelay();
+ if (timeoutInjectionEnabled) {
+ simulateTimeout("order creation");
+ }
+ if (shouldFail()) {
+ throw new RuntimeException("Simulated order creation failure");
+ }
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement("INSERT INTO
benchmark_order "
+ + "(order_id, user_id, product_id, quantity, amount,
status) "
+ + "VALUES (?, ?, ?, ?, ?, ?)")) {
+ pstmt.setString(1, orderId);
+ pstmt.setString(2, userId);
+ pstmt.setString(3, productId);
+ pstmt.setInt(4, quantity);
+ pstmt.setBigDecimal(5, amount);
+ pstmt.setString(6, "CREATED");
+ pstmt.executeUpdate();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to create order", e);
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("code", "S");
+ result.put("orderId", orderId);
+ result.put("userId", userId);
+ result.put("productId", productId);
+ return result;
+ }
+
+ public Map<String, Object> cancelOrder(Map<String, Object> params) {
+ String orderId = (String) params.get("orderId");
+ String userId = (String) params.get("userId");
+
+ simulateDelay();
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement pstmt =
+ conn.prepareStatement("UPDATE benchmark_order SET
status = ? WHERE order_id = ?")) {
+ pstmt.setString(1, "CANCELLED");
+ pstmt.setString(2, orderId);
+ pstmt.executeUpdate();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to cancel order", e);
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("code", "S");
+ result.put("orderId", orderId != null ? orderId : "unknown");
+ result.put("userId", userId);
+ return result;
+ }
+
+ public void destroy() {
+ if (failureRandom != null) {
+ failureRandom.remove();
+ }
+ }
+
+ private BigDecimal toBigDecimal(Object amountObj) {
+ return amountObj instanceof BigDecimal ? (BigDecimal) amountObj : new
BigDecimal(amountObj.toString());
+ }
+
+ private void simulateDelay() {
+ if (simulatedDelayMs > 0) {
+ try {
+
Thread.sleep(ThreadLocalRandom.current().nextInt(simulatedDelayMs));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private boolean shouldFail() {
+ return failInjectionEnabled && nextFailurePercent() <
rollbackPercentage;
+ }
+
+ private void simulateTimeout(String operation) {
+ try {
+ Thread.sleep(timeoutMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ throw new RuntimeException("Simulated " + operation + " timeout");
+ }
+
+ private int nextFailurePercent() {
+ return FailureRandomProvider.nextPercent(failureRandom);
+ }
+}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/OrderSagaService.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/OrderSagaService.java
index 4a08b958fe..95c9d091f9 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/OrderSagaService.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/OrderSagaService.java
@@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
+import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@@ -34,10 +35,24 @@ public class OrderSagaService {
private final int rollbackPercentage;
private final int simulatedDelayMs;
-
- public OrderSagaService(int rollbackPercentage, int simulatedDelayMs) {
+ private final boolean failInjectionEnabled;
+ private final ThreadLocal<Random> failureRandom;
+ private final boolean timeoutInjectionEnabled;
+ private final int timeoutMs;
+
+ public OrderSagaService(
+ int rollbackPercentage,
+ int simulatedDelayMs,
+ boolean failInjectionEnabled,
+ ThreadLocal<Random> failureRandom,
+ boolean timeoutInjectionEnabled,
+ int timeoutMs) {
this.rollbackPercentage = rollbackPercentage;
this.simulatedDelayMs = simulatedDelayMs;
+ this.failInjectionEnabled = failInjectionEnabled;
+ this.failureRandom = failureRandom;
+ this.timeoutInjectionEnabled = timeoutInjectionEnabled;
+ this.timeoutMs = timeoutMs;
}
/**
@@ -58,7 +73,11 @@ public class OrderSagaService {
// Simulate processing time
simulateDelay();
- // Simulate random failure based on rollback percentage
+ if (timeoutInjectionEnabled) {
+ simulateTimeout("order creation");
+ }
+
+ // Simulate failure injection on the selected step.
if (shouldFail()) {
LOGGER.debug("Order creation failed (simulated): userId={}",
userId);
throw new RuntimeException("Simulated order creation failure");
@@ -95,6 +114,12 @@ public class OrderSagaService {
return result;
}
+ public void destroy() {
+ if (failureRandom != null) {
+ failureRandom.remove();
+ }
+ }
+
private void simulateDelay() {
if (simulatedDelayMs > 0) {
try {
@@ -106,6 +131,23 @@ public class OrderSagaService {
}
private boolean shouldFail() {
- return ThreadLocalRandom.current().nextInt(100) < rollbackPercentage;
+ if (!failInjectionEnabled) {
+ return false;
+ }
+ return nextFailurePercent() < rollbackPercentage;
+ }
+
+ private void simulateTimeout(String operation) {
+ LOGGER.debug("Simulating {} timeout: {} ms", operation, timeoutMs);
+ try {
+ Thread.sleep(timeoutMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ throw new RuntimeException("Simulated " + operation + " timeout");
+ }
+
+ private int nextFailurePercent() {
+ return FailureRandomProvider.nextPercent(failureRandom);
}
}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentDbSagaService.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentDbSagaService.java
new file mode 100644
index 0000000000..8428eae01f
--- /dev/null
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentDbSagaService.java
@@ -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 org.apache.seata.benchmark.saga;
+
+import javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Database-backed payment service for Saga benchmark.
+ * @author zihenzzz
+ */
+public class PaymentDbSagaService {
+
+ private final DataSource dataSource;
+ private final int rollbackPercentage;
+ private final int simulatedDelayMs;
+ private final boolean failInjectionEnabled;
+ private final ThreadLocal<Random> failureRandom;
+ private final boolean timeoutInjectionEnabled;
+ private final int timeoutMs;
+
+ public PaymentDbSagaService(
+ DataSource dataSource,
+ int rollbackPercentage,
+ int simulatedDelayMs,
+ boolean failInjectionEnabled,
+ ThreadLocal<Random> failureRandom,
+ boolean timeoutInjectionEnabled,
+ int timeoutMs) {
+ this.dataSource = dataSource;
+ this.rollbackPercentage = rollbackPercentage;
+ this.simulatedDelayMs = simulatedDelayMs;
+ this.failInjectionEnabled = failInjectionEnabled;
+ this.failureRandom = failureRandom;
+ this.timeoutInjectionEnabled = timeoutInjectionEnabled;
+ this.timeoutMs = timeoutMs;
+ }
+
+ public Map<String, Object> debitPayment(Map<String, Object> params) {
+ String accountId = (String) params.get("accountId");
+ BigDecimal amount = toBigDecimal(params.get("amount"));
+
+ simulateDelay();
+ if (timeoutInjectionEnabled) {
+ simulateTimeout("payment debit");
+ }
+ if (shouldFail()) {
+ throw new RuntimeException("Simulated payment debit failure");
+ }
+
+ try (Connection conn = dataSource.getConnection()) {
+ boolean originalAutoCommit = conn.getAutoCommit();
+ conn.setAutoCommit(false);
+ try {
+ try (PreparedStatement pstmt =
+ conn.prepareStatement("UPDATE benchmark_account SET
balance = balance - ? "
+ + "WHERE account_id = ? AND balance >= ?")) {
+ pstmt.setBigDecimal(1, amount);
+ pstmt.setString(2, accountId);
+ pstmt.setBigDecimal(3, amount);
+ if (pstmt.executeUpdate() == 0) {
+ throw new RuntimeException("Insufficient balance for
account " + accountId);
+ }
+ }
+ conn.commit();
+ } catch (Exception e) {
+ rollbackQuietly(conn, e);
+ throw e;
+ } finally {
+ restoreAutoCommit(conn, originalAutoCommit);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to debit payment", e);
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("code", "S");
+ result.put("accountId", accountId);
+ result.put("amount", amount.toPlainString());
+ return result;
+ }
+
+ public Map<String, Object> refundPayment(Map<String, Object> params) {
+ String accountId = (String) params.get("accountId");
+ BigDecimal amount = toBigDecimal(params.get("amount"));
+
+ simulateDelay();
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement pstmt = conn.prepareStatement(
+ "UPDATE benchmark_account SET balance = balance + ?
WHERE account_id = ?")) {
+ pstmt.setBigDecimal(1, amount);
+ pstmt.setString(2, accountId);
+ pstmt.executeUpdate();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to refund payment", e);
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("code", "S");
+ result.put("accountId", accountId);
+ result.put("amount", amount.toPlainString());
+ return result;
+ }
+
+ public void destroy() {
+ if (failureRandom != null) {
+ failureRandom.remove();
+ }
+ }
+
+ private void rollbackQuietly(Connection conn, Exception original) {
+ try {
+ conn.rollback();
+ } catch (SQLException rollbackException) {
+ original.addSuppressed(rollbackException);
+ }
+ }
+
+ private void restoreAutoCommit(Connection conn, boolean
originalAutoCommit) {
+ try {
+ conn.setAutoCommit(originalAutoCommit);
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to restore auto-commit for
payment connection", e);
+ }
+ }
+
+ private BigDecimal toBigDecimal(Object amountObj) {
+ return amountObj instanceof BigDecimal ? (BigDecimal) amountObj : new
BigDecimal(amountObj.toString());
+ }
+
+ private void simulateDelay() {
+ if (simulatedDelayMs > 0) {
+ try {
+
Thread.sleep(ThreadLocalRandom.current().nextInt(simulatedDelayMs));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private boolean shouldFail() {
+ return failInjectionEnabled && nextFailurePercent() <
rollbackPercentage;
+ }
+
+ private void simulateTimeout(String operation) {
+ try {
+ Thread.sleep(timeoutMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ throw new RuntimeException("Simulated " + operation + " timeout");
+ }
+
+ private int nextFailurePercent() {
+ return FailureRandomProvider.nextPercent(failureRandom);
+ }
+}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentSagaService.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentSagaService.java
index fe781925df..4abefab69f 100644
---
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentSagaService.java
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentSagaService.java
@@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
+import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
/**
@@ -34,10 +35,24 @@ public class PaymentSagaService {
private final int rollbackPercentage;
private final int simulatedDelayMs;
-
- public PaymentSagaService(int rollbackPercentage, int simulatedDelayMs) {
+ private final boolean failInjectionEnabled;
+ private final ThreadLocal<Random> failureRandom;
+ private final boolean timeoutInjectionEnabled;
+ private final int timeoutMs;
+
+ public PaymentSagaService(
+ int rollbackPercentage,
+ int simulatedDelayMs,
+ boolean failInjectionEnabled,
+ ThreadLocal<Random> failureRandom,
+ boolean timeoutInjectionEnabled,
+ int timeoutMs) {
this.rollbackPercentage = rollbackPercentage;
this.simulatedDelayMs = simulatedDelayMs;
+ this.failInjectionEnabled = failInjectionEnabled;
+ this.failureRandom = failureRandom;
+ this.timeoutInjectionEnabled = timeoutInjectionEnabled;
+ this.timeoutMs = timeoutMs;
}
/**
@@ -57,7 +72,11 @@ public class PaymentSagaService {
// Simulate processing time
simulateDelay();
- // Simulate random failure based on rollback percentage
+ if (timeoutInjectionEnabled) {
+ simulateTimeout("payment debit");
+ }
+
+ // Simulate failure injection on the selected step.
if (shouldFail()) {
LOGGER.debug("Payment debit failed (simulated): accountId={}",
accountId);
throw new RuntimeException("Simulated payment debit failure");
@@ -96,6 +115,12 @@ public class PaymentSagaService {
return result;
}
+ public void destroy() {
+ if (failureRandom != null) {
+ failureRandom.remove();
+ }
+ }
+
private void simulateDelay() {
if (simulatedDelayMs > 0) {
try {
@@ -107,6 +132,23 @@ public class PaymentSagaService {
}
private boolean shouldFail() {
- return ThreadLocalRandom.current().nextInt(100) < rollbackPercentage;
+ if (!failInjectionEnabled) {
+ return false;
+ }
+ return nextFailurePercent() < rollbackPercentage;
+ }
+
+ private void simulateTimeout(String operation) {
+ LOGGER.debug("Simulating {} timeout: {} ms", operation, timeoutMs);
+ try {
+ Thread.sleep(timeoutMs);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ throw new RuntimeException("Simulated " + operation + " timeout");
+ }
+
+ private int nextFailurePercent() {
+ return FailureRandomProvider.nextPercent(failureRandom);
}
}
diff --git
a/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/SagaDbEnvironment.java
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/SagaDbEnvironment.java
new file mode 100644
index 0000000000..1b3435eba6
--- /dev/null
+++
b/test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/SagaDbEnvironment.java
@@ -0,0 +1,171 @@
+/*
+ * 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 org.apache.seata.benchmark.saga;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import org.apache.seata.benchmark.config.BenchmarkConfig;
+import org.apache.seata.benchmark.constant.BenchmarkConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+import javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+/**
+ * Database-backed environment for Saga benchmark business actions.
+ * @author zihenzzz
+ */
+public class SagaDbEnvironment {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SagaDbEnvironment.class);
+
+ private final BenchmarkConfig config;
+ private MySQLContainer<?> mysqlContainer;
+ private HikariDataSource dataSource;
+
+ public SagaDbEnvironment(BenchmarkConfig config) {
+ this.config = config;
+ }
+
+ public void init() {
+ try {
+ startMySQLContainer();
+ createDataSource();
+ initDatabase();
+ LOGGER.info(
+ "Saga DB workload initialized with {} accounts and {}
products",
+ BenchmarkConstants.ACCOUNT_COUNT,
+ BenchmarkConstants.SAGA_PRODUCT_COUNT);
+ } catch (Exception e) {
+ destroy();
+ throw e instanceof RuntimeException
+ ? (RuntimeException) e
+ : new RuntimeException("Failed to initialize Saga DB
workload", e);
+ }
+ }
+
+ public DataSource getDataSource() {
+ return dataSource;
+ }
+
+ public void destroy() {
+ if (dataSource != null && !dataSource.isClosed()) {
+ dataSource.close();
+ LOGGER.info("Saga DB DataSource closed");
+ }
+ if (mysqlContainer != null && mysqlContainer.isRunning()) {
+ mysqlContainer.stop();
+ LOGGER.info("Saga DB MySQL container stopped");
+ }
+ }
+
+ private void startMySQLContainer() {
+ LOGGER.info("Starting MySQL container for Saga DB workload...");
+ mysqlContainer = new
MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
+ .withDatabaseName("benchmark_saga")
+ .withUsername("test")
+ .withPassword("test")
+ .withCommand("--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci");
+ mysqlContainer.start();
+ LOGGER.info("Saga DB MySQL container started: {}",
mysqlContainer.getJdbcUrl());
+ }
+
+ private void createDataSource() {
+ HikariConfig hikariConfig = new HikariConfig();
+ hikariConfig.setJdbcUrl(mysqlContainer.getJdbcUrl());
+ hikariConfig.setUsername(mysqlContainer.getUsername());
+ hikariConfig.setPassword(mysqlContainer.getPassword());
+ hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
+ hikariConfig.setMaximumPoolSize(config.getThreads() * 2);
+ hikariConfig.setMinimumIdle(Math.max(1, config.getThreads()));
+ hikariConfig.setConnectionTimeout(30000);
+ hikariConfig.setIdleTimeout(600000);
+ hikariConfig.setMaxLifetime(1800000);
+ dataSource = new HikariDataSource(hikariConfig);
+ }
+
+ private void initDatabase() throws SQLException {
+ try (Connection conn = dataSource.getConnection();
+ Statement stmt = conn.createStatement()) {
+ stmt.execute("CREATE TABLE IF NOT EXISTS benchmark_inventory ("
+ + "product_id VARCHAR(64) PRIMARY KEY, "
+ + "available_qty INT NOT NULL, "
+ + "reserved_qty INT NOT NULL DEFAULT 0, "
+ + "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON
UPDATE CURRENT_TIMESTAMP"
+ + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+ stmt.execute("CREATE TABLE IF NOT EXISTS benchmark_account ("
+ + "account_id VARCHAR(64) PRIMARY KEY, "
+ + "balance DECIMAL(18,2) NOT NULL, "
+ + "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON
UPDATE CURRENT_TIMESTAMP"
+ + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+ stmt.execute("CREATE TABLE IF NOT EXISTS benchmark_order ("
+ + "order_id VARCHAR(64) PRIMARY KEY, "
+ + "user_id VARCHAR(64) NOT NULL, "
+ + "product_id VARCHAR(64) NOT NULL, "
+ + "quantity INT NOT NULL, "
+ + "amount DECIMAL(18,2) NOT NULL, "
+ + "status VARCHAR(32) NOT NULL, "
+ + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "
+ + "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON
UPDATE CURRENT_TIMESTAMP"
+ + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+
+ stmt.execute("TRUNCATE TABLE benchmark_order");
+ stmt.execute("TRUNCATE TABLE benchmark_inventory");
+ stmt.execute("TRUNCATE TABLE benchmark_account");
+
+ seedInventory(conn);
+ seedAccounts(conn);
+ }
+ }
+
+ private void seedInventory(Connection conn) throws SQLException {
+ try (PreparedStatement pstmt = conn.prepareStatement(
+ "INSERT INTO benchmark_inventory (product_id, available_qty,
reserved_qty) VALUES (?, ?, 0)")) {
+ for (int i = 0; i < BenchmarkConstants.SAGA_PRODUCT_COUNT; i++) {
+ pstmt.setString(1, "product-" + i);
+ pstmt.setInt(2, BenchmarkConstants.SAGA_INITIAL_INVENTORY);
+ pstmt.addBatch();
+ if ((i + 1) % 100 == 0) {
+ pstmt.executeBatch();
+ }
+ }
+ pstmt.executeBatch();
+ }
+ }
+
+ private void seedAccounts(Connection conn) throws SQLException {
+ try (PreparedStatement pstmt =
+ conn.prepareStatement("INSERT INTO benchmark_account
(account_id, balance) VALUES (?, ?)")) {
+ for (int i = 0; i < BenchmarkConstants.ACCOUNT_COUNT; i++) {
+ pstmt.setString(1, "account-" + i);
+ pstmt.setBigDecimal(2,
BigDecimal.valueOf(BenchmarkConstants.SAGA_INITIAL_BALANCE));
+ pstmt.addBatch();
+ if ((i + 1) % 100 == 0) {
+ pstmt.executeBatch();
+ }
+ }
+ pstmt.executeBatch();
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]