Copilot commented on code in PR #8042:
URL: https://github.com/apache/incubator-seata/pull/8042#discussion_r3069106763


##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/model/BenchmarkMetrics.java:
##########
@@ -80,6 +107,26 @@ public long getFailedCount() {
         return failedCount.get();
     }
 
+    public long getCommittedCount() {
+        return committedCount.get();
+    }
+
+    public long getCompensatedCount() {
+        return compensatedCount.get();
+    }
+
+    public long getExecutionFailedCount() {
+        return failedCount.get() - compensatedCount.get() - 
compensationFailedCount.get() - unknownCount.get();

Review Comment:
   `getExecutionFailedCount()` subtracts `compensatedCount` from `failedCount`, 
but compensated transactions are recorded as successes (they don’t increment 
`failedCount`). This can undercount execution failures and even return a 
negative number when many transactions are compensated. Compute execution 
failures from the failure-side buckets only (e.g., failed minus 
compensationFailed minus unknown), or track an explicit executionFailed counter.
   ```suggestion
           return failedCount.get() - compensationFailedCount.get() - 
unknownCount.get();
   ```



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentDbSagaService.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 javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+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 Random failureRandom;
+    private final boolean timeoutInjectionEnabled;
+    private final int timeoutMs;
+
+    public PaymentDbSagaService(
+            DataSource dataSource,
+            int rollbackPercentage,
+            int simulatedDelayMs,
+            boolean failInjectionEnabled,
+            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()) {
+            conn.setAutoCommit(false);
+            BigDecimal balance = queryBalance(conn, accountId);
+            if (balance.compareTo(amount) < 0) {
+                throw new RuntimeException("Insufficient balance for account " 
+ accountId);
+            }
+            try (PreparedStatement pstmt = conn.prepareStatement(
+                    "UPDATE benchmark_account SET balance = balance - ? WHERE 
account_id = ?")) {
+                pstmt.setBigDecimal(1, amount);
+                pstmt.setString(2, accountId);
+                pstmt.executeUpdate();
+            }
+            conn.commit();

Review Comment:
   This manual transaction doesn’t roll back on non-SQL exceptions (e.g., 
insufficient balance) before the connection is returned to the pool. Add a 
try/catch/finally that performs `rollback()` on any throwable after 
`setAutoCommit(false)` and restores connection state to avoid leaking an open 
transaction into the pool.
   ```suggestion
               boolean originalAutoCommit = conn.getAutoCommit();
               Throwable pending = null;
               conn.setAutoCommit(false);
               try {
                   BigDecimal balance = queryBalance(conn, accountId);
                   if (balance.compareTo(amount) < 0) {
                       throw new RuntimeException("Insufficient balance for 
account " + accountId);
                   }
                   try (PreparedStatement pstmt = conn.prepareStatement(
                           "UPDATE benchmark_account SET balance = balance - ? 
WHERE account_id = ?")) {
                       pstmt.setBigDecimal(1, amount);
                       pstmt.setString(2, accountId);
                       pstmt.executeUpdate();
                   }
                   conn.commit();
               } catch (Throwable t) {
                   pending = t;
                   try {
                       conn.rollback();
                   } catch (SQLException rollbackException) {
                       t.addSuppressed(rollbackException);
                   }
                   throw t;
               } finally {
                   try {
                       conn.setAutoCommit(originalAutoCommit);
                   } catch (SQLException resetException) {
                       if (pending != null) {
                           pending.addSuppressed(resetException);
                       } else {
                           throw new RuntimeException("Failed to restore 
auto-commit", resetException);
                       }
                   }
               }
   ```



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentSagaService.java:
##########
@@ -107,6 +126,28 @@ private void simulateDelay() {
     }
 
     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() {
+        if (failureRandom != null) {
+            synchronized (failureRandom) {
+                return failureRandom.nextInt(100);
+            }
+        }
+        return ThreadLocalRandom.current().nextInt(100);

Review Comment:
   `nextFailurePercent()` synchronizes on the shared `Random` instance. With 
multiple benchmark threads this introduces lock contention and can distort 
benchmark results. Prefer a per-thread RNG (e.g., a `ThreadLocal` RNG derived 
from the configured seed, or a `SplittableRandom` per thread) so failure 
injection remains reproducible without a global lock.



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventorySagaService.java:
##########
@@ -102,6 +121,28 @@ private void simulateDelay() {
     }
 
     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() {
+        if (failureRandom != null) {
+            synchronized (failureRandom) {
+                return failureRandom.nextInt(100);
+            }
+        }
+        return ThreadLocalRandom.current().nextInt(100);

Review Comment:
   `nextFailurePercent()` synchronizes on the shared `Random` instance. With 
multiple benchmark threads this introduces lock contention and can distort 
benchmark results. Prefer a per-thread RNG (e.g., a `ThreadLocal` RNG derived 
from the configured seed, or a `SplittableRandom` per thread) so failure 
injection remains reproducible without a global lock.



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventoryDbSagaService.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.ResultSet;
+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 Random failureRandom;
+    private final boolean timeoutInjectionEnabled;
+    private final int timeoutMs;
+
+    public InventoryDbSagaService(
+            DataSource dataSource,
+            int rollbackPercentage,
+            int simulatedDelayMs,
+            boolean failInjectionEnabled,
+            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()) {
+            conn.setAutoCommit(false);
+            int available = queryAvailableQuantity(conn, productId);
+            if (available < quantity) {
+                throw new RuntimeException("Insufficient inventory for product 
" + productId);
+            }
+            try (PreparedStatement pstmt = conn.prepareStatement(
+                    "UPDATE benchmark_inventory "
+                            + "SET available_qty = available_qty - ?, 
reserved_qty = reserved_qty + ? "
+                            + "WHERE product_id = ?")) {
+                pstmt.setInt(1, quantity);
+                pstmt.setInt(2, quantity);
+                pstmt.setString(3, productId);
+                pstmt.executeUpdate();
+            }
+            conn.commit();
+        } 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;
+    }
+
+    private int queryAvailableQuantity(Connection conn, String productId) 
throws SQLException {
+        try (PreparedStatement pstmt =
+                        conn.prepareStatement("SELECT available_qty FROM 
benchmark_inventory WHERE product_id = ?");
+                ResultSet rs = executeQuery(pstmt, productId)) {
+            if (!rs.next()) {
+                throw new RuntimeException("Product not found: " + productId);
+            }
+            return rs.getInt(1);
+        }
+    }
+
+    private ResultSet executeQuery(PreparedStatement pstmt, String productId) 
throws SQLException {
+        pstmt.setString(1, productId);
+        return pstmt.executeQuery();
+    }
+
+    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() {
+        if (failureRandom != null) {
+            synchronized (failureRandom) {
+                return failureRandom.nextInt(100);
+            }
+        }
+        return ThreadLocalRandom.current().nextInt(100);
+    }

Review Comment:
   `nextFailurePercent()` synchronizes on the shared `Random` instance. With 
multiple benchmark threads this introduces lock contention and can distort 
benchmark results. Prefer a per-thread RNG (e.g., a `ThreadLocal` RNG derived 
from the configured seed, or a `SplittableRandom` per thread) so failure 
injection remains reproducible without a global lock.



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventoryDbSagaService.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.ResultSet;
+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 Random failureRandom;
+    private final boolean timeoutInjectionEnabled;
+    private final int timeoutMs;
+
+    public InventoryDbSagaService(
+            DataSource dataSource,
+            int rollbackPercentage,
+            int simulatedDelayMs,
+            boolean failInjectionEnabled,
+            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()) {
+            conn.setAutoCommit(false);
+            int available = queryAvailableQuantity(conn, productId);
+            if (available < quantity) {
+                throw new RuntimeException("Insufficient inventory for product 
" + productId);
+            }
+            try (PreparedStatement pstmt = conn.prepareStatement(
+                    "UPDATE benchmark_inventory "
+                            + "SET available_qty = available_qty - ?, 
reserved_qty = reserved_qty + ? "
+                            + "WHERE product_id = ?")) {
+                pstmt.setInt(1, quantity);
+                pstmt.setInt(2, quantity);
+                pstmt.setString(3, productId);
+                pstmt.executeUpdate();

Review Comment:
   The inventory availability check is a separate read followed by an update 
that unconditionally decrements `available_qty`. With concurrent reservations 
this can drive `available_qty` negative or allow overselling. Consider locking 
the row during the check (e.g., `SELECT ... FOR UPDATE`) or using a single 
conditional update (`... SET available_qty = available_qty - ? ... WHERE 
product_id = ? AND available_qty >= ?`) and failing if no row is updated.
   ```suggestion
               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);
                   int updatedRows = pstmt.executeUpdate();
                   if (updatedRows == 0) {
                       throw new RuntimeException("Insufficient inventory for 
product " + productId);
                   }
   ```



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/InventoryDbSagaService.java:
##########
@@ -0,0 +1,178 @@
+/*
+ * 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.ResultSet;
+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 Random failureRandom;
+    private final boolean timeoutInjectionEnabled;
+    private final int timeoutMs;
+
+    public InventoryDbSagaService(
+            DataSource dataSource,
+            int rollbackPercentage,
+            int simulatedDelayMs,
+            boolean failInjectionEnabled,
+            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()) {
+            conn.setAutoCommit(false);
+            int available = queryAvailableQuantity(conn, productId);
+            if (available < quantity) {
+                throw new RuntimeException("Insufficient inventory for product 
" + productId);
+            }
+            try (PreparedStatement pstmt = conn.prepareStatement(
+                    "UPDATE benchmark_inventory "
+                            + "SET available_qty = available_qty - ?, 
reserved_qty = reserved_qty + ? "
+                            + "WHERE product_id = ?")) {
+                pstmt.setInt(1, quantity);
+                pstmt.setInt(2, quantity);
+                pstmt.setString(3, productId);
+                pstmt.executeUpdate();
+            }
+            conn.commit();

Review Comment:
   This manual transaction doesn’t roll back on non-SQL exceptions (e.g., 
insufficient inventory) before the connection is returned to the pool. Add a 
try/catch/finally that performs `rollback()` on any throwable after 
`setAutoCommit(false)` and restores connection state to avoid leaking an open 
transaction into the pool.
   ```suggestion
               boolean originalAutoCommit = conn.getAutoCommit();
               boolean restoreAutoCommit = false;
               try {
                   conn.setAutoCommit(false);
                   restoreAutoCommit = true;
                   int available = queryAvailableQuantity(conn, productId);
                   if (available < quantity) {
                       throw new RuntimeException("Insufficient inventory for 
product " + productId);
                   }
                   try (PreparedStatement pstmt = conn.prepareStatement(
                           "UPDATE benchmark_inventory "
                                   + "SET available_qty = available_qty - ?, 
reserved_qty = reserved_qty + ? "
                                   + "WHERE product_id = ?")) {
                       pstmt.setInt(1, quantity);
                       pstmt.setInt(2, quantity);
                       pstmt.setString(3, productId);
                       pstmt.executeUpdate();
                   }
                   conn.commit();
               } catch (Throwable t) {
                   if (restoreAutoCommit) {
                       try {
                           conn.rollback();
                       } catch (SQLException rollbackEx) {
                           LOGGER.warn("Failed to rollback inventory 
reservation transaction for productId={}",
                                   productId, rollbackEx);
                       }
                   }
                   if (t instanceof SQLException) {
                       throw new RuntimeException("Failed to reserve 
inventory", t);
                   }
                   throw t;
               } finally {
                   if (restoreAutoCommit) {
                       try {
                           conn.setAutoCommit(originalAutoCommit);
                       } catch (SQLException resetEx) {
                           LOGGER.warn("Failed to restore auto-commit for 
inventory reservation connection, productId={}",
                                   productId, resetEx);
                       }
                   }
               }
   ```



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentDbSagaService.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 javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+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 Random failureRandom;
+    private final boolean timeoutInjectionEnabled;
+    private final int timeoutMs;
+
+    public PaymentDbSagaService(
+            DataSource dataSource,
+            int rollbackPercentage,
+            int simulatedDelayMs,
+            boolean failInjectionEnabled,
+            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()) {
+            conn.setAutoCommit(false);
+            BigDecimal balance = queryBalance(conn, accountId);
+            if (balance.compareTo(amount) < 0) {
+                throw new RuntimeException("Insufficient balance for account " 
+ accountId);
+            }
+            try (PreparedStatement pstmt = conn.prepareStatement(
+                    "UPDATE benchmark_account SET balance = balance - ? WHERE 
account_id = ?")) {
+                pstmt.setBigDecimal(1, amount);
+                pstmt.setString(2, accountId);
+                pstmt.executeUpdate();

Review Comment:
   The balance check is done as a separate read (`SELECT balance`) followed by 
an `UPDATE balance = balance - ?`. Under concurrency this can overdraw accounts 
(race between the check and the update). Consider a single conditional update 
(e.g., update with `WHERE balance >= ?`) and treat 0 updated rows as 
insufficient funds, or lock the row (e.g., `SELECT ... FOR UPDATE`) within the 
transaction.
   ```suggestion
               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);
                   int updatedRows = pstmt.executeUpdate();
                   if (updatedRows == 0) {
                       throw new RuntimeException("Insufficient balance for 
account " + accountId);
                   }
   ```



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/OrderSagaService.java:
##########
@@ -106,6 +125,28 @@ private void simulateDelay() {
     }
 
     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() {
+        if (failureRandom != null) {
+            synchronized (failureRandom) {
+                return failureRandom.nextInt(100);
+            }
+        }
+        return ThreadLocalRandom.current().nextInt(100);

Review Comment:
   `nextFailurePercent()` synchronizes on the shared `Random` instance. With 
multiple benchmark threads this introduces lock contention and can distort 
benchmark results. Prefer a per-thread RNG (e.g., a `ThreadLocal` RNG derived 
from the configured seed, or a `SplittableRandom` per thread) so failure 
injection remains reproducible without a global lock.



##########
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 Random failureRandom;
+    private final boolean timeoutInjectionEnabled;
+    private final int timeoutMs;
+
+    public OrderDbSagaService(
+            DataSource dataSource,
+            int rollbackPercentage,
+            int simulatedDelayMs,
+            boolean failInjectionEnabled,
+            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;
+    }
+
+    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() {
+        if (failureRandom != null) {
+            synchronized (failureRandom) {
+                return failureRandom.nextInt(100);
+            }

Review Comment:
   `nextFailurePercent()` synchronizes on the shared `Random` instance. With 
multiple benchmark threads this introduces lock contention and can distort 
benchmark results. Prefer a per-thread RNG (e.g., a `ThreadLocal` RNG derived 
from the configured seed, or a `SplittableRandom` per thread) so failure 
injection remains reproducible without a global lock.
   ```suggestion
               return failureRandom.nextInt(100);
   ```



##########
test-suite/seata-benchmark-cli/src/main/java/org/apache/seata/benchmark/saga/PaymentDbSagaService.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 javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+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 Random failureRandom;
+    private final boolean timeoutInjectionEnabled;
+    private final int timeoutMs;
+
+    public PaymentDbSagaService(
+            DataSource dataSource,
+            int rollbackPercentage,
+            int simulatedDelayMs,
+            boolean failInjectionEnabled,
+            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()) {
+            conn.setAutoCommit(false);
+            BigDecimal balance = queryBalance(conn, accountId);
+            if (balance.compareTo(amount) < 0) {
+                throw new RuntimeException("Insufficient balance for account " 
+ accountId);
+            }
+            try (PreparedStatement pstmt = conn.prepareStatement(
+                    "UPDATE benchmark_account SET balance = balance - ? WHERE 
account_id = ?")) {
+                pstmt.setBigDecimal(1, amount);
+                pstmt.setString(2, accountId);
+                pstmt.executeUpdate();
+            }
+            conn.commit();
+        } 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;
+    }
+
+    private BigDecimal queryBalance(Connection conn, String accountId) throws 
SQLException {
+        try (PreparedStatement pstmt =
+                        conn.prepareStatement("SELECT balance FROM 
benchmark_account WHERE account_id = ?");
+                ResultSet rs = executeQuery(pstmt, accountId)) {
+            if (!rs.next()) {
+                throw new RuntimeException("Account not found: " + accountId);
+            }
+            return rs.getBigDecimal(1);
+        }
+    }
+
+    private ResultSet executeQuery(PreparedStatement pstmt, String accountId) 
throws SQLException {
+        pstmt.setString(1, accountId);
+        return pstmt.executeQuery();
+    }
+
+    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() {
+        if (failureRandom != null) {
+            synchronized (failureRandom) {
+                return failureRandom.nextInt(100);
+            }
+        }
+        return ThreadLocalRandom.current().nextInt(100);
+    }

Review Comment:
   `nextFailurePercent()` synchronizes on the shared `Random` instance. With 
multiple benchmark threads this introduces lock contention and can distort 
benchmark results. Prefer a per-thread RNG (e.g., a `ThreadLocal` RNG derived 
from the configured seed, or a `SplittableRandom` per thread) so failure 
injection remains reproducible without a global lock.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to