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..c0d265a211ac --- /dev/null +++ b/dubbo-cluster/src/test/java/org/apache/dubbo/rpc/cluster/router/condition/config/AppStateRouterFactoryTest.java @@ -0,0 +1,146 @@ +/* + * 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); + } + } +}