From 576363df99784f29bbbb464c648d77254fcdecb5 Mon Sep 17 00:00:00 2001 From: daguimu Date: Wed, 29 Apr 2026 23:15:25 +0800 Subject: [PATCH] fix: allow metadata service export when application deployer is PENDING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DefaultApplicationDeployer#doExportMetadataService used to bail out when the deployer was not in STARTING/STARTED/COMPLETION state. That blocked programmatic ServiceConfig.export() invocations made before the application has been started, because the deployer is still in PENDING state at that point and the metadata service was therefore silently skipped — instance-level registration would then run without metadata. Invert the guard so we only skip when the deployer is shutting down or has failed (STOPPING / STOPPED / FAILED). PENDING / INIT / STARTING / STARTED / COMPLETION all remain valid states for triggering the export. Add DefaultApplicationDeployerTest#exportMetadataServiceShouldFireListenersWhenDeployerIsPending which registers a deploy listener and asserts onModuleStarted is invoked exactly once after exportMetadataService() runs against a freshly-created (PENDING) deployer. Fixes #14859 --- .../deploy/DefaultApplicationDeployer.java | 6 +- .../DefaultApplicationDeployerTest.java | 67 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/deploy/DefaultApplicationDeployer.java b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/deploy/DefaultApplicationDeployer.java index c0353c6e8ef1..ecf502a70712 100644 --- a/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/deploy/DefaultApplicationDeployer.java +++ b/dubbo-config/dubbo-config-api/src/main/java/org/apache/dubbo/config/deploy/DefaultApplicationDeployer.java @@ -1296,7 +1296,11 @@ private void onInitialize() { } private void doExportMetadataService() { - if (!isStarting() && !isStarted() && !isCompletion()) { + // Skip only when the application is shutting down or has failed. + // PENDING is allowed so that programmatic ServiceConfig.export() invoked + // before the application has been started can still trigger the + // metadata service export (see issue #14859). + if (isStopping() || isStopped() || isFailed()) { return; } for (DeployListener listener : listeners) { diff --git a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/deploy/DefaultApplicationDeployerTest.java b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/deploy/DefaultApplicationDeployerTest.java index 6799c116ece9..a4b4b466dce3 100644 --- a/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/deploy/DefaultApplicationDeployerTest.java +++ b/dubbo-config/dubbo-config-api/src/test/java/org/apache/dubbo/config/deploy/DefaultApplicationDeployerTest.java @@ -16,10 +16,16 @@ */ package org.apache.dubbo.config.deploy; +import org.apache.dubbo.common.deploy.ApplicationDeployListener; import org.apache.dubbo.common.utils.Assert; import org.apache.dubbo.config.MetricsConfig; import org.apache.dubbo.metrics.utils.MetricsSupportUtil; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.FrameworkModel; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.apache.dubbo.common.constants.MetricsConstants.PROTOCOL_PROMETHEUS; @@ -40,4 +46,65 @@ void isImportPrometheus() { PROTOCOL_PROMETHEUS.equals(metricsConfig.getProtocol()) && !MetricsSupportUtil.isSupportPrometheus(); Assert.assertTrue(!importPrometheus, " should return false"); } + + /** + * See #14859. + *

+ * Programmatic {@code ServiceConfig.export()} may invoke + * {@code ApplicationDeployer.exportMetadataService()} while the application + * deployer is still in the {@code PENDING} state (e.g. when the user wires + * Dubbo via XML without any {@code } entry and then exports + * services manually). The metadata service must still be exported in that + * case, otherwise instance-level registration is silently skipped. + */ + @Test + void exportMetadataServiceShouldFireListenersWhenDeployerIsPending() { + FrameworkModel frameworkModel = new FrameworkModel(); + try { + ApplicationModel applicationModel = frameworkModel.newApplication(); + DefaultApplicationDeployer deployer = + (DefaultApplicationDeployer) DefaultApplicationDeployer.get(applicationModel); + + AtomicInteger moduleStartedInvocations = new AtomicInteger(); + deployer.addDeployListener(new ApplicationDeployListener() { + @Override + public void onInitialize(ApplicationModel scopeModel) {} + + @Override + public void onStarting(ApplicationModel scopeModel) {} + + @Override + public void onStarted(ApplicationModel scopeModel) {} + + @Override + public void onCompletion(ApplicationModel scopeModel) {} + + @Override + public void onStopping(ApplicationModel scopeModel) {} + + @Override + public void onStopped(ApplicationModel scopeModel) {} + + @Override + public void onFailure(ApplicationModel scopeModel, Throwable cause) {} + + @Override + public void onModuleStarted(ApplicationModel scopeModel) { + moduleStartedInvocations.incrementAndGet(); + } + }); + + // Sanity: the deployer is in PENDING state until start() is called. + Assertions.assertTrue(deployer.isPending()); + + deployer.exportMetadataService(); + + Assertions.assertEquals( + 1, + moduleStartedInvocations.get(), + "ApplicationDeployListener.onModuleStarted should be invoked even when the deployer is in PENDING state"); + } finally { + frameworkModel.destroy(); + } + } }