Copilot commented on code in PR #8027:
URL: https://github.com/apache/incubator-seata/pull/8027#discussion_r2992107959
##########
integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandler.java:
##########
@@ -53,6 +55,9 @@ public class ActionInterceptorHandler {
private static final Logger LOGGER =
LoggerFactory.getLogger(ActionInterceptorHandler.class);
+ private static final boolean ENABLE_ACTION_STATUS_REPORT =
ConfigurationFactory.getInstance()
+
.getBoolean(ConfigurationKeys.CLIENT_SAGA_ACTION_STATUS_REPORT_ENABLE, false);
Review Comment:
`ENABLE_ACTION_STATUS_REPORT` is read once at class-load time. This prevents
config hot-reload from taking effect and can also capture the wrong value if
`ConfigurationFactory` isn’t initialized yet when this class is first loaded.
Prefer reading the flag from `ConfigurationFactory.getInstance()` at execution
time (or caching via a config listener) instead of a `static final` snapshot.
```suggestion
private static boolean isActionStatusReportEnabled() {
return ConfigurationFactory.getInstance()
.getBoolean(ConfigurationKeys.CLIENT_SAGA_ACTION_STATUS_REPORT_ENABLE, false);
}
```
##########
saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/SagaAnnotationResourceManager.java:
##########
@@ -131,6 +137,7 @@ public BranchStatus branchRollback(
LOGGER.error(msg, ExceptionUtil.unwrap(t));
return BranchStatus.PhaseTwo_RollbackFailed_Retryable;
} finally {
+ doAfterSagaAnnotationRollback(xid, branchId,
resource.getActionName(), businessActionContext);
Review Comment:
`doAfterSagaAnnotationRollback(...)` is invoked in the `finally` block even
when `businessActionContext` was never created (e.g., if
`getBusinessActionContext(...)` throws). In that case hooks will receive a null
`context`, which can cause NPEs inside hook implementations. Guard the hook
invocation (or build a minimal context) when `businessActionContext == null`.
```suggestion
if (businessActionContext != null) {
doAfterSagaAnnotationRollback(xid, branchId,
resource.getActionName(), businessActionContext);
}
```
##########
integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandler.java:
##########
@@ -325,4 +343,32 @@ protected Map<String, Object>
fetchActionRequestContext(Method method, Object[]
}
return context;
}
+
+ /**
+ * Report action status to TC
+ *
+ * @param actionContext the action context
+ * @param status the action status (success/failed)
+ */
+ protected void reportActionStatus(BusinessActionContext actionContext,
String status) {
+ try {
+ actionContext.setActionStatus(status);
+ actionContext.setUpdated(true);
+ BusinessActionContextUtil.reportContext(actionContext);
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "Report action status: xid={}, branchId={}, status={}",
+ actionContext.getXid(),
+ actionContext.getBranchId(),
+ status);
+ }
+ } catch (Exception e) {
+ LOGGER.warn(
+ "Report action status failed: xid={}, branchId={},
status={}, error={}",
+ actionContext.getXid(),
+ actionContext.getBranchId(),
+ status,
+ e.getMessage());
Review Comment:
`reportActionStatus(...)` swallows exceptions from
`BusinessActionContextUtil.reportContext(...)`, but it leaves
`actionContext.updated=true`. The `finally` block later calls
`BusinessActionContextUtil.reportContext(actionContext)` again and may throw,
so the “swallow” here won’t actually prevent the prepare from failing. Either
make status reporting best-effort by clearing/resetting `updated` when
catching, or don’t catch and let failures propagate (and log the
exception/stacktrace accordingly).
```suggestion
// Make status reporting best-effort: roll back the updated flag
on failure
actionContext.setUpdated(false);
LOGGER.warn(
"Report action status failed: xid={}, branchId={},
status={}",
actionContext.getXid(),
actionContext.getBranchId(),
status,
e);
```
##########
saga/seata-saga-annotation/src/test/java/org/apache/seata/saga/rm/SagaAnnotationResourceManagerTest.java:
##########
@@ -0,0 +1,333 @@
+/*
+ * 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.saga.rm;
+
+import org.apache.seata.core.model.BranchStatus;
+import org.apache.seata.core.model.BranchType;
+import org.apache.seata.integration.tx.api.fence.hook.TccHook;
+import org.apache.seata.integration.tx.api.fence.hook.TccHookManager;
+import org.apache.seata.integration.tx.api.remoting.TwoPhaseResult;
+import org.apache.seata.rm.tcc.api.BusinessActionContext;
+import org.apache.seata.rm.tcc.api.BusinessActionContextUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for SagaAnnotationResourceManager.
+ *
+ * Focus areas:
+ * 1. branchRollback with TccHook before/after callbacks
+ * 2. Hook exception handling (should not break rollback)
+ * 3. Different compensation return types (boolean, TwoPhaseResult, null)
+ * 4. Compensation exception handling
+ */
+public class SagaAnnotationResourceManagerTest {
+
+ static {
+ System.setProperty("config.type", "file");
+ System.setProperty("config.file.name", "file.conf");
+ }
Review Comment:
This test sets global `System` properties in a static initializer and never
restores the previous values. That can leak state into other tests when the
full suite runs in the same JVM. Consider moving this to `@BeforeAll` and
clearing/restoring the properties in `@AfterAll`.
##########
saga/seata-saga-annotation/src/test/java/org/apache/seata/saga/rm/SagaAnnotationResourceManagerTest.java:
##########
@@ -0,0 +1,333 @@
+/*
+ * 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.saga.rm;
+
+import org.apache.seata.core.model.BranchStatus;
+import org.apache.seata.core.model.BranchType;
+import org.apache.seata.integration.tx.api.fence.hook.TccHook;
+import org.apache.seata.integration.tx.api.fence.hook.TccHookManager;
+import org.apache.seata.integration.tx.api.remoting.TwoPhaseResult;
+import org.apache.seata.rm.tcc.api.BusinessActionContext;
+import org.apache.seata.rm.tcc.api.BusinessActionContextUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for SagaAnnotationResourceManager.
+ *
+ * Focus areas:
+ * 1. branchRollback with TccHook before/after callbacks
+ * 2. Hook exception handling (should not break rollback)
+ * 3. Different compensation return types (boolean, TwoPhaseResult, null)
+ * 4. Compensation exception handling
+ */
+public class SagaAnnotationResourceManagerTest {
+
+ static {
+ System.setProperty("config.type", "file");
+ System.setProperty("config.file.name", "file.conf");
+ }
+
+ private SagaAnnotationResourceManager resourceManager;
+
+ @BeforeEach
+ void setUp() {
+ TccHookManager.clear();
+ resourceManager = new SagaAnnotationResourceManager();
+ }
+
+ @AfterEach
+ void tearDown() {
+ TccHookManager.clear();
+ BusinessActionContextUtil.clear();
+ }
+
+ // ---- Helper classes ----
+
+ public static class TestCompensationTarget {
+ public boolean compensate(BusinessActionContext context) {
+ return true;
+ }
+
+ public boolean compensateFail(BusinessActionContext context) {
+ return false;
+ }
+
+ public Boolean compensateReturnNull(BusinessActionContext context) {
+ return null;
+ }
+
+ public TwoPhaseResult
compensateWithResultSuccess(BusinessActionContext context) {
+ return new TwoPhaseResult(true, "ok");
+ }
+
+ public TwoPhaseResult compensateWithResultFail(BusinessActionContext
context) {
+ return new TwoPhaseResult(false, "fail");
+ }
+
+ public boolean compensateThrow(BusinessActionContext context) {
+ throw new RuntimeException("compensation error");
+ }
+ }
+
+ public static class TrackingTccHook implements TccHook {
+ boolean beforeRollbackCalled = false;
+ boolean afterRollbackCalled = false;
+ boolean shouldThrowInBefore = false;
+ boolean shouldThrowInAfter = false;
+
+ @Override
+ public void beforeTccPrepare(String xid, Long branchId, String
actionName, BusinessActionContext context) {}
+
+ @Override
+ public void afterTccPrepare(String xid, Long branchId, String
actionName, BusinessActionContext context) {}
+
+ @Override
+ public void beforeTccCommit(String xid, Long branchId, String
actionName, BusinessActionContext context) {}
+
+ @Override
+ public void afterTccCommit(String xid, Long branchId, String
actionName, BusinessActionContext context) {}
+
+ @Override
+ public void beforeTccRollback(String xid, Long branchId, String
actionName, BusinessActionContext context) {
+ beforeRollbackCalled = true;
+ if (shouldThrowInBefore) {
+ throw new RuntimeException("hook error in beforeTccRollback");
+ }
+ }
+
+ @Override
+ public void afterTccRollback(String xid, Long branchId, String
actionName, BusinessActionContext context) {
+ afterRollbackCalled = true;
+ if (shouldThrowInAfter) {
+ throw new RuntimeException("hook error in afterTccRollback");
+ }
+ }
+ }
+
+ private SagaAnnotationResource createResource(String actionName, String
methodName) throws NoSuchMethodException {
+ SagaAnnotationResource resource = new SagaAnnotationResource();
+ resource.setActionName(actionName);
+ resource.setTargetBean(new TestCompensationTarget());
+ resource.setCompensationMethod(
+ TestCompensationTarget.class.getDeclaredMethod(methodName,
BusinessActionContext.class));
+ resource.setCompensationArgsClasses(new Class<?>[]
{BusinessActionContext.class});
+ resource.setPhaseTwoCompensationKeys(new String[] {"unused"});
+ return resource;
+ }
+
+ // ---- Tests for hook invocation in branchRollback ----
+
+ @Test
+ void testBranchRollbackWithHooksInvoked() throws Exception {
+ TrackingTccHook hook = new TrackingTccHook();
+ TccHookManager.registerHook(hook);
+
+ SagaAnnotationResource resource = createResource("testAction",
"compensate");
+ resourceManager.getManagedResources().put("testAction", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 1L, "testAction", null);
+
+ assertEquals(BranchStatus.PhaseTwo_Rollbacked, status);
+ assertTrue(hook.beforeRollbackCalled, "beforeTccRollback should be
called");
+ assertTrue(hook.afterRollbackCalled, "afterTccRollback should be
called");
+ }
+
+ @Test
+ void testBranchRollbackHookExceptionInBeforeDoesNotBreakRollback() throws
Exception {
+ TrackingTccHook hook = new TrackingTccHook();
+ hook.shouldThrowInBefore = true;
+ TccHookManager.registerHook(hook);
+
+ SagaAnnotationResource resource = createResource("testAction2",
"compensate");
+ resourceManager.getManagedResources().put("testAction2", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 2L, "testAction2", null);
+
+ assertEquals(BranchStatus.PhaseTwo_Rollbacked, status);
+ assertTrue(hook.beforeRollbackCalled);
+ assertTrue(hook.afterRollbackCalled, "afterTccRollback should still be
called even if before throws");
+ }
+
+ @Test
+ void testBranchRollbackHookExceptionInAfterDoesNotBreakRollback() throws
Exception {
+ TrackingTccHook hook = new TrackingTccHook();
+ hook.shouldThrowInAfter = true;
+ TccHookManager.registerHook(hook);
+
+ SagaAnnotationResource resource = createResource("testAction3",
"compensate");
+ resourceManager.getManagedResources().put("testAction3", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 3L, "testAction3", null);
+
+ assertEquals(BranchStatus.PhaseTwo_Rollbacked, status);
+ assertTrue(hook.beforeRollbackCalled);
+ assertTrue(hook.afterRollbackCalled);
+ }
+
+ @Test
+ void testBranchRollbackWithoutHooks() throws Exception {
+ SagaAnnotationResource resource = createResource("testAction4",
"compensate");
+ resourceManager.getManagedResources().put("testAction4", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 4L, "testAction4", null);
+
+ assertEquals(BranchStatus.PhaseTwo_Rollbacked, status);
+ }
+
+ // ---- Tests for different compensation return types ----
+
+ @Test
+ void testBranchRollbackCompensationReturnsFalse() throws Exception {
+ SagaAnnotationResource resource = createResource("testAction5",
"compensateFail");
+ resourceManager.getManagedResources().put("testAction5", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 5L, "testAction5", null);
+
+ assertEquals(BranchStatus.PhaseTwo_RollbackFailed_Retryable, status);
+ }
+
+ @Test
+ void testBranchRollbackCompensationReturnsNull() throws Exception {
+ SagaAnnotationResource resource = createResource("testAction6",
"compensateReturnNull");
+ resourceManager.getManagedResources().put("testAction6", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 6L, "testAction6", null);
+
+ // null return is treated as success
+ assertEquals(BranchStatus.PhaseTwo_Rollbacked, status);
+ }
+
+ @Test
+ void testBranchRollbackCompensationReturnsTwoPhaseResultSuccess() throws
Exception {
+ SagaAnnotationResource resource = createResource("testAction7",
"compensateWithResultSuccess");
+ resourceManager.getManagedResources().put("testAction7", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 7L, "testAction7", null);
+
+ assertEquals(BranchStatus.PhaseTwo_Rollbacked, status);
+ }
+
+ @Test
+ void testBranchRollbackCompensationReturnsTwoPhaseResultFail() throws
Exception {
+ SagaAnnotationResource resource = createResource("testAction8",
"compensateWithResultFail");
+ resourceManager.getManagedResources().put("testAction8", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 8L, "testAction8", null);
+
+ assertEquals(BranchStatus.PhaseTwo_RollbackFailed_Retryable, status);
+ }
+
+ @Test
+ void testBranchRollbackCompensationThrowsException() throws Exception {
+ SagaAnnotationResource resource = createResource("testAction9",
"compensateThrow");
+ resourceManager.getManagedResources().put("testAction9", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 9L, "testAction9", null);
+
+ assertEquals(BranchStatus.PhaseTwo_RollbackFailed_Retryable, status);
+ }
+
+ // ---- Tests for hook invocation with compensation failure ----
+
+ @Test
+ void testBranchRollbackWithHooksWhenCompensationFails() throws Exception {
+ TrackingTccHook hook = new TrackingTccHook();
+ TccHookManager.registerHook(hook);
+
+ SagaAnnotationResource resource = createResource("testAction10",
"compensateFail");
+ resourceManager.getManagedResources().put("testAction10", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 10L, "testAction10", null);
+
+ assertEquals(BranchStatus.PhaseTwo_RollbackFailed_Retryable, status);
+ assertTrue(hook.beforeRollbackCalled, "beforeTccRollback should be
called even when compensation fails");
+ assertTrue(hook.afterRollbackCalled, "afterTccRollback should be
called in finally block");
+ }
+
+ @Test
+ void testBranchRollbackWithHooksWhenCompensationThrows() throws Exception {
+ TrackingTccHook hook = new TrackingTccHook();
+ TccHookManager.registerHook(hook);
+
+ SagaAnnotationResource resource = createResource("testAction11",
"compensateThrow");
+ resourceManager.getManagedResources().put("testAction11", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 11L, "testAction11", null);
+
+ assertEquals(BranchStatus.PhaseTwo_RollbackFailed_Retryable, status);
+ assertTrue(hook.beforeRollbackCalled);
+ assertTrue(hook.afterRollbackCalled, "afterTccRollback should be
called in finally block even on exception");
+ }
+
+ // ---- Tests for multiple hooks ----
+
+ @Test
+ void testBranchRollbackWithMultipleHooks() throws Exception {
+ TrackingTccHook hook1 = new TrackingTccHook();
+ TrackingTccHook hook2 = new TrackingTccHook();
+ TccHookManager.registerHook(hook1);
+ TccHookManager.registerHook(hook2);
+
+ SagaAnnotationResource resource = createResource("testAction12",
"compensate");
+ resourceManager.getManagedResources().put("testAction12", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 12L, "testAction12", null);
+
+ assertEquals(BranchStatus.PhaseTwo_Rollbacked, status);
+ assertTrue(hook1.beforeRollbackCalled);
+ assertTrue(hook1.afterRollbackCalled);
+ assertTrue(hook2.beforeRollbackCalled);
+ assertTrue(hook2.afterRollbackCalled);
+ }
+
+ @Test
+ void testBranchRollbackFirstHookThrowsSecondStillCalled() throws Exception
{
+ TrackingTccHook hook1 = new TrackingTccHook();
+ hook1.shouldThrowInBefore = true;
+ TrackingTccHook hook2 = new TrackingTccHook();
+ TccHookManager.registerHook(hook1);
+ TccHookManager.registerHook(hook2);
+
+ SagaAnnotationResource resource = createResource("testAction13",
"compensate");
+ resourceManager.getManagedResources().put("testAction13", resource);
+
+ BranchStatus status =
+ resourceManager.branchRollback(BranchType.SAGA_ANNOTATION,
"xid123", 13L, "testAction13", null);
+
+ assertEquals(BranchStatus.PhaseTwo_Rollbacked, status);
+ assertTrue(hook1.beforeRollbackCalled);
+ // hook2.beforeRollbackCalled is NOT guaranteed because hook1 throws
+ // the loop iterates hooks sequentially, and the exception breaks the
loop
Review Comment:
The comment says hook1’s exception breaks the hook loop and therefore
hook2’s `beforeTccRollback` isn’t guaranteed, but
`SagaAnnotationResourceManager#doBeforeSagaAnnotationRollback` catches
exceptions per-hook and continues iterating. This comment is misleading; update
it (and/or assert `hook2.beforeRollbackCalled` if that’s the intended behavior).
```suggestion
// Even if the first hook throws in beforeTccRollback, subsequent
hooks should still be invoked
assertTrue(hook2.beforeRollbackCalled);
```
--
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]