This is an automated email from the ASF dual-hosted git repository.
funky-eyes 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 bba87e7cbe feature: automatic update after BusinessActionContext
modify (#8140)
bba87e7cbe is described below
commit bba87e7cbe69779ebb07cfbab0a354d645f8b7b0
Author: Zhengcy05 <[email protected]>
AuthorDate: Fri Jun 19 13:22:54 2026 +0800
feature: automatic update after BusinessActionContext modify (#8140)
---
changes/en-us/2.x.md | 2 +
changes/zh-cn/2.x.md | 2 +
.../api/interceptor/ActionInterceptorHandler.java | 14 +-
.../seata/rm/tcc/api/BusinessActionContext.java | 203 +++++++++++++++++++++
.../interceptor/ActionInterceptorHandlerTest.java | 79 ++++++++
.../rm/tcc/api/BusinessActionContextTest.java | 114 ++++++++++++
6 files changed, 410 insertions(+), 4 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index de80e8f743..280ba232df 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -20,6 +20,7 @@ Add changes here for all PR submitted to the 2.x branch.
### feature:
+- [[#8140](https://github.com/apache/incubator-seata/pull/8140)] support
automatic updated marking after BusinessActionContext modifications
### bugfix:
@@ -44,6 +45,7 @@ Thanks to these contributors for their code commits. Please
report an unintended
<!-- Please make sure your Github ID is in the list below -->
- [slievrly](https://github.com/slievrly)
+- [Zhengcy05](https://github.com/Zhengcy05)
Also, we receive many valuable issues, questions and advices from our
community. Thanks for you all.
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index ae2a3808f0..37bbf1cacb 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -20,6 +20,7 @@
### feature:
+- [[#8140](https://github.com/apache/incubator-seata/pull/8140)] 支持在
BusinessActionContext 变更后自动标记 updated
### bugfix:
@@ -44,6 +45,7 @@
<!-- 请确保您的 GitHub ID 在以下列表中 -->
- [slievrly](https://github.com/slievrly)
+- [Zhengcy05](https://github.com/Zhengcy05)
同时,我们收到了社区反馈的很多有价值的issue和建议,非常感谢大家。
diff --git
a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandler.java
b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandler.java
index d47047b71c..35234139a7 100644
---
a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandler.java
+++
b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandler.java
@@ -91,6 +91,9 @@ public class ActionInterceptorHandler {
// MDC put branchId
MDC.put(RootContext.MDC_KEY_BRANCH_ID, branchId);
+ // enable mutation tracking only after framework initialization is
complete
+ actionContext.enableActionContextTracking();
+
// save the previous action context
BusinessActionContext previousActionContext =
BusinessActionContextUtil.getContext();
try {
@@ -232,10 +235,13 @@ public class ActionInterceptorHandler {
Map<String, Object> originContext = actionContext.getActionContext();
if (CollectionUtils.isNotEmpty(originContext)) {
- // Merge context and origin context if it exists.
- // @since: above 1.4.2
- originContext.putAll(context);
- context = originContext;
+ // Keep framework-side merge outside the tracked map to avoid
false updated flags.
+ // Merge framework context into a fresh map to avoid treating
framework-side
+ // initialization as a business mutation when tracking is already
enabled.
+ Map<String, Object> mergedContext = new HashMap<>(originContext);
+ mergedContext.putAll(context);
+ actionContext.setActionContext(mergedContext);
+ context = mergedContext;
} else {
actionContext.setActionContext(context);
}
diff --git
a/integration-tx-api/src/main/java/org/apache/seata/rm/tcc/api/BusinessActionContext.java
b/integration-tx-api/src/main/java/org/apache/seata/rm/tcc/api/BusinessActionContext.java
index a34b41e9ce..ada14bbb0e 100644
---
a/integration-tx-api/src/main/java/org/apache/seata/rm/tcc/api/BusinessActionContext.java
+++
b/integration-tx-api/src/main/java/org/apache/seata/rm/tcc/api/BusinessActionContext.java
@@ -22,7 +22,14 @@ import
org.apache.seata.integration.tx.api.interceptor.ActionContextUtil;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serializable;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
/**
* The type Business action context.
@@ -62,6 +69,11 @@ public class BusinessActionContext implements Serializable {
*/
private Map<String, Object> actionContext;
+ /**
+ * whether the action context should stay tracked on mutation
+ */
+ private transient boolean actionContextTrackingEnabled;
+
/**
* Instantiates a new Business action context.
*/
@@ -147,9 +159,27 @@ public class BusinessActionContext implements Serializable
{
* @param actionContext the action context
*/
public void setActionContext(Map<String, Object> actionContext) {
+ if (actionContextTrackingEnabled) {
+ this.actionContext = actionContext == null
+ ? new TrackedActionContextMap(this)
+ : new TrackedActionContextMap(this, actionContext);
+ return;
+ }
this.actionContext = actionContext;
}
+ /**
+ * Enable automatic updated tracking for action context mutations.
+ */
+ public void enableActionContextTracking() {
+ actionContextTrackingEnabled = true;
+ if (actionContext == null) {
+ actionContext = new TrackedActionContextMap(this);
+ } else if (!(actionContext instanceof TrackedActionContextMap)) {
+ actionContext = new TrackedActionContextMap(this, actionContext);
+ }
+ }
+
/**
* Gets xid.
*
@@ -233,6 +263,10 @@ public class BusinessActionContext implements Serializable
{
this.branchType = branchType;
}
+ private void markUpdatedOnActionContextMutation() {
+ setUpdated(true);
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -253,4 +287,173 @@ public class BusinessActionContext implements
Serializable {
.append("]");
return sb.toString();
}
+
+ /**
+ * The tracked action context map.
+ */
+ private static final class TrackedActionContextMap extends
AbstractMap<String, Object> implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final BusinessActionContext owner;
+
+ private final Map<String, Object> delegate;
+
+ private TrackedActionContextMap(BusinessActionContext owner) {
+ this.owner = owner;
+ this.delegate = new HashMap<>(8);
+ }
+
+ private TrackedActionContextMap(BusinessActionContext owner,
Map<String, Object> source) {
+ this.owner = owner;
+ this.delegate = new HashMap<>(source);
+ }
+
+ @Override
+ public Object put(String key, Object value) {
+ boolean hadKey = delegate.containsKey(key);
+ Object previousValue = delegate.put(key, value);
+ if (!hadKey || !Objects.equals(previousValue, value)) {
+ owner.markUpdatedOnActionContextMutation();
+ }
+ return previousValue;
+ }
+
+ @Override
+ public void putAll(Map<? extends String, ? extends Object> m) {
+ Objects.requireNonNull(m, "m");
+ if (m.isEmpty()) {
+ return;
+ }
+ for (Map.Entry<? extends String, ? extends Object> entry :
m.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public Object remove(Object key) {
+ boolean hadKey = delegate.containsKey(key);
+ Object previousValue = delegate.remove(key);
+ if (hadKey) {
+ owner.markUpdatedOnActionContextMutation();
+ }
+ return previousValue;
+ }
+
+ @Override
+ public void clear() {
+ if (!delegate.isEmpty()) {
+ delegate.clear();
+ owner.markUpdatedOnActionContextMutation();
+ }
+ }
+
+ @Override
+ public Set<Entry<String, Object>> entrySet() {
+ return new AbstractSet<Entry<String, Object>>() {
+ @Override
+ public Iterator<Entry<String, Object>> iterator() {
+ Iterator<Entry<String, Object>> iterator =
+ delegate.entrySet().iterator();
+ return new Iterator<Entry<String, Object>>() {
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public Entry<String, Object> next() {
+ Entry<String, Object> current = iterator.next();
+ return new TrackingEntry(current);
+ }
+
+ @Override
+ public void remove() {
+ iterator.remove();
+ owner.markUpdatedOnActionContextMutation();
+ }
+ };
+ }
+
+ // The following methods delegate directly to the underlying
Map.
+ @Override
+ public int size() {
+ return delegate.size();
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ if (!(o instanceof Entry)) {
+ return false;
+ }
+ Entry<?, ?> entry = (Entry<?, ?>) o;
+ if (!delegate.containsKey(entry.getKey())) {
+ return false;
+ }
+ if (!Objects.equals(delegate.get(entry.getKey()),
entry.getValue())) {
+ return false;
+ }
+ TrackedActionContextMap.this.remove(entry.getKey());
+ return true;
+ }
+ };
+ }
+
+ @Override
+ public int size() {
+ return delegate.size();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return delegate.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ return delegate.containsValue(value);
+ }
+
+ @Override
+ public Object get(Object key) {
+ return delegate.get(key);
+ }
+
+ private final class TrackingEntry implements Entry<String, Object> {
+ private final Entry<String, Object> delegateEntry;
+
+ private TrackingEntry(Entry<String, Object> delegateEntry) {
+ this.delegateEntry = delegateEntry;
+ }
+
+ @Override
+ public String getKey() {
+ return delegateEntry.getKey();
+ }
+
+ @Override
+ public Object getValue() {
+ return delegateEntry.getValue();
+ }
+
+ @Override
+ public Object setValue(Object value) {
+ Object previousValue = delegateEntry.setValue(value);
+ if (!Objects.equals(previousValue, value)) {
+ owner.markUpdatedOnActionContextMutation();
+ }
+ return previousValue;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return delegateEntry.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return delegateEntry.hashCode();
+ }
+ }
+ }
}
diff --git
a/integration-tx-api/src/test/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandlerTest.java
b/integration-tx-api/src/test/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandlerTest.java
index 55b796bd28..101165ddf1 100644
---
a/integration-tx-api/src/test/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandlerTest.java
+++
b/integration-tx-api/src/test/java/org/apache/seata/integration/tx/api/interceptor/ActionInterceptorHandlerTest.java
@@ -16,14 +16,32 @@
*/
package org.apache.seata.integration.tx.api.interceptor;
+import org.apache.seata.common.executor.Callback;
+import org.apache.seata.core.model.BranchStatus;
+import org.apache.seata.core.model.BranchType;
+import org.apache.seata.rm.DefaultResourceManager;
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.Assertions;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
/**
* The type Action interceptor handler test.
@@ -36,6 +54,11 @@ public class ActionInterceptorHandlerTest {
*/
protected ActionInterceptorHandler actionInterceptorHandler = new
ActionInterceptorHandler();
+ @AfterEach
+ public void tearDown() {
+ BusinessActionContextUtil.clear();
+ }
+
/**
* Test business action context.
*
@@ -57,4 +80,60 @@ public class ActionInterceptorHandlerTest {
Assertions.assertEquals("b", paramContext.get("b"));
Assertions.assertEquals("[email protected]", paramContext.get("email"));
}
+
+ @Test
+ public void testProceedTracksActionContextMutation() throws Throwable {
+ Method prepareMethod = TestAction.class.getDeclaredMethod(
+ "prepare", BusinessActionContext.class, int.class, List.class,
TestParam.class);
+ List<Object> list = new ArrayList<>();
+ list.add("b");
+ TestParam tccParam = new TestParam(1, "[email protected]");
+
+ TwoPhaseBusinessActionParam businessActionParam =
mock(TwoPhaseBusinessActionParam.class);
+
org.mockito.Mockito.doReturn("prepare").when(businessActionParam).getActionName();
+
org.mockito.Mockito.doReturn(BranchType.TCC).when(businessActionParam).getBranchType();
+
org.mockito.Mockito.doReturn(false).when(businessActionParam).getDelayReport();
+
org.mockito.Mockito.doReturn(false).when(businessActionParam).getUseCommonFence();
+ org.mockito.Mockito.doReturn(Collections.emptyMap())
+ .when(businessActionParam)
+ .getBusinessActionContext();
+
+ DefaultResourceManager resourceManager =
mock(DefaultResourceManager.class);
+ AtomicReference<BusinessActionContext> observedContext = new
AtomicReference<>();
+ ArgumentCaptor<String> applicationDataCaptor =
ArgumentCaptor.forClass(String.class);
+
+ try (MockedStatic<DefaultResourceManager> mocked =
mockStatic(DefaultResourceManager.class)) {
+
mocked.when(DefaultResourceManager::get).thenReturn(resourceManager);
+ when(resourceManager.branchRegister(
+ eq(BranchType.TCC), eq("prepare"), isNull(),
eq("test-xid"), anyString(), isNull()))
+ .thenReturn(1L);
+
+ Callback<Object> callback = () -> {
+ BusinessActionContext currentContext =
BusinessActionContextUtil.getContext();
+ Assertions.assertNotNull(currentContext);
+ Assertions.assertNull(currentContext.getUpdated());
+ currentContext.getActionContext().put("biz", "value");
+ Assertions.assertTrue(currentContext.getUpdated());
+ observedContext.set(currentContext);
+ return null;
+ };
+
+ Object result = actionInterceptorHandler.proceed(
+ prepareMethod, new Object[] {null, 10, list, tccParam},
"test-xid", businessActionParam, callback);
+
+ Assertions.assertNull(result);
+ }
+
+ Assertions.assertNotNull(observedContext.get());
+ Assertions.assertNull(observedContext.get().getUpdated());
+ verify(resourceManager)
+ .branchReport(
+ eq(BranchType.TCC),
+ eq("test-xid"),
+ eq(1L),
+ eq(BranchStatus.Registered),
+ applicationDataCaptor.capture());
+
Assertions.assertTrue(applicationDataCaptor.getValue().contains("biz"));
+
Assertions.assertTrue(applicationDataCaptor.getValue().contains("value"));
+ }
}
diff --git
a/integration-tx-api/src/test/java/org/apache/seata/rm/tcc/api/BusinessActionContextTest.java
b/integration-tx-api/src/test/java/org/apache/seata/rm/tcc/api/BusinessActionContextTest.java
index 7242540557..c2ea6deef0 100644
---
a/integration-tx-api/src/test/java/org/apache/seata/rm/tcc/api/BusinessActionContextTest.java
+++
b/integration-tx-api/src/test/java/org/apache/seata/rm/tcc/api/BusinessActionContextTest.java
@@ -19,7 +19,10 @@ package org.apache.seata.rm.tcc.api;
import org.apache.seata.core.model.BranchType;
import org.junit.jupiter.api.Test;
+import java.util.AbstractMap;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -72,6 +75,117 @@ public class BusinessActionContextTest {
context.getActionContext("payload", Payload.class).getName());
}
+ @Test
+ public void testTrackedActionContextMarksUpdatedOnMutationOperations() {
+ BusinessActionContext context = new BusinessActionContext("xid", "4",
new HashMap<>());
+ context.enableActionContextTracking();
+ context.setActionContext(new HashMap<>());
+
+ context.getActionContext().put("name", "seata");
+ assertTrue(context.getUpdated());
+
+ context.setUpdated(null);
+ context.getActionContext().putAll(Collections.singletonMap("status",
"prepared"));
+ assertTrue(context.getUpdated());
+
+ context.setUpdated(null);
+ context.getActionContext().remove("name");
+ assertTrue(context.getUpdated());
+
+ context.setUpdated(null);
+ context.getActionContext().put("name", "seata");
+ context.getActionContext().replace("name", "seata-updated");
+ assertTrue(context.getUpdated());
+
+ context.setUpdated(null);
+ context.getActionContext().computeIfAbsent("branch", key ->
"branch-1");
+ assertTrue(context.getUpdated());
+
+ context.setUpdated(null);
+ context.getActionContext().merge("branch", "branch-2", (oldValue,
newValue) -> newValue);
+ assertTrue(context.getUpdated());
+
+ context.setUpdated(null);
+ context.getActionContext().replaceAll((key, value) -> value);
+ assertNull(context.getUpdated());
+
+ context.getActionContext().clear();
+ assertTrue(context.getUpdated());
+ }
+
+ @Test
+ public void testTrackedActionContextMapViewOperations() {
+ BusinessActionContext context = new BusinessActionContext();
+ context.enableActionContextTracking();
+ context.enableActionContextTracking();
+
+ assertEquals(0, context.getActionContext().size());
+ assertFalse(context.getActionContext().containsKey("missing"));
+ assertFalse(context.getActionContext().containsValue("missing"));
+
+ context.getActionContext().putAll(Collections.emptyMap());
+ assertNull(context.getUpdated());
+
+ context.getActionContext().put("name", "seata");
+ context.setUpdated(null);
+ assertEquals("seata", context.getActionContext().put("name", "seata"));
+ assertNull(context.getUpdated());
+
+ assertEquals("seata", context.getActionContext().get("name"));
+ context.setUpdated(null);
+ assertNull(context.getActionContext().remove("missing"));
+ assertNull(context.getUpdated());
+
+ context.setActionContext(Collections.singletonMap("entry", "old"));
+ Map.Entry<String, Object> entry =
+ context.getActionContext().entrySet().iterator().next();
+ assertEquals("entry", entry.getKey());
+ assertEquals("old", entry.getValue());
+
+ assertEquals("old", entry.setValue("new"));
+ assertTrue(context.getUpdated());
+
+ context.setUpdated(null);
+ assertEquals("new", entry.setValue("new"));
+ assertNull(context.getUpdated());
+ assertEquals(1, context.getActionContext().entrySet().size());
+ assertEquals(entry,
context.getActionContext().entrySet().iterator().next());
+ assertEquals(
+ entry.hashCode(),
+
context.getActionContext().entrySet().iterator().next().hashCode());
+
+ context.setUpdated(null);
+ assertFalse(context.getActionContext().entrySet().remove("not-entry"));
+ assertNull(context.getUpdated());
+
+ assertFalse(context.getActionContext().entrySet().remove(new
AbstractMap.SimpleEntry<>("missing", "new")));
+ assertNull(context.getUpdated());
+
+ assertFalse(context.getActionContext().entrySet().remove(new
AbstractMap.SimpleEntry<>("entry", "wrong")));
+ assertNull(context.getUpdated());
+
+ assertTrue(context.getActionContext().entrySet().remove(new
AbstractMap.SimpleEntry<>("entry", "new")));
+ assertTrue(context.getUpdated());
+
+ context.setActionContext(Collections.singletonMap("remove", "value"));
+ context.setUpdated(null);
+ Iterator<Map.Entry<String, Object>> iterator =
+ context.getActionContext().entrySet().iterator();
+ assertTrue(iterator.hasNext());
+ iterator.next();
+ iterator.remove();
+ assertTrue(context.getUpdated());
+ assertEquals(0, context.getActionContext().size());
+ context.setUpdated(null);
+ context.getActionContext().clear();
+ assertNull(context.getUpdated());
+
+ context.setActionContext(null);
+ assertEquals(0, context.getActionContext().size());
+ context.getActionContext().put("created", "value");
+ assertTrue(context.getUpdated());
+ }
+
@Test
public void testToStringIncludesCoreFields() {
BusinessActionContext context = new BusinessActionContext("xid", "3",
new HashMap<>());
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]