diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md index 5885951f898..0012135fb05 100644 --- a/changes/en-us/2.x.md +++ b/changes/en-us/2.x.md @@ -20,7 +20,7 @@ Add changes here for all PR submitted to the 2.x branch. ### feature: -- [[#PR_NO](https://github.com/seata/seata/pull/PR_NO)] support XXX +- [[#7533](https://github.com/apache/incubator-seata/pull/7533)] support metadata-based registration and discovery, support namingserver type ### bugfix: diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md index 96c30b0c55a..56d6a50170c 100644 --- a/changes/zh-cn/2.x.md +++ b/changes/zh-cn/2.x.md @@ -19,8 +19,7 @@ ### feature: - -- [[#PR_NO](https://github.com/seata/seata/pull/PR_NO)] 支持 XXX +- [[#7533](https://github.com/apache/incubator-seata/pull/7533)] 支持元数据注册与发现能力并支持namingserver类型 ### bugfix: diff --git a/common/src/main/java/org/apache/seata/common/metadata/ServiceInstance.java b/common/src/main/java/org/apache/seata/common/metadata/ServiceInstance.java new file mode 100644 index 00000000000..df50daf0b1d --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/metadata/ServiceInstance.java @@ -0,0 +1,135 @@ +/* + * 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.common.metadata; + +import org.apache.seata.common.util.NetUtil; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * entity for packaging inetSocketAddress and metadata for loadBalance + */ +public class ServiceInstance { + private InetSocketAddress address; + private Map metadata; + + public ServiceInstance(InetSocketAddress address, Map metadata) { + this.address = address; + this.metadata = metadata; + } + + public ServiceInstance(Instance instance) { + this.address = new InetSocketAddress( + instance.getTransaction().getHost(), instance.getTransaction().getPort()); + this.metadata = instance.getMetadata(); + } + + public ServiceInstance(InetSocketAddress address) { + this.address = address; + } + + public InetSocketAddress getAddress() { + return address; + } + + public void setAddress(InetSocketAddress address) { + this.address = address; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + /** + * Converts a list of InetSocketAddress to a list of ServiceInstance. + * @param addresses list of InetSocketAddress + * @return list of ServiceInstance + */ + public static List convertToServiceInstanceList(List addresses) { + List serviceInstances = new ArrayList<>(); + if (addresses != null && !addresses.isEmpty()) { + for (InetSocketAddress address : addresses) { + NetUtil.validAddress(address); + serviceInstances.add(new ServiceInstance(address, null)); + } + } + return serviceInstances; + } + + /** + * Converts a set of InetSocketAddress to a set of ServiceInstance in RedisRegistryServiceImpl. + * @param addresses set of InetSocketAddress + * @return set of ServiceInstance + */ + public static Set convertToServiceInstanceSet(Set addresses) { + Set serviceInstances = new HashSet<>(); + if (addresses != null && !addresses.isEmpty()) { + for (InetSocketAddress address : addresses) { + NetUtil.validAddress(address); + serviceInstances.add(new ServiceInstance(address, null)); + } + } + return serviceInstances; + } + + /** + * Creates a ServiceInstance from an InetSocketAddress and a Map of metadata. + * @param address the InetSocketAddress + * @param stringMap the map of string metadata + * @return a new ServiceInstance + */ + public static ServiceInstance fromStringMap(InetSocketAddress address, Map stringMap) { + Map metadata = new HashMap<>(); + if (stringMap != null) { + metadata.putAll(stringMap); + } + return new ServiceInstance(address, metadata); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServiceInstance that = (ServiceInstance) o; + return Objects.equals(address, that.address) && Objects.equals(metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(address, metadata); + } + + @Override + public String toString() { + return "ServiceInstance{" + "address=" + address + ", metadata=" + metadata + '}'; + } +} diff --git a/common/src/test/java/org/apache/seata/common/metadata/ServiceInstanceTest.java b/common/src/test/java/org/apache/seata/common/metadata/ServiceInstanceTest.java new file mode 100644 index 00000000000..9aa055267e5 --- /dev/null +++ b/common/src/test/java/org/apache/seata/common/metadata/ServiceInstanceTest.java @@ -0,0 +1,123 @@ +/* + * 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.common.metadata; + +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ServiceInstanceTest { + + private final InetSocketAddress address1 = new InetSocketAddress("127.0.0.1", 8091); + private final InetSocketAddress address2 = new InetSocketAddress("127.0.0.1", 8092); + + @Test + public void testConstructorAndGetters() { + Map metadata = new HashMap<>(); + metadata.put("key1", "value1"); + + ServiceInstance instance1 = new ServiceInstance(address1, metadata); + + assertEquals(address1, instance1.getAddress()); + assertEquals(metadata, instance1.getMetadata()); + + Instance instance = Instance.getInstance(); + instance.setTransaction(new Node.Endpoint("127.0.0.1", 8093)); + + ServiceInstance instance2 = new ServiceInstance(Instance.getInstance()); + + assertEquals( + Instance.getInstance().getTransaction().getHost(), + instance2.getAddress().getAddress().getHostAddress()); + assertEquals( + Instance.getInstance().getTransaction().getPort(), + instance2.getAddress().getPort()); + + instance.setTransaction(null); // clean up after test + } + + @Test + public void testConvertToServiceInstanceList() { + List addresses = new ArrayList<>(); + addresses.add(address1); + addresses.add(address2); + + List serviceInstances = ServiceInstance.convertToServiceInstanceList(addresses); + + assertEquals(2, serviceInstances.size()); + assertEquals(address1, serviceInstances.get(0).getAddress()); + assertEquals(address2, serviceInstances.get(1).getAddress()); + } + + @Test + public void testConvertToServiceInstanceSet() { + Set addresses = new HashSet<>(); + addresses.add(address1); + addresses.add(address2); + + Set serviceInstances = ServiceInstance.convertToServiceInstanceSet(addresses); + + assertEquals(2, serviceInstances.size()); + } + + @Test + public void testSetAddressAndSetMetadata() { + ServiceInstance instance = new ServiceInstance(address1); + instance.setAddress(address2); + instance.setMetadata(new HashMap<>()); + + assertEquals(address2, instance.getAddress()); + assertNotNull(instance.getMetadata()); + } + + @Test + public void testFromStringMap() { + Map stringMap = new HashMap<>(); + stringMap.put("stringKey", "stringValue"); + + ServiceInstance instance = ServiceInstance.fromStringMap(address1, stringMap); + + assertEquals(address1, instance.getAddress()); + assertNotNull(instance.getMetadata()); + assertEquals("stringValue", instance.getMetadata().get("stringKey")); + } + + @Test + public void testEqualsAndToString() { + ServiceInstance instance1 = new ServiceInstance(address1); + ServiceInstance instance2 = new ServiceInstance(address1); + ServiceInstance instance3 = new ServiceInstance(address2); + + assertTrue(instance1.equals(instance2)); + assertTrue(instance1.equals(instance1)); + assertFalse(instance1.equals(instance3)); + assertFalse(instance1.equals("string")); + + assertTrue(instance1.toString().contains("8091")); + } +} diff --git a/core/src/main/java/org/apache/seata/core/rpc/netty/AbstractNettyRemotingClient.java b/core/src/main/java/org/apache/seata/core/rpc/netty/AbstractNettyRemotingClient.java index 28ff56baef5..22f3759092d 100644 --- a/core/src/main/java/org/apache/seata/core/rpc/netty/AbstractNettyRemotingClient.java +++ b/core/src/main/java/org/apache/seata/core/rpc/netty/AbstractNettyRemotingClient.java @@ -27,6 +27,7 @@ import io.netty.handler.timeout.IdleStateEvent; import org.apache.seata.common.exception.FrameworkErrorCode; import org.apache.seata.common.exception.FrameworkException; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.thread.NamedThreadFactory; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.NetUtil; @@ -48,6 +49,7 @@ import org.apache.seata.core.rpc.processor.RemotingProcessor; import org.apache.seata.discovery.loadbalance.LoadBalanceFactory; import org.apache.seata.discovery.registry.RegistryFactory; +import org.apache.seata.discovery.routing.RoutingManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -286,10 +288,13 @@ public NettyClientChannelManager getClientChannelManager() { protected String loadBalance(String transactionServiceGroup, Object msg) { InetSocketAddress address = null; try { - @SuppressWarnings("unchecked") - List inetSocketAddressList = + List serviceInstances = RegistryFactory.getInstance().aliveLookup(transactionServiceGroup); - address = this.doSelect(inetSocketAddressList, msg); + + // Apply routing filter + serviceInstances = applyRoutingFilter(serviceInstances); + + address = this.doSelect(serviceInstances, msg); } catch (Exception ex) { LOGGER.error("Select the address failed: {}", ex.getMessage()); } @@ -299,12 +304,37 @@ protected String loadBalance(String transactionServiceGroup, Object msg) { return NetUtil.toStringAddress(address); } - protected InetSocketAddress doSelect(List list, Object msg) throws Exception { + /** + * Apply routing filter + * + * @param serviceInstances original service instances list + * @return filtered service instances list + */ + private List applyRoutingFilter(List serviceInstances) { + try { + if (serviceInstances == null || serviceInstances.isEmpty()) { + return serviceInstances; + } + + // Get routing manager + RoutingManager routingManager = RoutingManager.getInstance(); + + // Execute routing filter + return routingManager.filter(serviceInstances); + } catch (Exception e) { + LOGGER.warn("Routing filter failed, using original service instances", e); + return serviceInstances; + } + } + + protected InetSocketAddress doSelect(List list, Object msg) throws Exception { if (CollectionUtils.isNotEmpty(list)) { if (list.size() > 1) { - return LoadBalanceFactory.getInstance().select(list, getXid(msg)); + return LoadBalanceFactory.getInstance() + .select(list, getXid(msg)) + .getAddress(); } else { - return list.get(0); + return list.get(0).getAddress(); } } return null; diff --git a/core/src/main/java/org/apache/seata/core/rpc/netty/NettyClientChannelManager.java b/core/src/main/java/org/apache/seata/core/rpc/netty/NettyClientChannelManager.java index 02cc6f8c3af..1a6733e6fb0 100644 --- a/core/src/main/java/org/apache/seata/core/rpc/netty/NettyClientChannelManager.java +++ b/core/src/main/java/org/apache/seata/core/rpc/netty/NettyClientChannelManager.java @@ -21,6 +21,7 @@ import org.apache.seata.common.ConfigurationKeys; import org.apache.seata.common.exception.FrameworkErrorCode; import org.apache.seata.common.exception.FrameworkException; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; @@ -31,7 +32,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -189,7 +189,7 @@ void initReconnect(String transactionServiceGroup, boolean failFast) { * @param failFast */ void doReconnect(String transactionServiceGroup, boolean failFast) { - List availList; + List availList; try { availList = getAvailServerList(transactionServiceGroup); } catch (Exception e) { @@ -234,16 +234,16 @@ void doReconnect(String transactionServiceGroup, boolean failFast) { * @param availList avail list * @param transactionServiceGroup transaction service group */ - void doReconnect(List availList, String transactionServiceGroup) { - Set channelAddress = new HashSet<>(availList.size()); - Map failedMap = new HashMap<>(); + void doReconnect(List availList, String transactionServiceGroup) { + Set channelAddress = new HashSet<>(availList.size()); + Map failedMap = new HashMap<>(); try { - for (String serverAddress : availList) { + for (ServiceInstance serviceInstance : availList) { try { - acquireChannel(serverAddress); - channelAddress.add(serverAddress); + acquireChannel(NetUtil.toStringAddress(serviceInstance.getAddress())); + channelAddress.add(serviceInstance); } catch (Exception e) { - failedMap.put(serverAddress, e); + failedMap.put(serviceInstance, e); } } if (failedMap.size() > 0) { @@ -272,12 +272,8 @@ void doReconnect(List availList, String transactionServiceGroup) { } } finally { if (CollectionUtils.isNotEmpty(channelAddress)) { - List aliveAddress = new ArrayList<>(channelAddress.size()); - for (String address : channelAddress) { - String[] array = NetUtil.splitIPPortStr(address); - aliveAddress.add(new InetSocketAddress(array[0], Integer.parseInt(array[1]))); - } - RegistryFactory.getInstance().refreshAliveLookup(transactionServiceGroup, aliveAddress); + RegistryFactory.getInstance() + .refreshAliveLookup(transactionServiceGroup, new ArrayList<>(channelAddress)); } else { RegistryFactory.getInstance().refreshAliveLookup(transactionServiceGroup, Collections.emptyList()); } @@ -315,14 +311,15 @@ private Channel doConnect(String serverAddress) { return channelFromPool; } - private List getAvailServerList(String transactionServiceGroup) throws Exception { - List availInetSocketAddressList = + private List getAvailServerList(String transactionServiceGroup) throws Exception { + @SuppressWarnings("unchecked") + List availServiceInstanceList = RegistryFactory.getInstance().lookup(transactionServiceGroup); - if (CollectionUtils.isEmpty(availInetSocketAddressList)) { + if (CollectionUtils.isEmpty(availServiceInstanceList)) { return Collections.emptyList(); } - return availInetSocketAddressList.stream().map(NetUtil::toStringAddress).collect(Collectors.toList()); + return availServiceInstanceList; } private Channel getExistAliveChannel(Channel rmChannel, String serverAddress) { diff --git a/core/src/main/java/org/apache/seata/core/rpc/netty/NettyServerBootstrap.java b/core/src/main/java/org/apache/seata/core/rpc/netty/NettyServerBootstrap.java index f6e99ffef2f..53a3daa172b 100644 --- a/core/src/main/java/org/apache/seata/core/rpc/netty/NettyServerBootstrap.java +++ b/core/src/main/java/org/apache/seata/core/rpc/netty/NettyServerBootstrap.java @@ -31,6 +31,7 @@ import org.apache.seata.common.XID; import org.apache.seata.common.metadata.Instance; import org.apache.seata.common.metadata.Node; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.thread.NamedThreadFactory; import org.apache.seata.config.ConfigurationFactory; import org.apache.seata.core.protocol.detector.Http2Detector; @@ -197,10 +198,13 @@ public void initChannel(SocketChannel ch) { Instance instance = Instance.getInstance(); // Lines 177-180 are just for compatibility with test cases if (instance.getTransaction() == null) { - Instance.getInstance().setTransaction(new Node.Endpoint(XID.getIpAddress(), XID.getPort(), "netty")); + instance.setTransaction(new Node.Endpoint(XID.getIpAddress(), XID.getPort(), "netty")); } + InetSocketAddress inetSocketAddress = new InetSocketAddress( + instance.getTransaction().getHost(), + instance.getTransaction().getPort()); for (RegistryService registryService : MultiRegistryFactory.getInstances()) { - registryService.register(Instance.getInstance()); + registryService.register(new ServiceInstance(inetSocketAddress, instance.getMetadata())); } initialized.set(true); } catch (SocketException se) { @@ -217,8 +221,12 @@ public void shutdown() { LOGGER.info("Shutting server down, the listen port: {}", XID.getPort()); } if (initialized.get()) { + Instance instance = Instance.getInstance(); + InetSocketAddress inetSocketAddress = new InetSocketAddress( + instance.getTransaction().getHost(), + instance.getTransaction().getPort()); for (RegistryService registryService : MultiRegistryFactory.getInstances()) { - registryService.unregister(Instance.getInstance()); + registryService.unregister(new ServiceInstance(inetSocketAddress, instance.getMetadata())); registryService.close(); } // wait a few seconds for server transport diff --git a/core/src/test/java/org/apache/seata/core/rpc/netty/AbstractNettyRemotingClientTest.java b/core/src/test/java/org/apache/seata/core/rpc/netty/AbstractNettyRemotingClientTest.java new file mode 100644 index 00000000000..e8c837fabde --- /dev/null +++ b/core/src/test/java/org/apache/seata/core/rpc/netty/AbstractNettyRemotingClientTest.java @@ -0,0 +1,501 @@ +/* + * 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.core.rpc.netty; + +import io.netty.channel.Channel; +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.core.protocol.AbstractMessage; +import org.apache.seata.core.protocol.transaction.BranchRegisterRequest; +import org.apache.seata.discovery.registry.RegistryFactory; +import org.apache.seata.discovery.registry.RegistryService; +import org.apache.seata.discovery.routing.RoutingManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadPoolExecutor; + +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.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * Test for AbstractNettyRemotingClient routing filter functionality + */ +@ExtendWith(MockitoExtension.class) +public class AbstractNettyRemotingClientTest { + + @Mock + private RegistryService registryService; + + @Mock + private RoutingManager routingManager; + + private TestNettyRemotingClient client; + + /** + * Concrete implementation of AbstractNettyRemotingClient for testing + */ + private static class TestNettyRemotingClient extends AbstractNettyRemotingClient { + + public TestNettyRemotingClient() { + super(mock(NettyClientConfig.class), mock(ThreadPoolExecutor.class), NettyPoolKey.TransactionRole.TMROLE); + } + + @Override + protected java.util.function.Function getPoolKeyFunction() { + return key -> new NettyPoolKey(NettyPoolKey.TransactionRole.TMROLE, key); + } + + @Override + protected String getTransactionServiceGroup() { + return "test-group"; + } + + @Override + protected boolean isEnableClientBatchSendRequest() { + return false; + } + + @Override + protected long getRpcRequestTimeout() { + return 30000L; + } + + // Expose loadBalance method for testing + public String testLoadBalance(String transactionServiceGroup, Object msg) { + return loadBalance(transactionServiceGroup, msg); + } + + @Override + public void onRegisterMsgSuccess( + String serverAddress, Channel channel, Object response, AbstractMessage requestMessage) {} + + @Override + public void onRegisterMsgFail( + String serverAddress, Channel channel, Object response, AbstractMessage requestMessage) {} + } + + @BeforeEach + public void setUp() { + client = new TestNettyRemotingClient(); + } + + @AfterEach + public void tearDown() { + // Clean up system properties + System.clearProperty("clientLat"); + System.clearProperty("clientLng"); + System.clearProperty("client.routing.enabled"); + System.clearProperty("client.routing.region-router.enabled"); + System.clearProperty("client.routing.region-router.topN"); + System.clearProperty("client.routing.metadata-routers.metadata-router-1.enabled"); + System.clearProperty("client.routing.metadata-routers.metadata-router-1.expression"); + System.clearProperty("client.routing.primary-backup.enabled"); + System.clearProperty("client.routing.primary-backup.order"); + } + + /** + * Test routing filter with region-based routing + * Test scenario: Beijing client should route to Beijing servers + */ + @Test + public void testRoutingFilterWithRegionRouting() { + // Configure routing settings + System.setProperty("client.routing.enabled", "true"); + System.setProperty("client.routing.region-router.enabled", "true"); + System.setProperty("client.routing.region-router.topN", "3"); + + // Set client location to Beijing + System.setProperty("clientLat", "39.9042"); + System.setProperty("clientLng", "116.4074"); + + // Create service instances with location metadata + List serviceInstances = createServiceInstances( + new ServerConfig("127.0.0.1", 8091, new HashMap() { + { + put("lat", "39.9042"); + put("lng", "116.4074"); + put("region", "beijing"); + } + }), + new ServerConfig("127.0.0.2", 8092, new HashMap() { + { + put("lat", "31.2304"); + put("lng", "121.4737"); + put("region", "shanghai"); + } + })); + + try (MockedStatic mockedRegistryFactory = mockStatic(RegistryFactory.class); + MockedStatic mockedRoutingManager = mockStatic(RoutingManager.class)) { + + // Mock RegistryFactory + mockedRegistryFactory.when(RegistryFactory::getInstance).thenReturn(registryService); + when(registryService.aliveLookup("test-group")).thenReturn(serviceInstances); + + // Mock RoutingManager + mockedRoutingManager.when(RoutingManager::getInstance).thenReturn(routingManager); + when(routingManager.filter(serviceInstances)) + .thenReturn(serviceInstances.subList(0, 1)); // Return only Beijing server + + // Test with BranchRegisterRequest + BranchRegisterRequest request = new BranchRegisterRequest(); + request.setXid("test-xid-123"); + + String result = client.testLoadBalance("test-group", request); + + assertNotNull(result); + // Beijing client should route to Beijing server (127.0.0.1) + assertEquals("127.0.0.1:8091", result); + } + } + + /** + * Test routing filter with metadata-based routing + * Test scenario: Production environment should route to production servers + */ + @Test + public void testRoutingFilterWithMetadataRouting() { + // Configure routing settings + System.setProperty("client.routing.enabled", "true"); + System.setProperty("client.routing.metadata-routers.metadata-router-1.enabled", "true"); + System.setProperty("client.routing.metadata-routers.metadata-router-1.expression", "env == 'prod'"); + + // Create service instances with metadata + List serviceInstances = createServiceInstances( + new ServerConfig("127.0.0.1", 8091, new HashMap() { + { + put("env", "prod"); + put("version", "1.0.0"); + put("zone", "zone-a"); + } + }), + new ServerConfig("127.0.0.2", 8092, new HashMap() { + { + put("env", "staging"); + put("version", "1.0.0"); + put("zone", "zone-b"); + } + }), + new ServerConfig("127.0.0.3", 8093, new HashMap() { + { + put("env", "dev"); + put("version", "1.0.0"); + put("zone", "zone-c"); + } + })); + + try (MockedStatic mockedRegistryFactory = mockStatic(RegistryFactory.class); + MockedStatic mockedRoutingManager = mockStatic(RoutingManager.class)) { + + // Mock RegistryFactory + mockedRegistryFactory.when(RegistryFactory::getInstance).thenReturn(registryService); + when(registryService.aliveLookup("test-group")).thenReturn(serviceInstances); + + // Mock RoutingManager + mockedRoutingManager.when(RoutingManager::getInstance).thenReturn(routingManager); + when(routingManager.filter(serviceInstances)) + .thenReturn(serviceInstances.subList(0, 1)); // Return only production server + + // Test with BranchRegisterRequest + BranchRegisterRequest request = new BranchRegisterRequest(); + request.setXid("test-xid-456"); + + String result = client.testLoadBalance("test-group", request); + + assertNotNull(result); + // Should route to production server (127.0.0.1) + assertEquals("127.0.0.1:8091", result); + } + } + + /** + * Test routing filter with primary-backup routing + * Test scenario: Should prefer primary servers over backup servers + */ + @Test + public void testRoutingFilterWithPrimaryBackupRouting() { + // Configure routing settings + System.setProperty("client.routing.enabled", "true"); + System.setProperty("client.routing.primary-backup.enabled", "true"); + System.setProperty("client.routing.primary-backup.order", "primary,backup"); + + // Create service instances with primary/backup roles + List serviceInstances = createServiceInstances( + new ServerConfig("127.0.0.1", 8091, new HashMap() { + { + put("role", "primary"); + put("priority", "1"); + } + }), + new ServerConfig("127.0.0.2", 8092, new HashMap() { + { + put("role", "backup"); + put("priority", "2"); + } + })); + + try (MockedStatic mockedRegistryFactory = mockStatic(RegistryFactory.class); + MockedStatic mockedRoutingManager = mockStatic(RoutingManager.class)) { + + // Mock RegistryFactory + mockedRegistryFactory.when(RegistryFactory::getInstance).thenReturn(registryService); + when(registryService.aliveLookup("test-group")).thenReturn(serviceInstances); + + // Mock RoutingManager + mockedRoutingManager.when(RoutingManager::getInstance).thenReturn(routingManager); + when(routingManager.filter(serviceInstances)) + .thenReturn(serviceInstances.subList(0, 1)); // Return only primary server + + // Test with BranchRegisterRequest + BranchRegisterRequest request = new BranchRegisterRequest(); + request.setXid("test-xid-789"); + + String result = client.testLoadBalance("test-group", request); + + assertNotNull(result); + // Should prefer primary server (127.0.0.1) + assertEquals("127.0.0.1:8091", result); + } + } + + /** + * Test routing filter with no routing configuration + * Test scenario: No routing rules configured, should return original instances + */ + @Test + public void testRoutingFilterWithNoConfiguration() { + // Disable routing + System.setProperty("client.routing.enabled", "false"); + + // Create basic service instances without special metadata + List serviceInstances = createServiceInstances( + new ServerConfig("127.0.0.1", 8091, new HashMap<>()), + new ServerConfig("127.0.0.2", 8092, new HashMap<>())); + + try (MockedStatic mockedRegistryFactory = mockStatic(RegistryFactory.class); + MockedStatic mockedRoutingManager = mockStatic(RoutingManager.class)) { + + // Mock RegistryFactory + mockedRegistryFactory.when(RegistryFactory::getInstance).thenReturn(registryService); + when(registryService.aliveLookup("test-group")).thenReturn(serviceInstances); + + // Mock RoutingManager - should return original list when routing is disabled + mockedRoutingManager.when(RoutingManager::getInstance).thenReturn(routingManager); + when(routingManager.filter(serviceInstances)).thenReturn(serviceInstances); + + // Test with BranchRegisterRequest + BranchRegisterRequest request = new BranchRegisterRequest(); + request.setXid("test-xid-no-config"); + + String result = client.testLoadBalance("test-group", request); + + assertNotNull(result); + // Should return one of the original instances + assertTrue(result.equals("127.0.0.1:8091") || result.equals("127.0.0.2:8092")); + } + } + + /** + * Test routing filter with multiple servers and region routing + * Test scenario: Shanghai client should route to Shanghai servers + */ + @Test + public void testRoutingFilterWithMultipleServers() { + // Configure routing settings + System.setProperty("client.routing.enabled", "true"); + System.setProperty("client.routing.region-router.enabled", "true"); + System.setProperty("client.routing.region-router.topN", "2"); + + // Set client location to Shanghai + System.setProperty("clientLat", "31.2304"); + System.setProperty("clientLng", "121.4737"); + + // Create multiple service instances with location metadata + List serviceInstances = createServiceInstances( + new ServerConfig("127.0.0.1", 8091, new HashMap() { + { + put("lat", "39.9042"); + put("lng", "116.4074"); + put("region", "beijing"); + } + }), + new ServerConfig("127.0.0.2", 8092, new HashMap() { + { + put("lat", "31.2304"); + put("lng", "121.4737"); + put("region", "shanghai"); + } + }), + new ServerConfig("127.0.0.3", 8093, new HashMap() { + { + put("lat", "23.1291"); + put("lng", "113.2644"); + put("region", "guangzhou"); + } + }), + new ServerConfig("127.0.0.4", 8094, new HashMap() { + { + put("lat", "22.3193"); + put("lng", "114.1694"); + put("region", "shenzhen"); + } + })); + + try (MockedStatic mockedRegistryFactory = mockStatic(RegistryFactory.class); + MockedStatic mockedRoutingManager = mockStatic(RoutingManager.class)) { + + // Mock RegistryFactory + mockedRegistryFactory.when(RegistryFactory::getInstance).thenReturn(registryService); + when(registryService.aliveLookup("test-group")).thenReturn(serviceInstances); + + // Mock RoutingManager - return Shanghai server + mockedRoutingManager.when(RoutingManager::getInstance).thenReturn(routingManager); + when(routingManager.filter(serviceInstances)) + .thenReturn(serviceInstances.subList(1, 2)); // Return Shanghai server + + // Test with BranchRegisterRequest + BranchRegisterRequest request = new BranchRegisterRequest(); + request.setXid("test-xid-multiple"); + + String result = client.testLoadBalance("test-group", request); + + assertNotNull(result); + // Shanghai client should route to Shanghai server (127.0.0.2) + assertEquals("127.0.0.2:8092", result); + } + } + + /** + * Test routing filter with complex metadata routing + * Test scenario: Multiple metadata conditions + */ + @Test + public void testRoutingFilterWithComplexMetadataRouting() { + // Configure routing settings + System.setProperty("client.routing.enabled", "true"); + System.setProperty("client.routing.metadata-routers.metadata-router-1.enabled", "true"); + System.setProperty( + "client.routing.metadata-routers.metadata-router-1.expression", "env == 'prod' && zone == 'zone-a'"); + + // Create service instances with complex metadata + List serviceInstances = createServiceInstances( + new ServerConfig("127.0.0.1", 8091, new HashMap() { + { + put("env", "prod"); + put("version", "1.0.0"); + put("zone", "zone-a"); + } + }), + new ServerConfig("127.0.0.2", 8092, new HashMap() { + { + put("env", "prod"); + put("version", "1.0.0"); + put("zone", "zone-b"); + } + }), + new ServerConfig("127.0.0.3", 8093, new HashMap() { + { + put("env", "staging"); + put("version", "1.0.0"); + put("zone", "zone-a"); + } + })); + + try (MockedStatic mockedRegistryFactory = mockStatic(RegistryFactory.class); + MockedStatic mockedRoutingManager = mockStatic(RoutingManager.class)) { + + // Mock RegistryFactory + mockedRegistryFactory.when(RegistryFactory::getInstance).thenReturn(registryService); + when(registryService.aliveLookup("test-group")).thenReturn(serviceInstances); + + // Mock RoutingManager - return production zone-a server + mockedRoutingManager.when(RoutingManager::getInstance).thenReturn(routingManager); + when(routingManager.filter(serviceInstances)) + .thenReturn(serviceInstances.subList(0, 1)); // Return production zone-a server + + // Test with BranchRegisterRequest + BranchRegisterRequest request = new BranchRegisterRequest(); + request.setXid("test-xid-complex"); + + String result = client.testLoadBalance("test-group", request); + + assertNotNull(result); + // Should route to production zone-a server (127.0.0.1) + assertEquals("127.0.0.1:8091", result); + } + } + + /** + * Server configuration for creating service instances + */ + private static class ServerConfig { + private final String host; + private final int port; + private final Map metadata; + + public ServerConfig(String host, int port, Map metadata) { + this.host = host; + this.port = port; + this.metadata = metadata; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public Map getMetadata() { + return metadata; + } + } + + /** + * Create service instances with given configurations + * @param configs server configurations + * @return list of service instances + */ + private List createServiceInstances(ServerConfig... configs) { + List serviceInstances = new ArrayList<>(); + + for (ServerConfig config : configs) { + ServiceInstance instance = mock(ServiceInstance.class); + lenient().when(instance.getAddress()).thenReturn(new InetSocketAddress(config.getHost(), config.getPort())); + lenient().when(instance.getMetadata()).thenReturn(config.getMetadata()); + serviceInstances.add(instance); + } + + return serviceInstances; + } +} diff --git a/discovery/seata-discovery-consul/src/main/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImpl.java b/discovery/seata-discovery-consul/src/main/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImpl.java index d7a8774f35b..afc3e780b98 100644 --- a/discovery/seata-discovery-consul/src/main/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImpl.java +++ b/discovery/seata-discovery-consul/src/main/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImpl.java @@ -22,6 +22,7 @@ import com.ecwid.consul.v1.agent.model.NewService; import com.ecwid.consul.v1.health.HealthServicesRequest; import com.ecwid.consul.v1.health.model.HealthService; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.thread.NamedThreadFactory; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; @@ -65,7 +66,7 @@ public class ConsulRegistryServiceImpl implements RegistryService> clusterAddressMap; + private ConcurrentMap> clusterAddressMap; private ConcurrentMap> listenerMap; private ExecutorService notifierExecutor; private ConcurrentMap notifiers; @@ -123,8 +124,10 @@ static ConsulRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception { + public void register(ServiceInstance instance) throws Exception { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); + doRegister(address); RegistryHeartBeats.addHeartBeat(REGISTRY_TYPE, address, this::doRegister); } @@ -134,13 +137,14 @@ private void doRegister(InetSocketAddress address) { } @Override - public void unregister(InetSocketAddress address) throws Exception { + public void unregister(ServiceInstance instance) { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); getConsulClient().agentServiceDeregister(createServiceId(address), getAclToken()); } @Override - public void subscribe(String cluster, ConsulListener listener) throws Exception { + public void subscribe(String cluster, ConsulListener listener) { // 1.add listener to subscribe list listenerMap.computeIfAbsent(cluster, key -> new HashSet<>()).add(listener); // 2.get healthy services @@ -153,7 +157,7 @@ public void subscribe(String cluster, ConsulListener listener) throws Exception } @Override - public void unsubscribe(String cluster, ConsulListener listener) throws Exception { + public void unsubscribe(String cluster, ConsulListener listener) { // 1.remove notifier for the cluster ConsulNotifier notifier = notifiers.remove(cluster); // 2.stop the notifier @@ -161,7 +165,7 @@ public void unsubscribe(String cluster, ConsulListener listener) throws Exceptio } @Override - public List lookup(String key) throws Exception { + public List lookup(String key) { transactionServiceGroup = key; final String cluster = getServiceGroup(key); if (cluster == null) { @@ -171,7 +175,7 @@ public List lookup(String key) throws Exception { return lookupByCluster(cluster); } - private List lookupByCluster(String cluster) throws Exception { + private List lookupByCluster(String cluster) { if (!listenerMap.containsKey(cluster)) { // 1.refresh cluster refreshCluster(cluster); @@ -314,14 +318,14 @@ private void refreshCluster(String cluster, List services) { return; } - List addresses = services.stream() + List instances = ServiceInstance.convertToServiceInstanceList(services.stream() .map(HealthService::getService) .map(service -> new InetSocketAddress(service.getAddress(), service.getPort())) - .collect(Collectors.toList()); + .collect(Collectors.toList())); - clusterAddressMap.put(cluster, addresses); + clusterAddressMap.put(cluster, instances); - removeOfflineAddressesIfNecessary(transactionServiceGroup, cluster, addresses); + removeOfflineAddressesIfNecessary(transactionServiceGroup, cluster, instances); } /** diff --git a/discovery/seata-discovery-consul/src/test/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImplMockTest.java b/discovery/seata-discovery-consul/src/test/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImplMockTest.java index d5a15b08441..39738c9efaf 100644 --- a/discovery/seata-discovery-consul/src/test/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImplMockTest.java +++ b/discovery/seata-discovery-consul/src/test/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImplMockTest.java @@ -20,6 +20,7 @@ import com.ecwid.consul.v1.ConsulClient; import com.ecwid.consul.v1.Response; import com.ecwid.consul.v1.health.model.HealthService; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.config.Configuration; import org.apache.seata.config.ConfigurationFactory; import org.apache.seata.config.exception.ConfigNotFoundException; @@ -76,14 +77,14 @@ public void testGetInstance() { @Order(2) @Test public void testRegister() throws Exception { - InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8080); + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8080)); when(client.agentServiceRegister(any())).thenReturn(null); - service.register(inetSocketAddress); + service.register(serviceInstance); verify(client).agentServiceRegister(any(), any()); when(client.agentServiceDeregister(any())).thenReturn(null); - service.unregister(inetSocketAddress); + service.unregister(serviceInstance); verify(client).agentServiceDeregister(any(), any()); } diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/ConsistentHashLoadBalance.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/ConsistentHashLoadBalance.java index 765e00ba18f..aa47290ec5a 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/ConsistentHashLoadBalance.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/ConsistentHashLoadBalance.java @@ -37,18 +37,13 @@ public class ConsistentHashLoadBalance implements LoadBalance { /** - * The constant LOAD_BALANCE_CONSISTENT_HASH_VIRTUAL_NODES. + * The number of virtual nodes */ - public static final String LOAD_BALANCE_CONSISTENT_HASH_VIRTUAL_NODES = - LoadBalanceFactory.LOAD_BALANCE_PREFIX + "virtualNodes"; - /** - * The constant VIRTUAL_NODES_NUM. - */ - private static final int VIRTUAL_NODES_NUM = ConfigurationFactory.getInstance() - .getInt(LOAD_BALANCE_CONSISTENT_HASH_VIRTUAL_NODES, VIRTUAL_NODES_DEFAULT); + private static final int VIRTUAL_NODES_NUM = + ConfigurationFactory.getInstance().getInt("client.loadBalance.virtualNodes", VIRTUAL_NODES_DEFAULT); /** - * The ConsistentHashSelectorWrapper that caches a {@link ConsistentHashSelector}. + * Consistent hashing selector wrapper for caching selector instances */ private volatile ConsistentHashSelectorWrapper selectorWrapper; @@ -66,6 +61,11 @@ public T select(List invokers, String xid) { return (T) selectorWrapper.getSelector(invokers).select(xid); } + /** + * Consistent hash selector wrapper + * + *

Cache selector instances and update selectors when the service instance list changes

