From af36bae3eb620366250a31ab86ab45f42c9a28c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <575121043@qq.com> Date: Mon, 26 Oct 2026 23:32:37 +0800 Subject: [PATCH 1/2] Fix application router isolation across service chains Cache application state routers by service key so building another service chain cannot rewrite the first chain's successor. Add regression coverage for both construction orders. Fixes #16545 --- .../config/AppStateRouterFactory.java | 22 +-- .../config/AppStateRouterFactoryTest.java | 147 ++++++++++++++++++ 2 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java diff --git a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactory.java b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactory.java index 14d05cde101e..70a1e2c6bbde 100644 --- a/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactory.java +++ b/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactory.java @@ -18,35 +18,19 @@ import org.apache.dubbo.common.URL; import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.rpc.cluster.router.state.CacheableStateRouterFactory; import org.apache.dubbo.rpc.cluster.router.state.StateRouter; -import org.apache.dubbo.rpc.cluster.router.state.StateRouterFactory; /** * Application level router factory * AppRouter should after ServiceRouter */ @Activate(order = 150) -public class AppStateRouterFactory implements StateRouterFactory { +public class AppStateRouterFactory extends CacheableStateRouterFactory { public static final String NAME = "app"; - @SuppressWarnings("rawtypes") - private volatile StateRouter router; - - @SuppressWarnings("unchecked") @Override - public StateRouter getRouter(Class interfaceClass, URL url) { - if (router != null) { - return router; - } - synchronized (this) { - if (router == null) { - router = createRouter(url); - } - } - return router; - } - - private StateRouter createRouter(URL url) { + protected StateRouter createRouter(Class interfaceClass, URL url) { return new AppStateRouter<>(url); } } diff --git a/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java new file mode 100644 index 000000000000..b460b4910cae --- /dev/null +++ b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java @@ -0,0 +1,147 @@ +/* + * 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.rpc.cluster.router.condition.config; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.utils.Holder; +import org.apache.dubbo.rpc.Invocation; +import org.apache.dubbo.rpc.Invoker; +import org.apache.dubbo.rpc.RpcInvocation; +import org.apache.dubbo.rpc.cluster.SingleRouterChain; +import org.apache.dubbo.rpc.cluster.router.MockInvoker; +import org.apache.dubbo.rpc.cluster.router.RouterSnapshotNode; +import org.apache.dubbo.rpc.cluster.router.state.AbstractStateRouter; +import org.apache.dubbo.rpc.cluster.router.state.BitList; +import org.apache.dubbo.rpc.cluster.router.state.StateRouter; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ModuleModel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Verifies that building another service's route chain does not change the first service's routing result. + * + * @date 2026-10-26 + */ +class AppStateRouterFactoryTest { + + @Test + void orderChainKeepsItsRouteAfterUserChainIsBuilt() { + assertFirstServiceChainKeepsItsRoute("OrderService", "UserService"); + } + + @Test + void userChainKeepsItsRouteAfterOrderChainIsBuilt() { + assertFirstServiceChainKeepsItsRoute("UserService", "OrderService"); + } + + private void assertFirstServiceChainKeepsItsRoute(String firstService, String secondService) { + ModuleModel moduleModel = ApplicationModel.defaultModel().getDefaultModule(); + URL firstUrl = consumerUrl(firstService, moduleModel); + URL secondUrl = consumerUrl(secondService, moduleModel); + AppStateRouterFactory factory = new AppStateRouterFactory(); + + // The first service has a downstream filtering router, as it would for a script rule. + SingleRouterChain firstChain = chain( + factory.getRouter(String.class, firstUrl), new PortFilterRouter(firstUrl, 23982)); + Invoker firstV1 = invoker(firstService, 23981, moduleModel); + Invoker firstV2 = invoker(firstService, 23982, moduleModel); + BitList> firstInvokers = new BitList<>(Arrays.asList(firstV1, firstV2)); + firstChain.setInvokers(firstInvokers); + assertEquals( + Collections.singletonList(firstV2), + firstChain.simpleRoute(firstUrl, firstInvokers, new RpcInvocation())); + + // Building a second service chain must not replace the first chain's successor. + SingleRouterChain secondChain = + chain(factory.getRouter(String.class, secondUrl), new PassThroughRouter(secondUrl)); + Invoker secondInvoker = invoker(secondService, 23983, moduleModel); + BitList> secondInvokers = new BitList<>(Collections.singletonList(secondInvoker)); + secondChain.setInvokers(secondInvokers); + assertEquals( + Collections.singletonList(secondInvoker), + secondChain.simpleRoute(secondUrl, secondInvokers, new RpcInvocation())); + assertEquals( + Collections.singletonList(firstV2), + firstChain.simpleRoute(firstUrl, firstInvokers, new RpcInvocation())); + } + + private URL consumerUrl(String service, ModuleModel moduleModel) { + return URL.valueOf("dubbo://127.0.0.1:20880/" + service + "?application=router-chain-consumer") + .setScopeModel(moduleModel); + } + + private Invoker invoker(String service, int port, ModuleModel moduleModel) { + return new MockInvoker<>( + URL.valueOf("dubbo://127.0.0.1:" + port + "/" + service).setScopeModel(moduleModel), true); + } + + private SingleRouterChain chain(StateRouter appRouter, StateRouter nextRouter) { + return new SingleRouterChain<>( + Collections.emptyList(), Arrays.asList(appRouter, nextRouter), true, null); + } + + private static class PassThroughRouter extends AbstractStateRouter { + PassThroughRouter(URL url) { + super(url); + } + + @Override + protected BitList> doRoute( + BitList> invokers, + URL url, + Invocation invocation, + boolean needToPrintMessage, + Holder> nodeHolder, + Holder messageHolder) { + return invokers; + } + } + + private static class PortFilterRouter extends AbstractStateRouter { + private final int port; + + PortFilterRouter(URL url, int port) { + super(url); + this.port = port; + } + + @Override + protected BitList> doRoute( + BitList> invokers, + URL url, + Invocation invocation, + boolean needToPrintMessage, + Holder> nodeHolder, + Holder messageHolder) { + List> selected = new ArrayList<>(); + for (Invoker invoker : invokers) { + if (invoker.getUrl().getPort() == port) { + selected.add(invoker); + } + } + return new BitList<>(selected); + } + } +} From 9666cb411c8bb2be1d2e62078838531376605784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD?= <575121043@qq.com> Date: Mon, 26 Oct 2026 23:43:27 +0800 Subject: [PATCH 2/2] Format router chain regression test for CI --- .../router/condition/config/AppStateRouterFactoryTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java index b460b4910cae..c0d265a211ac 100644 --- a/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java +++ b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java @@ -63,8 +63,8 @@ private void assertFirstServiceChainKeepsItsRoute(String firstService, String se AppStateRouterFactory factory = new AppStateRouterFactory(); // The first service has a downstream filtering router, as it would for a script rule. - SingleRouterChain firstChain = chain( - factory.getRouter(String.class, firstUrl), new PortFilterRouter(firstUrl, 23982)); + SingleRouterChain firstChain = + chain(factory.getRouter(String.class, firstUrl), new PortFilterRouter(firstUrl, 23982)); Invoker firstV1 = invoker(firstService, 23981, moduleModel); Invoker firstV2 = invoker(firstService, 23982, moduleModel); BitList> firstInvokers = new BitList<>(Arrays.asList(firstV1, firstV2)); @@ -98,8 +98,7 @@ private Invoker invoker(String service, int port, ModuleModel moduleMode } private SingleRouterChain chain(StateRouter appRouter, StateRouter nextRouter) { - return new SingleRouterChain<>( - Collections.emptyList(), Arrays.asList(appRouter, nextRouter), true, null); + return new SingleRouterChain<>(Collections.emptyList(), Arrays.asList(appRouter, nextRouter), true, null); } private static class PassThroughRouter extends AbstractStateRouter {