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(); + } + } }