diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/integration/RegistryProtocol.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/integration/RegistryProtocol.java index 17d941f55ca7..316f246c15c3 100644 --- a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/integration/RegistryProtocol.java +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/integration/RegistryProtocol.java @@ -23,12 +23,9 @@ import org.apache.dubbo.common.extension.ExtensionLoader; import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; import org.apache.dubbo.common.logger.LoggerFactory; -import org.apache.dubbo.common.threadpool.manager.FrameworkExecutorRepository; import org.apache.dubbo.common.timer.HashedWheelTimer; import org.apache.dubbo.common.url.component.ServiceConfigURL; import org.apache.dubbo.common.utils.CollectionUtils; -import org.apache.dubbo.common.utils.ConcurrentHashMapUtils; -import org.apache.dubbo.common.utils.ConcurrentHashSet; import org.apache.dubbo.common.utils.NamedThreadFactory; import org.apache.dubbo.common.utils.StringUtils; import org.apache.dubbo.common.utils.UrlUtils; @@ -39,7 +36,6 @@ import org.apache.dubbo.registry.RegistryFactory; import org.apache.dubbo.registry.RegistryService; import org.apache.dubbo.registry.client.ServiceDiscoveryRegistryDirectory; -import org.apache.dubbo.registry.client.migration.MigrationClusterInvoker; import org.apache.dubbo.registry.client.migration.ServiceDiscoveryMigrationInvoker; import org.apache.dubbo.registry.retry.ReExportTask; import org.apache.dubbo.registry.support.SkipFailbackWrapperException; @@ -73,7 +69,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @@ -101,7 +97,6 @@ import static org.apache.dubbo.common.constants.CommonConstants.USERNAME_KEY; import static org.apache.dubbo.common.constants.CommonConstants.VERSION_KEY; import static org.apache.dubbo.common.constants.LoggerCodeConstants.INTERNAL_ERROR; -import static org.apache.dubbo.common.constants.LoggerCodeConstants.REGISTRY_UNSUPPORTED_CATEGORY; import static org.apache.dubbo.common.constants.RegistryConstants.ALL_CATEGORIES; import static org.apache.dubbo.common.constants.RegistryConstants.CATEGORY_KEY; import static org.apache.dubbo.common.constants.RegistryConstants.CONFIGURATORS_CATEGORY; @@ -178,7 +173,8 @@ public class RegistryProtocol implements Protocol, ScopeModelAware { private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(RegistryProtocol.class); - private final Map serviceConfigurationListeners = new ConcurrentHashMap<>(); + private final Map> serviceConfigurationListeners = + new ConcurrentHashMap<>(); // To solve the problem of RMI repeated exposure port conflicts, the services that have been exposed are no longer // exposed. // provider url <--> registry url <--> exporter @@ -187,8 +183,8 @@ public class RegistryProtocol implements Protocol, ScopeModelAware { protected Protocol protocol; protected ProxyFactory proxyFactory; - private ConcurrentMap reExportFailedTasks = new ConcurrentHashMap<>(); - private HashedWheelTimer retryTimer = new HashedWheelTimer( + private final ConcurrentMap reExportFailedTasks = new ConcurrentHashMap<>(); + private final HashedWheelTimer retryTimer = new HashedWheelTimer( new NamedThreadFactory("DubboReexportTimer", true), DEFAULT_REGISTRY_RETRY_PERIOD, TimeUnit.MILLISECONDS, @@ -208,35 +204,11 @@ public void setProtocol(Protocol protocol) { this.protocol = protocol; } - public void setProxyFactory(ProxyFactory proxyFactory) { - this.proxyFactory = proxyFactory; - } - @Override public int getDefaultPort() { return 9090; } - public Map> getOverrideListeners() { - Map> map = new HashMap<>(); - List applicationModels = frameworkModel.getApplicationModels(); - if (applicationModels.size() == 1) { - return applicationModels - .get(0) - .getBeanFactory() - .getBean(ProviderConfigurationListener.class) - .getOverrideListeners(); - } else { - for (ApplicationModel applicationModel : applicationModels) { - map.putAll(applicationModel - .getBeanFactory() - .getBean(ProviderConfigurationListener.class) - .getOverrideListeners()); - } - } - return map; - } - private static void register(Registry registry, URL registeredProviderUrl) { ApplicationDeployer deployer = registeredProviderUrl.getOrDefaultApplicationModel().getDeployer(); @@ -280,32 +252,25 @@ public Exporter export(final Invoker originInvoker) throws RpcExceptio // subscription information to cover. final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl); final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker); - ConcurrentHashMap> overrideListeners = - getProviderConfigurationListener(overrideSubscribeUrl).getOverrideListeners(); - ConcurrentHashMapUtils.computeIfAbsent(overrideListeners, overrideSubscribeUrl, k -> new ConcurrentHashSet<>()) - .add(overrideSubscribeListener); - - providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener); - // export invoker - final ExporterChangeableWrapper exporter = doLocalExport(originInvoker, providerUrl); - // url to registry + URL overriddenUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener); + final ExporterChangeableWrapper exporter = doLocalExport(originInvoker, overriddenUrl); final Registry registry = getRegistry(registryUrl); - final URL registeredProviderUrl = customizeURL(providerUrl, registryUrl); + final URL registeredProviderUrl = customizeURL(overriddenUrl, registryUrl); - // decide if we need to delay publish (provider itself and registry should both need to register) - boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true); - if (register) { - register(registry, registeredProviderUrl); - } - - // register stated url on provider model - registerStatedUrl(registryUrl, registeredProviderUrl, register); + boolean register = + overriddenUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true); exporter.setRegisterUrl(registeredProviderUrl); exporter.setSubscribeUrl(overrideSubscribeUrl); exporter.setNotifyListener(overrideSubscribeListener); + + if (register) { + register(registry, registeredProviderUrl); + } + exporter.setRegistered(register); + registerStatedUrl(registryUrl, registeredProviderUrl, register); ApplicationModel applicationModel = getApplicationModel(providerUrl.getScopeModel()); if (applicationModel @@ -315,6 +280,7 @@ public Exporter export(final Invoker originInvoker) throws RpcExceptio if (!registry.isServiceDiscovery()) { // Deprecated! Subscribe to override rules in 2.6.x or before. registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener); + overrideSubscribeListener.doOverrideIfNecessary(); } } @@ -339,10 +305,21 @@ private URL overrideUrlWithConfig(URL providerUrl, OverrideListener listener) { ProviderConfigurationListener providerConfigurationListener = getProviderConfigurationListener(providerUrl); providerUrl = providerConfigurationListener.overrideUrl(providerUrl); - ServiceConfigurationListener serviceConfigurationListener = - new ServiceConfigurationListener(providerUrl.getOrDefaultModuleModel(), providerUrl, listener); - serviceConfigurationListeners.put(providerUrl.getServiceKey(), serviceConfigurationListener); - return serviceConfigurationListener.overrideUrl(providerUrl); + CopyOnWriteArrayList listeners = serviceConfigurationListeners.computeIfAbsent( + providerUrl.getServiceKey(), k -> new CopyOnWriteArrayList<>()); + + synchronized (listeners) { + for (ServiceConfigurationListener existing : listeners) { + if (existing.notifyListener == listener) { + return existing.overrideUrl(providerUrl); + } + } + + ServiceConfigurationListener serviceConfigurationListener = + new ServiceConfigurationListener(providerUrl.getOrDefaultModuleModel(), providerUrl, listener); + listeners.add(serviceConfigurationListener); + return serviceConfigurationListener.overrideUrl(providerUrl); + } } @SuppressWarnings("unchecked") @@ -353,18 +330,17 @@ private ExporterChangeableWrapper doLocalExport(final Invoker originIn ReferenceCountExporter exporter = exporterFactory.createExporter(providerUrlKey, () -> protocol.export(invokerDelegate)); - return (ExporterChangeableWrapper) ConcurrentHashMapUtils.computeIfAbsent( - ConcurrentHashMapUtils.computeIfAbsent(bounds, providerUrlKey, k -> new ConcurrentHashMap<>()), - registryUrlKey, - s -> new ExporterChangeableWrapper<>((ReferenceCountExporter) exporter, originInvoker)); - } + Map> registryMap = bounds.compute(providerUrlKey, (k, map) -> { + if (map == null) { + map = new ConcurrentHashMap<>(); + } + map.computeIfAbsent( + registryUrlKey, + s -> new ExporterChangeableWrapper<>((ReferenceCountExporter) exporter, originInvoker)); + return map; + }); - public void reExport(Exporter exporter, URL newInvokerUrl) { - if (exporter instanceof ExporterChangeableWrapper) { - ExporterChangeableWrapper exporterWrapper = (ExporterChangeableWrapper) exporter; - Invoker originInvoker = exporterWrapper.getOriginInvoker(); - reExport(originInvoker, newInvokerUrl); - } + return (ExporterChangeableWrapper) registryMap.get(registryUrlKey); } /** @@ -378,29 +354,40 @@ public void reExport(Exporter exporter, URL newInvokerUrl) { public void reExport(final Invoker originInvoker, URL newInvokerUrl) { String providerUrlKey = getProviderUrlKey(originInvoker); String registryUrlKey = getRegistryUrlKey(originInvoker); + URL registryUrl = getRegistryUrl(originInvoker); + URL retryKey = newInvokerUrl; Map> registryMap = bounds.get(providerUrlKey); if (registryMap == null) { - logger.warn( - INTERNAL_ERROR, - "error state, exporterMap can not be null", - "", - "error state, exporterMap can not be null", - new IllegalStateException("error state, exporterMap can not be null")); + scheduleReExportRetry( + originInvoker, + newInvokerUrl, + retryKey, + registryUrl, + "ExporterMap missing for providerKey=" + providerUrlKey); return; } ExporterChangeableWrapper exporter = (ExporterChangeableWrapper) registryMap.get(registryUrlKey); if (exporter == null) { - logger.warn( - INTERNAL_ERROR, - "error state, exporterMap can not be null", - "", - "error state, exporterMap can not be null", - new IllegalStateException("error state, exporterMap can not be null")); + scheduleReExportRetry( + originInvoker, + newInvokerUrl, + retryKey, + registryUrl, + "Exporter missing for providerKey=" + providerUrlKey + " registryKey=" + registryUrlKey); return; } - URL registeredUrl = exporter.getRegisterUrl(); - URL registryUrl = getRegistryUrl(originInvoker); + if (!exporter.isRegistered()) { + scheduleReExportRetry( + originInvoker, + newInvokerUrl, + retryKey, + registryUrl, + "Exporter exists but not registered yet for providerKey=" + providerUrlKey); + return; + } + + URL registeredUrl = exporter.getRegisterUrl(); URL newProviderUrl = customizeURL(newInvokerUrl, registryUrl); // update local exporter @@ -412,26 +399,33 @@ public void reExport(final Invoker originInvoker, URL newInvokerUrl) { try { doReExport(originInvoker, exporter, registryUrl, registeredUrl, newProviderUrl); } catch (Exception e) { - ReExportTask oldTask = reExportFailedTasks.get(registeredUrl); - if (oldTask != null) { - return; - } - ReExportTask task = new ReExportTask( - () -> doReExport(originInvoker, exporter, registryUrl, registeredUrl, newProviderUrl), - registeredUrl, - null); - oldTask = reExportFailedTasks.putIfAbsent(registeredUrl, task); - if (oldTask == null) { - // never has a retry task. then start a new task for retry. - retryTimer.newTimeout( - task, - registryUrl.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD), - TimeUnit.MILLISECONDS); - } + scheduleReExportRetry( + originInvoker, + newInvokerUrl, + retryKey, + registryUrl, + "Failed to doReExport, error: " + e.getMessage()); } } } + private void scheduleReExportRetry( + Invoker originInvoker, URL newInvokerUrl, URL retryKey, URL registryUrl, String logMessage) { + ReExportTask oldTask = reExportFailedTasks.get(retryKey); + if (oldTask != null) { + return; + } + ReExportTask task = new ReExportTask(() -> reExport(originInvoker, newInvokerUrl), retryKey, null); + ReExportTask prev = reExportFailedTasks.putIfAbsent(retryKey, task); + if (prev == null) { + retryTimer.newTimeout( + task, + registryUrl.getParameter(REGISTRY_RETRY_PERIOD_KEY, DEFAULT_REGISTRY_RETRY_PERIOD), + TimeUnit.MILLISECONDS); + logger.info(logMessage + ", scheduled retry"); + } + } + private void doReExport( final Invoker originInvoker, ExporterChangeableWrapper exporter, @@ -609,10 +603,10 @@ protected ClusterInvoker getMigrationInvoker( } /** - * This method tries to load all RegistryProtocolListener definitions, which are used to control the behaviour of invoker by interacting with defined, then uses those listeners to - * change the status and behaviour of the MigrationInvoker. + * This method tries to load all RegistryProtocolListener definitions, which are used to control the behavior of invoker by interacting with defined, then uses those listeners to + * change the status and behavior of the MigrationInvoker. *

- * Currently available Listener is MigrationRuleListener, one used to control the Migration behaviour with dynamically changing rules. + * Currently available Listener is MigrationRuleListener, one used to control the Migration behavior with dynamically changing rules. * * @param invoker MigrationInvoker that determines which type of invoker list to use * @param url The original url generated during refer, more like a registry:// style url @@ -669,21 +663,6 @@ protected ClusterInvoker doCreateInvoker( return (ClusterInvoker) cluster.join(directory, true); } - public void reRefer(ClusterInvoker invoker, URL newSubscribeUrl) { - if (!(invoker instanceof MigrationClusterInvoker)) { - logger.error( - REGISTRY_UNSUPPORTED_CATEGORY, - "", - "", - "Only invoker type of MigrationClusterInvoker supports reRefer, current invoker is " - + invoker.getClass()); - return; - } - - MigrationClusterInvoker migrationClusterInvoker = (MigrationClusterInvoker) invoker; - migrationClusterInvoker.reRefer(newSubscribeUrl); - } - public static URL toSubscribeUrl(URL url) { return url.addParameter(CATEGORY_KEY, ALL_CATEGORIES); } @@ -779,7 +758,7 @@ public Invoker getInvoker() { private static class DestroyableExporter implements Exporter { - private Exporter exporter; + private final Exporter exporter; public DestroyableExporter(Exporter exporter) { this.exporter = exporter; @@ -870,22 +849,10 @@ public synchronized void doOverrideIfNecessary() { String registryUrlKey = getRegistryUrlKey(originInvoker); Map> exporterMap = bounds.get(providerUrlKey); if (exporterMap == null) { - logger.warn( - INTERNAL_ERROR, - "error state, exporterMap can not be null", - "", - "error state, exporterMap can not be null", - new IllegalStateException("error state, exporterMap can not be null")); return; } ExporterChangeableWrapper exporter = exporterMap.get(registryUrlKey); if (exporter == null) { - logger.warn( - INTERNAL_ERROR, - "unknown error in registry module", - "", - "error state, exporter should not be null", - new IllegalStateException("error state, exporter should not be null")); return; } // The current, may have been merged many times @@ -895,8 +862,13 @@ public synchronized void doOverrideIfNecessary() { URL newUrl = getConfiguredInvokerUrl(configurators, originUrl); newUrl = getConfiguredInvokerUrl( getProviderConfigurationListener(originUrl).getConfigurators(), newUrl); - newUrl = getConfiguredInvokerUrl( - serviceConfigurationListeners.get(originUrl.getServiceKey()).getConfigurators(), newUrl); + List listeners = serviceConfigurationListeners.get(originUrl.getServiceKey()); + + if (listeners != null) { + for (ServiceConfigurationListener l : listeners) { + newUrl = getConfiguredInvokerUrl(l.getConfigurators(), newUrl); + } + } if (!newUrl.equals(currentUrl)) { if (newUrl.getParameter(Constants.NEED_REEXPORT, true)) { RegistryProtocol.this.reExport(originInvoker, newUrl); @@ -936,14 +908,13 @@ private ProviderConfigurationListener getProviderConfigurationListener(ModuleMod } private class ServiceConfigurationListener extends AbstractConfiguratorListener { - private URL providerUrl; - private OverrideListener notifyListener; + + private final OverrideListener notifyListener; private final ModuleModel moduleModel; public ServiceConfigurationListener(ModuleModel moduleModel, URL providerUrl, OverrideListener notifyListener) { super(moduleModel); - this.providerUrl = providerUrl; this.notifyListener = notifyListener; this.moduleModel = moduleModel; if (moduleModel @@ -954,7 +925,7 @@ public ServiceConfigurationListener(ModuleModel moduleModel, URL providerUrl, Ov } } - private URL overrideUrl(URL providerUrl) { + private URL overrideUrl(URL providerUrl) { return RegistryProtocol.getConfiguredInvokerUrl(configurators, providerUrl); } @@ -989,10 +960,9 @@ public ProviderConfigurationListener(ModuleModel moduleModel) { * Get existing configuration rule and override provider url before exporting. * * @param providerUrl - * @param * @return */ - private URL overrideUrl(URL providerUrl) { + private URL overrideUrl(URL providerUrl) { return RegistryProtocol.getConfiguredInvokerUrl(configurators, providerUrl); } @@ -1011,10 +981,6 @@ protected void notifyOverrides() { deployer.decreaseServiceRefreshCount(); } } - - public ConcurrentHashMap> getOverrideListeners() { - return overrideListeners; - } } /** @@ -1025,8 +991,6 @@ public ConcurrentHashMap> getOverrideListeners() { */ private class ExporterChangeableWrapper implements Exporter { - private final ScheduledExecutorService executor; - private final Invoker originInvoker; private Exporter exporter; private URL subscribeUrl; @@ -1039,12 +1003,6 @@ public ExporterChangeableWrapper(ReferenceCountExporter exporter, Invoker this.exporter = exporter; exporter.increaseCount(); this.originInvoker = originInvoker; - FrameworkExecutorRepository frameworkExecutorRepository = originInvoker - .getUrl() - .getOrDefaultFrameworkModel() - .getBeanFactory() - .getBean(FrameworkExecutorRepository.class); - this.executor = frameworkExecutorRepository.getSharedScheduledExecutor(); } public Invoker getOriginInvoker() { @@ -1111,44 +1069,43 @@ public synchronized void unregister() { try { if (subscribeUrl != null) { - Map> overrideListeners = - getProviderConfigurationListener(subscribeUrl).getOverrideListeners(); - Set listeners = overrideListeners.get(subscribeUrl); - if (listeners != null) { - if (listeners.remove(notifyListener)) { - ApplicationModel applicationModel = getApplicationModel(registerUrl.getScopeModel()); - if (applicationModel - .modelEnvironment() - .getConfiguration() - .convert(Boolean.class, ENABLE_26X_CONFIGURATION_LISTEN, true)) { - if (!registry.isServiceDiscovery()) { - registry.unsubscribe(subscribeUrl, notifyListener); - } - } - if (applicationModel - .modelEnvironment() - .getConfiguration() - .convert(Boolean.class, ENABLE_CONFIGURATION_LISTEN, true)) { - for (ModuleModel moduleModel : applicationModel.getPubModuleModels()) { - if (null != moduleModel.getServiceRepository() - && !moduleModel - .getServiceRepository() - .getExportedServices() - .isEmpty()) { - moduleModel - .getExtensionLoader(GovernanceRuleRepository.class) - .getDefaultExtension() - .removeListener( - subscribeUrl.getServiceKey() + CONFIGURATORS_SUFFIX, - serviceConfigurationListeners.remove( - subscribeUrl.getServiceKey())); + ApplicationModel applicationModel = getApplicationModel(registerUrl.getScopeModel()); + if (applicationModel + .modelEnvironment() + .getConfiguration() + .convert(Boolean.class, ENABLE_CONFIGURATION_LISTEN, true)) { + + String serviceKey = subscribeUrl.getServiceKey(); + String ruleKey = serviceKey + CONFIGURATORS_SUFFIX; + + for (ModuleModel moduleModel : applicationModel.getPubModuleModels()) { + if (moduleModel.getServiceRepository() != null + && !moduleModel + .getServiceRepository() + .getExportedServices() + .isEmpty()) { + + GovernanceRuleRepository repository = moduleModel + .getExtensionLoader(GovernanceRuleRepository.class) + .getDefaultExtension(); + + serviceConfigurationListeners.compute(serviceKey, (k, listeners) -> { + if (listeners != null) { + listeners.removeIf(listener -> { + if (listener.notifyListener == notifyListener) { + repository.removeListener(ruleKey, listener); + return true; + } + return false; + }); + if (listeners.isEmpty()) { + return null; + } } - } + return listeners; + }); } } - if (listeners.isEmpty()) { - overrideListeners.remove(subscribeUrl); - } } } } catch (Throwable t) { @@ -1159,14 +1116,18 @@ public synchronized void unregister() { @Override public synchronized void unexport() { + unregister(); String providerUrlKey = getProviderUrlKey(this.originInvoker); String registryUrlKey = getRegistryUrlKey(this.originInvoker); - Map> exporterMap = bounds.remove(providerUrlKey); - if (exporterMap != null) { - exporterMap.remove(registryUrlKey); - } - - unregister(); + bounds.compute(providerUrlKey, (k, exporterMap) -> { + if (exporterMap != null) { + exporterMap.remove(registryUrlKey); + if (exporterMap.isEmpty()) { + return null; + } + } + return exporterMap; + }); doUnExport(); } diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/integration/RegistryProtocolTest.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/integration/RegistryProtocolTest.java index 41688511a127..96a95b70f625 100644 --- a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/integration/RegistryProtocolTest.java +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/integration/RegistryProtocolTest.java @@ -28,17 +28,23 @@ import org.apache.dubbo.registry.client.migration.MigrationRuleListener; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.cluster.Cluster; +import org.apache.dubbo.rpc.cluster.Configurator; import org.apache.dubbo.rpc.cluster.support.FailoverCluster; +import org.apache.dubbo.rpc.cluster.support.MergeableCluster; import org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterWrapper; import org.apache.dubbo.rpc.cluster.support.wrapper.ScopeClusterWrapper; import org.apache.dubbo.rpc.model.ApplicationModel; import org.apache.dubbo.rpc.model.ModuleModel; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -114,7 +120,7 @@ void testConsumerUrlWithoutProtocol() { .getApplicationModel() .getApplicationConfigManager() .setApplication(new ApplicationConfig("application1")); - ExtensionLoader extensionLoaderMock = mock(ExtensionLoader.class); + ExtensionLoader extensionLoaderMock = mock(ExtensionLoader.class); Mockito.when(moduleModel.getExtensionLoader(RegistryProtocolListener.class)) .thenReturn(extensionLoaderMock); Mockito.when(extensionLoaderMock.getActivateExtension(url, REGISTRY_PROTOCOL_LISTENER_KEY)) @@ -127,7 +133,7 @@ void testConsumerUrlWithoutProtocol() { Invoker invoker = registryProtocol.doRefer(cluster, registry, DemoService.class, url, parameters); - Assertions.assertTrue(invoker instanceof MigrationInvoker); + Assertions.assertInstanceOf(MigrationInvoker.class, invoker); URL consumerUrl = ((MigrationInvoker) invoker).getConsumerUrl(); Assertions.assertTrue((consumerUrl != null)); @@ -195,7 +201,7 @@ void testConsumerUrlWithProtocol() { Invoker invoker = registryProtocol.doRefer(cluster, registry, DemoService.class, url, parameters); - Assertions.assertTrue(invoker instanceof MigrationInvoker); + Assertions.assertInstanceOf(MigrationInvoker.class, invoker); URL consumerUrl = ((MigrationInvoker) invoker).getConsumerUrl(); Assertions.assertTrue((consumerUrl != null)); @@ -263,15 +269,15 @@ void testReferWithoutGroup() { Invoker invoker = registryProtocol.refer(DemoService.class, url); - Assertions.assertTrue(invoker instanceof MigrationInvoker); - Assertions.assertTrue(((MigrationInvoker) invoker).getCluster() instanceof ScopeClusterWrapper); - Assertions.assertTrue( - ((ScopeClusterWrapper) ((MigrationInvoker) invoker).getCluster()).getCluster() - instanceof MockClusterWrapper); - Assertions.assertTrue( + Assertions.assertInstanceOf(MigrationInvoker.class, invoker); + Assertions.assertInstanceOf(ScopeClusterWrapper.class, ((MigrationInvoker) invoker).getCluster()); + Assertions.assertInstanceOf( + MockClusterWrapper.class, + ((ScopeClusterWrapper) ((MigrationInvoker) invoker).getCluster()).getCluster()); + Assertions.assertInstanceOf( + FailoverCluster.class, ((MockClusterWrapper) ((ScopeClusterWrapper) ((MigrationInvoker) invoker).getCluster()).getCluster()) - .getCluster() - instanceof FailoverCluster); + .getCluster()); } /** @@ -330,9 +336,9 @@ void testReferWithGroup() { Invoker invoker = registryProtocol.refer(DemoService.class, url); - Assertions.assertTrue(invoker instanceof MigrationInvoker); + Assertions.assertInstanceOf(MigrationInvoker.class, invoker); - Assertions.assertTrue(((MigrationInvoker) invoker).getCluster() instanceof ScopeClusterWrapper); + Assertions.assertInstanceOf(ScopeClusterWrapper.class, ((MigrationInvoker) invoker).getCluster()); // Assertions.assertTrue(((ScopeClusterWrapper) ((MigrationInvoker) // invoker).getCluster()).getCluster() instanceof MockClusterWrapper); @@ -525,7 +531,7 @@ void testRegisterConsumerUrl() { Invoker invoker = registryProtocol.doRefer(cluster, registry, DemoService.class, url, parameters); - Assertions.assertTrue(invoker instanceof MigrationInvoker); + Assertions.assertInstanceOf(MigrationInvoker.class, invoker); URL consumerUrl = ((MigrationInvoker) invoker).getConsumerUrl(); Assertions.assertTrue((consumerUrl != null)); @@ -544,4 +550,110 @@ void testRegisterConsumerUrl() { verify(registry, times(1)).register(registeredConsumerUrl); } + + /** + * Verifies that multiple ServiceConfigurationListeners registered for the same + * service are preserved and that their configurators are applied cumulatively. + */ + @Test + void testServiceConfigurationListenersAggregation() throws Exception { + ApplicationModel.defaultModel().getApplicationConfigManager().setApplication(new ApplicationConfig("test-app")); + + RegistryProtocol registryProtocol = new RegistryProtocol(); + + ModuleModel moduleModel = ApplicationModel.defaultModel().getDefaultModule(); + + Map params = new HashMap<>(); + params.put(INTERFACE_KEY, DemoService.class.getName()); + + ServiceConfigURL providerUrl = + new ServiceConfigURL("dubbo", "127.0.0.1", 20880, DemoService.class.getName(), params); + + URL url = providerUrl.setScopeModel(moduleModel); + + Invoker invoker = mock(Invoker.class); + when(invoker.getUrl()).thenReturn(url); + + Class overrideListenerClass = null; + for (Class c : RegistryProtocol.class.getDeclaredClasses()) { + if ("OverrideListener".equals(c.getSimpleName())) { + overrideListenerClass = c; + break; + } + } + Assertions.assertNotNull(overrideListenerClass); + + Constructor ctor = + overrideListenerClass.getDeclaredConstructor(RegistryProtocol.class, URL.class, Invoker.class); + ctor.setAccessible(true); + + Object listener1 = ctor.newInstance(registryProtocol, url, invoker); + Object listener2 = ctor.newInstance(registryProtocol, url, invoker); + + Method method = + RegistryProtocol.class.getDeclaredMethod("overrideUrlWithConfig", URL.class, overrideListenerClass); + method.setAccessible(true); + + method.invoke(registryProtocol, url, listener1); + method.invoke(registryProtocol, url, listener2); + + Field field = RegistryProtocol.class.getDeclaredField("serviceConfigurationListeners"); + field.setAccessible(true); + + @SuppressWarnings("unchecked") + Map> map = (Map>) field.get(registryProtocol); + + CopyOnWriteArrayList listeners = map.get(url.getServiceKey()); + Assertions.assertEquals(2, listeners.size()); + + Field cfgField = listeners.get(0).getClass().getSuperclass().getDeclaredField("configurators"); + cfgField.setAccessible(true); + + // Mock Configurators to simulate distinct override rules from different registries + Configurator c1 = new Configurator() { + @Override + public URL configure(URL u) { + return u.addParameter("a", "1"); + } + + @Override + public URL getUrl() { + return URL.valueOf("override://0.0.0.0"); + } + }; + Configurator c2 = new Configurator() { + @Override + public URL configure(URL u) { + return u.addParameter("b", "2"); + } + + @Override + public URL getUrl() { + return URL.valueOf("override://0.0.0.0"); + } + }; + + List l1 = new ArrayList<>(); + l1.add(c1); + + List l2 = new ArrayList<>(); + l2.add(c2); + + cfgField.set(listeners.get(0), l1); + cfgField.set(listeners.get(1), l2); + + // Uses reflection to validate internal aggregation behavior without exposing test-only APIs + Method agg = RegistryProtocol.class.getDeclaredMethod("getConfiguredInvokerUrl", List.class, URL.class); + agg.setAccessible(true); + + URL result = url; + for (Object l : listeners) { + @SuppressWarnings("unchecked") + List cs = (List) cfgField.get(l); + result = (URL) agg.invoke(null, cs, result); + } + + Assertions.assertEquals("1", result.getParameter("a")); + Assertions.assertEquals("2", result.getParameter("b")); + } } diff --git a/dubbo-registry/dubbo-registry-api/src/test/resources/dubbo.properties b/dubbo-registry/dubbo-registry-api/src/test/resources/dubbo.properties index 1aade88a5619..5ba2759a30a9 100644 --- a/dubbo-registry/dubbo-registry-api/src/test/resources/dubbo.properties +++ b/dubbo-registry/dubbo-registry-api/src/test/resources/dubbo.properties @@ -1,2 +1,3 @@ dubbo.application.enable-file-cache=false dubbo.service.shutdown.wait=200 +dubbo.application.service-discovery.migration=APPLICATION_FIRST