+ */ @SuppressWarnings({"rawtypes", "unchecked"}) private static final class ConsistentHashSelectorWrapper { @@ -103,6 +103,13 @@ private boolean equals(List invokers) { } } + /** + * Consistent hash selector + * + *

Maintain the mapping from virtual nodes to actual service instances and provide consistent hash selection function

+ * + * @param ServiceInstance + */ private static final class ConsistentHashSelector { private final SortedMap virtualInvokers = new TreeMap<>(); diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LeastActiveLoadBalance.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LeastActiveLoadBalance.java index f3d794e206e..01e3e436d11 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LeastActiveLoadBalance.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LeastActiveLoadBalance.java @@ -17,6 +17,7 @@ package org.apache.seata.discovery.loadbalance; import org.apache.seata.common.loader.LoadLevel; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.rpc.RpcStatus; import java.util.List; @@ -26,7 +27,6 @@ /** * The type Least Active load balance. - * */ @LoadLevel(name = LEAST_ACTIVE_LOAD_BALANCE) public class LeastActiveLoadBalance implements LoadBalance { @@ -38,7 +38,9 @@ public T select(List invokers, String xid) { int leastCount = 0; int[] leastIndexes = new int[length]; for (int i = 0; i < length; i++) { - long active = RpcStatus.getStatus(invokers.get(i).toString()).getActive(); + ServiceInstance serviceInstance = (ServiceInstance) invokers.get(i); + long active = + RpcStatus.getStatus(serviceInstance.getAddress().toString()).getActive(); if (leastActive == -1 || active < leastActive) { leastActive = active; leastCount = 1; diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LoadBalance.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LoadBalance.java index c1435945b4b..74a46b6cfa4 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LoadBalance.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LoadBalance.java @@ -20,19 +20,18 @@ /** * The interface Load balance. - * */ public interface LoadBalance { String SPLIT = ":"; /** - * Select t. + * Select ServiceInstances. * * @param the type parameter * @param invokers the invokers * @param xid the xid - * @return the t + * @return the ServiceInstance * @throws Exception the exception */ T select(List invokers, String xid) throws Exception; diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LoadBalanceFactory.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LoadBalanceFactory.java index e6b8331676a..1021e77aa9c 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LoadBalanceFactory.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/LoadBalanceFactory.java @@ -23,17 +23,10 @@ /** * The type Load balance factory. - * */ public class LoadBalanceFactory { - private static final String CLIENT_PREFIX = "client."; - /** - * The constant LOAD_BALANCE_PREFIX. - */ - public static final String LOAD_BALANCE_PREFIX = CLIENT_PREFIX + "loadBalance."; - - public static final String LOAD_BALANCE_TYPE = LOAD_BALANCE_PREFIX + "type"; + public static final String LOAD_BALANCE_TYPE = "client.loadBalance.type"; public static final String RANDOM_LOAD_BALANCE = "RandomLoadBalance"; @@ -45,6 +38,8 @@ public class LoadBalanceFactory { public static final String LEAST_ACTIVE_LOAD_BALANCE = "LeastActiveLoadBalance"; + public static final String WEIGHTED_RANDOM_LOAD_BALANCE = "WeightedRandomLoadBalance"; + /** * Get instance. * diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/RandomLoadBalance.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/RandomLoadBalance.java index 9443c2070a4..a5c63fcee8b 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/RandomLoadBalance.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/RandomLoadBalance.java @@ -23,7 +23,6 @@ /** * The type Random load balance. - * */ @LoadLevel(name = LoadBalanceFactory.RANDOM_LOAD_BALANCE) public class RandomLoadBalance implements LoadBalance { diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/RoundRobinLoadBalance.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/RoundRobinLoadBalance.java index ecac094c18b..983b2a2b6d3 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/RoundRobinLoadBalance.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/RoundRobinLoadBalance.java @@ -23,7 +23,6 @@ /** * The type Round robin load balance. - * */ @LoadLevel(name = LoadBalanceFactory.ROUND_ROBIN_LOAD_BALANCE) public class RoundRobinLoadBalance implements LoadBalance { diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/WeightedRandomLoadBalance.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/WeightedRandomLoadBalance.java new file mode 100644 index 00000000000..432c6f1c733 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/WeightedRandomLoadBalance.java @@ -0,0 +1,139 @@ +/* + * 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.discovery.loadbalance; + +import org.apache.seata.common.loader.EnhancedServiceLoader; +import org.apache.seata.common.loader.LoadLevel; +import org.apache.seata.common.metadata.ServiceInstance; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * The type Weighted random load balance(based on metadata). + * + * Weight rules: + * - Only supports non-negative weights (>= 0) + * - Weight can be configured in metadata with key "weight" as Number or String + * - Default weight is 1 if not specified + */ +@LoadLevel(name = LoadBalanceFactory.WEIGHTED_RANDOM_LOAD_BALANCE) +public class WeightedRandomLoadBalance implements LoadBalance { + + private static final LoadBalance RANDOM_LOAD_BALANCE = + EnhancedServiceLoader.load(LoadBalance.class, LoadBalanceFactory.RANDOM_LOAD_BALANCE); + + private static final String WEIGHT_KEY = "weight"; + private static final int DEFAULT_WEIGHT = 1; + + @Override + public T select(List invokers, String xid) throws Exception { + // Check if all instances have no weight, if so downgrade to random load balancing + if (!hasAnyWeight(invokers)) { + return RANDOM_LOAD_BALANCE.select(invokers, xid); + } + + // Calculate total weight + int totalWeight = calculateTotalWeight(invokers); + + // If total weight is 0, downgrade to random load balancing + if (totalWeight <= 0) { + return RANDOM_LOAD_BALANCE.select(invokers, xid); + } + + // Generate random numbers + int randomWeight = ThreadLocalRandom.current().nextInt(totalWeight); + + // Select instances based on weights + int currentWeight = 0; + for (T invoker : invokers) { + if (invoker instanceof ServiceInstance) { + ServiceInstance instance = (ServiceInstance) invoker; + int weight = getWeight(instance); + currentWeight += weight; + if (randomWeight < currentWeight) { + return invoker; + } + } + } + + return invokers.get(0); + } + + /** + * Check if any instances contain valid weight information (weight > 0) + * @param invokers Instance List + * @return Returns true if any instance contains valid weight information + */ + private boolean hasAnyWeight(List invokers) { + for (T invoker : invokers) { + if (invoker instanceof ServiceInstance) { + ServiceInstance instance = (ServiceInstance) invoker; + if (instance.getMetadata() != null && instance.getMetadata().containsKey(WEIGHT_KEY)) { + int weight = getWeight(instance); + if (weight > 0) { + return true; + } + } + } + } + return false; + } + + /** + * Calculate the total weight of all instances + * @param invokers Instance List + * @return Total weight + */ + private int calculateTotalWeight(List invokers) { + int totalWeight = 0; + for (T invoker : invokers) { + if (invoker instanceof ServiceInstance) { + ServiceInstance instance = (ServiceInstance) invoker; + totalWeight += getWeight(instance); + } + } + return totalWeight; + } + + /** + * Get the weight of the instance + * @param instance Instance + * @return Weight value, if not set returns the default weight of 1, negative weights are treated as 0 + */ + private int getWeight(ServiceInstance instance) { + if (instance.getMetadata() != null) { + Object weightObj = instance.getMetadata().get(WEIGHT_KEY); + if (weightObj != null) { + int weight = 0; + if (weightObj instanceof Number) { + weight = ((Number) weightObj).intValue(); + } else if (weightObj instanceof String) { + try { + weight = Integer.parseInt((String) weightObj); + } catch (NumberFormatException e) { + return DEFAULT_WEIGHT; + } + } + + // Only return non-negative weights + return Math.max(0, weight); + } + } + return DEFAULT_WEIGHT; + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/XIDLoadBalance.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/XIDLoadBalance.java index bc1d1c55cc7..3567edc3381 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/XIDLoadBalance.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/loadbalance/XIDLoadBalance.java @@ -18,6 +18,7 @@ import org.apache.seata.common.loader.EnhancedServiceLoader; import org.apache.seata.common.loader.LoadLevel; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,7 +31,6 @@ /** * The type xid load balance. - * */ @LoadLevel(name = XID_LOAD_BALANCE) public class XIDLoadBalance implements LoadBalance { @@ -52,9 +52,9 @@ public T select(List invokers, String xid) throws Exception { String ip = serverAddress.substring(0, index); InetSocketAddress xidInetSocketAddress = new InetSocketAddress(ip, port); for (T invoker : invokers) { - InetSocketAddress inetSocketAddress = (InetSocketAddress) invoker; + InetSocketAddress inetSocketAddress = ((ServiceInstance) invoker).getAddress(); if (Objects.equals(xidInetSocketAddress, inetSocketAddress)) { - return (T) inetSocketAddress; + return invoker; } } LOGGER.error("not found seata-server channel,xid: {}, try use random load balance", xid); diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/FileRegistryServiceImpl.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/FileRegistryServiceImpl.java index 6b247d8da75..da3082563bf 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/FileRegistryServiceImpl.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/FileRegistryServiceImpl.java @@ -16,6 +16,7 @@ */ package org.apache.seata.discovery.registry; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; import org.apache.seata.config.ConfigChangeListener; @@ -29,7 +30,6 @@ /** * The type File registry service. - * */ public class FileRegistryServiceImpl implements RegistryService { @@ -57,10 +57,10 @@ static FileRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception {} + public void register(ServiceInstance address) throws Exception {} @Override - public void unregister(InetSocketAddress address) throws Exception {} + public void unregister(ServiceInstance address) throws Exception {} @Override public void subscribe(String cluster, ConfigChangeListener listener) throws Exception {} @@ -69,7 +69,10 @@ public void subscribe(String cluster, ConfigChangeListener listener) throws Exce public void unsubscribe(String cluster, ConfigChangeListener listener) throws Exception {} @Override - public List lookup(String key) throws Exception { + public void close() throws Exception {} + + @Override + public List lookup(String key) throws Exception { String clusterName = getServiceGroup(key); if (clusterName == null) { String missingDataId = PREFIX_SERVICE_ROOT + CONFIG_SPLIT_CHAR + PREFIX_SERVICE_MAPPING + key; @@ -77,12 +80,13 @@ public List lookup(String key) throws Exception { } String endpointStr = getGroupListFromConfig(clusterName); String[] endpoints = endpointStr.split(ENDPOINT_SPLIT_CHAR); - List inetSocketAddresses = new ArrayList<>(); + List serviceInstances = new ArrayList<>(); for (String endpoint : endpoints) { String[] ipAndPort = NetUtil.splitIPPortStr(endpoint); - inetSocketAddresses.add(new InetSocketAddress(ipAndPort[0], Integer.parseInt(ipAndPort[1]))); + serviceInstances.add( + new ServiceInstance(new InetSocketAddress(ipAndPort[0], Integer.parseInt(ipAndPort[1])))); } - return inetSocketAddresses; + return serviceInstances; } private String getGroupListFromConfig(String clusterName) { @@ -95,7 +99,4 @@ private String getGroupListFromConfig(String clusterName) { } return endpointStr; } - - @Override - public void close() throws Exception {} } diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryHeartBeats.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryHeartBeats.java index 69b380ad550..7798b2d3832 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryHeartBeats.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryHeartBeats.java @@ -28,7 +28,7 @@ import java.util.concurrent.TimeUnit; /** - * @since 2021/6/13 5:09 pm + * Responsible for managing the heartbeat mechanism for registry centers. */ public class RegistryHeartBeats { diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryProvider.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryProvider.java index 565ad0d9f5e..c782754b551 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryProvider.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryProvider.java @@ -20,6 +20,7 @@ * the interface registry provider */ public interface RegistryProvider { + /** * provide a registry implementation instance * @return RegistryService diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryService.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryService.java index b11a5a59403..81e5bb3245a 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryService.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryService.java @@ -16,11 +16,10 @@ */ package org.apache.seata.discovery.registry; -import org.apache.seata.common.metadata.Instance; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.config.ConfigurationFactory; -import java.net.InetSocketAddress; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -31,9 +30,9 @@ import java.util.stream.Collectors; /** - * The interface Registry service. + * Service registry interface for managing service registration and discovery. * - * @param the type parameter + * @param the listener type parameter */ public interface RegistryService { @@ -41,65 +40,45 @@ public interface RegistryService { * The constant PREFIX_SERVICE_MAPPING. */ String PREFIX_SERVICE_MAPPING = "vgroupMapping."; + /** * The constant PREFIX_SERVICE_ROOT. */ String PREFIX_SERVICE_ROOT = "service"; + /** * The constant CONFIG_SPLIT_CHAR. */ String CONFIG_SPLIT_CHAR = "."; - Set SERVICE_GROUP_NAME = new HashSet<>(); - /** - * Service node health check + * Set of service group names. */ - Map>> CURRENT_ADDRESS_MAP = new ConcurrentHashMap<>(); - /** - * Register. - * - * @param address the address - * @throws Exception the exception - */ - @Deprecated - void register(InetSocketAddress address) throws Exception; + Set SERVICE_GROUP_NAME = new HashSet<>(); /** - * Register. - * - * @param instance the address - * @throws Exception the exception + * Current instance cache map for service node health check. */ - default void register(Instance instance) throws Exception { - InetSocketAddress inetSocketAddress = new InetSocketAddress( - instance.getTransaction().getHost(), instance.getTransaction().getPort()); - register(inetSocketAddress); - } + Map>> CURRENT_INSTANCE_MAP = new ConcurrentHashMap<>(); /** - * Unregister. + * Register a serviceInstance. * - * @param address the address + * @param address the serviceInstance to register * @throws Exception the exception */ - @Deprecated - void unregister(InetSocketAddress address) throws Exception; + void register(ServiceInstance address) throws Exception; /** - * Unregister. + * Unregister a serviceInstance. * - * @param instance the instance + * @param address the service instance to unregister * @throws Exception the exception */ - default void unregister(Instance instance) throws Exception { - InetSocketAddress inetSocketAddress = new InetSocketAddress( - instance.getTransaction().getHost(), instance.getTransaction().getPort()); - unregister(inetSocketAddress); - } + void unregister(ServiceInstance address) throws Exception; /** - * Subscribe. + * Subscribe to cluster changes. * * @param cluster the cluster * @param listener the listener @@ -108,7 +87,7 @@ default void unregister(Instance instance) throws Exception { void subscribe(String cluster, T listener) throws Exception; /** - * Unsubscribe. + * Unsubscribe from cluster changes. * * @param cluster the cluster * @param listener the listener @@ -117,22 +96,22 @@ default void unregister(Instance instance) throws Exception { void unsubscribe(String cluster, T listener) throws Exception; /** - * Lookup list. + * Look up serviceInstances by key. * * @param key the key - * @return the list + * @return list of serviceInstances * @throws Exception the exception */ - List lookup(String key) throws Exception; + List lookup(String key) throws Exception; /** - * Close. + * Close the registry service. * @throws Exception the exception */ void close() throws Exception; /** - * Get current service group name + * Get current service group name from configuration. * * @param key service group * @return the service group name @@ -145,57 +124,67 @@ default String getServiceGroup(String key) { return ConfigurationFactory.getInstance().getConfig(key); } - default List aliveLookup(String transactionServiceGroup) { - Map> clusterAddressMap = - CURRENT_ADDRESS_MAP.computeIfAbsent(transactionServiceGroup, k -> new ConcurrentHashMap<>()); + /** + * Look up alive serviceInstances for a transaction service group. + * + * @param transactionServiceGroup the transaction service group + * @return list of alive serviceInstances + */ + default List aliveLookup(String transactionServiceGroup) { + Map> clusterInstanceMap = + CURRENT_INSTANCE_MAP.computeIfAbsent(transactionServiceGroup, key -> new ConcurrentHashMap<>()); String clusterName = getServiceGroup(transactionServiceGroup); - List inetSocketAddresses = clusterAddressMap.get(clusterName); - if (CollectionUtils.isNotEmpty(inetSocketAddresses)) { - return inetSocketAddresses; + List serviceInstances = clusterInstanceMap.get(clusterName); + if (CollectionUtils.isNotEmpty(serviceInstances)) { + return serviceInstances; } // fall back to addresses of any cluster - return clusterAddressMap.values().stream() + return clusterInstanceMap.values().stream() .filter(CollectionUtils::isNotEmpty) .findAny() .orElse(Collections.emptyList()); } - default List refreshAliveLookup( - String transactionServiceGroup, List aliveAddress) { - - Map> clusterAddressMap = - CURRENT_ADDRESS_MAP.computeIfAbsent(transactionServiceGroup, key -> new ConcurrentHashMap<>()); + /** + * Refresh alive serviceInstances for a transaction service group. + * + * @param transactionServiceGroup the transaction service group + * @param aliveInstances the alive instances to update + * @return the previous list of instances + */ + default List refreshAliveLookup( + String transactionServiceGroup, List aliveInstances) { + Map> clusterInstanceMap = + CURRENT_INSTANCE_MAP.computeIfAbsent(transactionServiceGroup, key -> new ConcurrentHashMap<>()); String clusterName = getServiceGroup(transactionServiceGroup); - return clusterAddressMap.put(clusterName, aliveAddress); + return clusterInstanceMap.put(clusterName, aliveInstances); } /** + * Remove offline instances if necessary by intersecting old and new instances. * - * remove offline addresses if necessary. - * - * Intersection of the old and new addresses - * - * @param clusterName - * @param newAddressed + * @param transactionGroupService the transaction group service + * @param clusterName the cluster name + * @param onlineInstances the online instances */ default void removeOfflineAddressesIfNecessary( - String transactionGroupService, String clusterName, Collection newAddressed) { + String transactionGroupService, String clusterName, Collection onlineInstances) { - Map> clusterAddressMap = - CURRENT_ADDRESS_MAP.computeIfAbsent(transactionGroupService, key -> new ConcurrentHashMap<>()); + Map> clusterInstanceMap = + CURRENT_INSTANCE_MAP.computeIfAbsent(transactionGroupService, key -> new ConcurrentHashMap<>()); - List currentAddresses = clusterAddressMap.getOrDefault(clusterName, Collections.emptyList()); + List currentInstances = clusterInstanceMap.getOrDefault(clusterName, Collections.emptyList()); - List inetSocketAddresses = - currentAddresses.stream().filter(newAddressed::contains).collect(Collectors.toList()); + List serviceInstances = + currentInstances.stream().filter(onlineInstances::contains).collect(Collectors.toList()); // prevent empty update - if (CollectionUtils.isNotEmpty(inetSocketAddresses)) { - clusterAddressMap.put(clusterName, inetSocketAddresses); + if (CollectionUtils.isNotEmpty(serviceInstances)) { + clusterInstanceMap.put(clusterName, serviceInstances); } } } diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryType.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryType.java index 4ae18a30489..0b4ac6e707f 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryType.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryType.java @@ -20,7 +20,6 @@ /** * The enum Registry type. - * */ public enum RegistryType { /** diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/BitList.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/BitList.java new file mode 100644 index 00000000000..5137c9ec538 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/BitList.java @@ -0,0 +1,109 @@ +/* + * 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.discovery.routing; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.function.Predicate; + +/** + * High-performance routing intermediate structure based on BitSet. + * Each filter only marks valid bits, does not copy the array. + * + * @param element type + */ +public class BitList { + + private final List originalList; + private final BitSet validBits; + private final int size; + + /** + * Constructor + * + * @param list original list + */ + public BitList(List list) { + this.originalList = list; + this.size = list.size(); + this.validBits = new BitSet(size); + this.validBits.set(0, size); // All bits are valid initially + } + + /** + * Create BitList from list + * + * @param list list + * @param element type + * @return BitList instance + */ + public static BitList fromList(List list) { + return new BitList<>(list); + } + + /** + * Filter operation + * + * @param predicate filter condition + * @return filtered BitList + */ + public BitList filter(Predicate predicate) { + BitList result = new BitList<>(originalList); + result.validBits.clear(); + result.validBits.or(this.validBits); // Inherit the current valid bit first + for (int i = 0; i < size; i++) { + if (validBits.get(i) && !predicate.test(originalList.get(i))) { + result.validBits.clear(i); + } + } + return result; + } + + /** + * Convert to List + * + * @return List of valid elements + */ + public List toList() { + List result = new ArrayList<>(); + for (int i = 0; i < size; i++) { + if (validBits.get(i)) { + result.add(originalList.get(i)); + } + } + return result; + } + + /** + * Get the number of valid elements + * + * @return number of valid elements + */ + public int size() { + return validBits.cardinality(); + } + + /** + * Check if empty + * + * @return whether it is empty + */ + public boolean isEmpty() { + return validBits.isEmpty(); + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RouterSnapshotNode.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RouterSnapshotNode.java new file mode 100644 index 00000000000..494fcc8b3fc --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RouterSnapshotNode.java @@ -0,0 +1,107 @@ +/* + * 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.discovery.routing; + +import java.util.List; + +/** + * Used to record snapshot information during router execution + * + * @param service instance type + */ +public class RouterSnapshotNode { + + private final String routerName; + private final int inputSize; + private final int outputSize; + private final List selectedServers; + private final String snapshot; + private final long timestamp; + + /** + * Constructor + * @param routerName router name + * @param inputSize input size + * @param outputSize output size + * @param selectedServers selected server list + * @param snapshot snapshot information + */ + public RouterSnapshotNode( + String routerName, int inputSize, int outputSize, List selectedServers, String snapshot) { + this.routerName = routerName; + this.inputSize = inputSize; + this.outputSize = outputSize; + this.selectedServers = selectedServers; + this.snapshot = snapshot; + this.timestamp = System.currentTimeMillis(); + } + + /** + * Get router name + * @return router name + */ + public String getRouterName() { + return routerName; + } + + /** + * Get input size + * @return input size + */ + public int getInputSize() { + return inputSize; + } + + /** + * Get output size + * @return output size + */ + public int getOutputSize() { + return outputSize; + } + + /** + * Get selected server list + * @return selected server list + */ + public List getSelectedServers() { + return selectedServers; + } + + /** + * Get snapshot information + * @return snapshot information + */ + public String getSnapshot() { + return snapshot; + } + + /** + * Get timestamp + * @return timestamp + */ + public long getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return String.format( + "%s: input=%d, output=%d, selected=%s, snapshot=%s", + routerName, inputSize, outputSize, selectedServers, snapshot); + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RoutingContext.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RoutingContext.java new file mode 100644 index 00000000000..9ec88563976 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RoutingContext.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.discovery.routing; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Contains various information needed during routing process + */ +public class RoutingContext { + + private final Map attributes = new ConcurrentHashMap<>(); + + /** + * Constructor + */ + public RoutingContext() {} + + /** + * Set attribute + * @param key key + * @param value value + */ + public void setAttribute(String key, Object value) { + attributes.put(key, value); + } + + /** + * Get attribute + * @param key key + * @return value + */ + public Object getAttribute(String key) { + return attributes.get(key); + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RoutingManager.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RoutingManager.java new file mode 100644 index 00000000000..0615a648748 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/RoutingManager.java @@ -0,0 +1,129 @@ +/* + * 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.discovery.routing; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.chain.DefaultRouterChain; +import org.apache.seata.discovery.routing.chain.PrimaryBackupRouterChain; +import org.apache.seata.discovery.routing.chain.RouterChain; +import org.apache.seata.discovery.routing.config.RoutingProperties; +import org.apache.seata.discovery.routing.region.ClientLocationProvider; +import org.apache.seata.discovery.routing.region.DefaultClientLocationProvider; +import org.apache.seata.discovery.routing.region.GeoLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Responsible for routing filtering between service discovery and load balancing + */ +public class RoutingManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(RoutingManager.class); + + private static volatile RoutingManager INSTANCE; + + private final RouterChain routerChain; + private final ClientLocationProvider clientLocationProvider; + + /** + * Constructor + */ + private RoutingManager() { + // Choose which router chain mode to use based on configuration + if (RoutingProperties.isPrimaryBackupEnabled()) { + LOGGER.info("Using PrimaryBackupRouterChain Mode"); + this.routerChain = new PrimaryBackupRouterChain(); + } else { + LOGGER.info("Using RouterChain Mode"); + this.routerChain = new DefaultRouterChain(); + } + this.clientLocationProvider = new DefaultClientLocationProvider(); + } + + /** + * Get singleton instance + * @return routing manager instance + */ + public static RoutingManager getInstance() { + if (INSTANCE == null) { + synchronized (RoutingManager.class) { + if (INSTANCE == null) { + INSTANCE = new RoutingManager(); + } + } + } + return INSTANCE; + } + + /** + * Execute routing filter + * @param servers original service instances list + * @return filtered service instances list + */ + public List filter(List servers) { + // Check if routing feature is enabled + if (!RoutingProperties.isRoutingEnabled()) { + return servers; + } + + try { + // Create routing context + RoutingContext ctx = createRoutingContext(); + + // Execute routing filter + List filteredServers = routerChain.filterAll(servers, ctx); + + if (LOGGER.isDebugEnabled() || RoutingProperties.isRoutingDebugEnabled()) { + LOGGER.debug( + "Routing filter applied: original={}, filtered={}, group={}, xid={}", + servers.size(), + filteredServers.size()); + } + + return filteredServers; + } catch (Exception e) { + LOGGER.warn("Routing filter failed, returning original servers: {}", e.getMessage()); + return servers; + } + } + + /** + * Create routing context + * @return routing context + */ + private RoutingContext createRoutingContext() { + RoutingContext ctx = new RoutingContext(); + + // Add client location information + GeoLocation location = clientLocationProvider.getClientLocation(); + if (location != null) { + ctx.setAttribute("clientLat", location.getLatitude()); + ctx.setAttribute("clientLng", location.getLongitude()); + } + + // Add router configuration information + ctx.setAttribute("regionRouterEnabled", RoutingProperties.isRegionRouterEnabled()); + ctx.setAttribute("metadataRouterEnabled", RoutingProperties.isMetadataRouterEnabled()); + + // Add other context information + ctx.setAttribute("timestamp", System.currentTimeMillis()); + + return ctx; + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/StateRouter.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/StateRouter.java new file mode 100644 index 00000000000..13673946d60 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/StateRouter.java @@ -0,0 +1,64 @@ +/* + * 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.discovery.routing; + +import java.util.List; + +/** + * State router interface + * Defines basic behavior of routers, supports chain of responsibility pattern + * + * @param service instance type + */ +public interface StateRouter { + + /** + * Execute routing + * @param servers service instances list + * @param ctx routing context + * @param debugMode whether debug mode is enabled + * @param snapshots snapshot list, used to collect snapshots from all routers + * @return routed service instances list + */ + BitList route(BitList servers, RoutingContext ctx, boolean debugMode, List> snapshots); + + /** + * Whether it's a runtime router + * Runtime router: needs to calculate dynamically based on each request's context + * Non-runtime router: configuration-based, only loads configuration once at startup + * @return whether it's a runtime router + */ + boolean isRuntime(); + + /** + * Build snapshot + * @return snapshot string + */ + String buildSnapshot(); + + /** + * Set next router + * @param next next router + */ + void setNext(StateRouter next); + + /** + * Get next router + * @return next router + */ + StateRouter getNext(); +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/DefaultRouterChain.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/DefaultRouterChain.java new file mode 100644 index 00000000000..7950e74eb87 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/DefaultRouterChain.java @@ -0,0 +1,288 @@ +/* + * 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.discovery.routing.chain; + +import org.apache.seata.common.loader.EnhancedServiceLoader; +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.BitList; +import org.apache.seata.discovery.routing.RouterSnapshotNode; +import org.apache.seata.discovery.routing.RoutingContext; +import org.apache.seata.discovery.routing.StateRouter; +import org.apache.seata.discovery.routing.config.RoutingProperties; +import org.apache.seata.discovery.routing.router.MetadataRouter; +import org.apache.seata.discovery.routing.router.RegionRouter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Uses chain of responsibility pattern to encapsulate multiple routers + */ +public class DefaultRouterChain implements RouterChain { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRouterChain.class); + + private final List> routers = new ArrayList<>(); + + private final boolean fallbackToAny; + private final boolean debugEnabled; + + /** + * Default constructor + */ + public DefaultRouterChain() { + this.fallbackToAny = RoutingProperties.isRoutingFallbackEnabled(); + this.debugEnabled = RoutingProperties.isRoutingDebugEnabled(); + loadRouters(RoutingProperties.getRouterChainOrder()); + } + + /** + * Constructor with specified router order + * + * @param routerOrder router order string + */ + public DefaultRouterChain(String routerOrder) { + this.fallbackToAny = RoutingProperties.isRoutingFallbackEnabled(); + this.debugEnabled = RoutingProperties.isRoutingDebugEnabled(); + loadRouters(routerOrder); + } + + /** + * Execute routing filter + * + * @param servers service instances list + * @param ctx routing context + * @return filtered service instances list + */ + @Override + public List filterAll(List servers, RoutingContext ctx) { + if (servers == null || servers.isEmpty()) { + return servers; + } + + BitList bitList = BitList.fromList(servers); + BitList result = route(bitList, ctx); + return result.toList(); + } + + /** + * Execute routing filter (internal method) + * + * @param servers service instances list + * @param ctx routing context + * @return filtered service instances list + */ + private BitList route(BitList servers, RoutingContext ctx) { + BitList result = servers; + List> snapshots = debugEnabled ? new ArrayList<>() : null; + + for (StateRouter router : routers) { + result = router.route(result, ctx, debugEnabled, snapshots); + + if (result.isEmpty()) { + if (fallbackToAny) { + LOGGER.warn("Router chain produced empty result, falling back to all servers"); + return servers; + } else { + LOGGER.warn("Router chain produced empty result, no fallback allowed"); + return result; + } + } + } + + // If debug mode is enabled, print complete snapshot information + if (debugEnabled && snapshots != null && !snapshots.isEmpty()) { + logRouterChainSnapshot(snapshots, servers.size(), result.size()); + } + + return result; + } + + /** + * Print router chain debug information + * + * @param snapshots snapshot list + * @param initialSize initial server count + * @param finalSize final server count + */ + private void logRouterChainSnapshot( + List> snapshots, int initialSize, int finalSize) { + StringBuilder sb = new StringBuilder(); + sb.append("\n=== Router Chain Debug ===\n"); + sb.append(String.format("Initial servers: %d, Final servers: %d\n", initialSize, finalSize)); + sb.append("Router execution details:\n"); + + for (int i = 0; i < snapshots.size(); i++) { + RouterSnapshotNode snapshot = snapshots.get(i); + sb.append(String.format( + " [%d] %s: %d -> %d servers\n", + i + 1, snapshot.getRouterName(), snapshot.getInputSize(), snapshot.getOutputSize())); + + // If output count is small, show selected servers + if (snapshot.getOutputSize() <= 5 && snapshot.getOutputSize() > 0) { + sb.append(" Selected: ") + .append(snapshot.getSelectedServers()) + .append("\n"); + } + + // Show snapshot details + if (snapshot.getSnapshot() != null && !snapshot.getSnapshot().isEmpty()) { + sb.append(" Details: ").append(snapshot.getSnapshot()).append("\n"); + } + } + + sb.append("=== End Debug ===\n"); + LOGGER.info(sb.toString()); + } + + /** + * Load routers + * + * @param routerOrder router order string + */ + private void loadRouters(String routerOrder) { + // Parse router order + List chainOrder = parseRouterOrder(routerOrder); + + // Load routers according to configuration order + for (String routerName : chainOrder) { + if (isRouterEnabled(routerName)) { + List> routerInstances = createRoutersByName(routerName); + routers.addAll(routerInstances); + } + } + + // Build responsibility chain + for (int i = 0; i < routers.size() - 1; i++) { + routers.get(i).setNext(routers.get(i + 1)); + } + } + + /** + * Check if router is enabled + * + * @param routerName router name + * @return whether enabled + */ + private boolean isRouterEnabled(String routerName) { + if (routerName.startsWith("metadata-router-")) { + // Check if specific metadata-router is enabled + return RoutingProperties.isMetadataRouterEnabled(routerName); + } + + switch (routerName) { + case "region-router": + return RoutingProperties.isRegionRouterEnabled(); + case "metadata-router": + return RoutingProperties.isMetadataRouterEnabled(); + default: + return true; // SPI routers are enabled by default + } + } + + /** + * Create routers by name + * + * @param routerName router name + * @return router instances list + */ + @SuppressWarnings("unchecked") + private List> createRoutersByName(String routerName) { + List> routerInstances = new ArrayList<>(); + + switch (routerName) { + case "region-router": + routerInstances.add(new RegionRouter()); + break; + case "metadata-router": + routerInstances.add(new MetadataRouter()); + break; + default: + if (routerName.startsWith("metadata-router-")) { + // Create specific metadata-router instance + MetadataRouter router = new MetadataRouter(routerName); + routerInstances.add(router); + } else { + // Try to load custom router via SPI + try { + StateRouter customRouter = + EnhancedServiceLoader.load(StateRouter.class, routerName); + if (customRouter != null) { + routerInstances.add(customRouter); + } else { + LOGGER.warn( + "Custom router '{}' specified in configuration but not found via SPI", routerName); + } + } catch (Exception e) { + LOGGER.warn("Failed to load custom router: {}", routerName, e); + } + } + break; + } + + return routerInstances; + } + + /** + * Parse router order + * + * @param routerOrder router order string + * @return router names list + */ + public static List parseRouterOrder(String routerOrder) { + List order = new ArrayList<>(); + + if (routerOrder != null && !routerOrder.trim().isEmpty()) { + String[] orderNames = routerOrder.split(","); + for (String orderName : orderNames) { + String trimmedName = orderName.trim(); + if (!trimmedName.isEmpty()) { + order.add(trimmedName); + } + } + } + + // Validate configuration validity + if (order.isEmpty()) { + LOGGER.warn("Router order configuration is invalid or empty, using fallback strategy"); + order.add("region-router"); + order.add("metadata-router"); + } else { + // Validate router name validity + List validRouters = new ArrayList<>(); + for (String routerName : order) { + if (RouterChainUtils.isValidRouterName(routerName)) { + validRouters.add(routerName); + } else { + LOGGER.warn("Invalid router name in configuration: {}, skipping", routerName); + } + } + + if (validRouters.isEmpty()) { + LOGGER.warn("No valid routers found in configuration, using fallback strategy"); + validRouters.add("region-router"); + validRouters.add("metadata-router"); + } + + order = validRouters; + } + + return order; + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/PrimaryBackupRouterChain.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/PrimaryBackupRouterChain.java new file mode 100644 index 00000000000..d3447b1e5a9 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/PrimaryBackupRouterChain.java @@ -0,0 +1,150 @@ +/* + * 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.discovery.routing.chain; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.RoutingContext; +import org.apache.seata.discovery.routing.config.RoutingProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; + +/** + * Automatically switches to backup chain when primary chain filtering result is empty + */ +public class PrimaryBackupRouterChain implements RouterChain { + + private static final Logger LOGGER = LoggerFactory.getLogger(PrimaryBackupRouterChain.class); + + private final DefaultRouterChain primaryChain; + private final DefaultRouterChain backupChain; + + private volatile boolean hasSwitchedToBackup = false; // Whether already switched to backup chain + + /** + * Constructor + */ + public PrimaryBackupRouterChain() { + // Use clearer naming + String primaryChainOrder = RoutingProperties.getPrimaryBackupOrder(); + String fallbackChainOrder = RoutingProperties.getRouterChainOrder(); + + this.primaryChain = createRouterChain(primaryChainOrder, "primary"); + this.backupChain = createRouterChain(fallbackChainOrder, "backup"); + + // If the primary chain is invalid, try to use the backup chain as the primary + if (this.primaryChain == null && this.backupChain != null) { + LoggerFactory.getLogger(PrimaryBackupRouterChain.class) + .warn("Primary chain is invalid, using backup chain as primary"); + } + + // Ensure both chains are not null + if (this.primaryChain == null) { + LoggerFactory.getLogger(PrimaryBackupRouterChain.class) + .warn("Primary and backup chain are invalid, using default chain"); + } + } + + private DefaultRouterChain createRouterChain(String order, String chainType) { + if (order == null || order.trim().isEmpty()) { + LoggerFactory.getLogger(PrimaryBackupRouterChain.class).warn("{} chain order is not configured", chainType); + return null; + } + if (!isValidRouterOrder(order)) { + LoggerFactory.getLogger(PrimaryBackupRouterChain.class) + .warn("{} chain order configuration is invalid: {}", chainType, order); + return null; + } + return new DefaultRouterChain(order); + } + + /** + * Execute routing filtering + * + * @param servers service instance list + * @param ctx routing context + * @return filtered service instance list + */ + @Override + public List filterAll(List servers, RoutingContext ctx) { + if (servers == null || servers.isEmpty()) { + return servers; + } + + // If already switched to backup chain, use backup chain directly + if (hasSwitchedToBackup) { + List backupResult = backupChain.filterAll(servers, ctx); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug( + "Using backup chain (already switched), filtered {} servers from {}", + backupResult.size(), + servers.size()); + } + return backupResult; + } + + // Use primary chain + List primaryResult = primaryChain.filterAll(servers, ctx); + + // If primary chain result is not empty, return directly + if (!primaryResult.isEmpty()) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Primary chain filtered {} servers from {}", primaryResult.size(), servers.size()); + } + return primaryResult; + } + + // Primary chain result is empty + if (!RoutingProperties.isPrimaryBackupEnabled()) { + // Backup chain is disabled, return primary chain's empty result + return primaryResult; + } + + LOGGER.info("Primary chain produced empty result, switching to backup chain"); + hasSwitchedToBackup = true; + + List backupResult = backupChain.filterAll(servers, ctx); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Backup chain filtered {} servers from {}", backupResult.size(), servers.size()); + } + return backupResult; + } + + /** + * Validate if router order configuration is valid + * + * @param routerOrder router order string + * @return whether valid + */ + public static boolean isValidRouterOrder(String routerOrder) { + if (routerOrder == null || routerOrder.trim().isEmpty()) { + return false; + } + return Arrays.stream(routerOrder.split(",")) + .map(String::trim) + .filter(name -> !name.isEmpty()) + .allMatch(name -> { + if (!RouterChainUtils.isValidRouterName(name)) { + LOGGER.warn("Invalid router name in configuration: {}", name); + return false; + } + return true; + }); + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/RouterChain.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/RouterChain.java new file mode 100644 index 00000000000..e15219fd25c --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/RouterChain.java @@ -0,0 +1,37 @@ +/* + * 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.discovery.routing.chain; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.RoutingContext; + +import java.util.List; + +/** + * Router chain interface + * Unifies the interface for RouterChain and PrimaryBackupRouterChain + */ +public interface RouterChain { + + /** + * Execute routing filter + * @param servers list of service instances + * @param ctx routing context + * @return filtered list of service instances + */ + List filterAll(List servers, RoutingContext ctx); +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/RouterChainUtils.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/RouterChainUtils.java new file mode 100644 index 00000000000..9cdf249bfeb --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/chain/RouterChainUtils.java @@ -0,0 +1,38 @@ +/* + * 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.discovery.routing.chain; + +/** + * Provides common methods for router chains + */ +public class RouterChainUtils { + + /** + * Validate if router name is valid + * @param routerName router name + * @return whether valid + */ + public static boolean isValidRouterName(String routerName) { + if (routerName == null) { + return false; + } + return "region-router".equals(routerName) + || "metadata-router".equals(routerName) + || routerName.startsWith("metadata-router-") // Support multiple metadata-routers + || routerName.startsWith("custom-"); // Allow custom routers + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/config/RoutingProperties.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/config/RoutingProperties.java new file mode 100644 index 00000000000..88f7e44c693 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/config/RoutingProperties.java @@ -0,0 +1,210 @@ +/* + * 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.discovery.routing.config; + +import org.apache.seata.config.ConfigurationFactory; + +/** + * Manage routing-related configuration items + */ +public class RoutingProperties { + + /** + * Routing feature switch + */ + public static final String ROUTING_ENABLED = "client.routing.enabled"; + + /** + * Routing debug mode + */ + public static final String ROUTING_DEBUG = "client.routing.debug"; + + /** + * Routing fallback strategy + */ + public static final String ROUTING_FALLBACK = "client.routing.fallback"; + + /** + * Client routing location latitude + */ + public static final String CLIENT_ROUTING_LOCATION_LAT = "client.routing.location.lat"; + + /** + * Client routing location longitude + */ + public static final String CLIENT_ROUTING_LOCATION_LNG = "client.routing.location.lng"; + + /** + * Region router configuration + */ + public static final String REGION_ROUTER_ENABLED = "client.routing.region-router.enabled"; + + public static final String REGION_ROUTER_TOP_N = "client.routing.region-router.topN"; + + /** + * Metadata router configuration + */ + public static final String METADATA_ROUTER_ENABLED = "client.routing.metadata-router.enabled"; + + public static final String METADATA_ROUTER_EXPRESSION = "client.routing.metadata-router.expression"; + + /** + * Router chain order configuration + */ + public static final String ROUTER_CHAIN_ORDER = "client.routing.chain.order"; + + /** + * Primary backup chain configuration + */ + public static final String PRIMARY_BACKUP_ENABLED = "client.routing.primary-backup.enabled"; + + public static final String PRIMARY_BACKUP_ORDER = "client.routing.primary-backup.order"; + + /** + * Check if routing feature is enabled + * @return whether enabled + */ + public static boolean isRoutingEnabled() { + return ConfigurationFactory.getInstance().getBoolean(ROUTING_ENABLED, false); + } + + /** + * Check if routing debug mode is enabled + * @return whether enabled + */ + public static boolean isRoutingDebugEnabled() { + return ConfigurationFactory.getInstance().getBoolean(ROUTING_DEBUG, false); + } + + /** + * Check if routing fallback strategy is enabled + * @return whether enabled + */ + public static boolean isRoutingFallbackEnabled() { + return ConfigurationFactory.getInstance().getBoolean(ROUTING_FALLBACK, true); + } + + /** + * Get client routing location latitude + * @return latitude + */ + public static double getClientRoutingLocationLat() { + return Double.parseDouble(ConfigurationFactory.getInstance().getConfig(CLIENT_ROUTING_LOCATION_LAT, "0.0")); + } + + /** + * Get client routing location longitude + * @return longitude + */ + public static double getClientRoutingLocationLng() { + return Double.parseDouble(ConfigurationFactory.getInstance().getConfig(CLIENT_ROUTING_LOCATION_LNG, "0.0")); + } + + /** + * Check if region router is enabled + * @return whether enabled + */ + public static boolean isRegionRouterEnabled() { + return ConfigurationFactory.getInstance().getBoolean(REGION_ROUTER_ENABLED, false); + } + + /** + * Get region router TopN configuration + * @return TopN value + */ + public static int getRegionRouterTopN() { + return ConfigurationFactory.getInstance().getInt(REGION_ROUTER_TOP_N, 5); + } + + /** + * Check if metadata router is enabled + * @return whether enabled + */ + public static boolean isMetadataRouterEnabled() { + return ConfigurationFactory.getInstance().getBoolean(METADATA_ROUTER_ENABLED, false); + } + + /** + * Get metadata router expression + * @return expression + */ + public static String getMetadataRouterExpression() { + return ConfigurationFactory.getInstance().getConfig(METADATA_ROUTER_EXPRESSION, ""); + } + + /** + * Get router chain order + * @return chain order string + */ + public static String getRouterChainOrder() { + return ConfigurationFactory.getInstance().getConfig(ROUTER_CHAIN_ORDER); + } + + /** + * Check if primary backup chain is enabled + * @return whether enabled + */ + public static boolean isPrimaryBackupEnabled() { + return ConfigurationFactory.getInstance().getBoolean(PRIMARY_BACKUP_ENABLED, false); + } + + /** + * Get primary backup chain order + * @return primary backup chain order string + */ + public static String getPrimaryBackupOrder() { + return ConfigurationFactory.getInstance().getConfig(PRIMARY_BACKUP_ORDER); + } + + /** + * Check if specific metadata router is enabled + * @param routerName router name + * @return whether enabled + */ + public static boolean isMetadataRouterEnabled(String routerName) { + if ("metadata-router".equals(routerName)) { + return isMetadataRouterEnabled(); + } + + // For metadata-router-1, metadata-router-2, etc. + if (routerName.startsWith("metadata-router-")) { + String configKey = "client.routing." + routerName + ".enabled"; + return ConfigurationFactory.getInstance().getBoolean(configKey, true); + } + + return false; + } + + /** + * Get specific metadata router expression + * @param routerName router name + * @return expression + */ + public static String getMetadataRouterExpression(String routerName) { + if ("metadata-router".equals(routerName)) { + return getMetadataRouterExpression(); + } + + // For metadata-router-1, metadata-router-2, etc. + if (routerName.startsWith("metadata-router-")) { + String configKey = "client.routing." + routerName + ".expression"; + return ConfigurationFactory.getInstance().getConfig(configKey, ""); + } + + return ""; + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ConditionMatcher.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ConditionMatcher.java new file mode 100644 index 00000000000..63fdeb71c02 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ConditionMatcher.java @@ -0,0 +1,34 @@ +/* + * 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.discovery.routing.expression; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.RoutingContext; + +/** + * Used to match whether a service instance meets specific conditions + */ +public interface ConditionMatcher { + + /** + * Match service instance + * @param server service instance + * @param ctx routing context + * @return whether it matches + */ + boolean match(ServiceInstance server, RoutingContext ctx); +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ConfigurableConditionMatcher.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ConfigurableConditionMatcher.java new file mode 100644 index 00000000000..09df37c76a2 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ConfigurableConditionMatcher.java @@ -0,0 +1,120 @@ +/* + * 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.discovery.routing.expression; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.RoutingContext; + +import java.util.Map; + +/** + * Supports loading various filtering conditions from configuration + */ +public class ConfigurableConditionMatcher implements ConditionMatcher { + + private final String condition; + private final String key; + private final String operator; + private final String value; + + public ConfigurableConditionMatcher(String condition) { + this.condition = condition.trim(); + + // Split by spaces, must have three parts: key, operator, value + String[] parts = this.condition.split("\\s+"); + if (parts.length != 3) { + throw new IllegalArgumentException("Invalid condition format: " + condition); + } + + this.key = parts[0].trim(); + this.operator = parts[1].trim(); + this.value = parts[2].trim(); + } + + @Override + public boolean match(ServiceInstance server, RoutingContext ctx) { + Map metadata = server.getMetadata(); + + // Handle null metadata case + if (metadata == null) { + return false; // If metadata is null, don't pass through + } + + Object actualValue = metadata.get(key); + + if (actualValue == null) { + return false; // If the attribute doesn't exist, don't pass through + } + + return compareValues(actualValue.toString(), operator, value); + } + + private boolean compareValues(String actual, String operator, String expected) { + // = and != support both string and numeric comparison + if ("=".equals(operator) || "!=".equals(operator)) { + // Try numeric comparison + try { + double actualNum = Double.parseDouble(actual); + double expectedNum = Double.parseDouble(expected); + + if ("=".equals(operator)) { + return actualNum == expectedNum; + } else { + return actualNum != expectedNum; + } + } catch (NumberFormatException e) { + // If cannot convert to numeric, perform string comparison + if ("=".equals(operator)) { + return actual.equals(expected); + } else { + return !actual.equals(expected); + } + } + } + + // > < >= <= only support numeric comparison + try { + double actualNum = Double.parseDouble(actual); + double expectedNum = Double.parseDouble(expected); + + switch (operator) { + case ">": + return actualNum > expectedNum; + case ">=": + return actualNum >= expectedNum; + case "<": + return actualNum < expectedNum; + case "<=": + return actualNum <= expectedNum; + default: + return false; // Invalid operator + } + } catch (NumberFormatException e) { + // If cannot convert to numeric, return false + return false; + } + } + + @Override + public String toString() { + return "ConfigurableConditionMatcher{" + "condition='" + + condition + '\'' + ", key='" + + key + '\'' + ", operator='" + + operator + '\'' + ", value='" + + value + '\'' + '}'; + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ExpressionParser.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ExpressionParser.java new file mode 100644 index 00000000000..6a2921f3216 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/expression/ExpressionParser.java @@ -0,0 +1,245 @@ +/* + * 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.discovery.routing.expression; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Expression parser + * Parses routing expressions and generates condition matchers + * + * Supported basic syntax: + * - key = value: exact match + * - key >= value: greater than or equal + * - key <= value: less than or equal + * - key > value: greater than + * - key < value: less than + * - key != value: not equal + * + * Supports two modes: + * 1. Single expression: version >= 2.3 + * 2. OR logic expression: (version >= 2.3) | (env = dev) | (region = cn-bj) + * + * Note: AND logic is not supported, AND logic is implemented by configuring multiple MetadataRouters + */ +public class ExpressionParser { + + private static final Pattern PARENTHESES_PATTERN = Pattern.compile("\\(([^()]+)\\)"); + + /** + * Parse expression + * @param expression expression string + * @return list of condition matchers + * @throws IllegalArgumentException thrown when expression format is incorrect + */ + public static List parse(String expression) { + if (expression == null || expression.trim().isEmpty()) { + return Collections.emptyList(); + } + + String trimmedExpression = expression.trim(); + + // Validate expression format + if (!isValidExpression(trimmedExpression)) { + throw new IllegalArgumentException("Invalid expression format: " + expression); + } + + // Check if contains logical operators + if (trimmedExpression.contains("|")) { + return parseOrExpression(trimmedExpression); + } else { + return parseSingleExpression(trimmedExpression); + } + } + + /** + * Parse single expression + * @param expression single expression + * @return list of condition matchers + */ + private static List parseSingleExpression(String expression) { + List matchers = new ArrayList<>(); + + // Remove outer parentheses only + String cleanExpression = expression.trim(); + if (cleanExpression.startsWith("(") && cleanExpression.endsWith(")")) { + cleanExpression = + cleanExpression.substring(1, cleanExpression.length() - 1).trim(); + } + + if (!cleanExpression.isEmpty()) { + matchers.add(new ConfigurableConditionMatcher(cleanExpression)); + } + + return matchers; + } + + /** + * Parse OR logic expression + * Format: (condition1) | (condition2) | (condition3) + * @param expression OR logic expression + * @return list of condition matchers + */ + private static List parseOrExpression(String expression) { + List matchers = new ArrayList<>(); + + // Split by | + String[] parts = expression.split("\\|"); + + for (String part : parts) { + part = part.trim(); + if (!part.isEmpty()) { + // Extract content inside parentheses + Matcher matcher = PARENTHESES_PATTERN.matcher(part); + if (matcher.find()) { + String condition = matcher.group(1).trim(); + if (!condition.isEmpty()) { + matchers.add(new ConfigurableConditionMatcher(condition)); + } + } else { + // OR expression parts must have parentheses + throw new IllegalArgumentException("OR expression part must be enclosed in parentheses: " + part); + } + } + } + + return matchers; + } + + /** + * Validate if expression format is correct + * @param expression expression string + * @return whether valid + */ + public static boolean isValidExpression(String expression) { + if (expression == null || expression.trim().isEmpty()) { + return true; // Empty expression is considered valid + } + + String trimmedExpression = expression.trim(); + + // Check if contains OR logic + if (trimmedExpression.contains("|")) { + return isValidOrExpression(trimmedExpression); + } else { + return isValidSingleExpression(trimmedExpression); + } + } + + /** + * Validate single expression format + * @param expression single expression + * @return whether valid + */ + private static boolean isValidSingleExpression(String expression) { + // Handle parentheses: only remove if both sides have parentheses + String cleanExpression = expression.trim(); + if (cleanExpression.startsWith("(") && cleanExpression.endsWith(")")) { + cleanExpression = + cleanExpression.substring(1, cleanExpression.length() - 1).trim(); + } + + if (cleanExpression.isEmpty()) { + return false; + } + + // Split by spaces, must have three parts: key, operator, value + String[] parts = cleanExpression.split("\\s+"); + if (parts.length != 3) { + return false; + } + + String key = parts[0].trim(); + String operator = parts[1].trim(); + String value = parts[2].trim(); + + // Validate key name: cannot be empty and cannot contain spaces + if (key.isEmpty() || key.contains(" ")) { + return false; + } + + // Validate operator: must be a valid comparison operator + if (!isValidOperator(operator)) { + return false; + } + + // Validate value: cannot be empty + if (value.isEmpty()) { + return false; + } + + return true; + } + + /** + * Validate OR expression format + * @param expression OR expression + * @return whether valid + */ + private static boolean isValidOrExpression(String expression) { + // Split by | + String[] parts = expression.split("\\|"); + + for (String part : parts) { + part = part.trim(); + if (!part.isEmpty()) { + // Check if has parentheses + if (part.startsWith("(") && part.endsWith(")")) { + // Extract content inside parentheses + String innerExpression = + part.substring(1, part.length() - 1).trim(); + if (!isValidSingleExpression(innerExpression)) { + return false; + } + } else { + // OR expression without parentheses is invalid + return false; + } + } + } + + return true; + } + + /** + * Validate if operator is valid + * @param operator operator + * @return whether valid + */ + private static boolean isValidOperator(String operator) { + // Strictly match valid operators, reject any other combinations + return "=".equals(operator) + || "!=".equals(operator) + || ">".equals(operator) + || ">=".equals(operator) + || "<".equals(operator) + || "<=".equals(operator); + } + + /** + * Check if expression contains OR logic + * @param expression expression string + * @return whether contains OR logic + */ + public static boolean isOrExpression(String expression) { + return expression != null && expression.contains("|"); + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/ClientLocationProvider.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/ClientLocationProvider.java new file mode 100644 index 00000000000..aebbeb0ded8 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/ClientLocationProvider.java @@ -0,0 +1,28 @@ +/* + * 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.discovery.routing.region; + +/** + * Client location provider interface + */ +public interface ClientLocationProvider { + /** + * Get client location + * @return client location information + */ + GeoLocation getClientLocation(); +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/DefaultClientLocationProvider.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/DefaultClientLocationProvider.java new file mode 100644 index 00000000000..ef5ff454d5f --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/DefaultClientLocationProvider.java @@ -0,0 +1,34 @@ +/* + * 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.discovery.routing.region; + +import org.apache.seata.discovery.routing.config.RoutingProperties; + +/** + * Supports degradation mechanism: prefers dynamic location, falls back to configured location on failure + */ +public class DefaultClientLocationProvider implements ClientLocationProvider { + + @Override + public GeoLocation getClientLocation() { + // Configured location information + double lat = RoutingProperties.getClientRoutingLocationLat(); + double lng = RoutingProperties.getClientRoutingLocationLng(); + + return new GeoLocation(lat, lng); + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/GeoLocation.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/GeoLocation.java new file mode 100644 index 00000000000..59e52312d7a --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/GeoLocation.java @@ -0,0 +1,63 @@ +/* + * 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.discovery.routing.region; + +import java.util.Objects; + +/** + * Geographic location information + * Based on latitude and longitude coordinates, used for client and server location representation + */ +public class GeoLocation { + private final double latitude; + private final double longitude; + + public GeoLocation(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } + + @Override + public String toString() { + return String.format("Location{lat=%.6f, lng=%.6f}", latitude, longitude); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GeoLocation that = (GeoLocation) o; + return Double.compare(that.latitude, latitude) == 0 && Double.compare(that.longitude, longitude) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(latitude, longitude); + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/ServerWithDistance.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/ServerWithDistance.java new file mode 100644 index 00000000000..56d3fb3fd4f --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/region/ServerWithDistance.java @@ -0,0 +1,41 @@ +/* + * 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.discovery.routing.region; + +/** + * Server wrapper with distance + * Used to store server instance and its distance information during routing calculation + * + * @param server instance type + */ +public class ServerWithDistance { + private final T server; + private final double distance; + + public ServerWithDistance(T server, double distance) { + this.server = server; + this.distance = distance; + } + + public T getServer() { + return server; + } + + public double getDistance() { + return distance; + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/AbstractStateRouter.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/AbstractStateRouter.java new file mode 100644 index 00000000000..6757bb96a59 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/AbstractStateRouter.java @@ -0,0 +1,121 @@ +/* + * 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.discovery.routing.router; + +import org.apache.seata.discovery.routing.BitList; +import org.apache.seata.discovery.routing.RouterSnapshotNode; +import org.apache.seata.discovery.routing.RoutingContext; +import org.apache.seata.discovery.routing.StateRouter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Provides basic implementation for routers + * + * @param service instance type + */ +public abstract class AbstractStateRouter implements StateRouter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractStateRouter.class); + + private StateRouter next; + private final String routerName; + private final boolean runtime; + + /** + * Constructor + * @param routerName router name + * @param runtime whether it's a runtime router + */ + protected AbstractStateRouter(String routerName, boolean runtime) { + this.routerName = routerName; + this.runtime = runtime; + } + + @Override + public BitList route( + BitList servers, RoutingContext ctx, boolean debugMode, List> snapshots) { + // Record input size + int inputSize = servers.size(); + + // Execute specific routing logic + BitList result = doRoute(servers, ctx); + + // Record output size + int outputSize = result.size(); + + // Record snapshot (debug mode) + if (debugMode && snapshots != null) { + RouterSnapshotNode snapshot = + new RouterSnapshotNode<>(routerName, inputSize, outputSize, result.toList(), buildSnapshot()); + snapshots.add(snapshot); + } + + // If result is empty, try fallback + if (result.isEmpty()) { + return fallback(servers); + } + + // If there's a next router, continue execution + if (next != null) { + return next.route(result, ctx, debugMode, snapshots); + } + + return result; + } + + /** + * Execute specific routing logic + * @param servers service instances list + * @param ctx routing context + * @return routed service instances list + */ + protected abstract BitList doRoute(BitList servers, RoutingContext ctx); + + /** + * Fallback handling + * @param servers original service instances list + * @return fallback service instances list + */ + protected BitList fallback(BitList servers) { + // Default fallback strategy: return original list + LOGGER.info("Using fallback strategy for router " + routerName); + return servers; + } + + @Override + public boolean isRuntime() { + return runtime; + } + + @Override + public String buildSnapshot() { + return String.format("Router: %s, Runtime: %s", routerName, runtime); + } + + @Override + public void setNext(StateRouter next) { + this.next = next; + } + + @Override + public StateRouter getNext() { + return next; + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/MetadataRouter.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/MetadataRouter.java new file mode 100644 index 00000000000..9d57264fdb2 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/MetadataRouter.java @@ -0,0 +1,102 @@ +/* + * 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.discovery.routing.router; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.BitList; +import org.apache.seata.discovery.routing.RoutingContext; +import org.apache.seata.discovery.routing.config.RoutingProperties; +import org.apache.seata.discovery.routing.expression.ConditionMatcher; +import org.apache.seata.discovery.routing.expression.ExpressionParser; + +import java.util.List; + +/** + * Metadata router + * + * Supports two modes: + * 1. Single expression: version >= 2.0 (AND logic) + * 2. OR logic expression: (version >= 2.0) | (env = dev) | (region = cn-bj) (OR logic) + * + * Note: AND logic is implemented by configuring multiple MetadataRouters + */ +public class MetadataRouter extends AbstractStateRouter { + + private volatile String expression; + + /** + * Default constructor + */ + public MetadataRouter() { + super("MetadataRouter", false); + this.expression = RoutingProperties.getMetadataRouterExpression(); + } + + /** + * Named instance constructor + * @param routerName router name + */ + public MetadataRouter(String routerName) { + super(routerName, false); + this.expression = RoutingProperties.getMetadataRouterExpression(routerName); + } + + @Override + protected BitList doRoute(BitList servers, RoutingContext ctx) { + // Create a local copy to ensure consistency during method execution + String currentExpression = this.expression; + + // If expression is empty or contains only spaces, return original server list directly + if (currentExpression == null || currentExpression.trim().isEmpty()) { + return servers; + } + + // Parse expression + List matchers = ExpressionParser.parse(currentExpression); + + // Check if it's an OR expression + if (ExpressionParser.isOrExpression(currentExpression)) { + // OR logic: any condition satisfied is sufficient + return servers.filter(server -> matchers.stream().anyMatch(m -> m.match(server, ctx))); + } else { + // Single expression: all conditions must be satisfied (though there's only one condition) + return servers.filter(server -> matchers.stream().allMatch(m -> m.match(server, ctx))); + } + } + + /** + * Set expression + * @param expression expression + */ + public void setExpression(String expression) { + this.expression = expression; + } + + /** + * Get expression + * @return expression + */ + public String getExpression() { + return expression; + } + + @Override + public String buildSnapshot() { + return String.format( + "MetadataRouter: expression=%s, isOr=%s", expression, ExpressionParser.isOrExpression(expression)); + } +} diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/RegionRouter.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/RegionRouter.java new file mode 100644 index 00000000000..fa2c2195bf4 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/routing/router/RegionRouter.java @@ -0,0 +1,154 @@ +/* + * 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.discovery.routing.router; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.BitList; +import org.apache.seata.discovery.routing.RoutingContext; +import org.apache.seata.discovery.routing.config.RoutingProperties; +import org.apache.seata.discovery.routing.region.GeoLocation; +import org.apache.seata.discovery.routing.region.ServerWithDistance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Region router + * Routes based on geographic region or distance + */ +public class RegionRouter extends AbstractStateRouter { + + private static final Logger LOGGER = LoggerFactory.getLogger(RegionRouter.class); + + private final int regionTopN; + + /** + * Default constructor + */ + public RegionRouter() { + super("RegionRouter", true); + this.regionTopN = RoutingProperties.getRegionRouterTopN(); + } + + @Override + protected BitList doRoute(BitList servers, RoutingContext ctx) { + // Get client location information + GeoLocation geoLocation = getClientLocation(ctx); + + // If client location cannot be obtained, skip this router + if (geoLocation == null) { + LOGGER.error("Failed to get client location, skipping region router"); + return servers; // Return original server list, do not filter + } + + // Calculate distance and sort + List> sorted = servers.toList().stream() + .map(server -> new ServerWithDistance<>(server, calculateDistance(geoLocation, server))) + .sorted(Comparator.comparingDouble(ServerWithDistance::getDistance)) + .limit(regionTopN) + .collect(Collectors.toList()); + + // Convert to BitList + List selectedServers = + sorted.stream().map(ServerWithDistance::getServer).collect(Collectors.toList()); + + return BitList.fromList(selectedServers); + } + + /** + * Get client location + * @param ctx routing context + * @return client location, or null if not available + */ + private GeoLocation getClientLocation(RoutingContext ctx) { + // Get client location information from context + Object clientLat = ctx.getAttribute("clientLat"); + Object clientLng = ctx.getAttribute("clientLng"); + + if (clientLat != null && clientLng != null) { + try { + return new GeoLocation( + Double.parseDouble(clientLat.toString()), Double.parseDouble(clientLng.toString())); + } catch (NumberFormatException e) { + LOGGER.error("Invalid client location format: lat={}, lng={}", clientLat, clientLng, e); + return null; + } + } + + // Cannot get location information, return null + LOGGER.warn("Client location not found in routing context"); + return null; + } + + /** + * Calculate distance + * @param geoLocation client location + * @param server server instance + * @return distance + */ + private double calculateDistance(GeoLocation geoLocation, ServiceInstance server) { + // Get location information from server metadata + Object serverLat = server.getMetadata().get("lat"); + Object serverLng = server.getMetadata().get("lng"); + + if (serverLat != null && serverLng != null) { + try { + GeoLocation serverLocation = new GeoLocation( + Double.parseDouble(serverLat.toString()), Double.parseDouble(serverLng.toString())); + return calculateHaversineDistance(geoLocation, serverLocation); + } catch (NumberFormatException e) { + LOGGER.warn("Invalid server location format: lat={}, lng={}", serverLat, serverLng); + return Double.MAX_VALUE; + } + } + + // If no location information, return max distance + LOGGER.debug("No location metadata found for server"); + return Double.MAX_VALUE; + } + + /** + * Calculate Haversine distance between two points + * @param loc1 location 1 + * @param loc2 location 2 + * @return distance (km) + */ + private double calculateHaversineDistance(GeoLocation loc1, GeoLocation loc2) { + final double r = 6371; // Earth radius (km) + + double latDistance = Math.toRadians(loc2.getLatitude() - loc1.getLatitude()); + double lngDistance = Math.toRadians(loc2.getLongitude() - loc1.getLongitude()); + + double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + + Math.cos(Math.toRadians(loc1.getLatitude())) + * Math.cos(Math.toRadians(loc2.getLatitude())) + * Math.sin(lngDistance / 2) + * Math.sin(lngDistance / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return r * c; + } + + @Override + public String buildSnapshot() { + return String.format("RegionRouter: regionTopN=%d", regionTopN); + } +} diff --git a/discovery/seata-discovery-core/src/main/resources/META-INF/services/org.apache.seata.discovery.routing.StateRouter b/discovery/seata-discovery-core/src/main/resources/META-INF/services/org.apache.seata.discovery.routing.StateRouter new file mode 100644 index 00000000000..1215986dce3 --- /dev/null +++ b/discovery/seata-discovery-core/src/main/resources/META-INF/services/org.apache.seata.discovery.routing.StateRouter @@ -0,0 +1,18 @@ +# +# 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. +# +org.apache.seata.discovery.routing.router.RegionRouter +org.apache.seata.discovery.routing.router.MetadataRouter \ No newline at end of file diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/config/ConfigurationFactoryTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/config/ConfigurationFactoryTest.java deleted file mode 100644 index 8b205fdbbc2..00000000000 --- a/discovery/seata-discovery-core/src/test/java/org/apache/seata/config/ConfigurationFactoryTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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; - -import org.apache.seata.discovery.loadbalance.ConsistentHashLoadBalance; -import org.apache.seata.discovery.loadbalance.LoadBalanceFactory; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class ConfigurationFactoryTest { - - @Test - void getInstance() { - Configuration configuration = ConfigurationFactory.getInstance(); - // check singleton - Assertions.assertEquals( - configuration.getClass().getName(), - ConfigurationFactory.getInstance().getClass().getName()); - } - - @Test - void getLoadBalance() { - Configuration configuration = ConfigurationFactory.getInstance(); - String loadBalanceType = configuration.getConfig(LoadBalanceFactory.LOAD_BALANCE_TYPE); - int virtualNode = configuration.getInt(ConsistentHashLoadBalance.LOAD_BALANCE_CONSISTENT_HASH_VIRTUAL_NODES); - Assertions.assertEquals("XID", loadBalanceType); - Assertions.assertEquals(10, virtualNode); - } -} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/loadbalance/LoadBalanceFactoryTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/loadbalance/LoadBalanceFactoryTest.java index 29d510a2209..3d60ef782a4 100644 --- a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/loadbalance/LoadBalanceFactoryTest.java +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/loadbalance/LoadBalanceFactoryTest.java @@ -16,115 +16,47 @@ */ package org.apache.seata.discovery.loadbalance; -import org.apache.seata.discovery.registry.RegistryFactory; -import org.apache.seata.discovery.registry.RegistryService; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; - -import static org.apache.seata.common.DefaultValues.DEFAULT_TX_GROUP; +import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; /** * The type Load balance factory test. - * */ public class LoadBalanceFactoryTest { - private static final String XID = "XID"; + private MockedStatic mockedConfigurationFactory; - /** - * Test get registry. - * - * @param loadBalance the load balance - * @throws Exception the exception - */ - @ParameterizedTest - @MethodSource("instanceProvider") - @Disabled - public void testGetRegistry(LoadBalance loadBalance) throws Exception { - Assertions.assertNotNull(loadBalance); - RegistryService registryService = RegistryFactory.getInstance(); - InetSocketAddress address1 = new InetSocketAddress("127.0.0.1", 8091); - InetSocketAddress address2 = new InetSocketAddress("127.0.0.1", 8092); - registryService.register(address1); - registryService.register(address2); - List addressList = registryService.lookup(DEFAULT_TX_GROUP); - InetSocketAddress balanceAddress = loadBalance.select(addressList, XID); - Assertions.assertNotNull(balanceAddress); + @BeforeEach + public void setUp() { + mockedConfigurationFactory = mockStatic(ConfigurationFactory.class); } - /** - * Test get address. - * - * @param loadBalance the load balance - * @throws Exception the exception - */ - @ParameterizedTest - @MethodSource("instanceProvider") - @Disabled - public void testUnRegistry(LoadBalance loadBalance) throws Exception { - RegistryService registryService = RegistryFactory.getInstance(); - InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8091); - registryService.unregister(address); + @AfterEach + public void tearDown() { + if (mockedConfigurationFactory != null) { + mockedConfigurationFactory.close(); + } } - /** - * Test subscribe. - * - * @param loadBalance the load balance - * @throws Exception the exception - */ - @ParameterizedTest - @MethodSource("instanceProvider") - @Disabled - public void testSubscribe(LoadBalance loadBalance) throws Exception { - Assertions.assertNotNull(loadBalance); - RegistryService registryService = RegistryFactory.getInstance(); - InetSocketAddress address1 = new InetSocketAddress("127.0.0.1", 8091); - InetSocketAddress address2 = new InetSocketAddress("127.0.0.1", 8092); - registryService.register(address1); - registryService.register(address2); - List addressList = registryService.lookup(DEFAULT_TX_GROUP); - InetSocketAddress balanceAddress = loadBalance.select(addressList, XID); - Assertions.assertNotNull(balanceAddress); - // wait trigger testUnRegistry - TimeUnit.SECONDS.sleep(30); - List addressList1 = registryService.lookup(DEFAULT_TX_GROUP); - Assertions.assertEquals(1, addressList1.size()); - } + @Test + public void testGetInstance() { + Configuration mockConfig = mock(Configuration.class); + mockedConfigurationFactory.when(ConfigurationFactory::getInstance).thenReturn(mockConfig); - /** - * Test get address. - * - * @param loadBalance the load balance - * @throws Exception the exception - */ - @ParameterizedTest - @MethodSource("instanceProvider") - public void testGetAddress(LoadBalance loadBalance) throws Exception { - Assertions.assertNotNull(loadBalance); - InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8091); - List addressList = new ArrayList<>(); - addressList.add(address); - InetSocketAddress balanceAddress = loadBalance.select(addressList, XID); - Assertions.assertEquals(address, balanceAddress); - } + when(mockConfig.getConfig(eq("client.loadBalance.type"), anyString())).thenReturn("XID"); - /** - * Instance provider object [ ] [ ]. - * - * @return the object [ ] [ ] - */ - static Stream instanceProvider() { LoadBalance loadBalance = LoadBalanceFactory.getInstance(); - return Stream.of(Arguments.of(loadBalance)); + assertInstanceOf(XIDLoadBalance.class, loadBalance); } } diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/loadbalance/LoadBalanceTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/loadbalance/LoadBalanceTest.java index 8eac1190f06..5079fbe9c7e 100644 --- a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/loadbalance/LoadBalanceTest.java +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/loadbalance/LoadBalanceTest.java @@ -16,8 +16,8 @@ */ package org.apache.seata.discovery.loadbalance; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.rpc.RpcStatus; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -25,6 +25,7 @@ import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -32,9 +33,11 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; -/** - * Created by guoyao on 2019/2/14. - */ +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + public class LoadBalanceTest { private static final String XID = "XID"; @@ -42,155 +45,200 @@ public class LoadBalanceTest { /** * Test random load balance select. * - * @param addresses the addresses + * @param instances the instances */ @ParameterizedTest - @MethodSource("addressProvider") - public void testRandomLoadBalance_select(List addresses) { + @MethodSource("instanceProvider") + public void testRandomLoadBalance_select(List instances) throws Exception { int runs = 10000; - Map counter = getSelectedCounter(runs, addresses, new RandomLoadBalance()); - for (InetSocketAddress address : counter.keySet()) { - Long count = counter.get(address).get(); - Assertions.assertTrue(count > 0, "selecte one time at last"); + Map counter = getSelectedCounter(runs, instances, new RandomLoadBalance()); + for (ServiceInstance instance : counter.keySet()) { + Long count = counter.get(instance).get(); + assertTrue(count > 0, "selecte one time at last"); } } /** - * Test round robin load balance select. + * Test round-robin load balance select. * - * @param addresses the addresses + * @param instances the instances */ @ParameterizedTest - @MethodSource("addressProvider") - public void testRoundRobinLoadBalance_select(List addresses) { + @MethodSource("instanceProvider") + public void testRoundRobinLoadBalance_select(List instances) throws Exception { int runs = 10000; - Map counter = getSelectedCounter(runs, addresses, new RoundRobinLoadBalance()); - for (InetSocketAddress address : counter.keySet()) { - Long count = counter.get(address).get(); - Assertions.assertTrue(Math.abs(count - runs / (0f + addresses.size())) < 1f, "abs diff shoud < 1"); + Map counter = getSelectedCounter(runs, instances, new RoundRobinLoadBalance()); + for (ServiceInstance instance : counter.keySet()) { + Long count = counter.get(instance).get(); + assertTrue(Math.abs(count - runs / (0f + instances.size())) < 1f, "abs diff shoud < 1"); } } /** * Test xid load load balance select. * - * @param addresses the addresses + * @param instances the instances */ @ParameterizedTest - @MethodSource("addressProvider") - public void testXIDLoadBalance_select(List addresses) throws Exception { + @MethodSource("instanceProvider") + public void testXIDLoadBalance_select(List instances) throws Exception { XIDLoadBalance loadBalance = new XIDLoadBalance(); // ipv4 - InetSocketAddress inetSocketAddress = loadBalance.select(addresses, "127.0.0.1:8092:123456"); - Assertions.assertNotNull(inetSocketAddress); + ServiceInstance serviceInstance = loadBalance.select(instances, "127.0.0.1:8092:123456"); + assertNotNull(serviceInstance); // ipv6 - inetSocketAddress = loadBalance.select(addresses, "2000:0000:0000:0000:0001:2345:6789:abcd:8092:123456"); - Assertions.assertNotNull(inetSocketAddress); + serviceInstance = loadBalance.select(instances, "2000:0000:0000:0000:0001:2345:6789:abcd:8092:123456"); + assertNotNull(serviceInstance); // test not found tc channel - inetSocketAddress = loadBalance.select(addresses, "127.0.0.1:8199:123456"); - Assertions.assertNotEquals(inetSocketAddress.getPort(), 8199); + serviceInstance = loadBalance.select(instances, "127.0.0.1:8199:123456"); + assertNotEquals(serviceInstance.getAddress().getPort(), 8199); } /** - * Test consistent hash load load balance select. + * Test consistent hash load balance select. * - * @param addresses the addresses + * @param instances the instances */ @ParameterizedTest - @MethodSource("addressProvider") - public void testConsistentHashLoadBalance_select(List addresses) { + @MethodSource("instanceProvider") + public void testConsistentHashLoadBalance_select(List instances) throws Exception { int runs = 10000; int selected = 0; ConsistentHashLoadBalance loadBalance = new ConsistentHashLoadBalance(); - Map counter = getSelectedCounter(runs, addresses, loadBalance); - for (InetSocketAddress address : counter.keySet()) { - if (counter.get(address).get() > 0) { + Map counter = getSelectedCounter(runs, instances, loadBalance); + for (ServiceInstance instance : counter.keySet()) { + if (counter.get(instance).get() > 0) { selected++; } } - Assertions.assertEquals(1, selected, "selected must be equal to 1"); + assertEquals(1, selected, "selected must be equal to 1"); } /** * Test cached consistent hash load balance select. * - * @param addresses the addresses + * @param instances the instances */ @ParameterizedTest - @MethodSource("addressProvider") - public void testCachedConsistentHashLoadBalance_select(List addresses) throws Exception { + @MethodSource("instanceProvider") + public void testCachedConsistentHashLoadBalance_select(List instances) throws Exception { ConsistentHashLoadBalance loadBalance = new ConsistentHashLoadBalance(); - List addresses1 = new ArrayList<>(addresses); - loadBalance.select(addresses1, XID); + List instances1 = new ArrayList<>(instances); + loadBalance.select(instances1, XID); Object o1 = getConsistentHashSelectorByReflect(loadBalance); - List addresses2 = new ArrayList<>(addresses); - loadBalance.select(addresses2, XID); + List instances2 = new ArrayList<>(instances); + loadBalance.select(instances2, XID); Object o2 = getConsistentHashSelectorByReflect(loadBalance); - Assertions.assertEquals(o1, o2); + assertEquals(o1, o2); - List addresses3 = new ArrayList<>(addresses); - addresses3.remove(ThreadLocalRandom.current().nextInt(addresses.size())); - loadBalance.select(addresses3, XID); + List instances3 = new ArrayList<>(instances); + instances3.remove(ThreadLocalRandom.current().nextInt(instances.size())); + loadBalance.select(instances3, XID); Object o3 = getConsistentHashSelectorByReflect(loadBalance); - Assertions.assertNotEquals(o1, o3); + assertNotEquals(o1, o3); } /** * Test least active load balance select. * - * @param addresses the addresses + * @param instances the instances */ @ParameterizedTest - @MethodSource("addressProvider") - public void testLeastActiveLoadBalance_select(List addresses) throws Exception { + @MethodSource("instanceProvider") + public void testLeastActiveLoadBalance_select(List instances) throws Exception { int runs = 10000; - int size = addresses.size(); + int size = instances.size(); for (int i = 0; i < size - 1; i++) { - RpcStatus.beginCount(addresses.get(i).toString()); + RpcStatus.beginCount(instances.get(i).getAddress().toString()); } - InetSocketAddress socketAddress = addresses.get(size - 1); + ServiceInstance targetInstance = instances.get(size - 1); LoadBalance loadBalance = new LeastActiveLoadBalance(); for (int i = 0; i < runs; i++) { - InetSocketAddress selectAddress = loadBalance.select(addresses, XID); - Assertions.assertEquals(selectAddress, socketAddress); + ServiceInstance selectInstance = loadBalance.select(instances, XID); + assertEquals(selectInstance, targetInstance); } - RpcStatus.beginCount(socketAddress.toString()); - RpcStatus.beginCount(socketAddress.toString()); - Map counter = getSelectedCounter(runs, addresses, loadBalance); - for (InetSocketAddress address : counter.keySet()) { - Long count = counter.get(address).get(); - if (address == socketAddress) { - Assertions.assertEquals(count, 0); + RpcStatus.beginCount(targetInstance.getAddress().toString()); + RpcStatus.beginCount(targetInstance.getAddress().toString()); + Map counter = getSelectedCounter(runs, instances, loadBalance); + for (ServiceInstance instance : counter.keySet()) { + Long count = counter.get(instance).get(); + if (instance == targetInstance) { + assertEquals(count, 0); } else { - Assertions.assertTrue(count > 0); + assertTrue(count > 0); } } } /** - * Gets selected counter. + * Test weighted random load balance select with instances without weights. + * Should downgrade to random load balancing. * - * @param runs the runs - * @param addresses the addresses - * @param loadBalance the load balance - * @return the selected counter + * @param instances the instances without weights */ - public Map getSelectedCounter( - int runs, List addresses, LoadBalance loadBalance) { - Assertions.assertNotNull(loadBalance); - Map counter = new ConcurrentHashMap<>(); - for (InetSocketAddress address : addresses) { - counter.put(address, new AtomicLong(0)); + @ParameterizedTest + @MethodSource("instanceProvider") + public void testWeightedRandomLoadBalance_selectWithoutWeights(List instances) throws Exception { + int runs = 10000; + Map counter = getSelectedCounter(runs, instances, new WeightedRandomLoadBalance()); + + // Verify all instances are selected roughly equally (random distribution) + for (ServiceInstance instance : counter.keySet()) { + Long count = counter.get(instance).get(); + assertTrue(count > 0); + + // In random distribution, each instance should be selected roughly 1/n times + double expectedCount = runs / (double) instances.size(); + double actualCount = count; + double tolerance = expectedCount * 0.2; // 20% tolerance + assertTrue(Math.abs(actualCount - expectedCount) < tolerance); } - try { - for (int i = 0; i < runs; i++) { - InetSocketAddress selectAddress = loadBalance.select(addresses, XID); - counter.get(selectAddress).incrementAndGet(); - } - } catch (Exception e) { - // do nothing + } + + /** + * Test weighted random load balance select with weighted instances. + * Verifies that instances with higher weights are selected more frequently. + * + * @param instances the instances with weights configured + */ + @ParameterizedTest + @MethodSource("weightedInstanceProvider") + public void testWeightedRandomLoadBalance_selectWithWeights(List instances) throws Exception { + int runs = 10000; + Map counter = getSelectedCounter(runs, instances, new WeightedRandomLoadBalance()); + + // Verify all instances are selected at least once + for (ServiceInstance instance : counter.keySet()) { + Long count = counter.get(instance).get(); + assertTrue(count > 0); + } + + // Verify that instances with higher weights are selected more frequently + verifyWeightedDistribution(instances, counter); + } + + /** + * Get the selection count for each ServiceInstance after running the load balancing algorithm multiple times. + * + * @param runs the number of times to perform selection + * @param instances the list of service instances to select from + * @param loadBalance the load balancing strategy to use + * @return a map where the key is the ServiceInstance and the value is the number of times it was selected + */ + public Map getSelectedCounter( + int runs, List instances, LoadBalance loadBalance) throws Exception { + assertNotNull(loadBalance); + Map counter = new ConcurrentHashMap<>(); + for (ServiceInstance instance : instances) { + counter.put(instance, new AtomicLong(0)); + } + + for (int i = 0; i < runs; i++) { + ServiceInstance selectInstance = loadBalance.select(instances, XID); + counter.get(selectInstance).incrementAndGet(); } + return counter; } @@ -204,24 +252,111 @@ public Object getConsistentHashSelectorByReflect(ConsistentHashLoadBalance loadB Field selectorWrapperField = ConsistentHashLoadBalance.class.getDeclaredField("selectorWrapper"); selectorWrapperField.setAccessible(true); Object selectWrapper = selectorWrapperField.get(loadBalance); - Assertions.assertNotNull(selectWrapper); + assertNotNull(selectWrapper); Field selectorField = selectWrapper.getClass().getDeclaredField("selector"); selectorField.setAccessible(true); return selectorField.get(selectWrapper); } /** - * Address provider object [ ] [ ]. + * Provides a stream of service instance lists for parameterized tests. * - * @return Stream> + * @return Stream> service instance lists */ - static Stream> addressProvider() { + static Stream> instanceProvider() { return Stream.of(Arrays.asList( - new InetSocketAddress("127.0.0.1", 8091), - new InetSocketAddress("127.0.0.1", 8092), - new InetSocketAddress("127.0.0.1", 8093), - new InetSocketAddress("127.0.0.1", 8094), - new InetSocketAddress("127.0.0.1", 8095), - new InetSocketAddress("2000:0000:0000:0000:0001:2345:6789:abcd", 8092))); + new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)), + new ServiceInstance(new InetSocketAddress("127.0.0.1", 8092)), + new ServiceInstance(new InetSocketAddress("127.0.0.1", 8093)), + new ServiceInstance(new InetSocketAddress("127.0.0.1", 8094)), + new ServiceInstance(new InetSocketAddress("127.0.0.1", 8095)), + new ServiceInstance(new InetSocketAddress("2000:0000:0000:0000:0001:2345:6789:abcd", 8092)))); + } + + /** + * Provides a stream of weighted service instance lists for parameterized tests. + * + * @return Stream> weighted service instance lists + */ + static Stream> weightedInstanceProvider() { + return Stream.of(Arrays.asList( + createWeightedInstance("127.0.0.1", 8091, 2), + createWeightedInstance("127.0.0.1", 8092, 3), + createWeightedInstance("127.0.0.1", 8093, 1), + createWeightedInstance("127.0.0.1", 8094, 4), + createWeightedInstance("127.0.0.1", 8095, 2))); + } + + /** + * Verify that the selection distribution roughly matches the configured weights. + * Instances with higher weights should be selected more frequently. + * + * @param instances the list of service instances + * @param counter the selection count for each instance + */ + private void verifyWeightedDistribution(List instances, Map counter) { + // Calculate total weight and expected selection ratios + int totalWeight = 0; + Map weights = new HashMap<>(); + + for (ServiceInstance instance : instances) { + int weight = getWeightFromInstance(instance); + weights.put(instance, weight); + totalWeight += weight; + } + + if (totalWeight > 0) { + // Verify that instances with higher weights are selected more frequently + for (ServiceInstance instance : instances) { + int weight = weights.get(instance); + if (weight > 0) { + double expectedRatio = (double) weight / totalWeight; + double actualRatio = (double) counter.get(instance).get() + / counter.values().stream() + .mapToLong(AtomicLong::get) + .sum(); + + // Allow 30% tolerance for random distribution + double tolerance = expectedRatio * 0.3; + assertTrue(Math.abs(actualRatio - expectedRatio) < tolerance); + } + } + } + } + + /** + * Extract weight from ServiceInstance metadata. + * + * @param instance the service instance + * @return the weight value, default is 1 if not configured + */ + private int getWeightFromInstance(ServiceInstance instance) { + if (instance.getMetadata() != null && instance.getMetadata().containsKey("weight")) { + Object weightObj = instance.getMetadata().get("weight"); + if (weightObj instanceof Number) { + return Math.max(0, ((Number) weightObj).intValue()); + } else if (weightObj instanceof String) { + try { + return Math.max(0, Integer.parseInt((String) weightObj)); + } catch (NumberFormatException e) { + return 1; + } + } + } + return 1; + } + + /** + * Helper method to create a weighted service instance. + * + * @param host the host + * @param port the port + * @param weight the weight value + * @return ServiceInstance with weight configured in metadata + */ + private static ServiceInstance createWeightedInstance(String host, int port, int weight) { + Map metadata = new HashMap<>(); + metadata.put("weight", weight); + return new ServiceInstance(new InetSocketAddress(host, port), metadata); } } diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/FileRegistryProviderTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/FileRegistryProviderTest.java new file mode 100644 index 00000000000..f196c4fcf2f --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/FileRegistryProviderTest.java @@ -0,0 +1,32 @@ +/* + * 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.discovery.registry; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class FileRegistryProviderTest { + + @Test + void testProvide() { + RegistryProvider provider = new FileRegistryProvider(); + RegistryService service = provider.provide(); + + Assertions.assertNotNull(service); + Assertions.assertInstanceOf(FileRegistryServiceImpl.class, service); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/FileRegistryServiceImplTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/FileRegistryServiceImplTest.java new file mode 100644 index 00000000000..ebdc5db4868 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/FileRegistryServiceImplTest.java @@ -0,0 +1,161 @@ +/* + * 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.discovery.registry; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.config.ConfigChangeListener; +import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationFactory; +import org.apache.seata.config.exception.ConfigNotFoundException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.ExecutorService; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +class FileRegistryServiceImplTest { + + private static final String TEST_GROUP = "testGroup"; + private static final String TEST_CLUSTER = "default_tx_group"; + + private final ServiceInstance serviceInstance1 = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8080)); + private final ServiceInstance serviceInstance2 = new ServiceInstance(new InetSocketAddress("127.0.0.2", 8080)); + + private static FileRegistryServiceImpl fileRegistryService; + + @BeforeAll + public static void setUp() { + System.setProperty("service.vgroupMapping.testGroup", TEST_GROUP); + fileRegistryService = FileRegistryServiceImpl.getInstance(); + } + + @Test + public void testEmptyMethod() throws Exception { + fileRegistryService.register(serviceInstance1); + fileRegistryService.unregister(serviceInstance1); + + ConfigChangeListener configChangeListener = new ConfigChangeListener() { + @Override + public ExecutorService getExecutor() { + return null; + } + + @Override + public void receiveConfigInfo(String configInfo) {} + }; + + fileRegistryService.subscribe(TEST_CLUSTER, configChangeListener); + fileRegistryService.unsubscribe(TEST_CLUSTER, configChangeListener); + + fileRegistryService.close(); + } + + /** + * Tests the getServiceGroup method to ensure it retrieves the correct service group name + */ + @Test + public void testGetServiceGroup() { + String result = fileRegistryService.getServiceGroup(TEST_GROUP); + assertEquals(TEST_GROUP, result); + } + + /** + * Tests the aliveLookup and refreshAliveLookup methods. + */ + @Test + public void testAliveLookupAndRefreshAliveLookup() { + RegistryService.CURRENT_INSTANCE_MAP.clear(); + List serviceInstances = Collections.singletonList(serviceInstance1); + + // Test empty list + List result = fileRegistryService.aliveLookup(TEST_GROUP); + assertTrue(result.isEmpty()); + + // Test data is available + fileRegistryService.refreshAliveLookup(TEST_GROUP, serviceInstances); + result = fileRegistryService.aliveLookup(TEST_GROUP); + assertEquals(serviceInstances, result); + } + + /** + * Tests the removeOfflineAddressesIfNecessary method when there is no intersection + */ + @Test + public void testRemoveOfflineAddressesIfNecessaryWithNoIntersection() { + List currentInstances = Collections.singletonList(serviceInstance1); + Collection newInstances = Collections.singletonList(serviceInstance2); + + fileRegistryService.refreshAliveLookup(TEST_GROUP, currentInstances); + fileRegistryService.removeOfflineAddressesIfNecessary( + TEST_GROUP, fileRegistryService.getServiceGroup(TEST_GROUP), newInstances); + + List result = fileRegistryService.aliveLookup(TEST_GROUP); + assertFalse(result.isEmpty()); + assertEquals(currentInstances, result); + } + + /** + * Tests the removeOfflineAddressesIfNecessary method when there is an intersection + */ + @Test + public void testRemoveOfflineAddressesIfNecessaryWithIntersection() { + List currentInstances = Arrays.asList(serviceInstance1, serviceInstance2); + Collection newInstances = Collections.singletonList(serviceInstance1); + + fileRegistryService.refreshAliveLookup(TEST_GROUP, currentInstances); + fileRegistryService.removeOfflineAddressesIfNecessary( + TEST_GROUP, fileRegistryService.getServiceGroup(TEST_GROUP), newInstances); + + List result = fileRegistryService.aliveLookup(TEST_GROUP); + assertFalse(result.isEmpty()); + assertEquals(new HashSet<>(newInstances), new HashSet<>(result)); + } + + @Test + public void testLookup() throws Exception { + List lookup = fileRegistryService.lookup(TEST_CLUSTER); + assertEquals(serviceInstance1, lookup.get(0)); + + // Set the getServiceGroup() to return null by reflection + Configuration mockConfig = Mockito.mock(Configuration.class); + when(mockConfig.getConfig("service.vgroupMapping.default_tx_group")).thenReturn(null); + Field configField = ConfigurationFactory.class.getDeclaredField("instance"); + configField.setAccessible(true); + Object originalConfigInstance = configField.get(null); + configField.set(null, mockConfig); + + try { + assertThrows(ConfigNotFoundException.class, () -> fileRegistryService.lookup(TEST_CLUSTER)); + } finally { + // reset ConfigurationFactory.instance + configField.set(null, originalConfigInstance); + } + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MultiRegistryFactoryTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MultiRegistryFactoryTest.java index 3e82208ca9e..c39c996bb0c 100644 --- a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MultiRegistryFactoryTest.java +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MultiRegistryFactoryTest.java @@ -23,6 +23,7 @@ import org.apache.seata.common.ConfigurationKeys; import org.apache.seata.common.Constants; import org.apache.seata.common.exception.NotSupportYetException; +import org.apache.seata.discovery.registry.mock.MockNacosRegistryService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MockNacosRegistryProvider.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/mock/MockNacosRegistryProvider.java similarity index 86% rename from discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MockNacosRegistryProvider.java rename to discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/mock/MockNacosRegistryProvider.java index c9b769b88ef..c1bed823f1d 100644 --- a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MockNacosRegistryProvider.java +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/mock/MockNacosRegistryProvider.java @@ -14,9 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.seata.discovery.registry; +package org.apache.seata.discovery.registry.mock; import org.apache.seata.common.loader.LoadLevel; +import org.apache.seata.discovery.registry.RegistryProvider; +import org.apache.seata.discovery.registry.RegistryService; /** * the mock nacos RegistryProvider diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MockNacosRegistryService.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/mock/MockNacosRegistryService.java similarity index 77% rename from discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MockNacosRegistryService.java rename to discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/mock/MockNacosRegistryService.java index 03aaabf1dc8..c5c4efdff33 100644 --- a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/MockNacosRegistryService.java +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/registry/mock/MockNacosRegistryService.java @@ -14,9 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.seata.discovery.registry; +package org.apache.seata.discovery.registry.mock; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.registry.RegistryService; -import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; @@ -26,10 +28,10 @@ public class MockNacosRegistryService implements RegistryService { @Override - public void register(InetSocketAddress address) throws Exception {} + public void register(ServiceInstance address) throws Exception {} @Override - public void unregister(InetSocketAddress address) throws Exception {} + public void unregister(ServiceInstance address) throws Exception {} @Override public void subscribe(String cluster, Object listener) throws Exception {} @@ -38,7 +40,7 @@ public void subscribe(String cluster, Object listener) throws Exception {} public void unsubscribe(String cluster, Object listener) throws Exception {} @Override - public List lookup(String key) throws Exception { + public List lookup(String key) throws Exception { return new ArrayList<>(); } diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/BitListTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/BitListTest.java new file mode 100644 index 00000000000..2a550ad7ed2 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/BitListTest.java @@ -0,0 +1,85 @@ +/* + * 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.discovery.routing; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BitListTest { + + /** + * Test creating BitList from list - verify creation and size + */ + @Test + public void testFromList() { + List list = Arrays.asList("a", "b", "c"); + BitList bitList = BitList.fromList(list); + assertEquals(3, bitList.size()); + } + + /** + * Test converting to list - verify data integrity + */ + @Test + public void testToList() { + List original = Arrays.asList("a", "b", "c"); + BitList bitList = BitList.fromList(original); + List result = bitList.toList(); + assertEquals(original, result); + } + + /** + * Test filter functionality - verify conditional filtering + */ + @Test + public void testFilter() { + List original = Arrays.asList("a", "b", "c", "d"); + BitList bitList = BitList.fromList(original); + + BitList filtered = bitList.filter(item -> item.equals("a") || item.equals("c")); + assertEquals(2, filtered.size()); + assertTrue(filtered.toList().contains("a")); + assertTrue(filtered.toList().contains("c")); + } + + /** + * Test empty list - verify empty state check + */ + @Test + public void testIsEmpty() { + BitList emptyList = new BitList<>(Arrays.asList()); + assertTrue(emptyList.isEmpty()); + + BitList nonEmptyList = BitList.fromList(Arrays.asList("a")); + assertFalse(nonEmptyList.isEmpty()); + } + + /** + * Test size - verify element count + */ + @Test + public void testSize() { + BitList bitList = BitList.fromList(Arrays.asList("a", "b", "c")); + assertEquals(3, bitList.size()); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RouterSnapshotNodeTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RouterSnapshotNodeTest.java new file mode 100644 index 00000000000..9761aa8e63e --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RouterSnapshotNodeTest.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.discovery.routing; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RouterSnapshotNodeTest { + + /** + * Test constructor - create snapshot node + */ + @Test + public void testConstructor() { + String routerName = "test-router"; + int inputSize = 5; + int outputSize = 3; + List selectedServers = Arrays.asList("server1", "server2", "server3"); + String snapshot = "test-snapshot"; + + RouterSnapshotNode node = + new RouterSnapshotNode<>(routerName, inputSize, outputSize, selectedServers, snapshot); + + assertNotNull(node); + } + + /** + * Test getRouterName + */ + @Test + public void testGetRouterName() { + String routerName = "test-router"; + RouterSnapshotNode node = + new RouterSnapshotNode<>(routerName, 5, 3, Arrays.asList("server1"), "snapshot"); + + assertEquals(routerName, node.getRouterName()); + } + + /** + * Test getInputSize + */ + @Test + public void testGetInputSize() { + int inputSize = 10; + RouterSnapshotNode node = + new RouterSnapshotNode<>("router", inputSize, 5, Arrays.asList("server1"), "snapshot"); + + assertEquals(inputSize, node.getInputSize()); + } + + /** + * Test getOutputSize + */ + @Test + public void testGetOutputSize() { + int outputSize = 7; + RouterSnapshotNode node = + new RouterSnapshotNode<>("router", 10, outputSize, Arrays.asList("server1"), "snapshot"); + + assertEquals(outputSize, node.getOutputSize()); + } + + /** + * Test getSelectedServers + */ + @Test + public void testGetSelectedServers() { + List selectedServers = Arrays.asList("server1", "server2", "server3"); + RouterSnapshotNode node = new RouterSnapshotNode<>("router", 5, 3, selectedServers, "snapshot"); + + assertEquals(selectedServers, node.getSelectedServers()); + assertEquals(3, node.getSelectedServers().size()); + } + + /** + * Test getSnapshot + */ + @Test + public void testGetSnapshot() { + String snapshot = "test-snapshot-info"; + RouterSnapshotNode node = new RouterSnapshotNode<>("router", 5, 3, Arrays.asList("server1"), snapshot); + + assertEquals(snapshot, node.getSnapshot()); + } + + /** + * Test getTimestamp - verify timestamp accuracy + */ + @Test + public void testGetTimestamp() { + RouterSnapshotNode node = + new RouterSnapshotNode<>("router", 5, 3, Arrays.asList("server1"), "snapshot"); + + long timestamp = node.getTimestamp(); + assertTrue(timestamp > 0); + + // Timestamp should be within a reasonable range (a few seconds from now) + long currentTime = System.currentTimeMillis(); + assertTrue(Math.abs(timestamp - currentTime) < 1000); + } + + /** + * Test toString - verify formatted output + */ + @Test + public void testToString() { + String routerName = "test-router"; + int inputSize = 5; + int outputSize = 3; + List selectedServers = Arrays.asList("server1", "server2"); + String snapshot = "test-snapshot"; + + RouterSnapshotNode node = + new RouterSnapshotNode<>(routerName, inputSize, outputSize, selectedServers, snapshot); + + String result = node.toString(); + assertTrue(result.contains(routerName)); + assertTrue(result.contains(String.valueOf(inputSize))); + assertTrue(result.contains(String.valueOf(outputSize))); + assertTrue(result.contains(snapshot)); + assertTrue(result.contains("server1")); + assertTrue(result.contains("server2")); + } + + /** + * Test with empty selected server list - edge case + */ + @Test + public void testWithEmptySelectedServers() { + List emptyServers = Arrays.asList(); + RouterSnapshotNode node = new RouterSnapshotNode<>("router", 5, 0, emptyServers, "snapshot"); + + assertEquals(0, node.getOutputSize()); + assertEquals(0, node.getSelectedServers().size()); + } + + /** + * Test with null snapshot - edge case + */ + @Test + public void testWithNullSnapshot() { + RouterSnapshotNode node = new RouterSnapshotNode<>("router", 5, 3, Arrays.asList("server1"), null); + + assertNull(node.getSnapshot()); + } + + /** + * Test with Integer type - generic support + */ + @Test + public void testWithIntegerType() { + List selectedServers = Arrays.asList(1, 2, 3); + RouterSnapshotNode node = new RouterSnapshotNode<>("router", 5, 3, selectedServers, "snapshot"); + + assertEquals(selectedServers, node.getSelectedServers()); + assertEquals(3, node.getSelectedServers().size()); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RoutingContextTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RoutingContextTest.java new file mode 100644 index 00000000000..76a1c2d5799 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RoutingContextTest.java @@ -0,0 +1,59 @@ +/* + * 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.discovery.routing; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class RoutingContextTest { + + /** + * Test set and get attributes - verify basic attribute operations + */ + @Test + public void testSetAndGetAttribute() { + RoutingContext ctx = new RoutingContext(); + ctx.setAttribute("key1", "value1"); + ctx.setAttribute("key2", 123); + + assertEquals("value1", ctx.getAttribute("key1")); + assertEquals(123, ctx.getAttribute("key2")); + } + + /** + * Test get non-existent attribute - verify null return value + */ + @Test + public void testGetNonExistentAttribute() { + RoutingContext ctx = new RoutingContext(); + assertNull(ctx.getAttribute("non-existent")); + } + + /** + * Test overwrite attribute - verify attribute update mechanism + */ + @Test + public void testSetAttributeOverwrite() { + RoutingContext ctx = new RoutingContext(); + ctx.setAttribute("key", "value1"); + ctx.setAttribute("key", "value2"); + + assertEquals("value2", ctx.getAttribute("key")); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RoutingManagerTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RoutingManagerTest.java new file mode 100644 index 00000000000..0395f6d01be --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/RoutingManagerTest.java @@ -0,0 +1,63 @@ +/* + * 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.discovery.routing; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class RoutingManagerTest { + + /** + * Test singleton pattern - verify instance uniqueness + */ + @Test + public void testGetInstance() { + RoutingManager instance1 = RoutingManager.getInstance(); + RoutingManager instance2 = RoutingManager.getInstance(); + assertSame(instance1, instance2); + } + + /** + * Test empty server list - should return original empty list + */ + @Test + public void testFilterWithEmptyServers() { + RoutingManager manager = RoutingManager.getInstance(); + List servers = new ArrayList<>(); + + List result = manager.filter(servers); + assertEquals(servers, result); + } + + /** + * Test null server list - should return null + */ + @Test + public void testFilterWithNullServers() { + RoutingManager manager = RoutingManager.getInstance(); + + List result = manager.filter(null); + assertNull(result); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/DefaultRouterChainTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/DefaultRouterChainTest.java new file mode 100644 index 00000000000..2c9d1b12458 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/DefaultRouterChainTest.java @@ -0,0 +1,190 @@ +/* + * 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.discovery.routing.chain; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.RoutingContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class DefaultRouterChainTest { + + @BeforeEach + public void setUp() { + // Enable debug mode + System.setProperty("client.routing.debug", "true"); + } + + @AfterEach + public void tearDown() { + // Clean up debug mode settings + System.clearProperty("client.routing.debug"); + } + + /** + * Test constructor + */ + @Test + public void testConstructor() { + // Verify default order + DefaultRouterChain chain = new DefaultRouterChain(); + assertNotNull(chain); + + // Verify specified order construction + chain = new DefaultRouterChain("region-router,metadata-router"); + assertNotNull(chain); + } + + /** + * Test empty server list - should return original empty list + */ + @Test + public void testFilterAllWithEmptyServers() { + DefaultRouterChain chain = new DefaultRouterChain(); + List servers = new ArrayList<>(); + + List result = chain.filterAll(servers, new RoutingContext()); + assertEquals(servers, result); + } + + /** + * Test null server list - should return null + */ + @Test + public void testFilterAllWithNullServers() { + DefaultRouterChain chain = new DefaultRouterChain(); + + List result = chain.filterAll(null, new RoutingContext()); + assertNull(result); + } + + /** + * Test router order parsing + */ + @Test + public void testParseRouterOrder() { + // Verify basic parsing functionality + List order = DefaultRouterChain.parseRouterOrder("region-router,metadata-router"); + assertEquals(2, order.size()); + assertEquals("region-router", order.get(0)); + assertEquals("metadata-router", order.get(1)); + + // Verify whitespace handling + order = DefaultRouterChain.parseRouterOrder(" region-router , metadata-router "); + assertEquals(2, order.size()); + assertEquals("region-router", order.get(0)); + assertEquals("metadata-router", order.get(1)); + } + + /** + * Test debug mode - verify complete debug logging + */ + @Test + public void testDebugMode() { + // Create test servers + List servers = createTestServers(); + RoutingContext ctx = new RoutingContext(); + + // Set client location + ctx.setAttribute("clientLat", "39.9042"); + ctx.setAttribute("clientLng", "116.4074"); + + // Create router chain + DefaultRouterChain chain = new DefaultRouterChain("region-router,metadata-router"); + + // Execute routing (this will trigger debug logging, verified through log output) + List result = chain.filterAll(servers, ctx); + + // Verify result, verify debug mode is effective + assertNotNull(result); + } + + /** + * Test debug mode disabled scenario + */ + @Test + public void testDebugModeDisabled() { + // Temporarily disable debug mode + System.setProperty("client.routing.debug", "false"); + + try { + // Create test servers + List servers = createTestServers(); + RoutingContext ctx = new RoutingContext(); + + // Create router chain + DefaultRouterChain chain = new DefaultRouterChain("region-router,metadata-router"); + + // Execute routing (this won't trigger debug logging, verified by whether logs are output) + List result = chain.filterAll(servers, ctx); + + // Verify result + assertNotNull(result); + assertFalse(result.isEmpty()); + } finally { + // Restore debug mode + System.setProperty("client.routing.debug", "true"); + } + } + + /** + * Create test servers + */ + private List createTestServers() { + List servers = new ArrayList<>(); + + // Server 1: Beijing, version 2.0 + ServiceInstance server1 = createMockServer("server1", "39.9042", "116.4074", "2.0", "prod"); + servers.add(server1); + + // Server 2: Shanghai, version 1.5 + ServiceInstance server2 = createMockServer("server2", "31.2304", "121.4737", "1.5", "prod"); + servers.add(server2); + + // Server 3: Guangzhou, version 2.0 + ServiceInstance server3 = createMockServer("server3", "23.1291", "113.2644", "2.0", "prod"); + servers.add(server3); + + return servers; + } + + /** + * Create mock server + */ + private ServiceInstance createMockServer(String id, String lat, String lng, String version, String env) { + Map metadata = new HashMap<>(); + metadata.put("lat", lat); + metadata.put("lng", lng); + metadata.put("version", version); + metadata.put("env", env); + + InetSocketAddress address = new InetSocketAddress("localhost", 8080); + return new ServiceInstance(address, metadata); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/PrimaryBackupRouterChainTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/PrimaryBackupRouterChainTest.java new file mode 100644 index 00000000000..d3ed6c0fe36 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/PrimaryBackupRouterChainTest.java @@ -0,0 +1,79 @@ +/* + * 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.discovery.routing.chain; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.RoutingContext; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class PrimaryBackupRouterChainTest { + + /** + * Test default constructor + */ + @Test + public void testConstructor() { + PrimaryBackupRouterChain chain = new PrimaryBackupRouterChain(); + assertNotNull(chain); + } + + /** + * Test empty server list - should return original empty list + */ + @Test + public void testFilterAllWithEmptyServers() { + PrimaryBackupRouterChain chain = new PrimaryBackupRouterChain(); + List servers = new ArrayList<>(); + + List result = chain.filterAll(servers, new RoutingContext()); + assertEquals(servers, result); + } + + /** + * Test null server list - should return null + */ + @Test + public void testFilterAllWithNullServers() { + PrimaryBackupRouterChain chain = new PrimaryBackupRouterChain(); + + List result = chain.filterAll(null, new RoutingContext()); + assertNull(result); + } + + /** + * Test router order validation - valid and invalid configurations + */ + @Test + public void testIsValidRouterOrder() { + assertTrue(PrimaryBackupRouterChain.isValidRouterOrder("region-router,metadata-router")); + assertTrue(PrimaryBackupRouterChain.isValidRouterOrder("metadata-router-1,metadata-router-2")); + assertTrue(PrimaryBackupRouterChain.isValidRouterOrder("custom-router")); + + assertFalse(PrimaryBackupRouterChain.isValidRouterOrder("invalid-router")); + assertFalse(PrimaryBackupRouterChain.isValidRouterOrder("")); + assertFalse(PrimaryBackupRouterChain.isValidRouterOrder(null)); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/RouterChainUtilsTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/RouterChainUtilsTest.java new file mode 100644 index 00000000000..801563964cb --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/chain/RouterChainUtilsTest.java @@ -0,0 +1,44 @@ +/* + * 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.discovery.routing.chain; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RouterChainUtilsTest { + + /** + * Test router name validation - valid and invalid router names + */ + @Test + public void testIsValidRouterName() { + assertTrue(RouterChainUtils.isValidRouterName("region-router")); + assertTrue(RouterChainUtils.isValidRouterName("metadata-router")); + assertTrue(RouterChainUtils.isValidRouterName("metadata-router-1")); + assertTrue(RouterChainUtils.isValidRouterName("metadata-router-2")); + assertTrue(RouterChainUtils.isValidRouterName("custom-router")); + assertTrue(RouterChainUtils.isValidRouterName("custom-anything")); + + assertFalse(RouterChainUtils.isValidRouterName("invalid-router")); + assertFalse(RouterChainUtils.isValidRouterName("")); + assertFalse(RouterChainUtils.isValidRouterName(null)); + assertFalse(RouterChainUtils.isValidRouterName("router")); + assertFalse(RouterChainUtils.isValidRouterName("metadata")); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/expression/ConfigurableConditionMatcherTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/expression/ConfigurableConditionMatcherTest.java new file mode 100644 index 00000000000..3ff09b82774 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/expression/ConfigurableConditionMatcherTest.java @@ -0,0 +1,176 @@ +/* + * 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.discovery.routing.expression; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.RoutingContext; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ConfigurableConditionMatcherTest { + + /** + * Test constructor - verify condition parsing and exception handling + */ + @Test + public void testConstructor() { + ConfigurableConditionMatcher matcher = new ConfigurableConditionMatcher("version >= 2.3"); + assertTrue(matcher.toString().contains("key='version'")); + assertTrue(matcher.toString().contains("operator='>='")); + assertTrue(matcher.toString().contains("value='2.3'")); + + // Test invalid condition + assertThrows(IllegalArgumentException.class, () -> { + new ConfigurableConditionMatcher("invalid"); + }); + } + + /** + * Test string comparison - equals and not equals operations + */ + @Test + public void testStringComparison() { + ServiceInstance server = mock(ServiceInstance.class); + RoutingContext ctx = mock(RoutingContext.class); + Map metadata = new HashMap<>(); + metadata.put("env", "prod"); + when(server.getMetadata()).thenReturn(metadata); + + // Test equals + ConfigurableConditionMatcher matcher = new ConfigurableConditionMatcher("env = prod"); + assertTrue(matcher.match(server, ctx)); + + // Test not equals + matcher = new ConfigurableConditionMatcher("env != dev"); + assertTrue(matcher.match(server, ctx)); + + // Test no match + matcher = new ConfigurableConditionMatcher("env = dev"); + assertFalse(matcher.match(server, ctx)); + } + + /** + * Test numeric comparison - various comparison operators + */ + @Test + public void testNumericComparison() { + ServiceInstance server = mock(ServiceInstance.class); + RoutingContext ctx = mock(RoutingContext.class); + Map metadata = new HashMap<>(); + metadata.put("version", "2.3"); + when(server.getMetadata()).thenReturn(metadata); + + // Test greater than or equal + ConfigurableConditionMatcher matcher = new ConfigurableConditionMatcher("version >= 2.0"); + assertTrue(matcher.match(server, ctx)); + + // Test greater than + matcher = new ConfigurableConditionMatcher("version > 2.0"); + assertTrue(matcher.match(server, ctx)); + + // Test less than + matcher = new ConfigurableConditionMatcher("version < 3.0"); + assertTrue(matcher.match(server, ctx)); + + // Test less than or equal + matcher = new ConfigurableConditionMatcher("version <= 2.3"); + assertTrue(matcher.match(server, ctx)); + + // Test no match + matcher = new ConfigurableConditionMatcher("version > 3.0"); + assertFalse(matcher.match(server, ctx)); + } + + /** + * Test numeric precision - handle different numeric formats + */ + @Test + public void testNumericPrecision() { + ServiceInstance server = mock(ServiceInstance.class); + RoutingContext ctx = mock(RoutingContext.class); + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + when(server.getMetadata()).thenReturn(metadata); + + // Test precision issue: 1 = 1.0 + ConfigurableConditionMatcher matcher = new ConfigurableConditionMatcher("version = 1.0"); + assertTrue(matcher.match(server, ctx)); + + matcher = new ConfigurableConditionMatcher("version = 1"); + assertTrue(matcher.match(server, ctx)); + } + + /** + * Test mixed comparison - numeric and string comparison + */ + @Test + public void testMixedComparison() { + ServiceInstance server = mock(ServiceInstance.class); + RoutingContext ctx = mock(RoutingContext.class); + Map metadata = new HashMap<>(); + metadata.put("version", "2.3"); + metadata.put("env", "prod"); + when(server.getMetadata()).thenReturn(metadata); + + // Numeric comparison + ConfigurableConditionMatcher matcher = new ConfigurableConditionMatcher("version >= 2.0"); + assertTrue(matcher.match(server, ctx)); + + // String comparison + matcher = new ConfigurableConditionMatcher("env = prod"); + assertTrue(matcher.match(server, ctx)); + } + + /** + * Test missing metadata scenario - servers without corresponding metadata should not pass through + */ + @Test + public void testMissingMetadata() { + ServiceInstance server = mock(ServiceInstance.class); + RoutingContext ctx = mock(RoutingContext.class); + Map metadata = new HashMap<>(); + when(server.getMetadata()).thenReturn(metadata); + + // Should not pass through when metadata is missing + ConfigurableConditionMatcher matcher = new ConfigurableConditionMatcher("version >= 2.0"); + assertFalse(matcher.match(server, ctx)); + } + + /** + * Test invalid numeric comparison - perform numeric comparison on non-numeric values + */ + @Test + public void testInvalidNumericComparison() { + ServiceInstance server = mock(ServiceInstance.class); + RoutingContext ctx = mock(RoutingContext.class); + Map metadata = new HashMap<>(); + metadata.put("env", "prod"); + when(server.getMetadata()).thenReturn(metadata); + + // Should return false when performing numeric comparison on non-numeric values + ConfigurableConditionMatcher matcher = new ConfigurableConditionMatcher("env >= 2.0"); + assertFalse(matcher.match(server, ctx)); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/expression/ExpressionParserTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/expression/ExpressionParserTest.java new file mode 100644 index 00000000000..89ee5323599 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/expression/ExpressionParserTest.java @@ -0,0 +1,153 @@ +/* + * 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.discovery.routing.expression; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ExpressionParserTest { + + /** + * Test valid single expression - verify basic parsing functionality + */ + @Test + public void testParseValidSingleExpression() { + String expression = "version >= 2.3"; + List matchers = ExpressionParser.parse(expression); + assertEquals(1, matchers.size()); + assertFalse(ExpressionParser.isOrExpression(expression)); + } + + /** + * Test valid complex OR expression - verify multi-condition parsing + */ + @Test + public void testParseValidComplexOrExpression() { + String expression = "(version >= 2.3) | (env = dev) | (region = cn-bj)"; + List matchers = ExpressionParser.parse(expression); + assertEquals(3, matchers.size()); + assertTrue(ExpressionParser.isOrExpression(expression)); + } + + /** + * Test empty expression - verify empty value handling + */ + @Test + public void testParseEmptyExpression() { + List matchers = ExpressionParser.parse(""); + assertTrue(matchers.isEmpty()); + } + + /** + * Test null expression - verify null value handling + */ + @Test + public void testParseNullExpression() { + List matchers = ExpressionParser.parse(null); + assertTrue(matchers.isEmpty()); + } + + /** + * Test OR expression detection - verify OR logic recognition + */ + @Test + public void testIsOrExpression() { + assertTrue(ExpressionParser.isOrExpression("(version >= 2.3) | (env = dev)")); + assertFalse(ExpressionParser.isOrExpression("version >= 2.3")); + assertFalse(ExpressionParser.isOrExpression("(version >= 2.3)")); + } + + /** + * Test valid expressions - verify various valid formats + */ + @Test + public void testIsValidExpression() { + assertTrue(ExpressionParser.isValidExpression("version >= 2.3")); + assertTrue(ExpressionParser.isValidExpression("(version >= 2.3) | (env = dev)")); + assertTrue(ExpressionParser.isValidExpression("env = prod")); + assertTrue(ExpressionParser.isValidExpression("version != 1.0")); + assertTrue(ExpressionParser.isValidExpression("_version > 2.0")); + assertTrue(ExpressionParser.isValidExpression("1version >= 2.3")); // Allow numbers at start + assertTrue(ExpressionParser.isValidExpression("key-1 = value")); // Allow hyphens + assertTrue(ExpressionParser.isValidExpression("my_key = value")); // Allow underscores + + assertFalse(ExpressionParser.isValidExpression(">= 2.3")); // Missing key name + assertFalse(ExpressionParser.isValidExpression("version")); // Missing operator and value + assertFalse(ExpressionParser.isValidExpression("version ==")); // Missing value + assertFalse(ExpressionParser.isValidExpression("version >== 2.3")); // Invalid operator + assertFalse( + ExpressionParser.isValidExpression("version >= 2.3 | env = dev")); // OR expression missing parentheses + assertFalse(ExpressionParser.isValidExpression( + "(version >= 2.3) | (invalid)")); // Invalid expression inside parentheses + assertFalse(ExpressionParser.isValidExpression("()")); // Empty parentheses + assertFalse(ExpressionParser.isValidExpression("key 1 = 1")); // Key name contains spaces + assertFalse(ExpressionParser.isValidExpression("my key = value")); // Key name contains spaces + } + + /** + * Test parentheses handling logic - verify parentheses matching rules + */ + @Test + public void testParenthesesHandling() { + assertTrue(ExpressionParser.isValidExpression("(version >= 2.3)")); // Both sides have parentheses + assertTrue(ExpressionParser.isValidExpression("version >= 2.3")); // No parentheses + assertTrue(ExpressionParser.isValidExpression( + "(version >= 2.3")); // Only left parenthesis, treated as part of key-value + assertTrue(ExpressionParser.isValidExpression( + "version >= 2.3)")); // Only right parenthesis, treated as part of key-value + } + + /** + * Test parsing invalid expression throws exception - verify error handling + */ + @Test + public void testParseInvalidExpressionThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + ExpressionParser.parse(">= 2.3"); + }); + + assertThrows(IllegalArgumentException.class, () -> { + ExpressionParser.parse("version"); + }); + + assertThrows(IllegalArgumentException.class, () -> { + ExpressionParser.parse("version =="); + }); + + assertThrows(IllegalArgumentException.class, () -> { + ExpressionParser.parse("version >= 2.3 | env = dev"); + }); + + assertThrows(IllegalArgumentException.class, () -> { + ExpressionParser.parse("(version >= 2.3) | (invalid)"); + }); + + assertThrows(IllegalArgumentException.class, () -> { + ExpressionParser.parse("key 1 = 1"); + }); + + assertThrows(IllegalArgumentException.class, () -> { + ExpressionParser.parse("my key = value"); + }); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/DefaultClientLocationProviderTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/DefaultClientLocationProviderTest.java new file mode 100644 index 00000000000..82dcdeab229 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/DefaultClientLocationProviderTest.java @@ -0,0 +1,39 @@ +/* + * 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.discovery.routing.region; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DefaultClientLocationProviderTest { + + /** + * Test getting client location + */ + @Test + public void testGetClientLocation() { + DefaultClientLocationProvider provider = new DefaultClientLocationProvider(); + GeoLocation location = provider.getClientLocation(); + + assertNotNull(location); + // Verify the returned location is valid (default configured location) + assertTrue(location.getLatitude() >= -90 && location.getLatitude() <= 90); + assertTrue(location.getLongitude() >= -180 && location.getLongitude() <= 180); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/GeoLocationTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/GeoLocationTest.java new file mode 100644 index 00000000000..02fbd80dc7a --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/GeoLocationTest.java @@ -0,0 +1,57 @@ +/* + * 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.discovery.routing.region; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GeoLocationTest { + + /** + * Test constructor and getter methods - verify coordinate setting and retrieval + */ + @Test + public void testConstructorAndGetters() { + GeoLocation location = new GeoLocation(39.9042, 116.4074); + assertEquals(39.9042, location.getLatitude(), 0.0001); + assertEquals(116.4074, location.getLongitude(), 0.0001); + } + + /** + * Test toString method - verify formatted output + */ + @Test + public void testToString() { + GeoLocation location = new GeoLocation(39.9042, 116.4074); + String result = location.toString(); + assertTrue(result.contains("39.904200")); + assertTrue(result.contains("116.407400")); + assertTrue(result.contains("Location")); + } + + /** + * Test negative coordinates - verify negative value handling + */ + @Test + public void testNegativeCoordinates() { + GeoLocation location = new GeoLocation(-39.9042, -116.4074); + assertEquals(-39.9042, location.getLatitude(), 0.0001); + assertEquals(-116.4074, location.getLongitude(), 0.0001); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/ServerWithDistanceTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/ServerWithDistanceTest.java new file mode 100644 index 00000000000..0fc85cfbb35 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/region/ServerWithDistanceTest.java @@ -0,0 +1,73 @@ +/* + * 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.discovery.routing.region; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * ServerWithDistance test + */ +public class ServerWithDistanceTest { + + /** + * Test constructor and getter methods - verify server and distance setting/getting + */ + @Test + public void testConstructorAndGetters() { + String server = "test-server"; + double distance = 100.5; + ServerWithDistance serverWithDistance = new ServerWithDistance<>(server, distance); + + assertEquals(server, serverWithDistance.getServer()); + assertEquals(distance, serverWithDistance.getDistance(), 0.001); + } + + /** + * Test with null server - verify null value handling + */ + @Test + public void testWithNullServer() { + ServerWithDistance serverWithDistance = new ServerWithDistance<>(null, 100.0); + assertNull(serverWithDistance.getServer()); + assertEquals(100.0, serverWithDistance.getDistance(), 0.001); + } + + /** + * Test with zero distance - verify zero value handling + */ + @Test + public void testWithZeroDistance() { + String server = "test-server"; + ServerWithDistance serverWithDistance = new ServerWithDistance<>(server, 0.0); + assertEquals(server, serverWithDistance.getServer()); + assertEquals(0.0, serverWithDistance.getDistance(), 0.001); + } + + /** + * Test with negative distance - verify negative value handling + */ + @Test + public void testWithNegativeDistance() { + String server = "test-server"; + ServerWithDistance serverWithDistance = new ServerWithDistance<>(server, -50.0); + assertEquals(server, serverWithDistance.getServer()); + assertEquals(-50.0, serverWithDistance.getDistance(), 0.001); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/AbstractStateRouterTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/AbstractStateRouterTest.java new file mode 100644 index 00000000000..7b42eaa946d --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/AbstractStateRouterTest.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.discovery.routing.router; + +import org.apache.seata.discovery.routing.BitList; +import org.apache.seata.discovery.routing.RouterSnapshotNode; +import org.apache.seata.discovery.routing.RoutingContext; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AbstractStateRouterTest { + + /** + * Concrete router implementation for testing + */ + private static class TestRouter extends AbstractStateRouter { + + private final boolean shouldReturnEmpty; + + public TestRouter(String routerName, boolean runtime, boolean shouldReturnEmpty) { + super(routerName, runtime); + this.shouldReturnEmpty = shouldReturnEmpty; + } + + @Override + protected BitList doRoute(BitList servers, RoutingContext ctx) { + if (shouldReturnEmpty) { + return BitList.fromList(new ArrayList<>()); + } + return servers; + } + + @Override + public String buildSnapshot() { + return "TestRouter: test-router"; + } + } + + /** + * Test constructor + */ + @Test + public void testConstructor() { + TestRouter router = new TestRouter("test-router", false, false); + assertNotNull(router); + } + + /** + * Test isRuntime method - distinguish between runtime and non-runtime routers + */ + @Test + public void testIsRuntime() { + TestRouter runtimeRouter = new TestRouter("runtime-router", true, false); + TestRouter nonRuntimeRouter = new TestRouter("non-runtime-router", false, false); + + assertTrue(runtimeRouter.isRuntime()); + assertFalse(nonRuntimeRouter.isRuntime()); + } + + /** + * Test set and get next router - chained routing + */ + @Test + public void testSetAndGetNext() { + TestRouter router1 = new TestRouter("router1", false, false); + TestRouter router2 = new TestRouter("router2", false, false); + + assertNull(router1.getNext()); + router1.setNext(router2); + assertEquals(router2, router1.getNext()); + } + + /** + * Test normal routing flow - standard routing execution + */ + @Test + public void testRouteWithNormalFlow() { + TestRouter router = new TestRouter("test-router", false, false); + List servers = Arrays.asList("server1", "server2", "server3"); + BitList bitList = BitList.fromList(servers); + RoutingContext ctx = new RoutingContext(); + + BitList result = router.route(bitList, ctx, false, null); + assertEquals(3, result.size()); + assertTrue(result.toList().containsAll(servers)); + } + + /** + * Test empty result scenario - trigger fallback strategy + */ + @Test + public void testRouteWithEmptyResult() { + TestRouter router = new TestRouter("test-router", false, true); + List servers = Arrays.asList("server1", "server2", "server3"); + BitList bitList = BitList.fromList(servers); + RoutingContext ctx = new RoutingContext(); + + BitList result = router.route(bitList, ctx, false, null); + // Should use fallback strategy, return original list + assertEquals(3, result.size()); + assertTrue(result.toList().containsAll(servers)); + } + + /** + * Test debug mode - verify debug logging + */ + @Test + public void testRouteWithDebugMode() { + TestRouter router = new TestRouter("test-router", false, false); + List servers = Arrays.asList("server1", "server2"); + BitList bitList = BitList.fromList(servers); + RoutingContext ctx = new RoutingContext(); + List> snapshots = new ArrayList<>(); + + BitList result = router.route(bitList, ctx, true, snapshots); + assertEquals(2, result.size()); + + // Verify debug logging + assertEquals(1, snapshots.size()); + RouterSnapshotNode snapshot = snapshots.get(0); + assertEquals("test-router", snapshot.getRouterName()); + assertEquals(2, snapshot.getInputSize()); + assertEquals(2, snapshot.getOutputSize()); + assertEquals(2, snapshot.getSelectedServers().size()); + assertTrue(snapshot.getSnapshot().contains("TestRouter")); + } + + /** + * Test chained routing - multiple routers execute in series + */ + @Test + public void testRouteWithNextRouter() { + TestRouter router1 = new TestRouter("router1", false, false); + TestRouter router2 = new TestRouter("router2", false, false); + router1.setNext(router2); + + List servers = Arrays.asList("server1", "server2"); + BitList bitList = BitList.fromList(servers); + RoutingContext ctx = new RoutingContext(); + + BitList result = router1.route(bitList, ctx, false, null); + assertEquals(2, result.size()); + } + + /** + * Test build snapshot - verify snapshot information + */ + @Test + public void testBuildSnapshot() { + TestRouter router = new TestRouter("test-router", true, false); + String snapshot = router.buildSnapshot(); + assertTrue(snapshot.contains("TestRouter")); + assertTrue(snapshot.contains("test-router")); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/MetadataRouterTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/MetadataRouterTest.java new file mode 100644 index 00000000000..8f46f00f938 --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/MetadataRouterTest.java @@ -0,0 +1,198 @@ +/* + * 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.discovery.routing.router; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.BitList; +import org.apache.seata.discovery.routing.RoutingContext; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MetadataRouterTest { + + /** + * Test constructor + */ + @Test + public void testConstructor() { + // Default constructor + MetadataRouter router = new MetadataRouter(); + assertNotNull(router); + + // Named constructor + router = new MetadataRouter("metadata-router-1"); + assertNotNull(router); + } + + /** + * Test set and get expression + */ + @Test + public void testSetAndGetExpression() { + MetadataRouter router = new MetadataRouter(); + router.setExpression("version >= 2.0"); + assertEquals("version >= 2.0", router.getExpression()); + } + + /** + * Test build snapshot + */ + @Test + public void testBuildSnapshot() { + MetadataRouter router = new MetadataRouter(); + router.setExpression("version >= 2.0"); + String snapshot = router.buildSnapshot(); + assertTrue(snapshot.contains("MetadataRouter")); + assertTrue(snapshot.contains("version >= 2.0")); + } + + /** + * Test empty expression - should return original server list + */ + @Test + public void testDoRouteWithEmptyExpression() { + MetadataRouter router = new MetadataRouter(); + router.setExpression(""); + + ServiceInstance server = mock(ServiceInstance.class); + BitList servers = BitList.fromList(java.util.Arrays.asList(server)); + RoutingContext ctx = new RoutingContext(); + + BitList result = router.doRoute(servers, ctx); + assertEquals(servers, result); + } + + /** + * Test single expression - servers satisfying conditions pass through + */ + @Test + public void testDoRouteWithSingleExpression() { + MetadataRouter router = new MetadataRouter(); + router.setExpression("version >= 2.0"); + + ServiceInstance server1 = mock(ServiceInstance.class); + ServiceInstance server2 = mock(ServiceInstance.class); + Map metadata1 = new HashMap<>(); + Map metadata2 = new HashMap<>(); + metadata1.put("version", "2.5"); + metadata2.put("version", "1.5"); + when(server1.getMetadata()).thenReturn(metadata1); + when(server2.getMetadata()).thenReturn(metadata2); + + BitList servers = BitList.fromList(java.util.Arrays.asList(server1, server2)); + RoutingContext ctx = new RoutingContext(); + + BitList result = router.doRoute(servers, ctx); + assertEquals(1, result.size()); + assertTrue(result.toList().contains(server1)); + } + + /** + * Test OR expression - servers satisfying any condition pass through + */ + @Test + public void testDoRouteWithOrExpression() { + MetadataRouter router = new MetadataRouter(); + router.setExpression("(version >= 2.0) | (env = dev)"); + + ServiceInstance server1 = mock(ServiceInstance.class); + ServiceInstance server2 = mock(ServiceInstance.class); + ServiceInstance server3 = mock(ServiceInstance.class); + Map metadata1 = new HashMap<>(); + Map metadata2 = new HashMap<>(); + Map metadata3 = new HashMap<>(); + metadata1.put("version", "1.5"); // Doesn't satisfy version condition + metadata1.put("env", "prod"); // Doesn't satisfy env condition + metadata2.put("version", "2.5"); // Satisfies version condition + metadata3.put("env", "dev"); // Satisfies env condition + when(server1.getMetadata()).thenReturn(metadata1); + when(server2.getMetadata()).thenReturn(metadata2); + when(server3.getMetadata()).thenReturn(metadata3); + + BitList servers = BitList.fromList(java.util.Arrays.asList(server1, server2, server3)); + RoutingContext ctx = new RoutingContext(); + + BitList result = router.doRoute(servers, ctx); + assertEquals(2, result.size()); + assertTrue(result.toList().contains(server2)); + assertTrue(result.toList().contains(server3)); + } + + /** + * Test string comparison - exact match + */ + @Test + public void testDoRouteWithStringComparison() { + MetadataRouter router = new MetadataRouter(); + router.setExpression("env = prod"); + + ServiceInstance server1 = mock(ServiceInstance.class); + ServiceInstance server2 = mock(ServiceInstance.class); + Map metadata1 = new HashMap<>(); + Map metadata2 = new HashMap<>(); + metadata1.put("env", "prod"); + metadata2.put("env", "dev"); + when(server1.getMetadata()).thenReturn(metadata1); + when(server2.getMetadata()).thenReturn(metadata2); + + BitList servers = BitList.fromList(java.util.Arrays.asList(server1, server2)); + RoutingContext ctx = new RoutingContext(); + + BitList result = router.doRoute(servers, ctx); + assertEquals(1, result.size()); + assertTrue(result.toList().contains(server1)); + } + + /** + * Test missing metadata scenario - servers without version metadata should be filtered out + */ + @Test + public void testDoRouteWithMissingMetadata() { + MetadataRouter router = new MetadataRouter(); + router.setExpression("version >= 2.0"); + + ServiceInstance server1 = mock(ServiceInstance.class); + ServiceInstance server2 = mock(ServiceInstance.class); + ServiceInstance server3 = mock(ServiceInstance.class); + Map metadata1 = new HashMap<>(); + Map metadata2 = new HashMap<>(); + metadata1.put("version", "2.5"); + // server2 has no version metadata + // server3 has null metadata + when(server1.getMetadata()).thenReturn(metadata1); + when(server2.getMetadata()).thenReturn(metadata2); + when(server3.getMetadata()).thenReturn(null); + + BitList servers = BitList.fromList(java.util.Arrays.asList(server1, server2, server3)); + RoutingContext ctx = new RoutingContext(); + + BitList result = router.doRoute(servers, ctx); + assertEquals(1, result.size()); // Only server1 should pass through + assertTrue(result.toList().contains(server1)); + assertFalse(result.toList().contains(server2)); + assertFalse(result.toList().contains(server3)); + } +} diff --git a/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/RegionRouterTest.java b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/RegionRouterTest.java new file mode 100644 index 00000000000..91c4069450b --- /dev/null +++ b/discovery/seata-discovery-core/src/test/java/org/apache/seata/discovery/routing/router/RegionRouterTest.java @@ -0,0 +1,196 @@ +/* + * 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.discovery.routing.router; + +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.discovery.routing.BitList; +import org.apache.seata.discovery.routing.RoutingContext; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +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.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RegionRouterTest { + + /** + * Test default constructor + */ + @Test + public void testConstructor() { + RegionRouter router = new RegionRouter(); + assertNotNull(router); + } + + /** + * Test build snapshot - verify snapshot information + */ + @Test + public void testBuildSnapshot() { + RegionRouter router = new RegionRouter(); + String snapshot = router.buildSnapshot(); + assertTrue(snapshot.contains("RegionRouter")); + assertTrue(snapshot.contains("regionTopN")); + } + + /** + * Test routing with client location - location-based routing + */ + @Test + public void testDoRouteWithClientLocation() { + RegionRouter router = new RegionRouter(); + + ServiceInstance server1 = mock(ServiceInstance.class); + ServiceInstance server2 = mock(ServiceInstance.class); + Map metadata1 = new HashMap<>(); + Map metadata2 = new HashMap<>(); + metadata1.put("lat", "39.9042"); + metadata1.put("lng", "116.4074"); + metadata2.put("lat", "31.2304"); + metadata2.put("lng", "121.4737"); + when(server1.getMetadata()).thenReturn(metadata1); + when(server2.getMetadata()).thenReturn(metadata2); + + BitList servers = BitList.fromList(java.util.Arrays.asList(server1, server2)); + RoutingContext ctx = new RoutingContext(); + ctx.setAttribute("clientLat", "39.9042"); + ctx.setAttribute("clientLng", "116.4074"); + + BitList result = router.doRoute(servers, ctx); + assertEquals(2, result.size()); // Should return all servers, sorted by distance + + // Verify that servers are sorted by distance (server1 should be first as it has the same location as client) + assertEquals(server1, result.toList().get(0)); + assertEquals(server2, result.toList().get(1)); + } + + /** + * Test routing without client location - return original server list + */ + @Test + public void testDoRouteWithoutClientLocation() { + RegionRouter router = new RegionRouter(); + + ServiceInstance server = mock(ServiceInstance.class); + BitList servers = BitList.fromList(java.util.Arrays.asList(server)); + RoutingContext ctx = new RoutingContext(); + + BitList result = router.doRoute(servers, ctx); + assertEquals(servers, result); // Should return original server list + } + + /** + * Test routing with invalid client location - return original server list + */ + @Test + public void testDoRouteWithInvalidClientLocation() { + RegionRouter router = new RegionRouter(); + + ServiceInstance server = mock(ServiceInstance.class); + BitList servers = BitList.fromList(java.util.Arrays.asList(server)); + RoutingContext ctx = new RoutingContext(); + ctx.setAttribute("clientLat", "invalid"); + ctx.setAttribute("clientLng", "invalid"); + + BitList result = router.doRoute(servers, ctx); + assertEquals(servers, result); // Should return original server list + } + + /** + * Test routing with server location information - sort by distance + */ + @Test + public void testDoRouteWithServerLocation() { + RegionRouter router = new RegionRouter(); + + ServiceInstance server1 = mock(ServiceInstance.class); + ServiceInstance server2 = mock(ServiceInstance.class); + Map metadata1 = new HashMap<>(); + Map metadata2 = new HashMap<>(); + metadata1.put("lat", "39.9042"); + metadata1.put("lng", "116.4074"); + metadata2.put("lat", "31.2304"); + metadata2.put("lng", "121.4737"); + when(server1.getMetadata()).thenReturn(metadata1); + when(server2.getMetadata()).thenReturn(metadata2); + + BitList servers = BitList.fromList(java.util.Arrays.asList(server1, server2)); + RoutingContext ctx = new RoutingContext(); + ctx.setAttribute("clientLat", "39.9042"); + ctx.setAttribute("clientLng", "116.4074"); + + BitList result = router.doRoute(servers, ctx); + assertEquals(2, result.size()); // Should return all servers, sorted by distance + // server1 should be first (closer distance) + assertEquals(server1, result.toList().get(0)); + } + + /** + * Test routing with server without location information - prioritize servers with location info + */ + @Test + public void testDoRouteWithServerWithoutLocation() { + RegionRouter router = new RegionRouter(); + + ServiceInstance server1 = mock(ServiceInstance.class); + ServiceInstance server2 = mock(ServiceInstance.class); + Map metadata1 = new HashMap<>(); + Map metadata2 = new HashMap<>(); + metadata1.put("lat", "39.9042"); + metadata1.put("lng", "116.4074"); + // server2 has no location information + when(server1.getMetadata()).thenReturn(metadata1); + when(server2.getMetadata()).thenReturn(metadata2); + + BitList servers = BitList.fromList(java.util.Arrays.asList(server1, server2)); + RoutingContext ctx = new RoutingContext(); + ctx.setAttribute("clientLat", "39.9042"); + ctx.setAttribute("clientLng", "116.4074"); + + BitList result = router.doRoute(servers, ctx); + assertEquals(2, result.size()); + // server1 should be first (has location information) + assertEquals(server1, result.toList().get(0)); + } + + /** + * Test routing with invalid server location information - return original server list + */ + @Test + public void testDoRouteWithInvalidServerLocation() { + RegionRouter router = new RegionRouter(); + + ServiceInstance server = mock(ServiceInstance.class); + Map metadata = new HashMap<>(); + metadata.put("lat", "invalid"); + metadata.put("lng", "invalid"); + when(server.getMetadata()).thenReturn(metadata); + + BitList servers = BitList.fromList(java.util.Arrays.asList(server)); + RoutingContext ctx = new RoutingContext(); + ctx.setAttribute("clientLat", "39.9042"); + ctx.setAttribute("clientLng", "116.4074"); + + BitList result = router.doRoute(servers, ctx); + assertEquals(1, result.size()); // Should return original server list + } +} diff --git a/discovery/seata-discovery-core/src/test/resources/META-INF/services/org.apache.seata.discovery.registry.RegistryProvider b/discovery/seata-discovery-core/src/test/resources/META-INF/services/org.apache.seata.discovery.registry.RegistryProvider index f3809a6d55d..ef894d58ded 100644 --- a/discovery/seata-discovery-core/src/test/resources/META-INF/services/org.apache.seata.discovery.registry.RegistryProvider +++ b/discovery/seata-discovery-core/src/test/resources/META-INF/services/org.apache.seata.discovery.registry.RegistryProvider @@ -14,4 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.apache.seata.discovery.registry.MockNacosRegistryProvider \ No newline at end of file +org.apache.seata.discovery.registry.mock.MockNacosRegistryProvider \ No newline at end of file diff --git a/discovery/seata-discovery-core/src/test/resources/file.conf b/discovery/seata-discovery-core/src/test/resources/file.conf index c0f8fd33681..4ea9c29f35b 100644 --- a/discovery/seata-discovery-core/src/test/resources/file.conf +++ b/discovery/seata-discovery-core/src/test/resources/file.conf @@ -22,3 +22,12 @@ client { virtualNodes = 10 } } + +service { + # transaction service group mapping + vgroupMapping.default_tx_group = "default" + # only support when registry.type=file, please don't set multiple addresses + default.grouplist = "127.0.0.1:8080" + # disable seata + disableGlobalTransaction = false +} diff --git a/discovery/seata-discovery-core/src/test/resources/registry.conf b/discovery/seata-discovery-core/src/test/resources/registry.conf index 8b8a0f5b886..5ad014bf55a 100644 --- a/discovery/seata-discovery-core/src/test/resources/registry.conf +++ b/discovery/seata-discovery-core/src/test/resources/registry.conf @@ -19,44 +19,6 @@ registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "file" - nacos { - application = "seata-server" - serverAddr = "localhost" - namespace = "" - cluster = "default" - } - eureka { - serviceUrl = "http://localhost:8761/eureka" - application = "default" - weight = "1" - } - redis { - serverAddr = "localhost:6379" - db = "0" - } - zk { - cluster = "default" - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - } - consul { - cluster = "default" - serverAddr = "127.0.0.1:8500" - } - etcd3 { - cluster = "default" - serverAddr = "http://localhost:2379" - } - sofa { - serverAddr = "127.0.0.1:9603" - application = "default" - region = "DEFAULT_ZONE" - datacenter = "DefaultDataCenter" - cluster = "default" - group = "SEATA_GROUP" - addressWaitTime = "3000" - } file { name = "file.conf" } @@ -66,27 +28,6 @@ config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" - nacos { - serverAddr = "localhost" - namespace = "" - group = "SEATA_GROUP" - } - consul { - serverAddr = "127.0.0.1:8500" - } - apollo { - appId = "seata-server" - apolloMeta = "http://192.168.1.204:8801" - namespace = "application" - } - zk { - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - } - etcd3 { - serverAddr = "http://localhost:2379" - } file { name = "file.conf" } diff --git a/discovery/seata-discovery-custom/src/test/java/org/apache/seata/discovery/registry/custom/CustomRegistryServiceForTest.java b/discovery/seata-discovery-custom/src/test/java/org/apache/seata/discovery/registry/custom/CustomRegistryServiceForTest.java index 19d426e2735..fa830dce14f 100644 --- a/discovery/seata-discovery-custom/src/test/java/org/apache/seata/discovery/registry/custom/CustomRegistryServiceForTest.java +++ b/discovery/seata-discovery-custom/src/test/java/org/apache/seata/discovery/registry/custom/CustomRegistryServiceForTest.java @@ -16,35 +16,35 @@ */ package org.apache.seata.discovery.registry.custom; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.config.ConfigChangeListener; import org.apache.seata.discovery.registry.RegistryService; -import java.net.InetSocketAddress; import java.util.List; public class CustomRegistryServiceForTest implements RegistryService { @Override - public void register(InetSocketAddress address) throws Exception { + public void register(ServiceInstance instance) { throw new UnsupportedOperationException(); } @Override - public void unregister(InetSocketAddress address) throws Exception { + public void unregister(ServiceInstance instance) { throw new UnsupportedOperationException(); } @Override - public void subscribe(String cluster, ConfigChangeListener listener) throws Exception { + public void subscribe(String cluster, ConfigChangeListener listener) { throw new UnsupportedOperationException(); } @Override - public void unsubscribe(String cluster, ConfigChangeListener listener) throws Exception { + public void unsubscribe(String cluster, ConfigChangeListener listener) { throw new UnsupportedOperationException(); } @Override - public List lookup(String key) throws Exception { + public List lookup(String key) { throw new UnsupportedOperationException(); } diff --git a/discovery/seata-discovery-etcd3/src/main/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImpl.java b/discovery/seata-discovery-etcd3/src/main/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImpl.java index 710dd2dbb41..315acd88351 100644 --- a/discovery/seata-discovery-etcd3/src/main/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImpl.java +++ b/discovery/seata-discovery-etcd3/src/main/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImpl.java @@ -28,6 +28,7 @@ import io.etcd.jetcd.options.WatchOption; import io.etcd.jetcd.watch.WatchResponse; import org.apache.seata.common.exception.ShouldNeverHappenException; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.thread.NamedThreadFactory; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; @@ -90,7 +91,7 @@ public class EtcdRegistryServiceImpl implements RegistryService private static volatile EtcdRegistryServiceImpl instance; private static volatile Client client; - private ConcurrentMap>> clusterAddressMap; + private ConcurrentMap>> clusterAddressMap; private ConcurrentMap> listenerMap; private ConcurrentMap watcherMap; private static long leaseId = 0; @@ -131,7 +132,8 @@ static EtcdRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception { + public void register(ServiceInstance instance) throws Exception { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); doRegister(address); RegistryHeartBeats.addHeartBeat(REGISTRY_TYPE, address, this::doRegister); @@ -151,7 +153,8 @@ private void doRegister(InetSocketAddress address) throws Exception { } @Override - public void unregister(InetSocketAddress address) throws Exception { + public void unregister(ServiceInstance instance) throws Exception { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); doUnregister(address); } @@ -167,14 +170,14 @@ private void doUnregister(InetSocketAddress address) throws Exception { } @Override - public void subscribe(String cluster, Watch.Listener listener) throws Exception { + public void subscribe(String cluster, Watch.Listener listener) { listenerMap.computeIfAbsent(cluster, key -> new HashSet<>()).add(listener); EtcdWatcher watcher = watcherMap.computeIfAbsent(cluster, w -> new EtcdWatcher(cluster, listener)); executorService.submit(watcher); } @Override - public void unsubscribe(String cluster, Watch.Listener listener) throws Exception { + public void unsubscribe(String cluster, Watch.Listener listener) { Set subscribeSet = listenerMap.get(cluster); if (subscribeSet != null) { Set newSubscribeSet = subscribeSet.stream() @@ -187,7 +190,7 @@ public void unsubscribe(String cluster, Watch.Listener listener) throws Exceptio } @Override - public List lookup(String key) throws Exception { + public List lookup(String key) throws Exception { transactionServiceGroup = key; final String cluster = getServiceGroup(key); if (cluster == null) { @@ -197,7 +200,7 @@ public List lookup(String key) throws Exception { return lookupByCluster(cluster); } - private List lookupByCluster(String cluster) throws Exception { + private List lookupByCluster(String cluster) throws Exception { if (!listenerMap.containsKey(cluster)) { // 1.refresh refreshCluster(cluster); @@ -220,7 +223,7 @@ public void onError(Throwable throwable) {} public void onCompleted() {} }); } - Pair> pair = clusterAddressMap.get(cluster); + Pair> pair = clusterAddressMap.get(cluster); return Objects.isNull(pair) ? Collections.emptyList() : pair.getValue(); } @@ -275,13 +278,13 @@ private void refreshCluster(String cluster) throws Exception { .get(buildRegistryKeyPrefix(cluster), getOption) .get(); // 2.add to list - List instanceList = getResponse.getKvs().stream() + List instanceList = ServiceInstance.convertToServiceInstanceList(getResponse.getKvs().stream() .map(keyValue -> { String[] instanceInfo = NetUtil.splitIPPortStr(keyValue.getValue().toString(UTF_8)); return new InetSocketAddress(instanceInfo[0], Integer.parseInt(instanceInfo[1])); }) - .collect(Collectors.toList()); + .collect(Collectors.toList())); clusterAddressMap.put(cluster, new Pair<>(getResponse.getHeader().getRevision(), instanceList)); removeOfflineAddressesIfNecessary(transactionServiceGroup, cluster, instanceList); @@ -436,7 +439,7 @@ public void run() { Watch watchClient = getClient().getWatchClient(); WatchOption.Builder watchOptionBuilder = WatchOption.newBuilder().withPrefix(buildRegistryKeyPrefix(cluster)); - Pair> addressPair = clusterAddressMap.get(cluster); + Pair> addressPair = clusterAddressMap.get(cluster); if (Objects.nonNull(addressPair)) { // Maybe addressPair isn't newest now, but it's ok watchOptionBuilder.withRevision(addressPair.getKey()); diff --git a/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImplMockTest.java b/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImplMockTest.java index 93b3cf20c14..cf53f96f138 100644 --- a/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImplMockTest.java +++ b/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImplMockTest.java @@ -31,6 +31,7 @@ import io.etcd.jetcd.options.GetOption; import io.etcd.jetcd.options.PutOption; import io.etcd.jetcd.options.WatchOption; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.config.Configuration; import org.apache.seata.config.ConfigurationFactory; import org.apache.seata.config.exception.ConfigNotFoundException; @@ -162,7 +163,7 @@ public void testRegister() throws Exception { .thenReturn(CompletableFuture.completedFuture(new LeaseKeepAliveResponse(leaseKeepAliveResponse))); // Act - registryService.register(address); + registryService.register(new ServiceInstance(address)); // verify the method to register the new service is called verify(mockKVClient, times(1)).put(any(), any(), any(PutOption.class)); @@ -180,7 +181,7 @@ public void testUnregister() throws Exception { when(mockKVClient.delete(any())).thenReturn(CompletableFuture.completedFuture(null)); // Act - registryService.unregister(address); + registryService.unregister(new ServiceInstance(address)); // Verify verify(mockKVClient, times(1)).delete(any()); @@ -199,9 +200,10 @@ public void testLookup() throws Exception { mockConfig.when(ConfigurationFactory::getInstance).thenReturn(configuration); when(configuration.getConfig("service.vgroupMapping.default_tx_group")) .thenReturn(CLUSTER_NAME); - List lookup = registryService.lookup(DEFAULT_TX_GROUP); + List lookup = registryService.lookup(DEFAULT_TX_GROUP); List lookupServices = lookup.stream() - .map(address -> address.getHostString() + ":" + address.getPort()) + .map(instance -> instance.getAddress().getAddress().getHostAddress() + ":" + + instance.getAddress().getPort()) .collect(Collectors.toList()); // assert @@ -217,7 +219,7 @@ public void testLookup() throws Exception { @Order(4) @Test - public void testSubscribe() throws Exception { + public void testSubscribe() { Watch.Listener mockListener = mock(Watch.Listener.class); registryService.subscribe(CLUSTER_NAME, mockListener); diff --git a/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImplTest.java b/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImplTest.java index bfa91598215..37490b1a04a 100644 --- a/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImplTest.java +++ b/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImplTest.java @@ -23,6 +23,7 @@ import io.etcd.jetcd.options.DeleteOption; import io.etcd.jetcd.options.GetOption; import io.etcd.jetcd.watch.WatchResponse; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.discovery.registry.RegistryService; import org.junit.Rule; import org.junit.jupiter.api.AfterAll; @@ -68,7 +69,7 @@ public void testRegister() throws Exception { RegistryService registryService = new EtcdRegistryProvider().provide(); InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT); // 1.register - registryService.register(inetSocketAddress); + registryService.register(new ServiceInstance(inetSocketAddress)); // 2.get instance information GetOption getOption = GetOption.newBuilder().withPrefix(buildRegistryKeyPrefix()).build(); @@ -84,9 +85,9 @@ public void testRegister() throws Exception { @Test public void testUnregister() throws Exception { RegistryService registryService = new EtcdRegistryProvider().provide(); - InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT); + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress(HOST, PORT)); // 1.register - registryService.register(inetSocketAddress); + registryService.register(serviceInstance); // 2.get instance information GetOption getOption = GetOption.newBuilder().withPrefix(buildRegistryKeyPrefix()).build(); @@ -98,7 +99,7 @@ public void testUnregister() throws Exception { .count(); assertThat(count).isEqualTo(1); // 3.unregister - registryService.unregister(inetSocketAddress); + registryService.unregister(serviceInstance); // 4.again get instance information getOption = GetOption.newBuilder().withPrefix(buildRegistryKeyPrefix()).build(); count = client.getKVClient().get(buildRegistryKeyPrefix(), getOption).get().getKvs().stream() @@ -115,7 +116,7 @@ public void testSubscribe() throws Exception { RegistryService registryService = new EtcdRegistryProvider().provide(); InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT); // 1.register - registryService.register(inetSocketAddress); + registryService.register(new ServiceInstance(inetSocketAddress)); // 2.subscribe EtcdListener etcdListener = new EtcdListener(); registryService.subscribe(CLUSTER_NAME, etcdListener); @@ -131,7 +132,7 @@ public void testUnsubscribe() throws Exception { RegistryService registryService = new EtcdRegistryProvider().provide(); InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT); // 1.register - registryService.register(inetSocketAddress); + registryService.register(new ServiceInstance(inetSocketAddress)); // 2.subscribe EtcdListener etcdListener = new EtcdListener(); registryService.subscribe(CLUSTER_NAME, etcdListener); @@ -156,10 +157,10 @@ public void testLookup() throws Exception { RegistryService registryService = new EtcdRegistryProvider().provide(); InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT); // 1.register - registryService.register(inetSocketAddress); + registryService.register(new ServiceInstance(inetSocketAddress)); // 2.lookup - List inetSocketAddresses = registryService.lookup(DEFAULT_TX_GROUP); - assertThat(inetSocketAddresses).size().isEqualTo(1); + List serviceInstances = registryService.lookup(DEFAULT_TX_GROUP); + assertThat(serviceInstances).size().isEqualTo(1); } /** diff --git a/discovery/seata-discovery-eureka/src/main/java/org/apache/seata/discovery/registry/eureka/EurekaRegistryServiceImpl.java b/discovery/seata-discovery-eureka/src/main/java/org/apache/seata/discovery/registry/eureka/EurekaRegistryServiceImpl.java index 7cb2c797067..19aa7097b23 100644 --- a/discovery/seata-discovery-eureka/src/main/java/org/apache/seata/discovery/registry/eureka/EurekaRegistryServiceImpl.java +++ b/discovery/seata-discovery-eureka/src/main/java/org/apache/seata/discovery/registry/eureka/EurekaRegistryServiceImpl.java @@ -27,6 +27,7 @@ import com.netflix.discovery.shared.Application; import org.apache.seata.common.exception.EurekaRegistryException; import org.apache.seata.common.lock.ResourceLock; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; @@ -69,7 +70,7 @@ public class EurekaRegistryServiceImpl implements RegistryService> LISTENER_SERVICE_MAP = new ConcurrentHashMap<>(); - private static final ConcurrentMap> CLUSTER_ADDRESS_MAP = new ConcurrentHashMap<>(); + private static final ConcurrentMap> CLUSTER_INSTANCE_MAP = new ConcurrentHashMap<>(); private static final ConcurrentMap CLUSTER_LOCK = new ConcurrentHashMap<>(); private static volatile ApplicationInfoManager applicationInfoManager; @@ -94,7 +95,8 @@ static EurekaRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception { + public void register(ServiceInstance instance) throws Exception { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); instanceConfig.setIpAddress(address.getAddress().getHostAddress()); instanceConfig.setPort(address.getPort()); @@ -105,7 +107,7 @@ public void register(InetSocketAddress address) throws Exception { } @Override - public void unregister(InetSocketAddress address) throws Exception { + public void unregister(ServiceInstance instance) { if (eurekaClient == null) { return; } @@ -113,13 +115,13 @@ public void unregister(InetSocketAddress address) throws Exception { } @Override - public void subscribe(String cluster, EurekaEventListener listener) throws Exception { + public void subscribe(String cluster, EurekaEventListener listener) { LISTENER_SERVICE_MAP.computeIfAbsent(cluster, key -> new ArrayList<>()).add(listener); getEurekaClient(false).registerEventListener(listener); } @Override - public void unsubscribe(String cluster, EurekaEventListener listener) throws Exception { + public void unsubscribe(String cluster, EurekaEventListener listener) { List subscribeList = LISTENER_SERVICE_MAP.get(cluster); if (subscribeList != null) { List newSubscribeList = subscribeList.stream() @@ -131,7 +133,7 @@ public void unsubscribe(String cluster, EurekaEventListener listener) throws Exc } @Override - public List lookup(String key) throws Exception { + public List lookup(String key) { transactionServiceGroup = key; String clusterName = getServiceGroup(key); if (clusterName == null) { @@ -150,7 +152,7 @@ public List lookup(String key) throws Exception { } } } - return CLUSTER_ADDRESS_MAP.get(clusterUpperName); + return CLUSTER_INSTANCE_MAP.get(clusterUpperName); } @Override @@ -166,16 +168,17 @@ private void refreshCluster(String clusterName) { if (application == null || CollectionUtils.isEmpty(application.getInstances())) { LOGGER.info("refresh cluster success,but cluster empty! cluster name:{}", clusterName); } else { - List newAddressList = application.getInstances().stream() - .filter(instance -> InstanceInfo.InstanceStatus.UP.equals(instance.getStatus()) - && instance.getIPAddr() != null - && instance.getPort() > 0 - && instance.getPort() < 0xFFFF) - .map(instance -> new InetSocketAddress(instance.getIPAddr(), instance.getPort())) - .collect(Collectors.toList()); - CLUSTER_ADDRESS_MAP.put(clusterName, newAddressList); + List onlineInstanceList = + ServiceInstance.convertToServiceInstanceList(application.getInstances().stream() + .filter(instance -> InstanceInfo.InstanceStatus.UP.equals(instance.getStatus()) + && instance.getIPAddr() != null + && instance.getPort() > 0 + && instance.getPort() < 0xFFFF) + .map(instance -> new InetSocketAddress(instance.getIPAddr(), instance.getPort())) + .collect(Collectors.toList())); + CLUSTER_INSTANCE_MAP.put(clusterName, onlineInstanceList); - removeOfflineAddressesIfNecessary(transactionServiceGroup, clusterName, newAddressList); + removeOfflineAddressesIfNecessary(transactionServiceGroup, clusterName, onlineInstanceList); } } diff --git a/discovery/seata-discovery-eureka/src/test/java/org/apache/seata/discovery/registry/eureka/EurekaRegistryServiceImplTest.java b/discovery/seata-discovery-eureka/src/test/java/org/apache/seata/discovery/registry/eureka/EurekaRegistryServiceImplTest.java index 0e0e78c55ae..77b1d68f24a 100644 --- a/discovery/seata-discovery-eureka/src/test/java/org/apache/seata/discovery/registry/eureka/EurekaRegistryServiceImplTest.java +++ b/discovery/seata-discovery-eureka/src/test/java/org/apache/seata/discovery/registry/eureka/EurekaRegistryServiceImplTest.java @@ -21,6 +21,7 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.EurekaEventListener; import com.netflix.discovery.shared.Application; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.config.Configuration; import org.apache.seata.config.ConfigurationFactory; import org.apache.seata.config.exception.ConfigNotFoundException; @@ -79,7 +80,7 @@ private static void resetSingleton() throws Exception { setStaticField(EurekaRegistryServiceImpl.class, "eurekaClient", null); setStaticField(EurekaRegistryServiceImpl.class, "instanceConfig", null); clearStaticMap(EurekaRegistryServiceImpl.class, "LISTENER_SERVICE_MAP"); - clearStaticMap(EurekaRegistryServiceImpl.class, "CLUSTER_ADDRESS_MAP"); + clearStaticMap(EurekaRegistryServiceImpl.class, "CLUSTER_INSTANCE_MAP"); clearStaticMap(EurekaRegistryServiceImpl.class, "CLUSTER_LOCK"); } @@ -93,7 +94,7 @@ public void testGetInstance() { @Test public void testRegister() throws Exception { InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8091); - registryService.register(address); + registryService.register(new ServiceInstance(address)); CustomEurekaInstanceConfig instanceConfig = getInstanceConfig(); Assertions.assertEquals("127.0.0.1", instanceConfig.getIpAddress()); Assertions.assertEquals("default", instanceConfig.getAppname()); @@ -104,7 +105,7 @@ public void testRegister() throws Exception { void testRegisterWhenEurekaClientIsNull() throws Exception { setStaticField(EurekaRegistryServiceImpl.class, "eurekaClient", null); InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8091); - registryService.register(address); + registryService.register(new ServiceInstance(address)); verify(mockAppInfoManager, times(0)).setInstanceStatus(any()); } @@ -153,7 +154,7 @@ void testUnsubscribeWithNoExistingListeners() throws Exception { @Test public void testUnregister() throws Exception { - registryService.unregister(new InetSocketAddress("127.0.0.1", 8091)); + registryService.unregister(new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091))); verify(mockAppInfoManager).setInstanceStatus(InstanceInfo.InstanceStatus.DOWN); } @@ -172,16 +173,17 @@ public void testLookup() throws Exception { when(mockInstanceInfo.getIPAddr()).thenReturn("192.168.1.1"); when(mockInstanceInfo.getPort()).thenReturn(8091); - List addresses = registryService.lookup("test-group"); + List instances = registryService.lookup("test-group"); // Verify whether the transactionServiceGroup is set correctly Field serviceGroupField = EurekaRegistryServiceImpl.class.getDeclaredField("transactionServiceGroup"); serviceGroupField.setAccessible(true); String actualServiceGroup = (String) serviceGroupField.get(registryService); Assertions.assertEquals("test-group", actualServiceGroup); - Assertions.assertNotNull(addresses); - Assertions.assertEquals(1, addresses.size()); - Assertions.assertEquals(new InetSocketAddress("192.168.1.1", 8091), addresses.get(0)); + Assertions.assertNotNull(instances); + Assertions.assertEquals(1, instances.size()); + Assertions.assertEquals( + new InetSocketAddress("192.168.1.1", 8091), instances.get(0).getAddress()); } } diff --git a/discovery/seata-discovery-eureka/src/test/resources/registry.conf b/discovery/seata-discovery-eureka/src/test/resources/registry.conf index d229acd77ce..a26c1367c5d 100644 --- a/discovery/seata-discovery-eureka/src/test/resources/registry.conf +++ b/discovery/seata-discovery-eureka/src/test/resources/registry.conf @@ -19,74 +19,17 @@ registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "eureka" - nacos { - application = "seata-server" - serverAddr = "localhost" - namespace = "" - cluster = "default" - } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } - redis { - serverAddr = "localhost:6379" - db = "0" - } - zk { - cluster = "default" - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - } - consul { - cluster = "default" - serverAddr = "127.0.0.1:8500" - } - etcd3 { - cluster = "default" - serverAddr = "http://localhost:2379" - } - sofa { - serverAddr = "127.0.0.1:9603" - application = "default" - region = "DEFAULT_ZONE" - datacenter = "DefaultDataCenter" - cluster = "default" - group = "SEATA_GROUP" - addressWaitTime = "3000" - } - file { - name = "file.conf" - } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" - nacos { - serverAddr = "localhost" - namespace = "" - group = "SEATA_GROUP" - } - consul { - serverAddr = "127.0.0.1:8500" - } - apollo { - appId = "seata-server" - apolloMeta = "http://192.168.1.204:8801" - namespace = "application" - } - zk { - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - } - etcd3 { - serverAddr = "http://localhost:2379" - } file { name = "file.conf" } diff --git a/discovery/seata-discovery-nacos/src/main/java/org/apache/seata/discovery/registry/nacos/NacosRegistryServiceImpl.java b/discovery/seata-discovery-nacos/src/main/java/org/apache/seata/discovery/registry/nacos/NacosRegistryServiceImpl.java index 3edbd120c70..d0a1dc33de7 100644 --- a/discovery/seata-discovery-nacos/src/main/java/org/apache/seata/discovery/registry/nacos/NacosRegistryServiceImpl.java +++ b/discovery/seata-discovery-nacos/src/main/java/org/apache/seata/discovery/registry/nacos/NacosRegistryServiceImpl.java @@ -23,6 +23,7 @@ import com.alibaba.nacos.api.naming.listener.NamingEvent; import com.alibaba.nacos.api.naming.pojo.Instance; import com.alibaba.nacos.api.naming.pojo.Service; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; @@ -36,8 +37,9 @@ import java.net.InetSocketAddress; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -75,7 +77,7 @@ public class NacosRegistryServiceImpl implements RegistryService private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE; private static volatile NamingService naming; private static final ConcurrentMap> LISTENER_SERVICE_MAP = new ConcurrentHashMap<>(); - private static final ConcurrentMap> CLUSTER_ADDRESS_MAP = new ConcurrentHashMap<>(); + private static final ConcurrentMap> CLUSTER_INSTANCE_MAP = new ConcurrentHashMap<>(); private static volatile NacosRegistryServiceImpl instance; private static volatile NamingMaintainService namingMaintain; private static final Object LOCK_OBJ = new Object(); @@ -112,7 +114,8 @@ static NacosRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception { + public void register(ServiceInstance instance) throws Exception { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); getNamingInstance() .registerInstance( @@ -124,7 +127,8 @@ public void register(InetSocketAddress address) throws Exception { } @Override - public void unregister(InetSocketAddress address) throws Exception { + public void unregister(ServiceInstance instance) throws Exception { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); getNamingInstance() .deregisterInstance( @@ -158,7 +162,7 @@ public void unsubscribe(String cluster, EventListener listener) throws Exception } @Override - public List lookup(String key) throws Exception { + public List lookup(String key) throws Exception { transactionServiceGroup = key; String clusterName = getServiceGroup(key); if (clusterName == null) { @@ -169,19 +173,21 @@ public List lookup(String key) throws Exception { if (LOGGER.isDebugEnabled()) { LOGGER.debug("look up service address of SLB by nacos"); } - if (!CLUSTER_ADDRESS_MAP.containsKey(PUBLIC_NAMING_ADDRESS_PREFIX + clusterName)) { + if (!CLUSTER_INSTANCE_MAP.containsKey(PUBLIC_NAMING_ADDRESS_PREFIX + clusterName)) { Service service = getNamingMaintainInstance().queryService(DEFAULT_APPLICATION, clusterName); String pubnetIp = service.getMetadata().get(PUBLIC_NAMING_SERVICE_META_IP_KEY); String pubnetPort = service.getMetadata().get(PUBLIC_NAMING_SERVICE_META_PORT_KEY); if (StringUtils.isBlank(pubnetIp) || StringUtils.isBlank(pubnetPort)) { throw new Exception("cannot find service address from nacos naming mata-data"); } - InetSocketAddress publicAddress = new InetSocketAddress(pubnetIp, Integer.valueOf(pubnetPort)); - List publicAddressList = Arrays.asList(publicAddress); - CLUSTER_ADDRESS_MAP.put(PUBLIC_NAMING_ADDRESS_PREFIX + clusterName, publicAddressList); - return publicAddressList; + InetSocketAddress publicAddress = new InetSocketAddress(pubnetIp, Integer.parseInt(pubnetPort)); + Map metadata = service.getMetadata(); + List publicInstanceList = + Collections.singletonList(ServiceInstance.fromStringMap(publicAddress, metadata)); + CLUSTER_INSTANCE_MAP.put(PUBLIC_NAMING_ADDRESS_PREFIX + clusterName, publicInstanceList); + return publicInstanceList; } - return CLUSTER_ADDRESS_MAP.get(PUBLIC_NAMING_ADDRESS_PREFIX + clusterName); + return CLUSTER_INSTANCE_MAP.get(PUBLIC_NAMING_ADDRESS_PREFIX + clusterName); } if (!LISTENER_SERVICE_MAP.containsKey(clusterName)) { synchronized (LOCK_OBJ) { @@ -191,33 +197,36 @@ public List lookup(String key) throws Exception { List firstAllInstances = getNamingInstance().getAllInstances(getServiceName(), getServiceGroup(), clusters); if (null != firstAllInstances) { - List newAddressList = firstAllInstances.stream() + List newInstanceList = firstAllInstances.stream() .filter(eachInstance -> eachInstance.isEnabled() && eachInstance.isHealthy()) - .map(eachInstance -> - new InetSocketAddress(eachInstance.getIp(), eachInstance.getPort())) + .map(eachInstance -> ServiceInstance.fromStringMap( + new InetSocketAddress(eachInstance.getIp(), eachInstance.getPort()), + eachInstance.getMetadata())) .collect(Collectors.toList()); - CLUSTER_ADDRESS_MAP.put(clusterName, newAddressList); + CLUSTER_INSTANCE_MAP.put(clusterName, newInstanceList); } subscribe(clusterName, event -> { List instances = ((NamingEvent) event).getInstances(); - if (CollectionUtils.isEmpty(instances) && null != CLUSTER_ADDRESS_MAP.get(clusterName)) { + if (CollectionUtils.isEmpty(instances) && null != CLUSTER_INSTANCE_MAP.get(clusterName)) { LOGGER.info("receive empty server list,cluster:{}", clusterName); } else { - List newAddressList = instances.stream() + List newInstanceList = instances.stream() .filter(eachInstance -> eachInstance.isEnabled() && eachInstance.isHealthy()) - .map(eachInstance -> - new InetSocketAddress(eachInstance.getIp(), eachInstance.getPort())) + .map(eachInstance -> ServiceInstance.fromStringMap( + new InetSocketAddress(eachInstance.getIp(), eachInstance.getPort()), + eachInstance.getMetadata())) .collect(Collectors.toList()); - CLUSTER_ADDRESS_MAP.put(clusterName, newAddressList); + CLUSTER_INSTANCE_MAP.put(clusterName, newInstanceList); if (StringUtils.isNotEmpty(transactionServiceGroup)) { - removeOfflineAddressesIfNecessary(transactionServiceGroup, clusterName, newAddressList); + removeOfflineAddressesIfNecessary( + transactionServiceGroup, clusterName, newInstanceList); } } }); } } } - return CLUSTER_ADDRESS_MAP.get(clusterName); + return CLUSTER_INSTANCE_MAP.get(clusterName); } @Override diff --git a/discovery/seata-discovery-nacos/src/test/java/org/apache/seata/discovery/registry/nacos/NacosRegistryServiceImplTest.java b/discovery/seata-discovery-nacos/src/test/java/org/apache/seata/discovery/registry/nacos/NacosRegistryServiceImplTest.java index 360e11f7ba3..344942a35dc 100644 --- a/discovery/seata-discovery-nacos/src/test/java/org/apache/seata/discovery/registry/nacos/NacosRegistryServiceImplTest.java +++ b/discovery/seata-discovery-nacos/src/test/java/org/apache/seata/discovery/registry/nacos/NacosRegistryServiceImplTest.java @@ -28,7 +28,6 @@ /** * The type Nacos registry serivce impl test - * */ public class NacosRegistryServiceImplTest { diff --git a/discovery/seata-discovery-nacos/src/test/resources/registry.conf b/discovery/seata-discovery-nacos/src/test/resources/registry.conf index 1fbdb5694b1..1fd13bc9408 100644 --- a/discovery/seata-discovery-nacos/src/test/resources/registry.conf +++ b/discovery/seata-discovery-nacos/src/test/resources/registry.conf @@ -33,88 +33,13 @@ registry { ##if use Nacos naming meta-data for SLB service registry, specify nacos address pattern rules here #slbPattern = "" } - eureka { - serviceUrl = "http://localhost:8761/eureka" - weight = "1" - } - redis { - serverAddr = "localhost:6379" - db = "0" - password = "" - timeout = "0" - } - zk { - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - username = "" - password = "" - } - consul { - serverAddr = "127.0.0.1:8500" - aclToken = "" - } - etcd3 { - serverAddr = "http://localhost:2379" - } - sofa { - serverAddr = "127.0.0.1:9603" - region = "DEFAULT_ZONE" - datacenter = "DefaultDataCenter" - group = "SEATA_GROUP" - addressWaitTime = "3000" - } - file { - name = "file.conf" - } - custom { - name = "" - } } config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig、custom type = "file" - nacos { - serverAddr = "127.0.0.1:8848" - namespace = "" - group = "SEATA_GROUP" - username = "" - password = "" - contextPath = "/bar" - ##if use MSE Nacos with auth, mutex with username/password attribute - #accessKey = "" - #secretKey = "" - dataId = "seata.properties" - } - consul { - serverAddr = "127.0.0.1:8500" - key = "seata.properties" - aclToken = "" - } - apollo { - appId = "seata-server" - apolloMeta = "http://192.168.1.204:8801" - namespace = "application" - apolloAccesskeySecret = "" - } - zk { - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - username = "" - password = "" - nodePath = "/seata/seata.properties" - } - etcd3 { - serverAddr = "http://localhost:2379" - key = "seata.properties" - } file { name = "file.conf" } - custom { - name = "" - } } diff --git a/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingListener.java b/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingListener.java index e4e07417d81..3406b693232 100644 --- a/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingListener.java +++ b/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingListener.java @@ -16,11 +16,15 @@ */ package org.apache.seata.discovery.registry.namingserver; +/** + * Listener interface for namingserver events. + */ public interface NamingListener { + /** - * on event + * Called when a namingserver event occurs. * - * @param vGroup + * @param vGroup the vGroup */ void onEvent(String vGroup); } diff --git a/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingRegistryException.java b/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingRegistryException.java index ff44364bcfa..e7c2c4b9ba4 100644 --- a/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingRegistryException.java +++ b/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingRegistryException.java @@ -16,10 +16,13 @@ */ package org.apache.seata.discovery.registry.namingserver; +/** + * Exception thrown when namingserver registry fail. + */ public class NamingRegistryException extends RuntimeException { /** - * naming registry exception. + * Creates an exception with message. * * @param message the message */ diff --git a/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryProvider.java b/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryProvider.java index cfb267ec1f6..335a0765b95 100644 --- a/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryProvider.java +++ b/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryProvider.java @@ -20,8 +20,17 @@ import org.apache.seata.discovery.registry.RegistryProvider; import org.apache.seata.discovery.registry.RegistryService; +/** + * Registry provider for namingserver. + */ @LoadLevel(name = "Seata", order = 1) public class NamingserverRegistryProvider implements RegistryProvider { + + /** + * Provides the namingserver registryService instance. + * + * @return the namingserver registryService + */ @Override public RegistryService provide() { return NamingserverRegistryServiceImpl.getInstance(); diff --git a/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImpl.java b/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImpl.java index 9fe99573f94..5773c9afde1 100644 --- a/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImpl.java +++ b/discovery/seata-discovery-namingserver/src/main/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImpl.java @@ -33,6 +33,7 @@ import org.apache.seata.common.metadata.ClusterRole; import org.apache.seata.common.metadata.Instance; import org.apache.seata.common.metadata.Node; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.metadata.namingserver.MetaResponse; import org.apache.seata.common.metadata.namingserver.NamingServerNode; import org.apache.seata.common.metadata.namingserver.Unit; @@ -71,58 +72,56 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; +/** + * Namingserver registryService implementation. + */ public class NamingserverRegistryServiceImpl implements RegistryService { private static final Logger LOGGER = LoggerFactory.getLogger(NamingserverRegistryServiceImpl.class); - public static volatile NamingserverRegistryServiceImpl instance; - private static final String NAMESPACE_KEY = "namespace"; - private static final String VGROUP_KEY = "vGroup"; - private static final String CLIENT_TERM_KEY = "clientTerm"; - private static final String DEFAULT_NAMESPACE = "public"; - private static final String NAMING_SERVICE_URL_KEY = "server-addr"; + private static final String FILE_ROOT_REGISTRY = "registry"; private static final String FILE_CONFIG_SPLIT_CHAR = "."; private static final String REGISTRY_TYPE = "seata"; + + private static final String NAMESPACE_KEY = "namespace"; + private static final String VGROUP_KEY = "vGroup"; private static final String HTTP_PREFIX = "http://"; - private static final String TIME_OUT_KEY = "timeout"; - private static final String PRO_USERNAME_KEY = "username"; + private static final String PRO_USERNAME_KEY = "username"; private static final String PRO_PASSWORD_KEY = "password"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String TOKEN_VALID_TIME_MS_KEY = "tokenValidityInMilliseconds"; private static final String META_DATA_MAX_AGE_MS = "metadataMaxAgeMs"; - private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final long TOKEN_EXPIRE_TIME_IN_MILLISECONDS; - private static final String TOKEN_VALID_TIME_MS_KEY = "tokenValidityInMilliseconds"; + private static int healthcheckPeriod = 5 * 1000; + private static final int LONG_POLL_TIME_OUT_PERIOD = 28 * 1000; - private static final long TOKEN_EXPIRE_TIME_IN_MILLISECONDS; + private static final int THREAD_POOL_NUM = 1; + // namingserver is considered unhealthy if failing in healthy check more than 1 times + private static final int HEALTH_CHECK_THRESHOLD = 1; private static final String USERNAME; - private static final String PASSWORD; - public static String jwtToken; - private static long tokenTimeStamp = -1; - private static final String HEART_BEAT_KEY = "heartbeat-period"; - private static int healthcheckPeriod = 5 * 1000; - private static final int LONG_POLL_TIME_OUT_PERIOD = 28 * 1000; - private static final int THREAD_POOL_NUM = 1; - private static final int HEALTH_CHECK_THRESHOLD = - 1; // namingserver is considered unhealthy if failing in healthy check more than 1 times private volatile long term = 0; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private volatile boolean isSubscribed = false; - private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE; private String namingServerAddressCache; - private static final ConcurrentMap< - String /* namingserver address */, AtomicInteger /* Number of Health Check Continues Failures */> - AVAILABLE_NAMINGSERVER_MAP = new ConcurrentHashMap<>(); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE; + + // k: naming server address; v: Number of Health Check Continues Failures + private static final ConcurrentMap AVAILABLE_NAMINGSERVER_MAP = new ConcurrentHashMap<>(); private static final ConcurrentMap> VGROUP_ADDRESS_MAP = new ConcurrentHashMap<>(); private static final ConcurrentMap> LISTENER_SERVICE_MAP = new ConcurrentHashMap<>(); + protected static final ScheduledExecutorService SCHEDULED_THREAD_POOL_EXECUTOR = new ScheduledThreadPoolExecutor( 1, new NamedThreadFactory("seata-namingser-scheduled", THREAD_POOL_NUM, true)); private static final ExecutorService NOTIFIER_EXECUTOR = new ThreadPoolExecutor( @@ -143,7 +142,8 @@ public class NamingserverRegistryServiceImpl implements RegistryService urlList = getNamingAddrs(); checkAvailableNamingAddr(urlList); @@ -175,13 +175,19 @@ private void checkAvailableNamingAddr(List urlList) { } } - /** - * Gets instance. - * - * @return the instance - */ - static NamingserverRegistryServiceImpl getInstance() { + public boolean doHealthCheck(String url) { + url = HTTP_PREFIX + url + "/naming/v1/health"; + Map header = new HashMap<>(); + header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()); + try (CloseableHttpResponse response = HttpClientUtil.doGet(url, null, header, 3000)) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == 200; + } catch (Exception e) { + return false; + } + } + static NamingserverRegistryServiceImpl getInstance() { if (instance == null) { synchronized (NamingserverRegistryServiceImpl.class) { if (instance == null) { @@ -193,18 +199,12 @@ static NamingserverRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception { - register(Instance.getInstance()); - } - - @Override - public void register(Instance instance) throws Exception { + public void register(ServiceInstance serviceInstance) throws Exception { + Instance instance = Instance.getInstance(); instance.setTimestamp(System.currentTimeMillis()); doRegister(instance, getNamingAddrs()); } - public void doRegister(List instance, List urlList) {} - public void doRegister(Instance instance, List urlList) throws RetryableException { for (String urlSuffix : urlList) { // continue if name server node is unhealthy @@ -245,25 +245,9 @@ public void doRegister(Instance instance, List urlList) throws Retryable } } - public boolean doHealthCheck(String url) { - url = HTTP_PREFIX + url + "/naming/v1/health"; - Map header = new HashMap<>(); - header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType()); - try (CloseableHttpResponse response = HttpClientUtil.doGet(url, null, header, 3000)) { - int statusCode = response.getStatusLine().getStatusCode(); - return statusCode == 200; - } catch (Exception e) { - return false; - } - } - - @Override - public void unregister(InetSocketAddress inetSocketAddress) { - unregister(Instance.getInstance()); - } - @Override - public void unregister(Instance instance) { + public void unregister(ServiceInstance serviceInstance) { + Instance instance = Instance.getInstance(); for (String urlSuffix : getNamingAddrs()) { String url = HTTP_PREFIX + urlSuffix + "/naming/v1/unregister?"; String unit = instance.getUnit(); @@ -288,9 +272,9 @@ public void unregister(Instance instance) { } @Override - public void subscribe(String cluster, NamingListener listener) throws Exception {} + public void subscribe(String cluster, NamingListener listener) {} - public void subscribe(NamingListener listener, String vGroup) throws Exception { + public void subscribe(NamingListener listener, String vGroup) { LISTENER_SERVICE_MAP.computeIfAbsent(vGroup, key -> new ArrayList<>()).add(listener); isSubscribed = true; NOTIFIER_EXECUTOR.execute(() -> { @@ -343,11 +327,11 @@ public boolean watch(String vGroup) throws RetryableException { .append("=") .append(vGroup) .append("&") - .append(CLIENT_TERM_KEY) + .append("clientTerm") .append("=") .append(term) .append("&") - .append(TIME_OUT_KEY) + .append("timeout") .append("=") .append(LONG_POLL_TIME_OUT_PERIOD) .append("&clientAddr=") @@ -373,9 +357,9 @@ public boolean watch(String vGroup) throws RetryableException { } @Override - public void unsubscribe(String cluster, NamingListener listener) throws Exception {} + public void unsubscribe(String cluster, NamingListener listener) {} - public void unsubscribe(NamingListener listener, String vGroup) throws Exception { + public void unsubscribe(NamingListener listener, String vGroup) { // remove watchers List listeners = LISTENER_SERVICE_MAP.get(vGroup); if (listeners != null) { @@ -389,18 +373,8 @@ public void unsubscribe(NamingListener listener, String vGroup) throws Exception isSubscribed = false; } - public void unsubscribe(String vGroup) throws Exception { - LISTENER_SERVICE_MAP.remove(vGroup); - isSubscribed = false; - } - - /** - * @param key vGroup name - * @return List available instance list - * @throws Exception - */ @Override - public List lookup(String key) throws Exception { + public List lookup(String key) throws Exception { if (!isSubscribed) { // get available instanceList by vGroup refreshGroup(key); @@ -419,12 +393,13 @@ public List lookup(String key) throws Exception { return Optional.ofNullable(VGROUP_ADDRESS_MAP.get(key)).orElse(Collections.emptyList()).stream() .map(node -> { Node.Endpoint endpoint = node.getTransaction(); - return new InetSocketAddress(endpoint.getHost(), endpoint.getPort()); + Map metadata = node.getMetadata(); + return new ServiceInstance(new InetSocketAddress(endpoint.getHost(), endpoint.getPort()), metadata); }) .collect(Collectors.toList()); } - public List refreshGroup(String vGroup) throws IOException, RetryableException { + private List refreshGroup(String vGroup) throws IOException, RetryableException { Map paraMap = new HashMap<>(); String namingAddr = getNamingAddr(); if (isTokenExpired()) { @@ -453,7 +428,7 @@ public List refreshGroup(String vGroup) throws IOException, R } } - public List handleMetadata(MetaResponse metaResponse, String vGroup) { + private List handleMetadata(MetaResponse metaResponse, String vGroup) { // MetaResponse -> endpoint list List newAddressList = new ArrayList<>(); if (metaResponse.getTerm() > 0) { @@ -468,14 +443,15 @@ public List handleMetadata(MetaResponse metaResponse, String .collect(Collectors.toList())); } } - List inetSocketAddresses = new ArrayList<>(); + List serviceInstances = new ArrayList<>(); for (NamingServerNode node : newAddressList) { Node.Endpoint endpoint = node.getTransaction(); - inetSocketAddresses.add(new InetSocketAddress(endpoint.getHost(), endpoint.getPort())); + serviceInstances.add(new ServiceInstance( + new InetSocketAddress(endpoint.getHost(), endpoint.getPort()), node.getMetadata())); } - removeOfflineAddressesIfNecessary(vGroup, vGroup, inetSocketAddresses); + removeOfflineAddressesIfNecessary(vGroup, vGroup, serviceInstances); VGROUP_ADDRESS_MAP.put(vGroup, newAddressList); - return inetSocketAddresses; + return serviceInstances; } @Override @@ -486,23 +462,14 @@ public String getServiceGroup(String key) { return RegistryService.super.getServiceGroup(key); } - public String getNamespace() { - String namespaceKey = String.join(FILE_CONFIG_SPLIT_CHAR, FILE_ROOT_REGISTRY, REGISTRY_TYPE, NAMESPACE_KEY); - String namespace = FILE_CONFIG.getConfig(namespaceKey); - if (StringUtils.isBlank(namespace)) { - namespace = DEFAULT_NAMESPACE; - } - return namespace; - } - @Override - public List aliveLookup(String transactionServiceGroup) { - Map> clusterAddressMap = - CURRENT_ADDRESS_MAP.computeIfAbsent(transactionServiceGroup, k -> new ConcurrentHashMap<>()); + public List aliveLookup(String transactionServiceGroup) { + Map> clusterAddressMap = + CURRENT_INSTANCE_MAP.computeIfAbsent(transactionServiceGroup, k -> new ConcurrentHashMap<>()); - List inetSocketAddresses = clusterAddressMap.get(transactionServiceGroup); - if (CollectionUtils.isNotEmpty(inetSocketAddresses)) { - return inetSocketAddresses; + List serviceInstances = clusterAddressMap.get(transactionServiceGroup); + if (CollectionUtils.isNotEmpty(serviceInstances)) { + return serviceInstances; } // fall back to addresses of any cluster @@ -513,55 +480,11 @@ public List aliveLookup(String transactionServiceGroup) { } @Override - public List refreshAliveLookup( - String transactionServiceGroup, List aliveAddress) { - Map> clusterAddressMap = - CURRENT_ADDRESS_MAP.computeIfAbsent(transactionServiceGroup, key -> new ConcurrentHashMap<>()); - return clusterAddressMap.put(transactionServiceGroup, aliveAddress); - } - - /** - * get one namingserver url - * - * @return url - */ - public String getNamingAddr() { - if (namingServerAddressCache != null) { - return namingServerAddressCache; - } - Map availableNamingserverMap = new HashMap<>(AVAILABLE_NAMINGSERVER_MAP); - List availableNamingserverList = new ArrayList<>(); - for (Map.Entry entry : availableNamingserverMap.entrySet()) { - String namingServerAddress = entry.getKey(); - Integer numberOfFailures = entry.getValue().get(); - - if (numberOfFailures < HEALTH_CHECK_THRESHOLD) { - availableNamingserverList.add(namingServerAddress); - } - } - if (availableNamingserverList.isEmpty()) { - throw new NamingRegistryException("no available namingserver address!"); - } else { - namingServerAddressCache = availableNamingserverList.get( - ThreadLocalRandom.current().nextInt(availableNamingserverList.size())); - return namingServerAddressCache; - } - } - - /** - * get all namingserver urlList - * - * @return url List - */ - public List getNamingAddrs() { - String namingAddrsKey = - String.join(FILE_CONFIG_SPLIT_CHAR, FILE_ROOT_REGISTRY, REGISTRY_TYPE, NAMING_SERVICE_URL_KEY); - - String urlListStr = FILE_CONFIG.getConfig(namingAddrsKey); - if (StringUtils.isBlank(urlListStr)) { - throw new NamingRegistryException("Naming server url can not be null!"); - } - return Arrays.stream(urlListStr.split(",")).collect(Collectors.toList()); + public List refreshAliveLookup( + String transactionServiceGroup, List aliveInstances) { + Map> clusterInstanceMap = + CURRENT_INSTANCE_MAP.computeIfAbsent(transactionServiceGroup, key -> new ConcurrentHashMap<>()); + return clusterInstanceMap.put(transactionServiceGroup, aliveInstances); } private static void refreshToken(String tcAddress) throws RetryableException { @@ -617,6 +540,58 @@ private static boolean isTokenExpired() { return System.currentTimeMillis() >= tokenExpiredTime; } + /** + * Get a single available namingserver address with health check and load balancing. + * + * @return a randomly selected healthy naming server address + */ + private String getNamingAddr() { + if (namingServerAddressCache != null) { + return namingServerAddressCache; + } + Map availableNamingserverMap = new HashMap<>(AVAILABLE_NAMINGSERVER_MAP); + List availableNamingserverList = new ArrayList<>(); + for (Map.Entry entry : availableNamingserverMap.entrySet()) { + String namingServerAddress = entry.getKey(); + Integer numberOfFailures = entry.getValue().get(); + + if (numberOfFailures < HEALTH_CHECK_THRESHOLD) { + availableNamingserverList.add(namingServerAddress); + } + } + if (availableNamingserverList.isEmpty()) { + throw new NamingRegistryException("no available namingserver address!"); + } else { + namingServerAddressCache = availableNamingserverList.get( + ThreadLocalRandom.current().nextInt(availableNamingserverList.size())); + return namingServerAddressCache; + } + } + + /** + * Get all configured namingserver addresses from configuration file without health check. + * + * @return list of all configured naming server addresses + */ + private List getNamingAddrs() { + String namingAddrsKey = String.join(FILE_CONFIG_SPLIT_CHAR, FILE_ROOT_REGISTRY, REGISTRY_TYPE, "server-addr"); + + String urlListStr = FILE_CONFIG.getConfig(namingAddrsKey); + if (StringUtils.isBlank(urlListStr)) { + throw new NamingRegistryException("Naming server url can not be null!"); + } + return Arrays.stream(urlListStr.split(",")).collect(Collectors.toList()); + } + + private String getNamespace() { + String namespaceKey = String.join(FILE_CONFIG_SPLIT_CHAR, FILE_ROOT_REGISTRY, REGISTRY_TYPE, NAMESPACE_KEY); + String namespace = FILE_CONFIG.getConfig(namespaceKey); + if (StringUtils.isBlank(namespace)) { + namespace = "public"; + } + return namespace; + } + private static String getUserNameKey() { return String.join( ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR, diff --git a/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryProviderTest.java b/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryProviderTest.java new file mode 100644 index 00000000000..586b98a7829 --- /dev/null +++ b/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryProviderTest.java @@ -0,0 +1,32 @@ +/* + * 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.discovery.registry.namingserver; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test for NamingserverRegistryProvider + */ +public class NamingserverRegistryProviderTest { + + @Test + public void testProvide() { + NamingserverRegistryProvider provider = new NamingserverRegistryProvider(); + Assertions.assertInstanceOf(NamingserverRegistryServiceImpl.class, provider.provide()); + } +} diff --git a/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImplMockTest.java b/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImplMockTest.java new file mode 100644 index 00000000000..a982f0df7dc --- /dev/null +++ b/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImplMockTest.java @@ -0,0 +1,276 @@ +/* + * 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.discovery.registry.namingserver; + +import org.apache.seata.common.metadata.Cluster; +import org.apache.seata.common.metadata.ClusterRole; +import org.apache.seata.common.metadata.Instance; +import org.apache.seata.common.metadata.Node; +import org.apache.seata.common.metadata.ServiceInstance; +import org.apache.seata.common.metadata.namingserver.MetaResponse; +import org.apache.seata.common.metadata.namingserver.NamingServerNode; +import org.apache.seata.common.metadata.namingserver.Unit; +import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationFactory; +import org.apache.seata.discovery.registry.RegistryService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Mock test for NamingserverRegistryServiceImpl + */ +public class NamingserverRegistryServiceImplMockTest { + + private NamingserverRegistryServiceImpl registryService; + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + registryService = NamingserverRegistryServiceImpl.getInstance(); + } + + @AfterEach + public void tearDown() throws Exception { + // Clean up static maps to avoid test interference + Field listenerMapField = NamingserverRegistryServiceImpl.class.getDeclaredField("LISTENER_SERVICE_MAP"); + listenerMapField.setAccessible(true); + + @SuppressWarnings("unchecked") + Map> listenerMap = (Map>) listenerMapField.get(null); + listenerMap.clear(); + + Field currentInstanceMapField = RegistryService.class.getDeclaredField("CURRENT_INSTANCE_MAP"); + currentInstanceMapField.setAccessible(true); + @SuppressWarnings("unchecked") + Map>> currentInstanceMap = + (Map>>) currentInstanceMapField.get(null); + currentInstanceMap.clear(); + + // Reset isSubscribed field + Field isSubscribedField = NamingserverRegistryServiceImpl.class.getDeclaredField("isSubscribed"); + isSubscribedField.setAccessible(true); + isSubscribedField.set(registryService, false); + } + + @Test + public void testGetInstance() { + NamingserverRegistryServiceImpl instance = NamingserverRegistryServiceImpl.getInstance(); + assertInstanceOf(NamingserverRegistryServiceImpl.class, instance); + } + + @Test + public void testRegister() throws Exception { + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)); + + Instance mockInstance = mock(Instance.class); + when(mockInstance.getNamespace()).thenReturn("test-namespace"); + when(mockInstance.getClusterName()).thenReturn("test-cluster"); + when(mockInstance.getUnit()).thenReturn("test-unit"); + when(mockInstance.getTransaction()).thenReturn(new Node.Endpoint("127.0.0.1", 8091)); + when(mockInstance.getMetadata()).thenReturn(new HashMap<>()); + when(mockInstance.toJsonString(any())).thenReturn("{\"test\":\"value\"}"); + + try (MockedStatic mockedInstance = Mockito.mockStatic(Instance.class)) { + mockedInstance.when(Instance::getInstance).thenReturn(mockInstance); + + registryService.register(serviceInstance); + + verify(mockInstance).setTimestamp(any(Long.class)); + } + } + + @Test + public void testUnregister() { + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)); + + Instance mockInstance = mock(Instance.class); + when(mockInstance.getNamespace()).thenReturn("test-namespace"); + when(mockInstance.getClusterName()).thenReturn("test-cluster"); + when(mockInstance.getUnit()).thenReturn("test-unit"); + when(mockInstance.getTransaction()).thenReturn(new Node.Endpoint("127.0.0.1", 8091)); + when(mockInstance.getMetadata()).thenReturn(new HashMap<>()); + when(mockInstance.toJsonString(any())).thenReturn("{\"test\":\"value\"}"); + + try (MockedStatic mockedInstance = Mockito.mockStatic(Instance.class)) { + mockedInstance.when(Instance::getInstance).thenReturn(mockInstance); + + registryService.unregister(serviceInstance); + + verify(mockInstance).getUnit(); + verify(mockInstance).toJsonString(any()); + verify(mockInstance).getClusterName(); + verify(mockInstance).getNamespace(); + } + } + + @Test + public void testHandleMetadata_withMockResponse() throws Exception { + // Use reflection to set the isSubscribed field to true + Field isSubscribedField = NamingserverRegistryServiceImpl.class.getDeclaredField("isSubscribed"); + isSubscribedField.setAccessible(true); + isSubscribedField.set(registryService, true); + + // Create a mock MetaResponse + MetaResponse metaResponse = new MetaResponse(); + metaResponse.setTerm(1); + + Cluster cluster = new Cluster(); + Unit unit = new Unit(); + List namingInstanceList = new ArrayList<>(); + NamingServerNode node = new NamingServerNode(); + node.setRole(ClusterRole.LEADER); + node.setTerm(1); + node.setTransaction(new Node.Endpoint("127.0.0.1", 8091)); + namingInstanceList.add(node); + unit.setNamingInstanceList(namingInstanceList); + List unitData = new ArrayList<>(); + unitData.add(unit); + cluster.setUnitData(unitData); + List clusterList = new ArrayList<>(); + clusterList.add(cluster); + metaResponse.setClusterList(clusterList); + + // Call the method to test + Method handleMetadataMethod = + registryService.getClass().getDeclaredMethod("handleMetadata", MetaResponse.class, String.class); + handleMetadataMethod.setAccessible(true); + List result = + (List) handleMetadataMethod.invoke(registryService, metaResponse, "testGroup"); + + registryService.lookup("testGroup"); + + // Verify the result + assertEquals(1, result.size()); + assertEquals("127.0.0.1", result.get(0).getAddress().getAddress().getHostAddress()); + assertEquals(8091, result.get(0).getAddress().getPort()); + isSubscribedField.set(registryService, false); + } + + @Test + public void testSubscribeAndUnsubscribe() throws Exception { + NamingListener mockListener = mock(NamingListener.class); + String vGroup = "test-vgroup"; + + registryService.subscribe(mockListener, vGroup); + + // Verify that the listener was added to the map + Field listenerMapField = NamingserverRegistryServiceImpl.class.getDeclaredField("LISTENER_SERVICE_MAP"); + listenerMapField.setAccessible(true); + @SuppressWarnings("unchecked") + Map> listenerMap = (Map>) listenerMapField.get(null); + + assertNotNull(listenerMap); + assertTrue(listenerMap.containsKey(vGroup)); + List listeners = listenerMap.get(vGroup); + assertNotNull(listeners); + assertTrue(listeners.contains(mockListener)); + + registryService.unsubscribe(mockListener, vGroup); + + assertFalse(listenerMap.containsKey(vGroup)); + } + + @Test + public void testRefreshAliveLookup() { + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)); + String transactionServiceGroup = "test-group"; + + // Test refreshAliveLookup with a list of instances + List instances = Collections.singletonList(serviceInstance); + registryService.refreshAliveLookup(transactionServiceGroup, instances); + + assertEquals(1, instances.size()); + assertEquals("127.0.0.1", instances.get(0).getAddress().getAddress().getHostAddress()); + assertEquals(8091, instances.get(0).getAddress().getPort()); + } + + @Test + public void testEmptyMethod() throws Exception { + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)); + NamingListener mockListener = mock(NamingListener.class); + + registryService.register(serviceInstance); + + registryService.unregister(serviceInstance); + + registryService.subscribe("test-cluster", mockListener); + + registryService.unsubscribe("test-cluster", mockListener); + + registryService.close(); + } + + @Test + public void testGetNamespace() throws Exception { + System.setProperty("registry.seata.namespace", "dev"); + + Method getNamespaceMethod = NamingserverRegistryServiceImpl.class.getDeclaredMethod("getNamespace"); + getNamespaceMethod.setAccessible(true); + String result = (String) getNamespaceMethod.invoke(registryService); + + assertEquals("dev", result); + + System.clearProperty("registry.seata.namespace"); + } + + @Test + public void testGetMetadataMaxAgeMs() throws Exception { + Method getMetadataMaxAgeMsMethod = + NamingserverRegistryServiceImpl.class.getDeclaredMethod("getMetadataMaxAgeMs"); + getMetadataMaxAgeMsMethod.setAccessible(true); + String result = (String) getMetadataMaxAgeMsMethod.invoke(registryService); + + assertEquals("registry.seata.metadataMaxAgeMs", result); + } + + @Test + public void testGetServiceGroup() { + Configuration mockConfig = mock(Configuration.class); + when(mockConfig.getConfig("service.vgroupMapping.test-key")).thenReturn("test-cluster"); + + try (MockedStatic mockedFactory = mockStatic(ConfigurationFactory.class)) { + mockedFactory.when(ConfigurationFactory::getInstance).thenReturn(mockConfig); + + String result = registryService.getServiceGroup("test-key"); + assertEquals("test-cluster", result); + } + } +} diff --git a/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImplTest.java b/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImplTest.java index 46ac55ff35b..5a49604a03e 100644 --- a/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImplTest.java +++ b/discovery/seata-discovery-namingserver/src/test/java/org/apache/seata/discovery/registry/namingserver/NamingserverRegistryServiceImplTest.java @@ -16,20 +16,10 @@ */ package org.apache.seata.discovery.registry.namingserver; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.entity.ContentType; -import org.apache.http.protocol.HTTP; import org.apache.seata.common.holder.ObjectHolder; -import org.apache.seata.common.metadata.Cluster; -import org.apache.seata.common.metadata.ClusterRole; +import org.apache.seata.common.metadata.Instance; import org.apache.seata.common.metadata.Node; -import org.apache.seata.common.metadata.namingserver.MetaResponse; -import org.apache.seata.common.metadata.namingserver.NamingServerNode; -import org.apache.seata.common.metadata.namingserver.Unit; -import org.apache.seata.common.util.HttpClientUtil; -import org.apache.seata.config.Configuration; -import org.apache.seata.config.ConfigurationFactory; -import org.apache.seata.discovery.registry.RegistryService; +import org.apache.seata.common.metadata.ServiceInstance; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; @@ -40,37 +30,60 @@ import org.springframework.core.env.PropertiesPropertySource; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.InetSocketAddress; -import java.rmi.RemoteException; -import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.ConcurrentMap; import static org.apache.seata.common.Constants.OBJECT_KEY_SPRING_CONFIGURABLE_ENVIRONMENT; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +/** + * Test for NamingserverRegistryServiceImpl + * The @Disable annotation method requires local startup of namingserver for testing + */ class NamingserverRegistryServiceImplTest { - - private static final Configuration FILE_CONFIG = ConfigurationFactory.CURRENT_FILE_INSTANCE; + private final NamingserverRegistryServiceImpl registryService = NamingserverRegistryServiceImpl.getInstance(); @BeforeAll - public static void beforeClass() throws Exception { + public static void beforeClass() { + // set the global instance information for the register + Instance instance = Instance.getInstance(); + instance.setClusterName("cluster1"); + instance.setUnit("unit1"); + instance.setNamespace("dev"); + instance.setTransaction(new Node.Endpoint("127.0.0.1", 8888)); + instance.setControl(new Node.Endpoint("127.0.0.1", 8888)); + + Map vGroups = new HashMap<>(); + vGroups.put( + "group1", + "unit1"); // vGroup -> unitName, namingserver automatically adds transaction groups based on it + instance.addMetadata("vGroup", vGroups); + System.setProperty("registry.seata.namespace", "dev"); System.setProperty("registry.seata.cluster", "cluster1"); - System.setProperty("registry.seata.server-addr", "127.0.0.1:8080"); + System.setProperty("registry.seata.server-addr", "127.0.0.1:8081"); + + System.setProperty("registry.seata.username", "seata"); + System.setProperty("registry.seata.password", "seata"); + + // Set a smaller metadataMaxAgeMs for testing + System.setProperty("registry.seata.metadataMaxAgeMs", "1000"); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - // 获取应用程序环境 + // Get the application environment ConfigurableEnvironment environment = context.getEnvironment(); MutablePropertySources propertySources = environment.getPropertySources(); Properties customProperties = new Properties(); - customProperties.setProperty("seata.registry.namingserver.server-addr[0]", "127.0.0.1:8080"); + customProperties.setProperty("seata.registry.namingserver.server-addr[0]", "127.0.0.1:8081"); PropertiesPropertySource customPropertySource = new PropertiesPropertySource("customSource", customProperties); propertySources.addLast(customPropertySource); @@ -82,292 +95,135 @@ public static void afterClass() { System.clearProperty("registry.seata.namespace"); System.clearProperty("registry.seata.cluster"); System.clearProperty("registry.seata.server-addr"); + System.clearProperty("registry.seata.username"); + System.clearProperty("registry.seata.password"); + System.clearProperty("registry.seata.metadataMaxAgeMs"); } @Test - public void unregister1() throws Exception { - NamingserverRegistryServiceImpl namingserverRegistryService = NamingserverRegistryServiceImpl.getInstance(); - InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8080); - namingserverRegistryService.register(inetSocketAddress); - namingserverRegistryService.unregister(inetSocketAddress); - } + public void testGetNamingAddrs() throws Exception { + Method getNamingAddrsMethod = NamingserverRegistryServiceImpl.class.getDeclaredMethod("getNamingAddrs"); + getNamingAddrsMethod.setAccessible(true); - @Test - @Disabled - public void getNamingAddrsTest() { - NamingserverRegistryServiceImpl namingserverRegistryService = NamingserverRegistryServiceImpl.getInstance(); - List list = namingserverRegistryService.getNamingAddrs(); + List list = (List) getNamingAddrsMethod.invoke(registryService); assertEquals(list.size(), 1); } @Test @Disabled - public void getNamingAddrTest() { - NamingserverRegistryServiceImpl namingserverRegistryService = NamingserverRegistryServiceImpl.getInstance(); - String addr = namingserverRegistryService.getNamingAddr(); - assertEquals(addr, "127.0.0.1:8080"); + public void testGetNamingAddr() throws Exception { + Method getNamingAddrMethod = NamingserverRegistryServiceImpl.class.getDeclaredMethod("getNamingAddr"); + getNamingAddrMethod.setAccessible(true); + + String addr = (String) getNamingAddrMethod.invoke(registryService); + assertEquals(addr, "127.0.0.1:8081"); } @Test @Disabled - public void testRegister1() throws Exception { + public void testRegisterAndUnregister() throws Exception { + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8888)); - RegistryService registryService = new NamingserverRegistryProvider().provide(); + // The ServiceInstance parameter here has no effect and is only used for assertion testing. + // In fact, register is registered by calling Instance.getInstance() of the global singleton. + registryService.register(serviceInstance); - InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.1", 8088); - // 1.register - registryService.register(inetSocketAddress1); - - // 2.create vGroup in cluster - createGroupInCluster("dev", "group1", "cluster1"); - // 3.get instances - List list = registryService.lookup("group1"); - - assertEquals(list.size(), 1); - InetSocketAddress inetSocketAddress = list.get(0); - assertEquals(inetSocketAddress.getAddress().getHostAddress(), "127.0.0.1"); - assertEquals(inetSocketAddress.getPort(), 8088); - - registryService.unregister(inetSocketAddress1); - } - - @Test - public void testHandleMetadata() throws Exception { - NamingserverRegistryServiceImpl registryService = NamingserverRegistryServiceImpl.getInstance(); - // Use reflection to set the isSubscribed field to true - Field isSubscribedField = NamingserverRegistryServiceImpl.class.getDeclaredField("isSubscribed"); - isSubscribedField.setAccessible(true); - isSubscribedField.set(registryService, true); - - // Create a mock MetaResponse - MetaResponse metaResponse = new MetaResponse(); - metaResponse.setTerm(1); - - Cluster cluster = new Cluster(); - Unit unit = new Unit(); - List namingInstanceList = new ArrayList<>(); - NamingServerNode node = new NamingServerNode(); - node.setRole(ClusterRole.LEADER); - node.setTerm(1); - node.setTransaction(new Node.Endpoint("127.0.0.1", 8091)); - namingInstanceList.add(node); - unit.setNamingInstanceList(namingInstanceList); - List unitData = new ArrayList<>(); - unitData.add(unit); - cluster.setUnitData(unitData); - List clusterList = new ArrayList<>(); - clusterList.add(cluster); - metaResponse.setClusterList(clusterList); - - // Call the method to test - List result = registryService.handleMetadata(metaResponse, "testGroup"); - registryService.lookup("testGroup"); - // Verify the result - assertEquals(1, result.size()); - assertEquals("127.0.0.1", result.get(0).getAddress().getHostAddress()); - assertEquals(8091, result.get(0).getPort()); - isSubscribedField.set(registryService, false); - } - - @Test - @Disabled - public void testRegister2() throws Exception { - NamingserverRegistryServiceImpl registryService = - (NamingserverRegistryServiceImpl) new NamingserverRegistryProvider().provide(); - InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.1", 8088); - InetSocketAddress inetSocketAddress2 = new InetSocketAddress("127.0.0.1", 8088); - // 1.register - registryService.register(inetSocketAddress1); - registryService.register(inetSocketAddress2); - - // 2.create vGroup in cluster - String namespace = FILE_CONFIG.getConfig("registry.namingserver.namespace"); - createGroupInCluster(namespace, "group1", "cluster1"); - - // 3.get instances - List list = registryService.lookup("group1"); + List list = registryService.lookup("group1"); - assertEquals(list.size(), 1); + assertEquals(1, list.size()); + Map metadata = new HashMap<>(); + metadata.put("vGroup", Instance.getInstance().getMetadata().get("vGroup")); + serviceInstance.setMetadata(metadata); - registryService.unregister(inetSocketAddress1); - registryService.unregister(inetSocketAddress2); - registryService.unsubscribe("group1"); - } + assertEquals(list.get(0), serviceInstance); - @Test - @Disabled - public void testRegister3() throws Exception { - NamingserverRegistryServiceImpl registryService = - (NamingserverRegistryServiceImpl) new NamingserverRegistryProvider().provide(); - InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.1", 8088); - InetSocketAddress inetSocketAddress2 = new InetSocketAddress("127.0.0.1", 8089); - InetSocketAddress inetSocketAddress3 = new InetSocketAddress("127.0.0.1", 8090); - InetSocketAddress inetSocketAddress4 = new InetSocketAddress("127.0.0.1", 8091); - // 1.register - registryService.register(inetSocketAddress1); - registryService.register(inetSocketAddress2); - registryService.register(inetSocketAddress3); - registryService.register(inetSocketAddress4); - - // 2.create vGroup in cluster - String namespace = FILE_CONFIG.getConfig("registry.namingserver.namespace"); - createGroupInCluster(namespace, "group2", "cluster1"); - - // 3.get instances - List list = registryService.lookup("group2"); - - assertEquals(list.size(), 4); - - registryService.unregister(inetSocketAddress1); - registryService.unregister(inetSocketAddress2); - registryService.unregister(inetSocketAddress3); - registryService.unregister(inetSocketAddress4); - - registryService.unsubscribe("group2"); + registryService.unregister(serviceInstance); } @Test @Disabled - public void testUnregister() throws Exception { - RegistryService registryService = new NamingserverRegistryProvider().provide(); - InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.1", 8088); - // 1.register - registryService.register(inetSocketAddress1); + public void testRegister_withMetadata() throws Exception { + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8888)); + Instance.getInstance().addMetadata("key1", "value1"); + Instance.getInstance().addMetadata("key2", Collections.singletonMap("subKey", "subValue")); - // 2.create vGroup in cluster - String namespace = FILE_CONFIG.getConfig("registry.namingserver.namespace"); - createGroupInCluster(namespace, "group1", "cluster1"); + registryService.register(serviceInstance); - // 3.get instances - List list = registryService.lookup("group1"); + List list = registryService.lookup("group1"); - assertEquals(list.size(), 1); + assertEquals(1, list.size()); + Map metadata = Instance.getInstance().getMetadata(); + metadata.put("vGroup", Instance.getInstance().getMetadata().get("vGroup")); + serviceInstance.setMetadata(metadata); - // 4.unregister - registryService.unregister(inetSocketAddress1); + assertEquals(list.get(0), serviceInstance); - // 5.get instances - List list1 = registryService.lookup("group1"); - assertEquals(list1.size(), 0); + registryService.unregister(serviceInstance); } - @Disabled @Test - public void testWatch() throws Exception { - NamingserverRegistryServiceImpl registryService = - (NamingserverRegistryServiceImpl) new NamingserverRegistryProvider().provide(); - - // 1.注册cluster1下的一个节点 - InetSocketAddress inetSocketAddress1 = new InetSocketAddress("127.0.0.1", 8088); - registryService.register(inetSocketAddress1); - - ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); - int delaySeconds = 500; - // 2.延迟0.5s后在cluster1下创建事务分组group1 - executor.schedule( - () -> { - try { - - String namespace = FILE_CONFIG.getConfig("registry.namingserver.namespace"); - createGroupInCluster(namespace, "group1", "cluster1"); - } catch (Exception e) { - throw new RuntimeException(e); - } - executor.shutdown(); // 任务执行后关闭执行器 - }, - delaySeconds, - TimeUnit.MILLISECONDS); - // 3.watch事务分组group1 - long timestamp1 = System.currentTimeMillis(); - boolean needFetch = registryService.watch("group1"); - long timestamp2 = System.currentTimeMillis(); - // 4. 0.5s后group1被映射到cluster1下,应该有数据在1s内推送到client端 - assert timestamp2 - timestamp1 < 1500; - - // 5. 获取实例 - List list = registryService.lookup("group1"); - registryService.unsubscribe("group1"); - assertEquals(list.size(), 1); - InetSocketAddress inetSocketAddress = list.get(0); - assertEquals(inetSocketAddress.getAddress().getHostAddress(), "127.0.0.1"); - assertEquals(inetSocketAddress.getPort(), 8088); - } - @Disabled - @Test - public void testSubscribe() throws Exception { - NamingserverRegistryServiceImpl registryService = NamingserverRegistryServiceImpl.getInstance(); - - AtomicBoolean isNotified = new AtomicBoolean(false); - // 1.subscribe - registryService.subscribe( - vGroup -> { - try { - isNotified.set(true); - } catch (Exception e) { - throw new RuntimeException(e); - } - }, - "group2"); - - // 2.register - InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8088); - registryService.register(inetSocketAddress); - String namespace = FILE_CONFIG.getConfig("registry.namingserver.namespace"); - createGroupInCluster(namespace, "group2", "cluster1"); - - // 3.check - assertEquals(isNotified.get(), true); - registryService.unsubscribe("group2"); + public void testWatch() throws Exception { + // 1. registering an instance + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress("127.0.0.1", 8888)); + registryService.register(serviceInstance); + Thread.sleep(1000); + + // 2. test for no changes: should return 304 + boolean result1 = registryService.watch("group1"); + assertFalse(result1); + + // 3. triggering data changes: Re-registering instances + registryService.unregister(serviceInstance); + Thread.sleep(1000); + + // set a new term value + Instance instance = Instance.getInstance(); + instance.setTerm(System.currentTimeMillis()); + registryService.register(serviceInstance); + Thread.sleep(1000); + + // 4. test for changes: simulate the client using the old term, and return 200 + Field termField = NamingserverRegistryServiceImpl.class.getDeclaredField("term"); + termField.setAccessible(true); + termField.set(registryService, 0L); + + boolean result2 = registryService.watch("group1"); + assertTrue(result2); + + reflectUnsubscribe("group1"); } @Test - @Disabled public void testUnsubscribe() throws Exception { - NamingserverRegistryServiceImpl registryService = - (NamingserverRegistryServiceImpl) new NamingserverRegistryProvider().provide(); - NamingListenerimpl namingListenerimpl = new NamingListenerimpl(); - // 1.subscribe - registryService.subscribe(namingListenerimpl, "group1"); + registryService.subscribe(namingListenerimpl, "group3"); - // 2.register - InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8088); - registryService.register(inetSocketAddress); - String namespace = FILE_CONFIG.getConfig("registry.namingserver.namespace"); - createGroupInCluster(namespace, "group1", "cluster1"); + registryService.unsubscribe(namingListenerimpl, "group3"); - // 3.check - assertEquals(namingListenerimpl.isNotified, true); - namingListenerimpl.setNotified(false); + Thread.sleep(2000); - // 4.unsubscribe - registryService.unsubscribe(namingListenerimpl, "group1"); + assertEquals(namingListenerimpl.isNotified, false); + } - // 5.unregister + @Test + public void testAliveLookup() { + String transactionServiceGroup = "test-group"; - registryService.unregister(inetSocketAddress); - // 5.check - assertEquals(namingListenerimpl.isNotified, false); + List result = registryService.aliveLookup(transactionServiceGroup); + assertEquals(0, result.size()); } - public void createGroupInCluster(String namespace, String vGroup, String clusterName) throws Exception { - Map paraMap = new HashMap<>(); - paraMap.put("namespace", namespace); - paraMap.put("vGroup", vGroup); - paraMap.put("clusterName", clusterName); - String url = "http://127.0.0.1:8080/naming/v1/createGroup"; - Map header = new HashMap<>(); - header.put(HTTP.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()); - try { - CloseableHttpResponse response = HttpClientUtil.doGet(url, paraMap, header, 30000); - } catch (Exception e) { - throw new RemoteException(); - } + private void reflectUnsubscribe(String vGroup) throws Exception { + Field listenerServiceMapField = NamingserverRegistryServiceImpl.class.getDeclaredField("LISTENER_SERVICE_MAP"); + listenerServiceMapField.setAccessible(true); + ConcurrentMap> listenerServiceMap = + (ConcurrentMap>) listenerServiceMapField.get(null); + listenerServiceMap.remove(vGroup); } - private class NamingListenerimpl implements NamingListener { + private static class NamingListenerimpl implements NamingListener { public boolean isNotified = false; @@ -385,4 +241,3 @@ public void onEvent(String vGroup) { } } } -; diff --git a/discovery/seata-discovery-namingserver/src/test/resources/registry.conf b/discovery/seata-discovery-namingserver/src/test/resources/registry.conf index 274688f4bb9..143e43c1b93 100644 --- a/discovery/seata-discovery-namingserver/src/test/resources/registry.conf +++ b/discovery/seata-discovery-namingserver/src/test/resources/registry.conf @@ -17,58 +17,12 @@ registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa、custom - type = "namingserver" + type = "seata" - nacos { - application = "seata-server" - serverAddr = "127.0.0.1:8848" - group = "SEATA_GROUP" - namespace = "" - username = "" - password = "" - contextPath = "/foo" - ##if use MSE Nacos with auth, mutex with username/password attribute - #accessKey = "" - #secretKey = "" - ##if use Nacos naming meta-data for SLB service registry, specify nacos address pattern rules here - #slbPattern = "" - } - eureka { - serviceUrl = "http://localhost:8761/eureka" - weight = "1" - } - redis { - serverAddr = "localhost:6379" - db = "0" - password = "" - timeout = "0" - } - zk { - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - username = "" - password = "" - } - consul { - serverAddr = "127.0.0.1:8500" - aclToken = "" - } - etcd3 { - serverAddr = "http://localhost:2379" - } - sofa { - serverAddr = "127.0.0.1:9603" - region = "DEFAULT_ZONE" - datacenter = "DefaultDataCenter" - group = "SEATA_GROUP" - addressWaitTime = "3000" - } - file { - name = "file.conf" - } - custom { - name = "" + seata { + username = "seata" + password = "seata" + server-addr = "127.0.0.1:8081" } } @@ -76,45 +30,7 @@ config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig、custom type = "file" - nacos { - serverAddr = "127.0.0.1:8848" - namespace = "" - group = "SEATA_GROUP" - username = "" - password = "" - contextPath = "/bar" - ##if use MSE Nacos with auth, mutex with username/password attribute - #accessKey = "" - #secretKey = "" - dataId = "seata.properties" - } - consul { - serverAddr = "127.0.0.1:8500" - key = "seata.properties" - aclToken = "" - } - apollo { - appId = "seata-server" - apolloMeta = "http://192.168.1.204:8801" - namespace = "application" - apolloAccesskeySecret = "" - } - zk { - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - username = "" - password = "" - nodePath = "/seata/seata.properties" - } - etcd3 { - serverAddr = "http://localhost:2379" - key = "seata.properties" - } file { name = "file.conf" } - custom { - name = "" - } } diff --git a/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java b/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java index ded156e6c62..703cd970b7a 100644 --- a/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java +++ b/discovery/seata-discovery-raft/src/main/java/org/apache/seata/discovery/registry/raft/RaftRegistryServiceImpl.java @@ -33,6 +33,7 @@ import org.apache.seata.common.metadata.Metadata; import org.apache.seata.common.metadata.MetadataResponse; import org.apache.seata.common.metadata.Node; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.thread.NamedThreadFactory; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.HttpClientUtil; @@ -67,7 +68,6 @@ /** * The type File registry service. - * */ public class RaftRegistryServiceImpl implements RegistryService { @@ -103,7 +103,7 @@ public class RaftRegistryServiceImpl implements RegistryService> INIT_ADDRESSES = new HashMap<>(); + private static final Map> INIT_ADDRESSES = new HashMap<>(); private static final Metadata METADATA = new Metadata(); @@ -120,7 +120,7 @@ public class RaftRegistryServiceImpl implements RegistryService> ALIVE_NODES = new ConcurrentHashMap<>(); + private static final Map> ALIVE_NODES = new ConcurrentHashMap<>(); private static final String PREFERRED_NETWORKS; @@ -150,10 +150,10 @@ static RaftRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception {} + public void register(ServiceInstance address) throws Exception {} @Override - public void unregister(InetSocketAddress address) throws Exception {} + public void unregister(ServiceInstance address) throws Exception {} @Override public void subscribe(String cluster, ConfigChangeListener listener) throws Exception {} @@ -224,15 +224,15 @@ protected static void startQueryMetadata() { private static String queryHttpAddress(String clusterName, String group) { List nodeList = METADATA.getNodes(clusterName, group); List addressList = null; - Stream stream = null; + Stream stream = null; if (CollectionUtils.isNotEmpty(nodeList)) { - List inetSocketAddresses = ALIVE_NODES.get(CURRENT_TRANSACTION_SERVICE_GROUP); - if (CollectionUtils.isEmpty(inetSocketAddresses)) { + List serviceInstances = ALIVE_NODES.get(CURRENT_TRANSACTION_SERVICE_GROUP); + if (CollectionUtils.isEmpty(serviceInstances)) { addressList = nodeList.stream() .map(RaftRegistryServiceImpl::selectControlEndpointStr) .collect(Collectors.toList()); } else { - stream = inetSocketAddresses.stream(); + stream = serviceInstances.stream(); } } else { stream = INIT_ADDRESSES.get(clusterName).stream(); @@ -247,16 +247,20 @@ private static String queryHttpAddress(String clusterName, String group) { map.put(inetSocketAddress.getHostString() + IP_PORT_SPLIT_CHAR + inetSocketAddress.getPort(), node); } } - addressList = stream.map(inetSocketAddress -> { - String host = NetUtil.toStringHost(inetSocketAddress); - Node node = map.get(host + IP_PORT_SPLIT_CHAR + inetSocketAddress.getPort()); + addressList = stream.map(instance -> { + String host = NetUtil.toStringHost(instance.getAddress()); + Node node = map.get(host + + IP_PORT_SPLIT_CHAR + + instance.getAddress().getPort()); InetSocketAddress controlEndpoint = null; if (node != null) { controlEndpoint = selectControlEndpoint(node); } return host + IP_PORT_SPLIT_CHAR - + (controlEndpoint != null ? controlEndpoint.getPort() : inetSocketAddress.getPort()); + + (controlEndpoint != null + ? controlEndpoint.getPort() + : instance.getAddress().getPort()); }) .collect(Collectors.toList()); return addressList.get(ThreadLocalRandom.current().nextInt(addressList.size())); @@ -399,12 +403,13 @@ public void close() { } @Override - public List aliveLookup(String transactionServiceGroup) { + public List aliveLookup(String transactionServiceGroup) { if (METADATA.isRaftMode()) { String clusterName = getServiceGroup(transactionServiceGroup); Node leader = METADATA.getLeader(clusterName); if (leader != null) { - return Collections.singletonList(selectTransactionEndpoint(leader)); + return ServiceInstance.convertToServiceInstanceList( + Collections.singletonList(selectTransactionEndpoint(leader))); } } return RegistryService.super.aliveLookup(transactionServiceGroup); @@ -449,21 +454,22 @@ private static boolean watch() throws RetryableException { } @Override - public List refreshAliveLookup( - String transactionServiceGroup, List aliveAddress) { + public List refreshAliveLookup( + String transactionServiceGroup, List aliveInstances) { if (METADATA.isRaftMode()) { Node leader = METADATA.getLeader(getServiceGroup(transactionServiceGroup)); InetSocketAddress leaderAddress = selectTransactionEndpoint(leader); return ALIVE_NODES.put( transactionServiceGroup, - aliveAddress.isEmpty() - ? aliveAddress - : aliveAddress.parallelStream() - .filter(inetSocketAddress -> { + aliveInstances.isEmpty() + ? aliveInstances + : aliveInstances.parallelStream() + .filter(serviceInstance -> { // Since only follower will turn into leader, only the follower node needs to be // listened to - return inetSocketAddress.getPort() != leaderAddress.getPort() - || !inetSocketAddress + return serviceInstance.getAddress().getPort() != leaderAddress.getPort() + || !serviceInstance + .getAddress() .getAddress() .getHostAddress() .equals(leaderAddress @@ -472,7 +478,7 @@ public List refreshAliveLookup( }) .collect(Collectors.toList())); } else { - return RegistryService.super.refreshAliveLookup(transactionServiceGroup, aliveAddress); + return RegistryService.super.refreshAliveLookup(transactionServiceGroup, aliveInstances); } } @@ -570,7 +576,7 @@ private static void refreshToken(String tcAddress) throws RetryableException { } @Override - public List lookup(String key) throws Exception { + public List lookup(String key) throws Exception { String clusterName = getServiceGroup(key); if (clusterName == null) { return null; @@ -580,13 +586,13 @@ public List lookup(String key) throws Exception { if (!METADATA.containsGroup(clusterName)) { String raftClusterAddress = CONFIG.getConfig(getRaftAddrFileKey()); if (StringUtils.isNotBlank(raftClusterAddress)) { - List list = new ArrayList<>(); + List list = new ArrayList<>(); String[] addresses = raftClusterAddress.split(","); for (String address : addresses) { String[] endpoint = address.split(IP_PORT_SPLIT_CHAR); String host = endpoint[0]; int port = Integer.parseInt(endpoint[1]); - list.add(new InetSocketAddress(host, port)); + list.add(new ServiceInstance(new InetSocketAddress(host, port))); } if (CollectionUtils.isEmpty(list)) { return null; @@ -605,9 +611,9 @@ public List lookup(String key) throws Exception { } List nodes = METADATA.getNodes(clusterName); if (CollectionUtils.isNotEmpty(nodes)) { - return nodes.parallelStream() + return ServiceInstance.convertToServiceInstanceList(nodes.parallelStream() .map(RaftRegistryServiceImpl::selectTransactionEndpoint) - .collect(Collectors.toList()); + .collect(Collectors.toList())); } return Collections.emptyList(); } diff --git a/discovery/seata-discovery-raft/src/test/resources/registry.conf b/discovery/seata-discovery-raft/src/test/resources/registry.conf index 5a15b8eec3f..ed256c394e4 100644 --- a/discovery/seata-discovery-raft/src/test/resources/registry.conf +++ b/discovery/seata-discovery-raft/src/test/resources/registry.conf @@ -18,6 +18,7 @@ registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa、custom、raft type = "raft" + raft { metadata-max-age-ms = 30000 serverAddr = "127.0.0.1:8848" @@ -30,14 +31,8 @@ registry { config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig、custom type = "file" - raft { - metadata-max-age-ms = 30000 - serverAddr = "127.0.0.1:8848" - } + file { name = "file.conf" } - custom { - name = "" - } } diff --git a/discovery/seata-discovery-redis/src/main/java/org/apache/seata/discovery/registry/redis/RedisRegistryServiceImpl.java b/discovery/seata-discovery-redis/src/main/java/org/apache/seata/discovery/registry/redis/RedisRegistryServiceImpl.java index 80186fcb713..9fd93b2e065 100644 --- a/discovery/seata-discovery-redis/src/main/java/org/apache/seata/discovery/registry/redis/RedisRegistryServiceImpl.java +++ b/discovery/seata-discovery-redis/src/main/java/org/apache/seata/discovery/registry/redis/RedisRegistryServiceImpl.java @@ -19,6 +19,7 @@ import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.apache.seata.common.ConfigurationKeys; import org.apache.seata.common.exception.ShouldNeverHappenException; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.thread.NamedThreadFactory; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.NetUtil; @@ -65,7 +66,7 @@ public class RedisRegistryServiceImpl implements RegistryService private static final String REDIS_DB = "db"; private static final String REDIS_PASSWORD = "password"; private static final ConcurrentMap> LISTENER_SERVICE_MAP = new ConcurrentHashMap<>(); - private static final ConcurrentMap> CLUSTER_ADDRESS_MAP = new ConcurrentHashMap<>(); + private static final ConcurrentMap> CLUSTER_INSTANCE_MAP = new ConcurrentHashMap<>(); private static volatile RedisRegistryServiceImpl instance; private static volatile JedisPool jedisPool; @@ -151,7 +152,8 @@ static RedisRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) { + public void register(ServiceInstance instance) { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); doRegisterOrExpire(address, true); RegistryHeartBeats.addHeartBeat(REGISTRY_TYPE, address, KEY_REFRESH_PERIOD, this::doRegisterOrExpire); @@ -175,7 +177,8 @@ private void doRegisterOrExpire(InetSocketAddress address, boolean publish) { } @Override - public void unregister(InetSocketAddress address) { + public void unregister(ServiceInstance instance) { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); String serverAddr = NetUtil.toStringAddress(address); try (Jedis jedis = jedisPool.getResource(); @@ -226,7 +229,7 @@ public void subscribe(String cluster, RedisListener listener) { public void unsubscribe(String cluster, RedisListener listener) {} @Override - public List lookup(String key) { + public List lookup(String key) { transactionServiceGroup = key; String clusterName = getServiceGroup(key); if (clusterName == null) { @@ -237,7 +240,7 @@ public List lookup(String key) { } // default visible for test - List lookupByCluster(String clusterName) { + List lookupByCluster(String clusterName) { if (!LISTENER_SERVICE_MAP.containsKey(clusterName)) { String redisRegistryKey = REDIS_FILEKEY_PREFIX + clusterName; try (Jedis jedis = jedisPool.getResource()) { @@ -250,8 +253,8 @@ List lookupByCluster(String clusterName) { switch (eventType) { case RedisListener.REGISTER: CollectionUtils.computeIfAbsent( - CLUSTER_ADDRESS_MAP, clusterName, value -> ConcurrentHashMap.newKeySet(2)) - .add(NetUtil.toInetSocketAddress(serverAddr)); + CLUSTER_INSTANCE_MAP, clusterName, value -> ConcurrentHashMap.newKeySet(2)) + .add(new ServiceInstance(NetUtil.toInetSocketAddress(serverAddr))); break; case RedisListener.UN_REGISTER: removeServerAddressByPushEmptyProtection(clusterName, serverAddr); @@ -262,7 +265,7 @@ List lookupByCluster(String clusterName) { }); } return new ArrayList<>(CollectionUtils.computeIfAbsent( - CLUSTER_ADDRESS_MAP, clusterName, value -> ConcurrentHashMap.newKeySet(2))); + CLUSTER_INSTANCE_MAP, clusterName, value -> ConcurrentHashMap.newKeySet(2))); } /** @@ -275,10 +278,10 @@ List lookupByCluster(String clusterName) { */ private void removeServerAddressByPushEmptyProtection(String notifyCluserName, String serverAddr) { - Set socketAddresses = CollectionUtils.computeIfAbsent( - CLUSTER_ADDRESS_MAP, notifyCluserName, value -> ConcurrentHashMap.newKeySet(2)); - InetSocketAddress inetSocketAddress = NetUtil.toInetSocketAddress(serverAddr); - if (socketAddresses.size() == 1 && socketAddresses.contains(inetSocketAddress)) { + Set serviceInstances = CollectionUtils.computeIfAbsent( + CLUSTER_INSTANCE_MAP, notifyCluserName, value -> ConcurrentHashMap.newKeySet(2)); + ServiceInstance serviceInstance = new ServiceInstance(NetUtil.toInetSocketAddress(serverAddr)); + if (serviceInstances.size() == 1 && serviceInstances.contains(serviceInstance)) { String txServiceGroupName = ConfigurationFactory.getInstance().getConfig(ConfigurationKeys.TX_SERVICE_GROUP); @@ -289,9 +292,9 @@ private void removeServerAddressByPushEmptyProtection(String notifyCluserName, S } } } - socketAddresses.remove(inetSocketAddress); + serviceInstances.remove(serviceInstance); - removeOfflineAddressesIfNecessary(transactionServiceGroup, notifyCluserName, socketAddresses); + removeOfflineAddressesIfNecessary(transactionServiceGroup, notifyCluserName, serviceInstances); } @Override @@ -380,8 +383,11 @@ private void updateClusterAddressMap(Jedis jedis, String redisRegistryKey, Strin } } while (!cursor.equals(ScanParams.SCAN_POINTER_START)); - if (CollectionUtils.isNotEmpty(newAddressSet) && !newAddressSet.equals(CLUSTER_ADDRESS_MAP.get(clusterName))) { - CLUSTER_ADDRESS_MAP.put(clusterName, newAddressSet); + Set currentInstances = ServiceInstance.convertToServiceInstanceSet(newAddressSet); + + if (CollectionUtils.isNotEmpty(currentInstances) + && !currentInstances.equals(CLUSTER_INSTANCE_MAP.get(clusterName))) { + CLUSTER_INSTANCE_MAP.put(clusterName, currentInstances); } } diff --git a/discovery/seata-discovery-redis/src/test/java/org/apache/seata/discovery/registry/redis/RedisRegisterServiceImplTest.java b/discovery/seata-discovery-redis/src/test/java/org/apache/seata/discovery/registry/redis/RedisRegisterServiceImplTest.java index b7de1ca66df..d8738f2d55d 100644 --- a/discovery/seata-discovery-redis/src/test/java/org/apache/seata/discovery/registry/redis/RedisRegisterServiceImplTest.java +++ b/discovery/seata-discovery-redis/src/test/java/org/apache/seata/discovery/registry/redis/RedisRegisterServiceImplTest.java @@ -16,6 +16,7 @@ */ package org.apache.seata.discovery.registry.redis; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.NetUtil; import org.apache.seata.config.Configuration; import org.apache.seata.config.ConfigurationFactory; @@ -66,12 +67,12 @@ public static void init() throws IOException { @Test @Order(1) public void testFlow() { - - redisRegistryService.register(new InetSocketAddress(NetUtil.getLocalIp(), 8091)); + ServiceInstance serviceInstance = new ServiceInstance(new InetSocketAddress(NetUtil.getLocalIp(), 8091)); + redisRegistryService.register(serviceInstance); Assertions.assertTrue(redisRegistryService.lookup("default_tx_group").size() > 0); - redisRegistryService.unregister(new InetSocketAddress(NetUtil.getLocalIp(), 8091)); + redisRegistryService.unregister(serviceInstance); Assertions.assertTrue(redisRegistryService.lookup("default_tx_group").size() > 0); } @@ -80,19 +81,19 @@ public void testFlow() { @Order(2) public void testRemoveServerAddressByPushEmptyProtection() throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { - MockedStatic configurationFactoryMockedStatic = mockStatic(ConfigurationFactory.class); Configuration configuration = mock(Configuration.class); when(configuration.getConfig(anyString())).thenReturn("cluster"); configurationFactoryMockedStatic.when(ConfigurationFactory::getInstance).thenReturn(configuration); - Field field = RedisRegistryServiceImpl.class.getDeclaredField("CLUSTER_ADDRESS_MAP"); + Field field = RedisRegistryServiceImpl.class.getDeclaredField("CLUSTER_INSTANCE_MAP"); field.setAccessible(true); - ConcurrentMap> CLUSTER_ADDRESS_MAP = - (ConcurrentMap>) field.get(null); - CLUSTER_ADDRESS_MAP.put("cluster", Sets.newSet(NetUtil.toInetSocketAddress("127.0.0.1:8091"))); + ConcurrentMap> CLUSTER_INSTANCE_MAP = + (ConcurrentMap>) field.get(null); + CLUSTER_INSTANCE_MAP.put( + "cluster", Sets.newSet(new ServiceInstance(NetUtil.toInetSocketAddress("127.0.0.1:8091")))); Method method = RedisRegistryServiceImpl.class.getDeclaredMethod( "removeServerAddressByPushEmptyProtection", String.class, String.class); @@ -100,7 +101,7 @@ public void testRemoveServerAddressByPushEmptyProtection() method.invoke(redisRegistryService, "cluster", "127.0.0.1:8091"); // test the push empty protection situation - Assertions.assertEquals(1, CLUSTER_ADDRESS_MAP.get("cluster").size()); + Assertions.assertEquals(1, CLUSTER_INSTANCE_MAP.get("cluster").size()); when(configuration.getConfig(anyString())).thenReturn("mycluster"); @@ -108,7 +109,7 @@ public void testRemoveServerAddressByPushEmptyProtection() configurationFactoryMockedStatic.close(); // test the normal remove situation - Assertions.assertEquals(0, CLUSTER_ADDRESS_MAP.get("cluster").size()); + Assertions.assertEquals(0, CLUSTER_INSTANCE_MAP.get("cluster").size()); } @Test diff --git a/discovery/seata-discovery-sofa/src/main/java/org/apache/seata/discovery/registry/sofa/SofaRegistryServiceImpl.java b/discovery/seata-discovery-sofa/src/main/java/org/apache/seata/discovery/registry/sofa/SofaRegistryServiceImpl.java index 0d25de73072..989b84183fd 100644 --- a/discovery/seata-discovery-sofa/src/main/java/org/apache/seata/discovery/registry/sofa/SofaRegistryServiceImpl.java +++ b/discovery/seata-discovery-sofa/src/main/java/org/apache/seata/discovery/registry/sofa/SofaRegistryServiceImpl.java @@ -26,6 +26,7 @@ import com.alipay.sofa.registry.client.provider.DefaultRegistryClientConfigBuilder; import com.alipay.sofa.registry.core.model.ScopeEnum; import org.apache.commons.lang.StringUtils; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.NetUtil; import org.apache.seata.config.Configuration; import org.apache.seata.config.ConfigurationFactory; @@ -74,7 +75,7 @@ public class SofaRegistryServiceImpl implements RegistryService> LISTENER_SERVICE_MAP = new ConcurrentHashMap<>(); - private static final ConcurrentMap> CLUSTER_ADDRESS_MAP = new ConcurrentHashMap<>(); + private static final ConcurrentMap> CLUSTER_INSTANCE_MAP = new ConcurrentHashMap<>(); private static Properties registryProps; private static volatile RegistryClient registryClient; @@ -102,7 +103,8 @@ static SofaRegistryServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception { + public void register(ServiceInstance instance) throws Exception { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); String clusterName = registryProps.getProperty(PRO_CLUSTER_KEY); PublisherRegistration publisherRegistration = new PublisherRegistration(clusterName); @@ -112,8 +114,8 @@ public void register(InetSocketAddress address) throws Exception { } @Override - public void unregister(InetSocketAddress address) throws Exception { - NetUtil.validAddress(address); + public void unregister(ServiceInstance instance) { + NetUtil.validAddress(instance.getAddress()); String clusterName = registryProps.getProperty(PRO_CLUSTER_KEY); getRegistryInstance().unregister(clusterName, registryProps.getProperty(PRO_GROUP_KEY), RegistryType.PUBLISHER); } @@ -143,7 +145,7 @@ private RegistryClient getRegistryInstance() { } @Override - public void subscribe(String cluster, SubscriberDataObserver listener) throws Exception { + public void subscribe(String cluster, SubscriberDataObserver listener) { SubscriberRegistration subscriberRegistration = new SubscriberRegistration(cluster, listener); subscriberRegistration.setScopeEnum(ScopeEnum.global); subscriberRegistration.setGroup(registryProps.getProperty(PRO_GROUP_KEY)); @@ -153,12 +155,12 @@ public void subscribe(String cluster, SubscriberDataObserver listener) throws Ex } @Override - public void unsubscribe(String cluster, SubscriberDataObserver listener) throws Exception { + public void unsubscribe(String cluster, SubscriberDataObserver listener) { getRegistryInstance().unregister(cluster, registryProps.getProperty(PRO_GROUP_KEY), RegistryType.SUBSCRIBER); } @Override - public List lookup(String key) throws Exception { + public List lookup(String key) throws Exception { transactionServiceGroup = key; String clusterName = getServiceGroup(key); if (clusterName == null) { @@ -169,13 +171,13 @@ public List lookup(String key) throws Exception { CountDownLatch respondRegistries = new CountDownLatch(1); subscribe(clusterName, (dataId, data) -> { Map> instances = data.getZoneData(); - if (instances == null && CLUSTER_ADDRESS_MAP.get(clusterName) != null) { - CLUSTER_ADDRESS_MAP.remove(clusterName); + if (instances == null && CLUSTER_INSTANCE_MAP.get(clusterName) != null) { + CLUSTER_INSTANCE_MAP.remove(clusterName); } else { - List newAddressList = flatData(instances); - CLUSTER_ADDRESS_MAP.put(clusterName, newAddressList); + List newInstanceList = flatData(instances); + CLUSTER_INSTANCE_MAP.put(clusterName, newInstanceList); - removeOfflineAddressesIfNecessary(transactionServiceGroup, clusterName, newAddressList); + removeOfflineAddressesIfNecessary(transactionServiceGroup, clusterName, newInstanceList); } respondRegistries.countDown(); }); @@ -184,18 +186,18 @@ public List lookup(String key) throws Exception { final String property = registryProps.getProperty(PRO_ADDRESS_WAIT_TIME_KEY); respondRegistries.await(Integer.parseInt(property), TimeUnit.MILLISECONDS); } - return CLUSTER_ADDRESS_MAP.get(clusterName); + return CLUSTER_INSTANCE_MAP.get(clusterName); } - private List flatData(Map> instances) { - List result = new ArrayList<>(); + private List flatData(Map> instances) { + List result = new ArrayList<>(); for (Map.Entry> entry : instances.entrySet()) { for (String str : entry.getValue()) { String ip = StringUtils.substringBeforeLast(str, HOST_SEPERATOR); String port = StringUtils.substringAfterLast(str, HOST_SEPERATOR); InetSocketAddress inetSocketAddress = new InetSocketAddress(ip, Integer.parseInt(port)); - result.add(inetSocketAddress); + result.add(new ServiceInstance(inetSocketAddress)); } } return result; diff --git a/discovery/seata-discovery-zk/src/main/java/org/apache/seata/discovery/registry/zk/ZookeeperRegisterServiceImpl.java b/discovery/seata-discovery-zk/src/main/java/org/apache/seata/discovery/registry/zk/ZookeeperRegisterServiceImpl.java index 8680a4513eb..eaffdf85f4f 100644 --- a/discovery/seata-discovery-zk/src/main/java/org/apache/seata/discovery/registry/zk/ZookeeperRegisterServiceImpl.java +++ b/discovery/seata-discovery-zk/src/main/java/org/apache/seata/discovery/registry/zk/ZookeeperRegisterServiceImpl.java @@ -27,6 +27,7 @@ import org.apache.curator.framework.state.ConnectionState; import org.apache.curator.framework.state.ConnectionStateListener; import org.apache.curator.retry.RetryNTimes; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; @@ -81,7 +82,7 @@ public class ZookeeperRegisterServiceImpl implements RegistryService> CLUSTER_ADDRESS_MAP = new ConcurrentHashMap<>(); + private static final ConcurrentMap> CLUSTER_INSTANCE_MAP = new ConcurrentHashMap<>(); private static final ConcurrentMap> LISTENER_SERVICE_MAP = new ConcurrentHashMap<>(); private static final ConcurrentMap CLUSTER_LOCK = new ConcurrentHashMap<>(); @@ -107,7 +108,8 @@ static ZookeeperRegisterServiceImpl getInstance() { } @Override - public void register(InetSocketAddress address) throws Exception { + public void register(ServiceInstance instance) throws Exception { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); String path = getRegisterPathByPath(address); @@ -145,7 +147,8 @@ private boolean checkExists(String path) { } @Override - public void unregister(InetSocketAddress address) throws Exception { + public void unregister(ServiceInstance instance) { + InetSocketAddress address = instance.getAddress(); NetUtil.validAddress(address); String path = getRegisterPathByPath(address); @@ -154,7 +157,7 @@ public void unregister(InetSocketAddress address) throws Exception { } @Override - public void subscribe(String cluster, CuratorCacheListener listener) throws Exception { + public void subscribe(String cluster, CuratorCacheListener listener) { if (cluster == null) { return; } @@ -185,7 +188,7 @@ private void unsubscribeChildChanges(String path, CuratorCacheListener listener) } @Override - public void unsubscribe(String cluster, CuratorCacheListener listener) throws Exception { + public void unsubscribe(String cluster, CuratorCacheListener listener) { if (cluster == null) { return; } @@ -208,7 +211,7 @@ public void unsubscribe(String cluster, CuratorCacheListener listener) throws Ex * @throws Exception the exception */ @Override - public List lookup(String key) throws Exception { + public List lookup(String key) throws Exception { transactionServiceGroup = key; String clusterName = getServiceGroup(key); @@ -221,7 +224,7 @@ public List lookup(String key) throws Exception { } // visible for test. - List doLookup(String clusterName) throws Exception { + List doLookup(String clusterName) throws Exception { if (!LISTENER_SERVICE_MAP.containsKey(clusterName)) { Object lock = CLUSTER_LOCK.putIfAbsent(clusterName, new Object()); if (null == lock) { @@ -236,13 +239,13 @@ List doLookup(String clusterName) throws Exception { List childClusterPath = getClientInstance().getChildren().forPath(ROOT_PATH + clusterName); - refreshClusterAddressMap(clusterName, childClusterPath); + refreshClusterInstanceMap(clusterName, childClusterPath); subscribeCluster(clusterName); } } } - return CLUSTER_ADDRESS_MAP.get(clusterName); + return CLUSTER_INSTANCE_MAP.get(clusterName); } @Override @@ -338,10 +341,10 @@ public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) List currentChilds = getClientInstance().getChildren().forPath(path); if (CollectionUtils.isEmpty(currentChilds) - && CLUSTER_ADDRESS_MAP.get(cluster) != null) { - CLUSTER_ADDRESS_MAP.remove(cluster); + && CLUSTER_INSTANCE_MAP.get(cluster) != null) { + CLUSTER_INSTANCE_MAP.remove(cluster); } else if (!CollectionUtils.isEmpty(currentChilds)) { - ZookeeperRegisterServiceImpl.this.refreshClusterAddressMap(cluster, currentChilds); + ZookeeperRegisterServiceImpl.this.refreshClusterInstanceMap(cluster, currentChilds); } } }) @@ -350,23 +353,24 @@ public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) subscribe(cluster, listener); } - private void refreshClusterAddressMap(String clusterName, List instances) { - List newAddressList = new ArrayList<>(); + private void refreshClusterInstanceMap(String clusterName, List instances) { + List newInstanceList = new ArrayList<>(); if (instances == null) { - CLUSTER_ADDRESS_MAP.put(clusterName, newAddressList); + CLUSTER_INSTANCE_MAP.put(clusterName, newInstanceList); return; } for (String path : instances) { try { String[] ipAndPort = NetUtil.splitIPPortStr(path); - newAddressList.add(new InetSocketAddress(ipAndPort[0], Integer.parseInt(ipAndPort[1]))); + newInstanceList.add( + new ServiceInstance(new InetSocketAddress(ipAndPort[0], Integer.parseInt(ipAndPort[1])))); } catch (Exception e) { LOGGER.warn("The cluster instance info is error, instance info:{}", path); } } - CLUSTER_ADDRESS_MAP.put(clusterName, newAddressList); + CLUSTER_INSTANCE_MAP.put(clusterName, newInstanceList); - removeOfflineAddressesIfNecessary(transactionServiceGroup, clusterName, newAddressList); + removeOfflineAddressesIfNecessary(transactionServiceGroup, clusterName, newInstanceList); } private String getClusterName() { diff --git a/discovery/seata-discovery-zk/src/test/java/org/apache/seata/discovery/registry/zk/ZookeeperRegisterServiceImplTest.java b/discovery/seata-discovery-zk/src/test/java/org/apache/seata/discovery/registry/zk/ZookeeperRegisterServiceImplTest.java index baffb356308..8ccea8ec3c9 100644 --- a/discovery/seata-discovery-zk/src/test/java/org/apache/seata/discovery/registry/zk/ZookeeperRegisterServiceImplTest.java +++ b/discovery/seata-discovery-zk/src/test/java/org/apache/seata/discovery/registry/zk/ZookeeperRegisterServiceImplTest.java @@ -20,6 +20,7 @@ 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.common.metadata.ServiceInstance; import org.apache.seata.common.util.NetUtil; import org.apache.seata.config.exception.ConfigNotFoundException; import org.apache.zookeeper.CreateMode; @@ -56,7 +57,7 @@ public static void adAfterClass() throws Exception { } } - ZookeeperRegisterServiceImpl service = (ZookeeperRegisterServiceImpl) new ZookeeperRegistryProvider().provide(); + ZookeeperRegisterServiceImpl service = ZookeeperRegisterServiceImpl.getInstance(); @Test public void getInstance() { @@ -78,7 +79,7 @@ public void buildZkTest() { @Test public void testAll() throws Exception { - service.register(new InetSocketAddress(NetUtil.getLocalAddress(), 33333)); + service.register(new ServiceInstance(new InetSocketAddress(NetUtil.getLocalAddress(), 33333))); Assertions.assertThrows(ConfigNotFoundException.class, new Executable() { @Override @@ -86,7 +87,7 @@ public void execute() throws Throwable { service.lookup("xxx"); } }); - List lookup2 = service.doLookup("default"); + List lookup2 = service.doLookup("default"); Assertions.assertEquals(1, lookup2.size()); final List data = new ArrayList<>(); @@ -120,7 +121,7 @@ public void execute() throws Throwable { service.subscribe("default", listener2); - service.unregister(new InetSocketAddress(NetUtil.getLocalAddress(), 33333)); + service.unregister(new ServiceInstance(new InetSocketAddress(NetUtil.getLocalAddress(), 33333))); latch2.await(1000, TimeUnit.MILLISECONDS); Assertions.assertEquals(0, data2.size()); @@ -143,22 +144,26 @@ public void testLookUp() throws Exception { System.setProperty("txServiceGroup", "default_tx_group"); System.setProperty("service.vgroupMapping.default_tx_group", "cluster"); - List addressList = zookeeperRegisterService.lookup("default_tx_group"); + List addressList = zookeeperRegisterService.lookup("default_tx_group"); - Assertions.assertEquals(addressList, Collections.singletonList(new InetSocketAddress("127.0.0.1", 8091))); + Assertions.assertEquals( + addressList, Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)))); } @Test public void testRemoveOfflineAddressesIfNecessaryNoRemoveCase() { - Map> addresses = - service.CURRENT_ADDRESS_MAP.computeIfAbsent("default_tx_group", k -> new HashMap<>()); - addresses.put("cluster", Collections.singletonList(new InetSocketAddress("127.0.0.1", 8091))); + Map> addresses = + service.CURRENT_INSTANCE_MAP.computeIfAbsent("default_tx_group", k -> new HashMap<>()); + addresses.put( + "cluster", Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)))); service.removeOfflineAddressesIfNecessary( - "default_tx_group", "cluster", Collections.singletonList(new InetSocketAddress("127.0.0.1", 8091))); + "default_tx_group", + "cluster", + Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)))); Assertions.assertEquals( 1, - service.CURRENT_ADDRESS_MAP + service.CURRENT_INSTANCE_MAP .get("default_tx_group") .get("cluster") .size()); @@ -166,17 +171,20 @@ public void testRemoveOfflineAddressesIfNecessaryNoRemoveCase() { @Test public void testRemovePreventEmptyPushCase() { - Map> addresses = - service.CURRENT_ADDRESS_MAP.computeIfAbsent("default_tx_group", k -> new HashMap<>()); + Map> addresses = + service.CURRENT_INSTANCE_MAP.computeIfAbsent("default_tx_group", k -> new HashMap<>()); - addresses.put("cluster", Collections.singletonList(new InetSocketAddress("127.0.0.1", 8091))); + addresses.put( + "cluster", Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)))); service.removeOfflineAddressesIfNecessary( - "default_tx_group", "cluster", Collections.singletonList(new InetSocketAddress("127.0.0.2", 8091))); + "default_tx_group", + "cluster", + Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.2", 8091)))); Assertions.assertEquals( 1, - service.CURRENT_ADDRESS_MAP + service.CURRENT_INSTANCE_MAP .get("default_tx_group") .get("cluster") .size()); @@ -184,16 +192,17 @@ public void testRemovePreventEmptyPushCase() { @Test public void testAliveLookup() { - System.setProperty("txServiceGroup", "default_tx_group"); System.setProperty("service.vgroupMapping.default_tx_group", "cluster"); - Map> addresses = - service.CURRENT_ADDRESS_MAP.computeIfAbsent("default_tx_group", k -> new HashMap<>()); - addresses.put("cluster", Collections.singletonList(new InetSocketAddress("127.0.0.1", 8091))); - List result = service.aliveLookup("default_tx_group"); + Map> addresses = + service.CURRENT_INSTANCE_MAP.computeIfAbsent("default_tx_group", k -> new HashMap<>()); + addresses.put( + "cluster", Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)))); + List result = service.aliveLookup("default_tx_group"); - Assertions.assertEquals(result, Collections.singletonList(new InetSocketAddress("127.0.0.1", 8091))); + Assertions.assertEquals( + result, Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.1", 8091)))); } @Test @@ -203,10 +212,11 @@ public void tesRefreshAliveLookup() { System.setProperty("service.vgroupMapping.default_tx_group", "cluster"); service.refreshAliveLookup( - "default_tx_group", Collections.singletonList(new InetSocketAddress("127.0.0.2", 8091))); + "default_tx_group", + Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.2", 8091)))); Assertions.assertEquals( - service.CURRENT_ADDRESS_MAP.get("default_tx_group").get("cluster"), - Collections.singletonList(new InetSocketAddress("127.0.0.2", 8091))); + service.CURRENT_INSTANCE_MAP.get("default_tx_group").get("cluster"), + Collections.singletonList(new ServiceInstance(new InetSocketAddress("127.0.0.2", 8091)))); } } diff --git a/discovery/seata-discovery-zk/src/test/resources/registry.conf b/discovery/seata-discovery-zk/src/test/resources/registry.conf index 8b8a0f5b886..e3f682f6424 100644 --- a/discovery/seata-discovery-zk/src/test/resources/registry.conf +++ b/discovery/seata-discovery-zk/src/test/resources/registry.conf @@ -19,74 +19,22 @@ registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "file" - nacos { - application = "seata-server" - serverAddr = "localhost" - namespace = "" - cluster = "default" - } - eureka { - serviceUrl = "http://localhost:8761/eureka" - application = "default" - weight = "1" - } - redis { - serverAddr = "localhost:6379" - db = "0" - } - zk { - cluster = "default" - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - } - consul { - cluster = "default" - serverAddr = "127.0.0.1:8500" - } - etcd3 { - cluster = "default" - serverAddr = "http://localhost:2379" - } - sofa { - serverAddr = "127.0.0.1:9603" - application = "default" - region = "DEFAULT_ZONE" - datacenter = "DefaultDataCenter" - cluster = "default" - group = "SEATA_GROUP" - addressWaitTime = "3000" - } file { name = "file.conf" } + + zk { + cluster = "default" + serverAddr = "127.0.0.1:2181" + sessionTimeout = 6000 + connectTimeout = 2000 + } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" - nacos { - serverAddr = "localhost" - namespace = "" - group = "SEATA_GROUP" - } - consul { - serverAddr = "127.0.0.1:8500" - } - apollo { - appId = "seata-server" - apolloMeta = "http://192.168.1.204:8801" - namespace = "application" - } - zk { - serverAddr = "127.0.0.1:2181" - sessionTimeout = 6000 - connectTimeout = 2000 - } - etcd3 { - serverAddr = "http://localhost:2379" - } file { name = "file.conf" } diff --git a/script/client/spring/application.properties b/script/client/spring/application.properties index 8f69e6045f4..741078ef541 100755 --- a/script/client/spring/application.properties +++ b/script/client/spring/application.properties @@ -60,6 +60,26 @@ seata.client.undo.compress.type=zip seata.client.undo.compress.threshold=64k seata.client.load-balance.type=XID seata.client.load-balance.virtual-nodes=10 + +client.routing.enabled=false +client.routing.debug=false +client.routing.fallback=true +client.routing.chain.order= +client.routing.region-router.enabled=true +client.routing.region-router.topN=5 + +client.routing.metadata-routers.metadata-router-1.enabled=true +client.routing.metadata-routers.metadata-router-1.expression= +client.routing.metadata-routers.metadata-router-2.enabled=false +client.routing.metadata-routers.metadata-router-2.expression= + +client.routing.primary-backup.order=region-router +client.routing.primary-backup.enabled=true + +# Client location configuration for region router +client.routing.location.lat= +client.routing.location.lng= + seata.log.exception-rate=100 seata.service.vgroup-mapping.default_tx_group=default seata.service.grouplist.default=127.0.0.1:8091 diff --git a/script/client/spring/application.yml b/script/client/spring/application.yml index 67e17de6118..26e891d0f36 100755 --- a/script/client/spring/application.yml +++ b/script/client/spring/application.yml @@ -66,6 +66,31 @@ seata: load-balance: type: XID virtual-nodes: 10 + routing: + enabled: false + debug: false + fallback: true + # Client location configuration for region router + location: + lat: "" + lng: "" + chain: + order: "" + region-router: + enabled: true + topN: 5 + # Dynamic metadata-router configuration + metadata-routers: + metadata-router-1: + enabled: true + expression: "" + metadata-router-2: + enabled: true + expression: "" + primary-backup: + # backup chain order + order: "" + enabled: false service: vgroup-mapping: default_tx_group: default diff --git a/script/config-center/config.txt b/script/config-center/config.txt index dc140e6fdb7..d558bfbbd72 100644 --- a/script/config-center/config.txt +++ b/script/config-center/config.txt @@ -66,6 +66,31 @@ service.vgroupMapping.default_tx_group=default service.default.grouplist=127.0.0.1:8091 service.disableGlobalTransaction=false +# Client routing configuration +client.routing.enabled=false +client.routing.debug=false +client.routing.fallback=true +# e.g. region-router,metadata-router-1,metadata-router-2 +client.routing.chain.order= +client.routing.region-router.enabled=true +client.routing.region-router.topN=5 + +# Dynamic metadata-router configuration examples +# client.routing.metadata-routers.metadata-router-1.enabled=false +# client.routing.metadata-routers.metadata-router-1.expression= +# client.routing.metadata-routers.metadata-router-2.enabled=false +# client.routing.metadata-routers.metadata-router-2.expression= +# client.routing.metadata-routers.custom-router.enabled=false +# client.routing.metadata-routers.custom-router.expression= + +client.routing.primary-backup.order= +client.routing.primary-backup.enabled=false + +# e.g. 39.9042 +client.routing.location.lat= +# e.g. 116.4074 +client.routing.location.lng= + client.metadataMaxAgeMs=30000 #Transaction rule configuration, only for the client client.rm.asyncCommitBufferLimit=10000 diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataClientEnvironmentPostProcessor.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataClientEnvironmentPostProcessor.java index ae1fcb759a9..f9e0d24c8fd 100644 --- a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataClientEnvironmentPostProcessor.java +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/SeataClientEnvironmentPostProcessor.java @@ -25,6 +25,7 @@ import org.apache.seata.spring.boot.autoconfigure.properties.client.LoadBalanceProperties; import org.apache.seata.spring.boot.autoconfigure.properties.client.LockProperties; import org.apache.seata.spring.boot.autoconfigure.properties.client.RmProperties; +import org.apache.seata.spring.boot.autoconfigure.properties.client.RoutingProperties; import org.apache.seata.spring.boot.autoconfigure.properties.client.ServiceProperties; import org.apache.seata.spring.boot.autoconfigure.properties.client.TmProperties; import org.apache.seata.spring.boot.autoconfigure.properties.client.UndoCompressProperties; @@ -41,6 +42,7 @@ import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.LOAD_BALANCE_PREFIX; import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.LOCK_PREFIX; import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.PROPERTY_BEAN_MAP; +import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.ROUTING_PREFIX; import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.SAGA_ASYNC_THREAD_POOL_PREFIX; import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.SAGA_STATE_MACHINE_PREFIX; import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.SEATA_PREFIX; @@ -67,6 +69,7 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp PROPERTY_BEAN_MAP.put(SAGA_STATE_MACHINE_PREFIX, StateMachineConfig.class); PROPERTY_BEAN_MAP.put(SAGA_ASYNC_THREAD_POOL_PREFIX, SagaAsyncThreadPoolProperties.class); PROPERTY_BEAN_MAP.put(TCC_PREFIX, SeataTccProperties.class); + PROPERTY_BEAN_MAP.put(ROUTING_PREFIX, RoutingProperties.class); } @Override diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/client/MetadataRouterConfig.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/client/MetadataRouterConfig.java new file mode 100644 index 00000000000..7e2cc5f042d --- /dev/null +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/client/MetadataRouterConfig.java @@ -0,0 +1,47 @@ +/* + * 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.spring.boot.autoconfigure.properties.client; + +public class MetadataRouterConfig { + /** + * Whether enable this metadata router + */ + private boolean enabled = false; + + /** + * Expression for this metadata router, e.g. version >= 2.0 + */ + private String expression = ""; + + public boolean isEnabled() { + return enabled; + } + + public MetadataRouterConfig setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public String getExpression() { + return expression; + } + + public MetadataRouterConfig setExpression(String expression) { + this.expression = expression; + return this; + } +} diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/client/RoutingProperties.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/client/RoutingProperties.java new file mode 100644 index 00000000000..df50f59c46f --- /dev/null +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/client/RoutingProperties.java @@ -0,0 +1,203 @@ +/* + * 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.spring.boot.autoconfigure.properties.client; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.ROUTING_PREFIX; + +@Component +@ConfigurationProperties(prefix = ROUTING_PREFIX) +public class RoutingProperties { + /** + * Whether enable routing feature + */ + private boolean enabled = false; + + /** + * Whether enable routing debug mode + */ + private boolean debug = false; + + /** + * Whether enable routing fallback strategy + */ + private boolean fallback = true; + + /** + * Router chain order, e.g. region-router,metadata-router-1,metadata-router-2 + */ + private String chainOrder = ""; + + /** + * Whether enable region router + */ + private boolean regionRouterEnabled = true; + + /** + * Top N servers for region router + */ + private int regionRouterTopN = 5; + + /** + * Dynamic metadata router configurations + * Key: router name (e.g. metadata-router-1, metadata-router-2, custom-router) + * Value: router configuration + */ + private Map metadataRouters = new HashMap<>(); + + /** + * Primary backup router order + */ + private String primaryBackupOrder = "region-router"; + + /** + * Whether enable primary backup router + */ + private boolean primaryBackupEnabled = true; + + /** + * Client location latitude, e.g. 39.9042 + */ + private String locationLat = ""; + + /** + * Client location longitude, e.g. 116.4074 + */ + private String locationLng = ""; + + public boolean isEnabled() { + return enabled; + } + + public RoutingProperties setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public boolean isDebug() { + return debug; + } + + public RoutingProperties setDebug(boolean debug) { + this.debug = debug; + return this; + } + + public boolean isFallback() { + return fallback; + } + + public RoutingProperties setFallback(boolean fallback) { + this.fallback = fallback; + return this; + } + + public String getChainOrder() { + return chainOrder; + } + + public RoutingProperties setChainOrder(String chainOrder) { + this.chainOrder = chainOrder; + return this; + } + + public boolean isRegionRouterEnabled() { + return regionRouterEnabled; + } + + public RoutingProperties setRegionRouterEnabled(boolean regionRouterEnabled) { + this.regionRouterEnabled = regionRouterEnabled; + return this; + } + + public int getRegionRouterTopN() { + return regionRouterTopN; + } + + public RoutingProperties setRegionRouterTopN(int regionRouterTopN) { + this.regionRouterTopN = regionRouterTopN; + return this; + } + + public java.util.Map getMetadataRouters() { + return metadataRouters; + } + + public RoutingProperties setMetadataRouters(java.util.Map metadataRouters) { + this.metadataRouters = metadataRouters; + return this; + } + + /** + * Get metadata router configuration by name + * @param routerName router name + * @return metadata router configuration + */ + public MetadataRouterConfig getMetadataRouter(String routerName) { + return metadataRouters.get(routerName); + } + + /** + * Add metadata router configuration + * @param routerName router name + * @param config router configuration + */ + public void addMetadataRouter(String routerName, MetadataRouterConfig config) { + metadataRouters.put(routerName, config); + } + + public String getPrimaryBackupOrder() { + return primaryBackupOrder; + } + + public RoutingProperties setPrimaryBackupOrder(String primaryBackupOrder) { + this.primaryBackupOrder = primaryBackupOrder; + return this; + } + + public boolean isPrimaryBackupEnabled() { + return primaryBackupEnabled; + } + + public RoutingProperties setPrimaryBackupEnabled(boolean primaryBackupEnabled) { + this.primaryBackupEnabled = primaryBackupEnabled; + return this; + } + + public String getLocationLat() { + return locationLat; + } + + public RoutingProperties setLocationLat(String locationLat) { + this.locationLat = locationLat; + return this; + } + + public String getLocationLng() { + return locationLng; + } + + public RoutingProperties setLocationLng(String locationLng) { + this.locationLng = locationLng; + return this; + } +} diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 2130b92ea10..f3048c0fcc8 100644 --- a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -317,6 +317,92 @@ "type": "java.lang.Integer", "sourceType": "org.apache.seata.saga.engine.impl.DefaultStateMachineConfig", "defaultValue": 1800000 + }, + { + "name": "seata.client.routing.enabled", + "type": "java.lang.Boolean", + "description": "Whether enable routing feature.", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties", + "defaultValue": false + }, + { + "name": "seata.client.routing.debug", + "type": "java.lang.Boolean", + "description": "Whether enable routing debug mode.", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties", + "defaultValue": false + }, + { + "name": "seata.client.routing.fallback", + "type": "java.lang.Boolean", + "description": "Whether enable routing fallback strategy.", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties", + "defaultValue": true + }, + { + "name": "seata.client.routing.chain.order", + "type": "java.lang.String", + "description": "Router chain order, e.g. region-router,metadata-router-1,metadata-router-2", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties" + }, + { + "name": "seata.client.routing.region-router.enabled", + "type": "java.lang.Boolean", + "description": "Whether enable region router.", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties", + "defaultValue": true + }, + { + "name": "seata.client.routing.region-router.topN", + "type": "java.lang.Integer", + "description": "Top N servers for region router.", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties", + "defaultValue": 5 + }, + { + "name": "seata.client.routing.metadata-routers", + "type": "java.util.Map", + "description": "Dynamic metadata router configurations. Key: router name (e.g. metadata-router-1, metadata-router-2, custom-router), Value: router configuration", + "sourceType": "org.apache.seata.spring.boot.autoconfigure.properties.client.RoutingProperties" + }, + + { + "name": "seata.client.routing.primary-backup.order", + "type": "java.lang.String", + "description": "Primary backup router order.", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties", + "defaultValue": "region-router" + }, + { + "name": "seata.client.routing.primary-backup.enabled", + "type": "java.lang.Boolean", + "description": "Whether enable primary backup router.", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties", + "defaultValue": false + }, + { + "name": "seata.client.routing.location.lat", + "type": "java.lang.String", + "description": "Client location latitude, e.g. 39.9042", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties" + }, + { + "name": "seata.client.routing.location.lng", + "type": "java.lang.String", + "description": "Client location longitude, e.g. 116.4074", + "sourceType": "org.apache.seata.discovery.routing.config.RoutingProperties" + }, + { + "name": "seata.registry.metadata.lat", + "type": "java.lang.String", + "description": "Server location latitude for region routing, e.g. 39.9042", + "sourceType": "org.apache.seata.spring.boot.autoconfigure.properties.registry.RegistryMetadataProperties" + }, + { + "name": "seata.registry.metadata.lng", + "type": "java.lang.String", + "description": "Server location longitude for region routing, e.g. 116.4074", + "sourceType": "org.apache.seata.spring.boot.autoconfigure.properties.registry.RegistryMetadataProperties" } ], "hints": [ diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/client/MetadataRouterConfigTest.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/client/MetadataRouterConfigTest.java new file mode 100644 index 00000000000..daf8861ce2d --- /dev/null +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/client/MetadataRouterConfigTest.java @@ -0,0 +1,76 @@ +/* + * 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.spring.boot.autoconfigure.properties.client; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MetadataRouterConfigTest { + + @Test + public void testDefaultValues() { + MetadataRouterConfig config = new MetadataRouterConfig(); + + assertFalse(config.isEnabled()); + assertEquals("", config.getExpression()); + } + + @Test + public void testSetAndGetProperties() { + MetadataRouterConfig config = new MetadataRouterConfig(); + + config.setEnabled(false); + config.setExpression("version >= 2.0"); + + assertFalse(config.isEnabled()); + assertEquals("version >= 2.0", config.getExpression()); + } + + @Test + public void testFluentApi() { + MetadataRouterConfig config = + new MetadataRouterConfig().setEnabled(true).setExpression("env = prod"); + + assertTrue(config.isEnabled()); + assertEquals("env = prod", config.getExpression()); + } + + @Test + public void testComplexExpression() { + MetadataRouterConfig config = new MetadataRouterConfig(); + + String complexExpression = "(version >= 2.0) | (env = dev) | (region = cn-bj)"; + config.setExpression(complexExpression); + + assertEquals(complexExpression, config.getExpression()); + } + + @Test + public void testEmptyExpression() { + MetadataRouterConfig config = new MetadataRouterConfig(); + + config.setExpression(""); + assertEquals("", config.getExpression()); + + config.setExpression(null); + assertNull(config.getExpression()); + } +} diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/client/RoutingPropertiesTest.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/client/RoutingPropertiesTest.java new file mode 100644 index 00000000000..373a8c1e359 --- /dev/null +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-client/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/client/RoutingPropertiesTest.java @@ -0,0 +1,164 @@ +/* + * 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.spring.boot.autoconfigure.properties.client; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test for RoutingProperties + */ +public class RoutingPropertiesTest { + + /** + * Test default values + */ + @Test + public void testDefaultValues() { + RoutingProperties properties = new RoutingProperties(); + + // Verify default values + assertFalse(properties.isEnabled()); + assertFalse(properties.isDebug()); + assertTrue(properties.isFallback()); + assertEquals("", properties.getChainOrder()); + assertTrue(properties.isRegionRouterEnabled()); + assertEquals(5, properties.getRegionRouterTopN()); + + assertEquals("region-router", properties.getPrimaryBackupOrder()); + assertTrue(properties.isPrimaryBackupEnabled()); + assertEquals("", properties.getLocationLat()); + assertEquals("", properties.getLocationLng()); + assertNotNull(properties.getMetadataRouters()); + assertTrue(properties.getMetadataRouters().isEmpty()); + } + + /** + * Test dynamic metadata router configuration + */ + @Test + public void testDynamicMetadataRouters() { + RoutingProperties properties = new RoutingProperties(); + + // Add metadata router configuration + MetadataRouterConfig config1 = new MetadataRouterConfig(); + config1.setEnabled(true); + config1.setExpression("version >= 2.0"); + properties.addMetadataRouter("metadata-router-1", config1); + + MetadataRouterConfig config2 = new MetadataRouterConfig(); + config2.setEnabled(true); + config2.setExpression("env = prod"); + properties.addMetadataRouter("metadata-router-2", config2); + + MetadataRouterConfig customConfig = new MetadataRouterConfig(); + customConfig.setEnabled(false); + customConfig.setExpression("region = cn-bj"); + properties.addMetadataRouter("custom-router", customConfig); + + // Verify configuration + assertEquals(3, properties.getMetadataRouters().size()); + + MetadataRouterConfig retrievedConfig1 = properties.getMetadataRouter("metadata-router-1"); + assertNotNull(retrievedConfig1); + assertTrue(retrievedConfig1.isEnabled()); + assertEquals("version >= 2.0", retrievedConfig1.getExpression()); + + MetadataRouterConfig retrievedConfig2 = properties.getMetadataRouter("metadata-router-2"); + assertNotNull(retrievedConfig2); + assertTrue(retrievedConfig2.isEnabled()); + assertEquals("env = prod", retrievedConfig2.getExpression()); + + MetadataRouterConfig retrievedCustomConfig = properties.getMetadataRouter("custom-router"); + assertNotNull(retrievedCustomConfig); + assertFalse(retrievedCustomConfig.isEnabled()); + assertEquals("region = cn-bj", retrievedCustomConfig.getExpression()); + + // Verify non-existent router returns null + assertNull(properties.getMetadataRouter("non-existent-router")); + } + + /** + * Test setter and getter properties + */ + @Test + public void testSetAndGetProperties() { + RoutingProperties properties = new RoutingProperties(); + + // Set properties + properties.setEnabled(true); + properties.setDebug(true); + properties.setFallback(false); + properties.setChainOrder("region-router,metadata-router-1,metadata-router-2"); + properties.setRegionRouterEnabled(false); + properties.setRegionRouterTopN(10); + + properties.setPrimaryBackupOrder("metadata-router-1"); + properties.setPrimaryBackupEnabled(false); + properties.setLocationLat("39.9042"); + properties.setLocationLng("116.4074"); + + // Verify properties + assertTrue(properties.isEnabled()); + assertTrue(properties.isDebug()); + assertFalse(properties.isFallback()); + assertEquals("region-router,metadata-router-1,metadata-router-2", properties.getChainOrder()); + assertFalse(properties.isRegionRouterEnabled()); + assertEquals(10, properties.getRegionRouterTopN()); + + assertEquals("metadata-router-1", properties.getPrimaryBackupOrder()); + assertFalse(properties.isPrimaryBackupEnabled()); + assertEquals("39.9042", properties.getLocationLat()); + assertEquals("116.4074", properties.getLocationLng()); + } + + /** + * Test fluent API + */ + @Test + public void testFluentApi() { + RoutingProperties properties = new RoutingProperties() + .setEnabled(true) + .setDebug(true) + .setFallback(false) + .setChainOrder("region-router,metadata-router-1") + .setRegionRouterEnabled(false) + .setRegionRouterTopN(8) + .setPrimaryBackupOrder("region-router") + .setPrimaryBackupEnabled(false) + .setLocationLat("31.2304") + .setLocationLng("121.4737"); + + // Verify fluent API result + assertTrue(properties.isEnabled()); + assertTrue(properties.isDebug()); + assertFalse(properties.isFallback()); + assertEquals("region-router,metadata-router-1", properties.getChainOrder()); + assertFalse(properties.isRegionRouterEnabled()); + assertEquals(8, properties.getRegionRouterTopN()); + + assertEquals("region-router", properties.getPrimaryBackupOrder()); + assertFalse(properties.isPrimaryBackupEnabled()); + assertEquals("31.2304", properties.getLocationLat()); + assertEquals("121.4737", properties.getLocationLng()); + } +} diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java index e6fc8c5b4b5..ffecd191f40 100644 --- a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/StarterConstants.java @@ -36,6 +36,7 @@ public interface StarterConstants { String UNDO_PREFIX = CLIENT_PREFIX + ".undo"; String LOAD_BALANCE_PREFIX_KEBAB_STYLE = CLIENT_PREFIX + ".load-balance"; String LOAD_BALANCE_PREFIX = CLIENT_PREFIX + ".loadBalance"; + String ROUTING_PREFIX = CLIENT_PREFIX + ".routing"; String HTTP_PREFIX = CLIENT_PREFIX + ".http"; String LOG_PREFIX = SEATA_PREFIX + ".log"; String COMPRESS_PREFIX = UNDO_PREFIX + ".compress"; diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/registry/RegistryMetadataProperties.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/registry/RegistryMetadataProperties.java index 898c45e7a9a..725d0bb65b3 100644 --- a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/registry/RegistryMetadataProperties.java +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/properties/registry/RegistryMetadataProperties.java @@ -19,6 +19,10 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + import static org.apache.seata.spring.boot.autoconfigure.StarterConstants.REGISTRY_METADATA_PREFIX; @Component @@ -26,6 +30,12 @@ public class RegistryMetadataProperties { private String external; + /** + * Dynamic metadata configuration map + * This allows loading all metadata properties dynamically + */ + private Map metadata = new ConcurrentHashMap<>(); + public String getExternal() { return external; } @@ -34,4 +44,54 @@ public RegistryMetadataProperties setExternal(String external) { this.external = external; return this; } + + public Map getMetadata() { + return new HashMap<>(metadata); + } + + public RegistryMetadataProperties setMetadata(Map metadata) { + this.metadata = metadata; + return this; + } + + /** + * Set metadata value by key + * @param key metadata key + * @param value metadata value + */ + public void setMetadataValue(String key, String value) { + metadata.put(key, value); + } + + /** + * Get location latitude + * @return latitude value + */ + public String getLat() { + return metadata.get("lat"); + } + + /** + * Set location latitude + * @param lat latitude value + */ + public void setLat(String lat) { + metadata.put("lat", lat); + } + + /** + * Get location longitude + * @return longitude value + */ + public String getLng() { + return metadata.get("lng"); + } + + /** + * Set location longitude + * @param lng longitude value + */ + public void setLng(String lng) { + metadata.put("lng", lng); + } } diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/registry/RegistryMetadataPropertiesTest.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/registry/RegistryMetadataPropertiesTest.java new file mode 100644 index 00000000000..7e723a64c2a --- /dev/null +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/properties/registry/RegistryMetadataPropertiesTest.java @@ -0,0 +1,35 @@ +/* + * 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.spring.boot.autoconfigure.properties.registry; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RegistryMetadataPropertiesTest { + + @Test + public void testLocationConfiguration() { + RegistryMetadataProperties properties = new RegistryMetadataProperties(); + + properties.setLat("39.9042"); + properties.setLng("116.4074"); + + assertEquals("39.9042", properties.getLat()); + assertEquals("116.4074", properties.getLng()); + } +} diff --git a/server/src/main/java/org/apache/seata/server/cluster/raft/RaftServerManager.java b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftServerManager.java index bf09e53a4d2..acdf6dee2dd 100644 --- a/server/src/main/java/org/apache/seata/server/cluster/raft/RaftServerManager.java +++ b/server/src/main/java/org/apache/seata/server/cluster/raft/RaftServerManager.java @@ -101,9 +101,9 @@ public static void init() { return; } else { if (RAFT_MODE) { - for (RegistryService instance : MultiRegistryFactory.getInstances()) { - if (!(instance instanceof FileRegistryServiceImpl) - && !(instance instanceof NamingserverRegistryServiceImpl)) { + for (RegistryService registryService : MultiRegistryFactory.getInstances()) { + if (!(registryService instanceof FileRegistryServiceImpl) + && !(registryService instanceof NamingserverRegistryServiceImpl)) { throw new IllegalArgumentException("Raft store mode not support other Registration Center"); } } diff --git a/server/src/main/java/org/apache/seata/server/storage/raft/store/RaftVGroupMappingStoreManager.java b/server/src/main/java/org/apache/seata/server/storage/raft/store/RaftVGroupMappingStoreManager.java index 1396ded4a7b..e81c6ed2429 100644 --- a/server/src/main/java/org/apache/seata/server/storage/raft/store/RaftVGroupMappingStoreManager.java +++ b/server/src/main/java/org/apache/seata/server/storage/raft/store/RaftVGroupMappingStoreManager.java @@ -20,6 +20,7 @@ import org.apache.seata.common.loader.LoadLevel; import org.apache.seata.common.metadata.ClusterRole; import org.apache.seata.common.metadata.Instance; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.core.store.MappingDO; import org.apache.seata.discovery.registry.MultiRegistryFactory; import org.apache.seata.discovery.registry.RegistryService; @@ -29,6 +30,7 @@ import org.apache.seata.server.cluster.raft.util.RaftTaskUtil; import org.apache.seata.server.store.VGroupMappingStoreManager; +import java.net.InetSocketAddress; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -136,8 +138,11 @@ public void notifyMapping() { Instance node = instance.clone(); node.setRole(RaftServerManager.isLeader(group) ? ClusterRole.LEADER : ClusterRole.FOLLOWER); Instance.getInstances().add(node); + InetSocketAddress inetSocketAddress = new InetSocketAddress( + node.getTransaction().getHost(), node.getTransaction().getPort()); + ServiceInstance serviceInstance = new ServiceInstance(inetSocketAddress, node.getMetadata()); for (RegistryService registryService : MultiRegistryFactory.getInstances()) { - registryService.register(node); + registryService.register(serviceInstance); } } } catch (Exception e) { diff --git a/server/src/main/java/org/apache/seata/server/store/VGroupMappingStoreManager.java b/server/src/main/java/org/apache/seata/server/store/VGroupMappingStoreManager.java index f58a7d11fcd..d6241fdd212 100644 --- a/server/src/main/java/org/apache/seata/server/store/VGroupMappingStoreManager.java +++ b/server/src/main/java/org/apache/seata/server/store/VGroupMappingStoreManager.java @@ -18,6 +18,7 @@ import org.apache.seata.common.XID; import org.apache.seata.common.metadata.Instance; +import org.apache.seata.common.metadata.ServiceInstance; import org.apache.seata.core.store.MappingDO; import org.apache.seata.discovery.registry.MultiRegistryFactory; import org.apache.seata.discovery.registry.RegistryService; @@ -59,9 +60,10 @@ default void notifyMapping() { Map map = this.readVGroups(); instance.addMetadata("vGroup", map); try { - InetSocketAddress address = new InetSocketAddress(XID.getIpAddress(), XID.getPort()); + ServiceInstance serviceInstance = new ServiceInstance( + new InetSocketAddress(XID.getIpAddress(), XID.getPort()), instance.getMetadata()); for (RegistryService registryService : MultiRegistryFactory.getInstances()) { - registryService.register(address); + registryService.register(serviceInstance); } } catch (Exception e) { throw new RuntimeException("vGroup mapping relationship notified failed! ", e); diff --git a/server/src/main/resources/application.example.yml b/server/src/main/resources/application.example.yml index b3aafbe4d91..f7f253028ff 100644 --- a/server/src/main/resources/application.example.yml +++ b/server/src/main/resources/application.example.yml @@ -87,6 +87,9 @@ seata: preferred-networks: 30.240.* metadata: weight: 100 + # Server location configuration for region routing + lat: "" + lng: "" seata: server-addr: 127.0.0.1:8081 cluster: default diff --git a/server/src/main/resources/application.raft.example.yml b/server/src/main/resources/application.raft.example.yml index 14ce8432fa3..2c62fa17da0 100644 --- a/server/src/main/resources/application.raft.example.yml +++ b/server/src/main/resources/application.raft.example.yml @@ -82,6 +82,11 @@ seata: # support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa type: file preferred-networks: 30.240.* + metadata: + weight: 100 + # Server location configuration for region routing + lat: "" + lng: "" seata: server-addr: 127.0.0.1:8081 cluster: default