This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 0f4487a7de perf(plugin): select matching candidates in one pass (#7264)
0f4487a7de is described below

commit 0f4487a7de4f0e4324a58a533dcc218714e5b5f6
Author: Liming Deng <[email protected]>
AuthorDate: Wed Sep 30 11:36:07 2026 +0800

    perf(plugin): select matching candidates in one pass (#7264)
    
    * perf(plugin): select matching candidates in one pass
    
    * test(plugin): pin matching precedence with real conditions
---
 .../shenyu/plugin/base/AbstractShenyuPlugin.java   |  90 +++++++---------
 .../shenyu/plugin/base/SinglePassMatchingTest.java | 117 +++++++++++++++++++++
 2 files changed, 155 insertions(+), 52 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/AbstractShenyuPlugin.java
 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/AbstractShenyuPlugin.java
index 87b4214c05..981ea6437a 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/AbstractShenyuPlugin.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/AbstractShenyuPlugin.java
@@ -41,12 +41,8 @@ import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
-import java.util.stream.Collectors;
 
 /**
  * abstract shenyu plugin please extends.
@@ -231,32 +227,26 @@ public abstract class AbstractShenyuPlugin implements 
ShenyuPlugin {
     }
 
     private Pair<Boolean, SelectorData> matchSelector(final ServerWebExchange 
exchange, final Collection<SelectorData> selectors) {
-        List<SelectorData> filterCollectors = selectors.stream()
-                .filter(selector -> selector.getEnabled() && 
filterSelector(selector, exchange))
-                .distinct()
-                .collect(Collectors.toList());
-        if (filterCollectors.size() > 1) {
-            return Pair.of(Boolean.FALSE, manyMatchSelector(filterCollectors));
-        } else {
-            return Pair.of(Boolean.TRUE, 
filterCollectors.stream().findFirst().orElse(null));
+        SelectorData best = null;
+        int bestSpecificity = -1;
+        boolean unique = true;
+        for (SelectorData selector : selectors) {
+            if (!selector.getEnabled() || !filterSelector(selector, exchange)) 
{
+                continue;
+            }
+            if (Objects.nonNull(best)) {
+                if (best.equals(selector)) {
+                    continue;
+                }
+                unique = false;
+            }
+            int specificity = MatchModeEnum.match(selector.getMatchMode(), 
MatchModeEnum.AND) ? CollectionUtils.size(selector.getConditionList()) : 0;
+            if (Objects.isNull(best) || specificity > bestSpecificity || 
specificity == bestSpecificity && selector.getSort() < best.getSort()) {
+                best = selector;
+                bestSpecificity = specificity;
+            }
         }
-    }
-
-    private SelectorData manyMatchSelector(final List<SelectorData> 
filterCollectors) {
-        //What needs to be dealt with here is the and condition. If the number 
of and conditions is the same and is matched at the same time,
-        // it will be sorted by the sort field.
-        Map<Integer, List<Pair<Integer, SelectorData>>> collect =
-                filterCollectors.stream().map(selector -> {
-                    boolean match = 
MatchModeEnum.match(selector.getMatchMode(), MatchModeEnum.AND);
-                    int sort = 0;
-                    if (match) {
-                        sort = selector.getConditionList().size();
-                    }
-                    return Pair.of(sort, selector);
-                }).collect(Collectors.groupingBy(Pair::getLeft));
-        Integer max = Collections.max(collect.keySet());
-        List<Pair<Integer, SelectorData>> pairs = collect.get(max);
-        return 
pairs.stream().map(Pair::getRight).min(Comparator.comparing(SelectorData::getSort)).orElse(null);
+        return Pair.of(unique, best);
     }
 
     private Boolean filterSelector(final SelectorData selector, final 
ServerWebExchange exchange) {
@@ -270,30 +260,26 @@ public abstract class AbstractShenyuPlugin implements 
ShenyuPlugin {
     }
 
     private Pair<Boolean, RuleData> matchRule(final ServerWebExchange 
exchange, final Collection<RuleData> rules) {
-        List<RuleData> filterRuleData = rules.stream()
-                .filter(rule -> filterRule(rule, exchange))
-                .distinct()
-                .collect(Collectors.toList());
-        if (filterRuleData.size() > 1) {
-            return Pair.of(Boolean.FALSE, manyMatchRule(filterRuleData));
-        } else {
-            return Pair.of(Boolean.TRUE, 
filterRuleData.stream().findFirst().orElse(null));
+        RuleData best = null;
+        int bestSpecificity = -1;
+        boolean unique = true;
+        for (RuleData rule : rules) {
+            if (!filterRule(rule, exchange)) {
+                continue;
+            }
+            if (Objects.nonNull(best)) {
+                if (best.equals(rule)) {
+                    continue;
+                }
+                unique = false;
+            }
+            int specificity = MatchModeEnum.match(rule.getMatchMode(), 
MatchModeEnum.AND) ? CollectionUtils.size(rule.getConditionDataList()) : 0;
+            if (Objects.isNull(best) || specificity > bestSpecificity || 
specificity == bestSpecificity && rule.getSort() < best.getSort()) {
+                best = rule;
+                bestSpecificity = specificity;
+            }
         }
-    }
-
-    private RuleData manyMatchRule(final List<RuleData> filterRuleData) {
-        Map<Integer, List<Pair<Integer, RuleData>>> collect =
-                filterRuleData.stream().map(rule -> {
-                    boolean match = MatchModeEnum.match(rule.getMatchMode(), 
MatchModeEnum.AND);
-                    int sort = 0;
-                    if (match) {
-                        sort = rule.getConditionDataList().size();
-                    }
-                    return Pair.of(sort, rule);
-                }).collect(Collectors.groupingBy(Pair::getLeft));
-        Integer max = Collections.max(collect.keySet());
-        List<Pair<Integer, RuleData>> pairs = collect.get(max);
-        return 
pairs.stream().map(Pair::getRight).min(Comparator.comparing(RuleData::getSort)).orElse(null);
+        return Pair.of(unique, best);
     }
 
     private Boolean filterRule(final RuleData ruleData, final 
ServerWebExchange exchange) {
diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/SinglePassMatchingTest.java
 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/SinglePassMatchingTest.java
new file mode 100644
index 0000000000..ed755923a5
--- /dev/null
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/SinglePassMatchingTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.shenyu.plugin.base;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.shenyu.common.dto.ConditionData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.SelectorTypeEnum;
+import org.apache.shenyu.plugin.base.condition.strategy.MatchStrategyFactory;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.server.ServerWebExchange;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+
+class SinglePassMatchingTest {
+
+    @Test
+    void specificityThenSortThenEncounterOrderDetermineTheWinner() {
+        final AbstractShenyuPlugin plugin = mock(AbstractShenyuPlugin.class, 
CALLS_REAL_METHODS);
+        final ServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.get("/test"));
+        ConditionData condition = new ConditionData();
+        condition.setParamType("uri");
+        condition.setOperator("=");
+        condition.setParamValue("/test");
+        List<SelectorData> selectors = new ArrayList<>();
+        List<RuleData> rules = new ArrayList<>();
+        // Candidate 1 wins over a less-specific lower sort, a higher sort, 
and an equally ranked later entry.
+        for (int i = 0; i < 4; i++) {
+            List<ConditionData> conditions = Collections.nCopies(i == 0 ? 1 : 
2, condition);
+            int sort = i == 0 ? 0 : i == 2 ? 10 : 5;
+            
selectors.add(SelectorData.builder().id(String.valueOf(i)).enabled(true).matchMode(0).sort(sort)
+                    
.type(SelectorTypeEnum.CUSTOM_FLOW.getCode()).conditionList(conditions).build());
+            
rules.add(RuleData.builder().id(String.valueOf(i)).enabled(true).matchMode(0).sort(sort).conditionDataList(conditions).build());
+        }
+        Pair<Boolean, SelectorData> selectorResult = 
ReflectionTestUtils.invokeMethod(plugin, "matchSelector", exchange, selectors);
+        Pair<Boolean, RuleData> ruleResult = 
ReflectionTestUtils.invokeMethod(plugin, "matchRule", exchange, rules);
+        assertSame(selectors.get(1), selectorResult.getRight());
+        assertSame(rules.get(1), ruleResult.getRight());
+        assertEquals(Boolean.FALSE, selectorResult.getLeft());
+        assertEquals(Boolean.FALSE, ruleResult.getLeft());
+    }
+
+    @Test
+    void preservesSelectionAndCacheEligibilityAcrossCandidateOrderings() {
+        AbstractShenyuPlugin plugin = mock(AbstractShenyuPlugin.class, 
CALLS_REAL_METHODS);
+        ServerWebExchange exchange = 
MockServerWebExchange.from(MockServerHttpRequest.get("/test"));
+        Random random = new Random(6579);
+        try (MockedStatic<MatchStrategyFactory> matches = 
mockStatic(MatchStrategyFactory.class)) {
+            matches.when(() -> MatchStrategyFactory.match(anyInt(), any(), 
any())).thenReturn(true);
+            for (int round = 0; round < 100; round++) {
+                List<SelectorData> selectors = new ArrayList<>();
+                List<RuleData> rules = new ArrayList<>();
+                for (int i = 0; i < round % 10; i++) {
+                    int mode = random.nextInt(2);
+                    int sort = random.nextInt(4);
+                    boolean enabled = random.nextBoolean();
+                    List<ConditionData> conditions = 
Collections.nCopies(random.nextInt(4), new ConditionData());
+                    
selectors.add(SelectorData.builder().id(String.valueOf(i)).enabled(enabled).matchMode(mode).sort(sort)
+                            
.type(SelectorTypeEnum.FULL_FLOW.getCode()).conditionList(conditions).build());
+                    
rules.add(RuleData.builder().id(String.valueOf(i)).enabled(enabled).matchMode(mode).sort(sort).conditionDataList(conditions).build());
+                }
+                selectors.addAll(new ArrayList<>(selectors));
+                rules.addAll(new ArrayList<>(rules));
+                Collections.shuffle(selectors, random);
+                Collections.shuffle(rules, random);
+                List<SelectorData> expectedSelectors = 
selectors.stream().filter(SelectorData::getEnabled).distinct().collect(Collectors.toList());
+                SelectorData expectedSelector = 
expectedSelectors.stream().min(Comparator
+                        .comparingInt((SelectorData selector) -> 
selector.getMatchMode() == 0 ? -selector.getConditionList().size() : 0)
+                        .thenComparing(SelectorData::getSort)).orElse(null);
+                Pair<Boolean, SelectorData> selectorResult = 
ReflectionTestUtils.invokeMethod(plugin, "matchSelector", exchange, selectors);
+                assertSame(expectedSelector, selectorResult.getRight());
+                assertEquals(expectedSelectors.size() <= 1, 
selectorResult.getLeft());
+                List<RuleData> expectedRules = 
rules.stream().filter(RuleData::getEnabled).distinct().collect(Collectors.toList());
+                RuleData expectedRule = expectedRules.stream().min(Comparator
+                        .comparingInt((RuleData rule) -> rule.getMatchMode() 
== 0 ? -rule.getConditionDataList().size() : 0)
+                        .thenComparing(RuleData::getSort)).orElse(null);
+                Pair<Boolean, RuleData> ruleResult = 
ReflectionTestUtils.invokeMethod(plugin, "matchRule", exchange, rules);
+                assertSame(expectedRule, ruleResult.getRight());
+                assertEquals(expectedRules.size() <= 1, ruleResult.getLeft());
+            }
+        }
+    }
+}

Reply via email to