This is an automated email from the ASF dual-hosted git repository. jianbin 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 b056604771 optimize: Add empty push protection for Configuration (#7576) b056604771 is described below commit b0566047717a485ba0b62e16d8724d50ffe77e6c Author: xiaoyu <93440108+yvce...@users.noreply.github.com> AuthorDate: Fri Aug 15 09:17:06 2025 +0800 optimize: Add empty push protection for Configuration (#7576) --- changes/en-us/2.x.md | 5 +- changes/zh-cn/2.x.md | 1 + .../seata/common/util/Http5ClientUtilTest.java | 5 +- .../seata/config/consul/ConsulConfiguration.java | 4 + .../config/consul/ConsulConfigurationTest.java | 27 ++++ .../seata/config/processor/ProcessorYamlTest.java | 67 +++++++++ .../seata/config/etcd3/EtcdConfiguration.java | 4 + .../seata/config/nacos/NacosConfiguration.java | 17 ++- .../seata/config/nacos/NacosConfigurationTest.java | 163 +++++++++++++++++++++ .../seata/config/zk/ZookeeperConfiguration.java | 17 ++- .../seata/config/zk/ZkConfigurationTest.java | 51 +++++++ 11 files changed, 343 insertions(+), 18 deletions(-) diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md index 4cfd3314b4..4033b5b6b0 100644 --- a/changes/en-us/2.x.md +++ b/changes/en-us/2.x.md @@ -40,8 +40,9 @@ Add changes here for all PR submitted to the 2.x branch. - [[#7478](https://github.com/apache/incubator-seata/pull/7484)] optimize: remove client id metric - [[#7557](https://github.com/seata/seata/pull/7557)] upgrade some npmjs dependencies -- [[#7577](https://github.com/seata/seata/pull/7577)] remove the 4MB size limit when decompressing with zstd -- [[#7578](https://github.com/seata/seata/pull/7578)] zstd decompression is changed from jni to ZstdInputStream +- [[#7576](https://github.com/seata/seata/pull/7576)] Add empty push protection for Configuration +- [[#7577](https://github.com/seata/seata/pull/7577)] remove the 4MB size limit when decompressing with zstd +- [[#7578](https://github.com/seata/seata/pull/7578)] zstd decompression is changed from jni to ZstdInputStream ### security: diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md index c6e1db12f6..9515ca0a4a 100644 --- a/changes/zh-cn/2.x.md +++ b/changes/zh-cn/2.x.md @@ -39,6 +39,7 @@ - [[#7478](https://github.com/apache/incubator-seata/pull/7484)] 删除client id指标 - [[#7557](https://github.com/seata/seata/pull/7557)] 升级 npmjs 依赖 +- [[#7576](https://github.com/seata/seata/pull/7576)] 针对配置变更增加空推保护 - [[#7577](https://github.com/seata/seata/pull/7577)] 去除zstd解压时4MB的限制 ### security: diff --git a/common/src/test/java/org/apache/seata/common/util/Http5ClientUtilTest.java b/common/src/test/java/org/apache/seata/common/util/Http5ClientUtilTest.java index 058ea439e0..9b550067e7 100644 --- a/common/src/test/java/org/apache/seata/common/util/Http5ClientUtilTest.java +++ b/common/src/test/java/org/apache/seata/common/util/Http5ClientUtilTest.java @@ -26,7 +26,10 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; class Http5ClientUtilTest { diff --git a/config/seata-config-consul/src/main/java/org/apache/seata/config/consul/ConsulConfiguration.java b/config/seata-config-consul/src/main/java/org/apache/seata/config/consul/ConsulConfiguration.java index 86ef1de6c2..f7d8d4be2e 100644 --- a/config/seata-config-consul/src/main/java/org/apache/seata/config/consul/ConsulConfiguration.java +++ b/config/seata-config-consul/src/main/java/org/apache/seata/config/consul/ConsulConfiguration.java @@ -351,6 +351,10 @@ public class ConsulConfiguration extends AbstractConfiguration { String value = response.getValue().getDecodedValue(); consulIndex = currentIndex; if (dataId.equals(getConsulConfigKey())) { + if (StringUtils.isBlank(value)) { + LOGGER.warn("Empty config from Consul, dataId='{}'. Skipped.", dataId); + continue; + } // The new config change listener Properties seataConfigNew; try { diff --git a/config/seata-config-consul/src/test/java/org/apache/seata/config/consul/ConsulConfigurationTest.java b/config/seata-config-consul/src/test/java/org/apache/seata/config/consul/ConsulConfigurationTest.java index 652a437f59..6dc8e84933 100644 --- a/config/seata-config-consul/src/test/java/org/apache/seata/config/consul/ConsulConfigurationTest.java +++ b/config/seata-config-consul/src/test/java/org/apache/seata/config/consul/ConsulConfigurationTest.java @@ -17,11 +17,13 @@ package org.apache.seata.config.consul; import com.ecwid.consul.v1.ConsulClient; +import com.ecwid.consul.v1.QueryParams; import com.ecwid.consul.v1.Response; import com.ecwid.consul.v1.kv.model.GetValue; import com.ecwid.consul.v1.kv.model.PutParams; import org.apache.seata.common.util.NetUtil; import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationChangeEvent; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -131,6 +133,31 @@ class ConsulConfigurationTest { assertEquals("val1", value, "KV should be visible after a short await"); } + @Test + void testOnChangeEvent_skipWhenValueIsBlank() throws InterruptedException { + + String dataId = "seata.properties"; + ConsulConfiguration.ConsulListener listener = new ConsulConfiguration.ConsulListener(dataId, null); + + GetValue blankValue = mock(GetValue.class); + when(blankValue.getDecodedValue()).thenReturn(""); + + Response<GetValue> blankResponse = new Response<>(blankValue, 2L, false, 2L); + when(mockConsulClient.getKVValue(eq(dataId), isNull(), any(QueryParams.class))) + .thenReturn(blankResponse); + + // Run onChangeEvent in a separate thread since it loops indefinitely + Thread thread = new Thread(() -> listener.onChangeEvent(new ConfigurationChangeEvent())); + thread.start(); + Thread.sleep(100); + // Interrupt to break the loop + thread.interrupt(); + thread.join(500); + + // Assert: Test passes as long as no exceptions are thrown + assertTrue(true); + } + // Utility method to set private fields via reflection private void setField(Object target, String fieldName, Object value) { try { diff --git a/config/seata-config-core/src/test/java/org/apache/seata/config/processor/ProcessorYamlTest.java b/config/seata-config-core/src/test/java/org/apache/seata/config/processor/ProcessorYamlTest.java new file mode 100644 index 0000000000..cb63334a18 --- /dev/null +++ b/config/seata-config-core/src/test/java/org/apache/seata/config/processor/ProcessorYamlTest.java @@ -0,0 +1,67 @@ +/* + * 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.config.processor; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ProcessorYamlTest { + + @Test + void testProcessor_NormalYaml() { + String yamlConfig = "server:\n" + " port: 8080\n" + + " host: localhost\n" + + "spring:\n" + + " datasource:\n" + + " url: jdbc:mysql://localhost:3306/test\n" + + " username: root"; + + ProcessorYaml processorYaml = new ProcessorYaml(); + Properties props = processorYaml.processor(yamlConfig); + + assertEquals(8080, props.get("server.port")); + assertEquals("", props.getProperty("server.port", "")); + assertEquals("localhost", props.getProperty("server.host")); + assertEquals("jdbc:mysql://localhost:3306/test", props.getProperty("spring.datasource.url")); + assertEquals("root", props.getProperty("spring.datasource.username")); + } + + @Test + void testProcessor_InvalidYaml_ShouldThrowException() { + + String invalidYaml = "server:\n" + " port: 8080\n" + "::host localhost"; + + ProcessorYaml processorYaml = new ProcessorYaml(); + + Assertions.assertThrows(Exception.class, () -> { + processorYaml.processor(invalidYaml); + }); + } + + @Test + void testProcessor_EmptyYaml() { + String emptyYaml = ""; + ProcessorYaml processorYaml = new ProcessorYaml(); + Properties props = processorYaml.processor(emptyYaml); + assertTrue(props.size() == 1); + } +} diff --git a/config/seata-config-etcd3/src/main/java/org/apache/seata/config/etcd3/EtcdConfiguration.java b/config/seata-config-etcd3/src/main/java/org/apache/seata/config/etcd3/EtcdConfiguration.java index d911084e06..b4a9631674 100644 --- a/config/seata-config-etcd3/src/main/java/org/apache/seata/config/etcd3/EtcdConfiguration.java +++ b/config/seata-config-etcd3/src/main/java/org/apache/seata/config/etcd3/EtcdConfiguration.java @@ -400,6 +400,10 @@ public class EtcdConfiguration extends AbstractConfiguration { .getValue() .getBytes(); Properties seataConfigNew; + if (bytes == null || bytes.length == 0) { + LOGGER.warn("config '{}' value is empty from watchResponse", dataId); + return; + } try { seataConfigNew = ConfigProcessor.processConfig( new String(bytes, StandardCharsets.UTF_8), getEtcdDataType()); diff --git a/config/seata-config-nacos/src/main/java/org/apache/seata/config/nacos/NacosConfiguration.java b/config/seata-config-nacos/src/main/java/org/apache/seata/config/nacos/NacosConfiguration.java index 82eaf348f8..81b76f429b 100644 --- a/config/seata-config-nacos/src/main/java/org/apache/seata/config/nacos/NacosConfiguration.java +++ b/config/seata-config-nacos/src/main/java/org/apache/seata/config/nacos/NacosConfiguration.java @@ -442,14 +442,17 @@ public class NacosConfiguration extends AbstractConfiguration implements Dispose public void innerReceive(String dataId, String group, String configInfo) { // The new configuration method to puts all configurations into a dateId if (getNacosDataId().equals(dataId)) { + if (StringUtils.isBlank(configInfo)) { + LOGGER.warn("Empty config from Nacos, dataId='{}'. Skipped.", dataId); + return; + } Properties seataConfigNew = new Properties(); - if (StringUtils.isNotBlank(configInfo)) { - try { - seataConfigNew = ConfigProcessor.processConfig(configInfo, getNacosDataType()); - } catch (IOException e) { - LOGGER.error("load config properties error", e); - return; - } + + try { + seataConfigNew = ConfigProcessor.processConfig(configInfo, getNacosDataType()); + } catch (IOException e) { + LOGGER.error("load config properties error", e); + return; } // Get all the monitored dataids and judge whether it has been modified diff --git a/config/seata-config-nacos/src/test/java/org/apache/seata/config/nacos/NacosConfigurationTest.java b/config/seata-config-nacos/src/test/java/org/apache/seata/config/nacos/NacosConfigurationTest.java index 7346ac5b49..470cce840d 100644 --- a/config/seata-config-nacos/src/test/java/org/apache/seata/config/nacos/NacosConfigurationTest.java +++ b/config/seata-config-nacos/src/test/java/org/apache/seata/config/nacos/NacosConfigurationTest.java @@ -19,14 +19,28 @@ package org.apache.seata.config.nacos; import com.alibaba.nacos.api.exception.NacosException; import org.apache.seata.common.util.ReflectionUtil; import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationChangeEvent; +import org.apache.seata.config.ConfigurationChangeListener; import org.apache.seata.config.ConfigurationFactory; import org.apache.seata.config.Dispose; +import org.apache.seata.config.processor.ConfigProcessor; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static org.mockito.ArgumentMatchers.anyString; /** * The type Nacos configuration test @@ -58,4 +72,153 @@ public class NacosConfigurationTest { Assertions.assertEquals("/foo", properties.getProperty("contextPath")); System.clearProperty("contextPath"); } + + @Test + public void testInnerReceiveEmptyPushShouldNotUpdateConfig() throws Exception { + + String dataId = "seata.properties"; + String group = "SEATA_GROUP"; + String configKey = "session.mode"; + + Properties oldConfig = new Properties(); + oldConfig.setProperty(configKey, "db"); + + Field seataConfigField = NacosConfiguration.class.getDeclaredField("seataConfig"); + seataConfigField.setAccessible(true); + seataConfigField.set(null, oldConfig); + + TestListener listener = new TestListener(); + NacosConfiguration.NacosListener nacosListener = getNacosListener(dataId, listener); + + ConcurrentMap<ConfigurationChangeListener, NacosConfiguration.NacosListener> innerMap = + new ConcurrentHashMap<>(); + innerMap.put(listener, nacosListener); + + ConcurrentMap<String, ConcurrentMap<ConfigurationChangeListener, NacosConfiguration.NacosListener>> outerMap = + new ConcurrentHashMap<>(); + outerMap.put(dataId, innerMap); + + Field listenerMapField = NacosConfiguration.class.getDeclaredField("CONFIG_LISTENERS_MAP"); + listenerMapField.setAccessible(true); + listenerMapField.set(null, outerMap); + + // execute + nacosListener.innerReceive(dataId, group, ""); + + Properties actualConfig = (Properties) seataConfigField.get(null); + Assertions.assertEquals("db", actualConfig.getProperty(configKey)); + + Assertions.assertFalse(listener.invoked); + } + + @Test + public void testInnerReceiveShouldReturn() throws Exception { + + String dataId = "seata.properties"; + String group = "SEATA_GROUP"; + String configKey = "session.mode"; + + Properties oldConfig = new Properties(); + oldConfig.setProperty(configKey, "db"); + + Field seataConfigField = NacosConfiguration.class.getDeclaredField("seataConfig"); + seataConfigField.setAccessible(true); + seataConfigField.set(null, oldConfig); + + TestListener listener = new TestListener(); + NacosConfiguration.NacosListener nacosListener = getNacosListener(dataId, listener); + + ConcurrentMap<ConfigurationChangeListener, NacosConfiguration.NacosListener> innerMap = + new ConcurrentHashMap<>(); + innerMap.put(listener, nacosListener); + + ConcurrentMap<String, ConcurrentMap<ConfigurationChangeListener, NacosConfiguration.NacosListener>> outerMap = + new ConcurrentHashMap<>(); + outerMap.put(dataId, innerMap); + + Field listenerMapField = NacosConfiguration.class.getDeclaredField("CONFIG_LISTENERS_MAP"); + listenerMapField.setAccessible(true); + listenerMapField.set(null, outerMap); + + // execute + nacosListener.innerReceive(dataId, group, "session.mode=redis"); + + Properties actualConfig = (Properties) seataConfigField.get(null); + Assertions.assertEquals("redis", actualConfig.getProperty(configKey)); + + Assertions.assertFalse(listener.invoked); + } + + @Test + public void testInnerReceiveThrowException() throws Exception { + + String dataId = "seata.properties"; + String group = "SEATA_GROUP"; + String configKey = "session.mode"; + + Properties oldConfig = new Properties(); + oldConfig.setProperty(configKey, "db"); + + Field seataConfigField = NacosConfiguration.class.getDeclaredField("seataConfig"); + seataConfigField.setAccessible(true); + seataConfigField.set(null, oldConfig); + + TestListener listener = new TestListener(); + NacosConfiguration.NacosListener nacosListener = getNacosListener(dataId, listener); + + ConcurrentMap<ConfigurationChangeListener, NacosConfiguration.NacosListener> innerMap = + new ConcurrentHashMap<>(); + innerMap.put(listener, nacosListener); + + ConcurrentMap<String, ConcurrentMap<ConfigurationChangeListener, NacosConfiguration.NacosListener>> outerMap = + new ConcurrentHashMap<>(); + outerMap.put(dataId, innerMap); + + Field listenerMapField = NacosConfiguration.class.getDeclaredField("CONFIG_LISTENERS_MAP"); + listenerMapField.setAccessible(true); + listenerMapField.set(null, outerMap); + + try (MockedStatic<ConfigProcessor> processorMockedStatic = Mockito.mockStatic(ConfigProcessor.class)) { + processorMockedStatic + .when(() -> ConfigProcessor.resolverConfigDataType(anyString())) + .thenReturn("yaml"); + processorMockedStatic + .when(() -> ConfigProcessor.processConfig(anyString(), anyString())) + .thenThrow(new IOException("mock io exception")); + // execute + nacosListener.innerReceive(dataId, group, "session.mode=redis"); + } + + Properties actualConfig = (Properties) seataConfigField.get(null); + Assertions.assertEquals("db", actualConfig.getProperty(configKey)); + + Assertions.assertFalse(listener.invoked); + } + + @NotNull + private static NacosConfiguration.NacosListener getNacosListener(String dataId, TestListener listener) + throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, + InvocationTargetException { + Class<?> outerClass = Class.forName("org.apache.seata.config.nacos.NacosConfiguration"); + Constructor<?> constructor = outerClass.getDeclaredConstructor(); + constructor.setAccessible(true); + Object nacosConfigurationInstance = constructor.newInstance(); + Class<?> innerClass = Class.forName("org.apache.seata.config.nacos.NacosConfiguration$NacosListener"); + + Constructor<?> innerConstructor = + innerClass.getDeclaredConstructor(outerClass, String.class, ConfigurationChangeListener.class); + innerConstructor.setAccessible(true); + NacosConfiguration.NacosListener nacosListener = (NacosConfiguration.NacosListener) + innerConstructor.newInstance(nacosConfigurationInstance, dataId, listener); + return nacosListener; + } + + private static class TestListener implements ConfigurationChangeListener { + boolean invoked = false; + + @Override + public void onChangeEvent(ConfigurationChangeEvent event) { + invoked = true; + } + } } diff --git a/config/seata-config-zk/src/main/java/org/apache/seata/config/zk/ZookeeperConfiguration.java b/config/seata-config-zk/src/main/java/org/apache/seata/config/zk/ZookeeperConfiguration.java index eb9dcf2c21..1bf720de63 100644 --- a/config/seata-config-zk/src/main/java/org/apache/seata/config/zk/ZookeeperConfiguration.java +++ b/config/seata-config-zk/src/main/java/org/apache/seata/config/zk/ZookeeperConfiguration.java @@ -394,15 +394,16 @@ public class ZookeeperConfiguration extends AbstractConfiguration { o = new String(data.getData()); } if (path.equals(getConfigPath())) { + if (StringUtils.isBlank(o.toString())) { + LOGGER.warn("Empty config from Zookeeper, path='{}'. Skipped.", path); + return; + } Properties seataConfigNew = new Properties(); - if (StringUtils.isNotBlank(o.toString())) { - try { - seataConfigNew = ConfigProcessor.processConfig(o.toString(), getZkDataType()); - - } catch (IOException e) { - LOGGER.error("load config properties error", e); - return; - } + try { + seataConfigNew = ConfigProcessor.processConfig(o.toString(), getZkDataType()); + } catch (IOException e) { + LOGGER.error("load config properties error", e); + return; } for (Map.Entry<String, ConcurrentMap<ConfigurationChangeListener, NodeCacheListenerImpl>> entry : diff --git a/config/seata-config-zk/src/test/java/org/apache/seata/config/zk/ZkConfigurationTest.java b/config/seata-config-zk/src/test/java/org/apache/seata/config/zk/ZkConfigurationTest.java index d34d863c51..f34b43f4e0 100644 --- a/config/seata-config-zk/src/test/java/org/apache/seata/config/zk/ZkConfigurationTest.java +++ b/config/seata-config-zk/src/test/java/org/apache/seata/config/zk/ZkConfigurationTest.java @@ -16,20 +16,32 @@ */ package org.apache.seata.config.zk; +import org.apache.curator.framework.recipes.cache.ChildData; +import org.apache.curator.framework.recipes.cache.CuratorCacheListener; import org.apache.curator.test.TestingServer; import org.apache.seata.config.ConfigurationChangeEvent; import org.apache.seata.config.ConfigurationChangeListener; import org.apache.seata.config.ConfigurationChangeType; +import org.apache.seata.config.processor.ConfigProcessor; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + /** * The type zk configuration test */ @@ -124,4 +136,43 @@ public class ZkConfigurationTest { } Assertions.assertTrue(listened[0]); } + + @Test + public void testEvent_pathEqualsConfigPath_blankValue() throws Exception { + Method getConfigPath = ZookeeperConfiguration.class.getDeclaredMethod("getConfigPath"); + getConfigPath.setAccessible(true); + + String configPath = getConfigPath.invoke(null).toString(); + + ZookeeperConfiguration.NodeCacheListenerImpl listener = + new ZookeeperConfiguration.NodeCacheListenerImpl(configPath, null); + + ChildData mockData = mock(ChildData.class); + when(mockData.getData()).thenReturn(new byte[0]); + + listener.event(CuratorCacheListener.Type.NODE_CHANGED, null, mockData); + + // If it can run to this point, it indicates that the null value branch has been overwritten + } + + @Test + public void testEvent_pathEqualsConfigPath_throwException() throws Exception { + Method getConfigPathMethod = ZookeeperConfiguration.class.getDeclaredMethod("getConfigPath"); + getConfigPathMethod.setAccessible(true); + String configPath = getConfigPathMethod.invoke(null).toString(); + ZookeeperConfiguration.NodeCacheListenerImpl listener = + new ZookeeperConfiguration.NodeCacheListenerImpl(configPath, null); + String invalidYaml = "server:\n" + " port: 8080\n" + "::host localhost"; + ChildData mockData = mock(ChildData.class); + when(mockData.getData()).thenReturn(invalidYaml.getBytes(StandardCharsets.UTF_8)); + try (MockedStatic<ConfigProcessor> processorMockedStatic = Mockito.mockStatic(ConfigProcessor.class)) { + processorMockedStatic + .when(() -> ConfigProcessor.resolverConfigDataType(anyString())) + .thenReturn("yaml"); + processorMockedStatic + .when(() -> ConfigProcessor.processConfig(anyString(), anyString())) + .thenThrow(new IOException("mock io exception")); + listener.event(CuratorCacheListener.Type.NODE_CHANGED, null, mockData); + } + } } --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@seata.apache.org For additional commands, e-mail: notifications-h...@seata.apache.org