diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceDiscoveryRegistryDirectory.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceDiscoveryRegistryDirectory.java index e4297c0991f..ac04528717f 100644 --- a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceDiscoveryRegistryDirectory.java +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/ServiceDiscoveryRegistryDirectory.java @@ -368,13 +368,31 @@ private void refreshInvoker(List invokerUrls) { logger.info(String.format("Refreshed invoker size %s from registry %s", newUrlInvokerMap.size(), this)); if (CollectionUtils.isEmptyMap(newUrlInvokerMap)) { - logger.error( - PROTOCOL_UNSUPPORTED, - "", - "", - "Unsupported protocol.", - new IllegalStateException(String.format( - "Cannot create invokers from url address list (total %s)", invokerUrls.size()))); + // Check if all instances are Spring Cloud instances (no Dubbo metadata) + boolean allSpringCloud = !invokerUrls.isEmpty() + && invokerUrls.stream().allMatch(url -> { + if (url instanceof InstanceAddressURL) { + ServiceInstance si = ((InstanceAddressURL) url).getInstance(); + return si != null && "SPRING_CLOUD".equals(si.getMetadata("preserved.register.source")); + } + return false; + }); + if (allSpringCloud) { + logger.warn( + PROTOCOL_UNSUPPORTED, + "", + "", + "No matching Dubbo protocol invokers found. All discovered instances are Spring Cloud instances, " + + "which cannot be invoked via Dubbo protocol. Waiting for Dubbo provider instances to register."); + } else { + logger.error( + PROTOCOL_UNSUPPORTED, + "", + "", + "Unsupported protocol.", + new IllegalStateException(String.format( + "Cannot create invokers from url address list (total %s)", invokerUrls.size()))); + } return; } List> newInvokers = Collections.unmodifiableList(new ArrayList<>(newUrlInvokerMap.values())); diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/metadata/SpringCloudServiceInstanceNotificationCustomizer.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/metadata/SpringCloudServiceInstanceNotificationCustomizer.java index 44b32b6a68c..23556c6062a 100644 --- a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/metadata/SpringCloudServiceInstanceNotificationCustomizer.java +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/client/metadata/SpringCloudServiceInstanceNotificationCustomizer.java @@ -52,15 +52,12 @@ public void customize(List serviceInstance) { @Override public List getMatchedServiceInfos(ProtocolServiceKey consumerProtocolServiceKey) { String consumerProtocol = consumerProtocolServiceKey.getProtocol(); - if (consumerProtocol != null && !REST_PROTOCOL.equalsIgnoreCase(consumerProtocol)) { + // When consumer protocol is null or not REST, skip Spring Cloud instances. + // Only match when consumer explicitly requests REST protocol. + if (!REST_PROTOCOL.equalsIgnoreCase(consumerProtocol)) { return Collections.emptyList(); } - String protocol = consumerProtocol; - if (protocol == null) { - protocol = REST_PROTOCOL; - } - getServices() .putIfAbsent( consumerProtocolServiceKey.getServiceKeyString(), @@ -68,7 +65,7 @@ public List getMatchedServiceInfos(ProtocolServiceKey consumerProto consumerProtocolServiceKey.getInterfaceName(), consumerProtocolServiceKey.getGroup(), consumerProtocolServiceKey.getVersion(), - protocol, + consumerProtocol, instance.getPort(), consumerProtocolServiceKey.getInterfaceName(), new HashMap<>())); diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/ServiceDiscoveryRegistryDirectoryTest.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/ServiceDiscoveryRegistryDirectoryTest.java new file mode 100644 index 00000000000..969063d6cb1 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/ServiceDiscoveryRegistryDirectoryTest.java @@ -0,0 +1,194 @@ +/* + * 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.dubbo.registry.client; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.url.component.ServiceConfigURL; +import org.apache.dubbo.metadata.MetadataInfo; +import org.apache.dubbo.registry.client.metadata.SpringCloudServiceInstanceNotificationCustomizer; +import org.apache.dubbo.registry.integration.DemoService; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.Protocol; +import org.apache.dubbo.rpc.cluster.RouterChain; +import org.apache.dubbo.rpc.cluster.router.state.BitList; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.FrameworkModel; +import org.apache.dubbo.rpc.model.ModuleModel; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.apache.dubbo.common.constants.CommonConstants.INTERFACE_KEY; +import static org.apache.dubbo.common.constants.CommonConstants.PROTOCOL_KEY; +import static org.apache.dubbo.rpc.cluster.Constants.CONSUMER_URL_KEY; +import static org.apache.dubbo.rpc.cluster.Constants.REFER_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ServiceDiscoveryRegistryDirectoryTest { + private static final String TEST_PROTOCOL = "mock"; + + @AfterEach + void tearDown() { + FrameworkModel.destroyAll(); + } + + @Test + void testNotifyDeduplicatesInstanceUrlsBeforeRefer() { + ApplicationModel applicationModel = ApplicationModel.defaultModel(); + ModuleModel moduleModel = applicationModel.getDefaultModule(); + ServiceDiscoveryRegistryDirectory directory = + new ServiceDiscoveryRegistryDirectory<>(DemoService.class, createDirectoryUrl(moduleModel)); + + @SuppressWarnings("unchecked") + RouterChain routerChain = mock(RouterChain.class); + doAnswer(invocation -> { + invocation.getArgument(1, Runnable.class).run(); + return null; + }) + .when(routerChain) + .setInvokers(any(BitList.class), any(Runnable.class)); + directory.setRouterChain(routerChain); + + Protocol protocol = mock(Protocol.class); + @SuppressWarnings("unchecked") + Invoker invoker = mock(Invoker.class); + directory.setProtocol(protocol); + + InstanceAddressURL instanceUrl = + createProviderInstance(applicationModel).toURL(TEST_PROTOCOL); + when(invoker.getUrl()).thenReturn(instanceUrl); + when(protocol.refer(eq(DemoService.class), any(InstanceAddressURL.class))) + .thenReturn(invoker); + + directory.notify(Arrays.asList(instanceUrl, instanceUrl)); + + verify(protocol, times(1)).refer(eq(DemoService.class), any(InstanceAddressURL.class)); + assertEquals(1, directory.getInvokers().size()); + } + + @Test + void testNotifyIgnoresEmptyInstanceUrlList() { + ApplicationModel applicationModel = ApplicationModel.defaultModel(); + ModuleModel moduleModel = applicationModel.getDefaultModule(); + ServiceDiscoveryRegistryDirectory directory = + new ServiceDiscoveryRegistryDirectory<>(DemoService.class, createDirectoryUrl(moduleModel)); + + Protocol protocol = mock(Protocol.class); + directory.setProtocol(protocol); + + directory.notify(Collections.emptyList()); + + verify(protocol, never()).refer(eq(DemoService.class), any(InstanceAddressURL.class)); + assertEquals(0, directory.getInvokers().size()); + } + + @Test + void testNotifySkipsSpringCloudInstancesForNonRestConsumer() { + ApplicationModel applicationModel = ApplicationModel.defaultModel(); + ModuleModel moduleModel = applicationModel.getDefaultModule(); + ServiceDiscoveryRegistryDirectory directory = + new ServiceDiscoveryRegistryDirectory<>(DemoService.class, createDirectoryUrl(moduleModel)); + + Protocol protocol = mock(Protocol.class); + directory.setProtocol(protocol); + + List instances = Collections.singletonList( + createSpringCloudInstance(applicationModel, "demo-provider", "127.0.0.1", 8080)); + SpringCloudServiceInstanceNotificationCustomizer customizer = + new SpringCloudServiceInstanceNotificationCustomizer(); + customizer.customize(instances); + + directory.notify(Collections.singletonList(instances.get(0).toURL(TEST_PROTOCOL))); + + verify(protocol, never()).refer(eq(DemoService.class), any(InstanceAddressURL.class)); + assertEquals(0, directory.getInvokers().size()); + } + + private URL createDirectoryUrl(ModuleModel moduleModel) { + Map params = new HashMap<>(); + params.put(INTERFACE_KEY, DemoService.class.getName()); + params.put(PROTOCOL_KEY, TEST_PROTOCOL); + + URL registryUrl = new ServiceConfigURL( + "registry", "127.0.0.1", 2181, "org.apache.dubbo.registry.RegistryService", params); + URL consumerUrl = URL.valueOf("consumer://127.0.0.1/" + DemoService.class.getName() + "?interface=" + + DemoService.class.getName() + "&check=false&protocol=" + TEST_PROTOCOL) + .setScopeModel(moduleModel); + + Map attributes = new HashMap<>(); + attributes.put(REFER_KEY, Collections.emptyMap()); + attributes.put(CONSUMER_URL_KEY, consumerUrl); + return registryUrl.addAttributes(attributes).setScopeModel(moduleModel); + } + + private DefaultServiceInstance createProviderInstance(ApplicationModel applicationModel) { + DefaultServiceInstance instance = new DefaultServiceInstance(); + instance.setServiceName("demo-provider"); + instance.setHost("127.0.0.1"); + instance.setPort(20880); + instance.setEnabled(true); + instance.setHealthy(true); + instance.setApplicationModel(applicationModel); + instance.setMetadata(new HashMap<>()); + + MetadataInfo.ServiceInfo serviceInfo = new MetadataInfo.ServiceInfo( + DemoService.class.getName(), + null, + null, + TEST_PROTOCOL, + 20880, + DemoService.class.getName(), + new HashMap<>()); + MetadataInfo metadataInfo = new MetadataInfo( + "demo-provider", + "rev-1", + new ConcurrentHashMap<>(Collections.singletonMap(serviceInfo.getMatchKey(), serviceInfo))); + instance.setServiceMetadata(metadataInfo); + return instance; + } + + private DefaultServiceInstance createSpringCloudInstance( + ApplicationModel applicationModel, String serviceName, String host, int port) { + DefaultServiceInstance instance = new DefaultServiceInstance(); + instance.setServiceName(serviceName); + instance.setHost(host); + instance.setPort(port); + instance.setEnabled(true); + instance.setHealthy(true); + instance.setApplicationModel(applicationModel); + + Map metadata = new HashMap<>(); + metadata.put("preserved.register.source", "SPRING_CLOUD"); + instance.setMetadata(metadata); + return instance; + } +} diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/metadata/SpringCloudServiceInstanceNotificationCustomizerTest.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/metadata/SpringCloudServiceInstanceNotificationCustomizerTest.java new file mode 100644 index 00000000000..7e1cf3ccd74 --- /dev/null +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/client/metadata/SpringCloudServiceInstanceNotificationCustomizerTest.java @@ -0,0 +1,228 @@ +/* + * 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.dubbo.registry.client.metadata; + +import org.apache.dubbo.common.ProtocolServiceKey; +import org.apache.dubbo.metadata.MetadataInfo; +import org.apache.dubbo.registry.client.DefaultServiceInstance; +import org.apache.dubbo.registry.client.ServiceInstance; +import org.apache.dubbo.rpc.model.ApplicationModel; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * {@link SpringCloudServiceInstanceNotificationCustomizer} Test + */ +class SpringCloudServiceInstanceNotificationCustomizerTest { + + private SpringCloudServiceInstanceNotificationCustomizer customizer; + private ApplicationModel applicationModel; + + @AfterEach + public void clearUp() { + applicationModel.destroy(); + } + + @BeforeEach + public void setUp() { + customizer = new SpringCloudServiceInstanceNotificationCustomizer(); + applicationModel = ApplicationModel.defaultModel(); + } + + @Test + void testCustomizeWithEmptyList() { + List instances = Collections.emptyList(); + // no exception thrown + Assertions.assertDoesNotThrow(() -> customizer.customize(instances)); + } + + @Test + void testCustomizeSkipsNonSpringCloudInstances() { + DefaultServiceInstance instance = createDubboInstance("app1", "192.168.1.1", 20880); + List instances = Collections.singletonList(instance); + + customizer.customize(instances); + + // Non-Spring Cloud instance should not have metadata set by customizer + assertNull(instance.getServiceMetadata()); + } + + @Test + void testCustomizeSetsMetadataForSpringCloudInstances() { + DefaultServiceInstance instance = createSpringCloudInstance("app1", "192.168.1.1", 8080); + List instances = Collections.singletonList(instance); + + customizer.customize(instances); + + MetadataInfo metadata = instance.getServiceMetadata(); + assertNotNull(metadata); + assertEquals("app1", metadata.getApp()); + assertTrue(metadata.getRevision().startsWith("SPRING_CLOUD-")); + assertNotNull(metadata.getServices().get("*")); + assertEquals("rest", metadata.getServices().get("*").getProtocol()); + } + + @Test + void testCustomizeSkipsMixedInstances() { + DefaultServiceInstance springCloudInstance = createSpringCloudInstance("app1", "192.168.1.1", 8080); + DefaultServiceInstance dubboInstance = createDubboInstance("app1", "192.168.1.2", 20880); + List instances = Arrays.asList(springCloudInstance, dubboInstance); + + customizer.customize(instances); + + // Mixed instances: customizer should skip all (allMatch fails) + assertNull(springCloudInstance.getServiceMetadata()); + assertNull(dubboInstance.getServiceMetadata()); + } + + @Test + void testGetMatchedServiceInfos_consumerProtocolNull_returnsEmpty() { + DefaultServiceInstance instance = createSpringCloudInstance("app1", "192.168.1.1", 8080); + customizer.customize(Collections.singletonList(instance)); + + MetadataInfo metadata = instance.getServiceMetadata(); + assertNotNull(metadata); + + // Consumer protocol is null (e.g., @DubboReference without explicit protocol) + ProtocolServiceKey consumerKey = new ProtocolServiceKey("com.example.DemoService", null, null, null); + + List matched = metadata.getMatchedServiceInfos(consumerKey); + assertTrue(matched.isEmpty(), "Should return empty when consumer protocol is null"); + } + + @Test + void testGetMatchedServiceInfos_consumerProtocolDubbo_returnsEmpty() { + DefaultServiceInstance instance = createSpringCloudInstance("app1", "192.168.1.1", 8080); + customizer.customize(Collections.singletonList(instance)); + + MetadataInfo metadata = instance.getServiceMetadata(); + assertNotNull(metadata); + + // Consumer explicitly requests dubbo protocol + ProtocolServiceKey consumerKey = new ProtocolServiceKey("com.example.DemoService", null, null, "dubbo"); + + List matched = metadata.getMatchedServiceInfos(consumerKey); + assertTrue(matched.isEmpty(), "Should return empty when consumer protocol is dubbo"); + } + + @Test + void testGetMatchedServiceInfos_consumerProtocolTriple_returnsEmpty() { + DefaultServiceInstance instance = createSpringCloudInstance("app1", "192.168.1.1", 8080); + customizer.customize(Collections.singletonList(instance)); + + MetadataInfo metadata = instance.getServiceMetadata(); + + // Consumer explicitly requests triple protocol + ProtocolServiceKey consumerKey = new ProtocolServiceKey("com.example.DemoService", null, null, "tri"); + + List matched = metadata.getMatchedServiceInfos(consumerKey); + assertTrue(matched.isEmpty(), "Should return empty when consumer protocol is tri"); + } + + @Test + void testGetMatchedServiceInfos_consumerProtocolRest_returnsMatched() { + DefaultServiceInstance instance = createSpringCloudInstance("app1", "192.168.1.1", 8080); + customizer.customize(Collections.singletonList(instance)); + + MetadataInfo metadata = instance.getServiceMetadata(); + assertNotNull(metadata); + + // Consumer explicitly requests REST protocol — should match + ProtocolServiceKey consumerKey = new ProtocolServiceKey("com.example.DemoService", null, null, "rest"); + + List matched = metadata.getMatchedServiceInfos(consumerKey); + assertEquals(1, matched.size(), "Should return matched service info when consumer protocol is rest"); + assertEquals("rest", matched.get(0).getProtocol()); + } + + @Test + void testGetMatchedServiceInfos_consumerProtocolUppercaseRest_returnsMatched() { + DefaultServiceInstance instance = createSpringCloudInstance("app1", "192.168.1.1", 8080); + customizer.customize(Collections.singletonList(instance)); + + MetadataInfo metadata = instance.getServiceMetadata(); + assertNotNull(metadata); + + ProtocolServiceKey consumerKey = new ProtocolServiceKey("com.example.DemoService", null, null, "REST"); + + List matched = metadata.getMatchedServiceInfos(consumerKey); + assertEquals(1, matched.size(), "Should return matched service info when consumer protocol is REST"); + assertEquals("REST", matched.get(0).getProtocol()); + } + + @Test + void testCustomizeMultipleSpringCloudInstances() { + DefaultServiceInstance instance1 = createSpringCloudInstance("app1", "192.168.1.1", 8080); + DefaultServiceInstance instance2 = createSpringCloudInstance("app1", "192.168.1.2", 8081); + List instances = Arrays.asList(instance1, instance2); + + customizer.customize(instances); + + // Both instances should have metadata set + assertNotNull(instance1.getServiceMetadata()); + assertNotNull(instance2.getServiceMetadata()); + + // Each instance should have a unique revision + String revision1 = instance1.getServiceMetadata().getRevision(); + String revision2 = instance2.getServiceMetadata().getRevision(); + assertNotEquals(revision1, revision2, "Each instance should have a unique revision"); + } + + // --- Helper methods --- + + private DefaultServiceInstance createSpringCloudInstance(String serviceName, String host, int port) { + DefaultServiceInstance instance = new DefaultServiceInstance(); + instance.setServiceName(serviceName); + instance.setHost(host); + instance.setPort(port); + instance.setEnabled(true); + instance.setHealthy(true); + instance.setApplicationModel(applicationModel); + + Map metadata = new HashMap<>(); + metadata.put("preserved.register.source", "SPRING_CLOUD"); + instance.setMetadata(metadata); + return instance; + } + + private DefaultServiceInstance createDubboInstance(String serviceName, String host, int port) { + DefaultServiceInstance instance = new DefaultServiceInstance(); + instance.setServiceName(serviceName); + instance.setHost(host); + instance.setPort(port); + instance.setEnabled(true); + instance.setHealthy(true); + instance.setApplicationModel(applicationModel); + instance.setMetadata(new HashMap<>()); + return instance; + } +}