From 15d657be78cb53fdd6c4c527300500283157b7e1 Mon Sep 17 00:00:00 2001 From: Aryamann Singh <107678802+AryamannSingh7@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:26:52 +0530 Subject: [PATCH] Fix startup NPE when a weakly dependent registry is unavailable with check=false When a registry cannot be created and check=false (e.g. a weakly dependent ZooKeeper is down at startup), AbstractRegistryFactory returns a null registry which RegistryFactoryWrapper still wraps in a ListenerRegistryWrapper. The wrapper already guards register/unregister/subscribe against a null delegate, but getUrl/isAvailable/destroy/unsubscribe/isServiceDiscovery/lookup did not, and RegistryDirectory#subscribe dereferences registry.getUrl() unconditionally to build a metrics label (added in #12582), causing an NPE since 3.2.5. Complete the null-safety in ListenerRegistryWrapper and guard the metrics label computation in RegistryDirectory#subscribe so a service can still start when a non-critical registry is unavailable. Fixes #16178 --- .../registry/ListenerRegistryWrapper.java | 17 +++++++---- .../integration/RegistryDirectory.java | 11 +++++--- .../registry/ListenerRegistryWrapperTest.java | 28 +++++++++++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/ListenerRegistryWrapper.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/ListenerRegistryWrapper.java index 936cc6433a15..feb884585f4f 100644 --- a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/ListenerRegistryWrapper.java +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/ListenerRegistryWrapper.java @@ -22,6 +22,7 @@ import org.apache.dubbo.common.utils.CollectionUtils; import org.apache.dubbo.common.utils.UrlUtils; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -41,17 +42,19 @@ public ListenerRegistryWrapper(Registry registry, List @Override public URL getUrl() { - return registry.getUrl(); + return registry == null ? null : registry.getUrl(); } @Override public boolean isAvailable() { - return registry.isAvailable(); + return registry != null && registry.isAvailable(); } @Override public void destroy() { - registry.destroy(); + if (registry != null) { + registry.destroy(); + } } @Override @@ -94,7 +97,9 @@ public void subscribe(URL url, NotifyListener listener) { @Override public void unsubscribe(URL url, NotifyListener listener) { try { - registry.unsubscribe(url, listener); + if (registry != null) { + registry.unsubscribe(url, listener); + } } finally { listenerEvent(serviceListener -> serviceListener.onUnsubscribe(url, registry)); } @@ -102,12 +107,12 @@ public void unsubscribe(URL url, NotifyListener listener) { @Override public boolean isServiceDiscovery() { - return registry.isServiceDiscovery(); + return registry != null && registry.isServiceDiscovery(); } @Override public List lookup(URL url) { - return registry.lookup(url); + return registry == null ? Collections.emptyList() : registry.lookup(url); } public Registry getRegistry() { diff --git a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/integration/RegistryDirectory.java b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/integration/RegistryDirectory.java index c034b6cafe1d..b3ec0f28b44a 100644 --- a/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/integration/RegistryDirectory.java +++ b/dubbo-registry/dubbo-registry-api/src/main/java/org/apache/dubbo/registry/integration/RegistryDirectory.java @@ -152,10 +152,13 @@ public void subscribe(URL url) { consumerConfigurationListener.addNotifyListener(this); referenceConfigurationListener = new ReferenceConfigurationListener(moduleModel, this, url); } - String registryClusterName = registry.getUrl() - .getParameter( - RegistryConstants.REGISTRY_CLUSTER_KEY, - registry.getUrl().getParameter(PROTOCOL_KEY)); + // registry.getUrl() may be null when the registry is unavailable and check=false (e.g. a weakly + // dependent registry is down at startup), in which case there is no cluster name to report. + URL registryUrl = registry.getUrl(); + String registryClusterName = registryUrl == null + ? null + : registryUrl.getParameter( + RegistryConstants.REGISTRY_CLUSTER_KEY, registryUrl.getParameter(PROTOCOL_KEY)); MetricsEventBus.post(RegistryEvent.toSubscribeEvent(applicationModel, registryClusterName), () -> { super.subscribe(url); return null; diff --git a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/ListenerRegistryWrapperTest.java b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/ListenerRegistryWrapperTest.java index ecdcb694349c..2bac094f5f7b 100644 --- a/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/ListenerRegistryWrapperTest.java +++ b/dubbo-registry/dubbo-registry-api/src/test/java/org/apache/dubbo/registry/ListenerRegistryWrapperTest.java @@ -20,6 +20,7 @@ import org.apache.dubbo.common.url.component.ServiceConfigURL; import org.apache.dubbo.registry.integration.DemoService; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -73,4 +74,31 @@ void testSubscribe() { registryWrapper.subscribe(subscribeUrl, notifyListener); verify(listener, times(1)).onSubscribe(subscribeUrl, registry); } + + @Test + void testNullRegistryIsTolerated() { + // When a registry is unavailable and check=false (e.g. a weakly dependent registry is down at + // startup), AbstractRegistryFactory returns a null registry which RegistryFactoryWrapper still + // wraps. The wrapper must tolerate the null delegate instead of throwing NullPointerException. + // See https://github.com/apache/dubbo/issues/16178. + ListenerRegistryWrapper wrapper = new ListenerRegistryWrapper(null, Collections.emptyList()); + + URL url = new ServiceConfigURL("dubbo", "127.0.0.1", 20881, DemoService.class.getName(), new HashMap<>()); + NotifyListener notifyListener = mock(NotifyListener.class); + + Assertions.assertNull(wrapper.getRegistry()); + Assertions.assertNull(wrapper.getUrl()); + Assertions.assertFalse(wrapper.isAvailable()); + Assertions.assertFalse(wrapper.isServiceDiscovery()); + Assertions.assertTrue(wrapper.lookup(url).isEmpty()); + + // register / subscribe / lifecycle methods must be no-ops rather than throwing + Assertions.assertDoesNotThrow(() -> { + wrapper.register(url); + wrapper.unregister(url); + wrapper.subscribe(url, notifyListener); + wrapper.unsubscribe(url, notifyListener); + wrapper.destroy(); + }); + } }