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

liubao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-java-chassis.git


The following commit(s) were added to refs/heads/master by this push:
     new eef99bf3a [#4132] router support set fallback route as downgrading 
routing strategy (#4134)
eef99bf3a is described below

commit eef99bf3afff34ec71d0157733ab2c3a2bfebf2a
Author: Cheng YouLing <[email protected]>
AuthorDate: Fri Dec 15 09:07:45 2023 +0800

    [#4132] router support set fallback route as downgrading routing strategy 
(#4134)
---
 .../consumer/src/main/resources/application.yml    |  17 +++
 .../apache/servicecomb/samples/HelloWorldIT.java   |  47 ++++++
 .../distribute/AbstractRouterDistributor.java      |  89 +++++++----
 .../servicecomb/router/model/PolicyRuleItem.java   |  22 +++
 .../servicecomb/router/model/ServiceInfoCache.java |  27 +++-
 .../RouterDistributorFileWithFallbackTest.java     | 162 +++++++++++++++++++++
 governance/src/test/resources/application.yaml     |  52 ++++++-
 7 files changed, 381 insertions(+), 35 deletions(-)

diff --git a/demo/demo-cse-v1/consumer/src/main/resources/application.yml 
b/demo/demo-cse-v1/consumer/src/main/resources/application.yml
index e56621ef6..3708895be 100644
--- a/demo/demo-cse-v1/consumer/src/main/resources/application.yml
+++ b/demo/demo-cse-v1/consumer/src/main/resources/application.yml
@@ -59,6 +59,23 @@ servicecomb:
           - weight: 100
             tags:
               version: 0.0.1
+      - precedence: 3
+        match:
+          headers:
+            canary:
+              exact: fallback
+        route:
+          - weight: 100
+            tags:
+              version: 0.0.3
+        fallback:
+          - weight: 20
+            tags:
+              version: 0.0.1
+          - weight: 80
+            tags:
+              version: 0.0.2
+
   router:
     type: router
     header: canary
diff --git 
a/demo/demo-cse-v1/test-client/src/main/java/org/apache/servicecomb/samples/HelloWorldIT.java
 
b/demo/demo-cse-v1/test-client/src/main/java/org/apache/servicecomb/samples/HelloWorldIT.java
index c49295d11..7657b5971 100644
--- 
a/demo/demo-cse-v1/test-client/src/main/java/org/apache/servicecomb/samples/HelloWorldIT.java
+++ 
b/demo/demo-cse-v1/test-client/src/main/java/org/apache/servicecomb/samples/HelloWorldIT.java
@@ -34,6 +34,8 @@ public class HelloWorldIT implements CategorizedTestCase {
 
   @Override
   public void testRestTransport() throws Exception {
+    testHelloWorldFallback();
+    testHelloWorldNoHeader();
     testHelloWorld();
     testHelloWorldCanary();
   }
@@ -72,4 +74,49 @@ public class HelloWorldIT implements CategorizedTestCase {
     double ratio = oldCount / (float) (oldCount + newCount);
     TestMgr.check(ratio > 0.1 && ratio < 0.3, true);
   }
+
+  private void testHelloWorldFallback() {
+    int oldCount = 0;
+    int newCount = 0;
+
+    for (int i = 0; i < 20; i++) {
+      MultiValueMap<String, String> headers = new HttpHeaders();
+      headers.add("canary", "fallback");
+      HttpEntity<Object> entity = new HttpEntity<>(headers);
+      String result = template
+          .exchange(Config.GATEWAY_URL + "/sayHelloCanary?name=World", 
HttpMethod.GET, entity, String.class).getBody();
+      if (result.equals("\"Hello Canary World\"")) {
+        oldCount++;
+      } else if (result.equals("\"Hello Canary in canary World\"")) {
+        newCount++;
+      } else {
+        TestMgr.fail("not expected result testHelloWorldCanary");
+        return;
+      }
+    }
+
+    double ratio = oldCount / (float) (oldCount + newCount);
+    TestMgr.check(ratio > 0.1 && ratio < 0.3, true);
+  }
+
+  private void testHelloWorldNoHeader() {
+    int oldCount = 0;
+    int newCount = 0;
+
+    for (int i = 0; i < 20; i++) {
+      String result = template
+          .getForObject(Config.GATEWAY_URL + "/sayHelloCanary?name=World", 
String.class);
+      if (result.equals("\"Hello Canary World\"")) {
+        oldCount++;
+      } else if (result.equals("\"Hello Canary in canary World\"")) {
+        newCount++;
+      } else {
+        TestMgr.fail("not expected result testHelloWorldCanary");
+        return;
+      }
+    }
+
+    double ratio = oldCount / (float) (oldCount + newCount);
+    TestMgr.check(ratio == 0.5, true);
+  }
 }
diff --git 
a/governance/src/main/java/org/apache/servicecomb/router/distribute/AbstractRouterDistributor.java
 
b/governance/src/main/java/org/apache/servicecomb/router/distribute/AbstractRouterDistributor.java
index 731d05d97..8e5b1af1f 100644
--- 
a/governance/src/main/java/org/apache/servicecomb/router/distribute/AbstractRouterDistributor.java
+++ 
b/governance/src/main/java/org/apache/servicecomb/router/distribute/AbstractRouterDistributor.java
@@ -52,22 +52,30 @@ public abstract class AbstractRouterDistributor<INSTANCE> 
implements
   protected AbstractRouterDistributor() {
   }
 
+  /**
+   * distribute logic:
+   * 1、First according to the set route rules to choose target instances, if 
have just return.
+   * 2、if route rules not match instance, check if fallback rules are set, if 
set and match instances then return.
+   * 3、if route and fallback routes all have not match instance, then if route 
rules weight count less 100, return
+   * unset instances, otherwise return all instances.
+   * @param targetServiceName
+   * @param list
+   * @param invokeRule
+   * @return
+   */
   @Override
   public List<INSTANCE> distribute(String targetServiceName, List<INSTANCE> 
list, PolicyRuleItem invokeRule) {
-
     invokeRule.check();
 
     // unSetTags instance list
     List<INSTANCE> unSetTagInstances = new ArrayList<>();
 
-    // get tag list
-    Map<TagItem, List<INSTANCE>> versionServerMap = 
getDistributList(targetServiceName, list, invokeRule, unSetTagInstances);
+    // record fallback router targItem instance
+    Map<TagItem, List<INSTANCE>> fallbackVersionServerMap = new HashMap<>();
 
-    if (CollectionUtils.isEmpty(versionServerMap)) {
-      LOGGER.debug("route management can not match any rule and route the 
latest version");
-      // rule note matched instance babel, all instance return, select 
instance for load balancing later
-      return list;
-    }
+    // get tag instance map, fallbackVersionServerMap, unSetTagInstances
+    Map<TagItem, List<INSTANCE>> versionServerMap = 
getDistributList(targetServiceName, list, invokeRule,
+        unSetTagInstances, fallbackVersionServerMap);
 
     // weight calculation to obtain the next tags instance
     TagItem targetTag = getFiltedServerTagItem(invokeRule, targetServiceName);
@@ -75,8 +83,16 @@ public abstract class AbstractRouterDistributor<INSTANCE> 
implements
       return versionServerMap.get(targetTag);
     }
 
-    // has weightLess situation
-    if (invokeRule.isWeightLess() && unSetTagInstances.size() > 0) {
+    if (!fallbackVersionServerMap.isEmpty()) {
+      // weight calculation to obtain the next fallback tags instance
+      TagItem fallbackTargetTag = getFallbackFiltedServerTagItem(invokeRule, 
targetServiceName);
+      if (fallbackTargetTag != null && 
fallbackVersionServerMap.containsKey(fallbackTargetTag)) {
+        return fallbackVersionServerMap.get(fallbackTargetTag);
+      }
+    }
+
+    // has weightLess situation and unSetTagInstances has values
+    if (invokeRule.isWeightLess() && !unSetTagInstances.isEmpty()) {
       return unSetTagInstances;
     }
     return list;
@@ -96,33 +112,26 @@ public abstract class AbstractRouterDistributor<INSTANCE> 
implements
         .getNextInvokeVersion(rule);
   }
 
+  public TagItem getFallbackFiltedServerTagItem(PolicyRuleItem rule, String 
targetServiceName) {
+    return routerRuleCache.getServiceInfoCacheMap().get(targetServiceName)
+        .getFallbackNextInvokeVersion(rule);
+  }
+
   /**
-   * 1.filter targetService
+   * 1.filter set route rules targetService, build fallback targetService map 
and unSetTagInstances list.
    * 2.establish map is a more complicate way than direct traversal, because 
of multiple matches.
    *
    * the method getProperties() contains other field that we don't need.
    */
-  private Map<TagItem, List<INSTANCE>> getDistributList(String serviceName,
-      List<INSTANCE> list, PolicyRuleItem invokeRule, List<INSTANCE> 
unSetTagInstances) {
+  private Map<TagItem, List<INSTANCE>> getDistributList(String serviceName, 
List<INSTANCE> list,
+      PolicyRuleItem invokeRule, List<INSTANCE> unSetTagInstances, 
Map<TagItem, List<INSTANCE>> fallbackVersionMap) {
     Map<TagItem, List<INSTANCE>> versionServerMap = new HashMap<>();
     for (INSTANCE instance : list) {
       //get server
       if (getServerName.apply(instance).equals(serviceName)) {
-        //most matching
         TagItem tagItem = new TagItem(getVersion.apply(instance), 
getProperties.apply(instance));
-        TagItem targetTag = null;
-        int maxMatch = 0;
-        // obtain the rule with the most parameter matches
-        for (RouteItem entry : invokeRule.getRoute()) {
-          if (entry.getTagitem() == null){
-            continue;
-          }
-          int nowMatch = entry.getTagitem().matchNum(tagItem);
-          if (nowMatch > maxMatch) {
-            maxMatch = nowMatch;
-            targetTag = entry.getTagitem();
-          }
-        }
+        // route most matching TagItem
+        TagItem targetTag = buildTargetTag(invokeRule.getRoute(), tagItem);
         if (targetTag != null) {
           if (!versionServerMap.containsKey(targetTag)) {
             versionServerMap.put(targetTag, new ArrayList<>());
@@ -132,8 +141,34 @@ public abstract class AbstractRouterDistributor<INSTANCE> 
implements
           // not matched, placed in the unset tag instances collection
           unSetTagInstances.add(instance);
         }
+        // ensure the tags can build when set for both route and fallback at 
the same time
+        if (!CollectionUtils.isEmpty(invokeRule.getFallback())) {
+          // fallback most matching TagItem
+          TagItem targetTagFallback = buildTargetTag(invokeRule.getFallback(), 
tagItem);
+          if (!fallbackVersionMap.containsKey(targetTagFallback)) {
+            fallbackVersionMap.put(targetTagFallback, new ArrayList<>());
+          }
+          fallbackVersionMap.get(targetTagFallback).add(instance);
+        }
       }
     }
     return versionServerMap;
   }
+
+  private TagItem buildTargetTag(List<RouteItem> route, TagItem tagItem) {
+    int maxMatch = 0;
+    TagItem targetTag = null;
+    // obtain the rule with the most parameter matches
+    for (RouteItem entry : route) {
+      if (entry.getTagitem() == null){
+        continue;
+      }
+      int nowMatch = entry.getTagitem().matchNum(tagItem);
+      if (nowMatch > maxMatch) {
+        maxMatch = nowMatch;
+        targetTag = entry.getTagitem();
+      }
+    }
+    return targetTag;
+  }
 }
diff --git 
a/governance/src/main/java/org/apache/servicecomb/router/model/PolicyRuleItem.java
 
b/governance/src/main/java/org/apache/servicecomb/router/model/PolicyRuleItem.java
index 769b07bdc..11c95099f 100644
--- 
a/governance/src/main/java/org/apache/servicecomb/router/model/PolicyRuleItem.java
+++ 
b/governance/src/main/java/org/apache/servicecomb/router/model/PolicyRuleItem.java
@@ -42,6 +42,10 @@ public class PolicyRuleItem implements 
Comparable<PolicyRuleItem> {
 
   private boolean weightLess = false;
 
+  private List<RouteItem> fallback;
+
+  private Integer fallbackTotal;
+
   public PolicyRuleItem() {
   }
 
@@ -113,6 +117,22 @@ public class PolicyRuleItem implements 
Comparable<PolicyRuleItem> {
     this.weightLess = weightLess;
   }
 
+  public List<RouteItem> getFallback() {
+    return fallback;
+  }
+
+  public void setFallback(List<RouteItem> fallback) {
+    this.fallback = fallback;
+  }
+
+  public Integer getFallbackTotal() {
+    return fallbackTotal;
+  }
+
+  public void setFallbackTotal(Integer fallbackTotal) {
+    this.fallbackTotal = fallbackTotal;
+  }
+
   @Override
   public String toString() {
     return "PolicyRuleItem{" +
@@ -121,6 +141,8 @@ public class PolicyRuleItem implements 
Comparable<PolicyRuleItem> {
         ", route=" + route +
         ", total=" + total +
         ", weightLess=" + weightLess +
+        ", fallback=" + fallback +
+        ", fallbackTotal=" + fallbackTotal +
         '}';
   }
 }
diff --git 
a/governance/src/main/java/org/apache/servicecomb/router/model/ServiceInfoCache.java
 
b/governance/src/main/java/org/apache/servicecomb/router/model/ServiceInfoCache.java
index 15cc08c94..676403206 100644
--- 
a/governance/src/main/java/org/apache/servicecomb/router/model/ServiceInfoCache.java
+++ 
b/governance/src/main/java/org/apache/servicecomb/router/model/ServiceInfoCache.java
@@ -19,6 +19,8 @@ package org.apache.servicecomb.router.model;
 import java.util.List;
 import java.util.stream.Collectors;
 
+import org.springframework.util.CollectionUtils;
+
 /**
  * @Author GuoYl123
  * @Date 2019/10/17
@@ -34,9 +36,12 @@ public class ServiceInfoCache {
   public ServiceInfoCache(List<PolicyRuleItem> policyRuleItemList) {
     this.allrule = 
policyRuleItemList.stream().sorted().collect(Collectors.toList());
 
-    this.getAllrule().forEach(rule ->
-        rule.getRoute().forEach(RouteItem::initTagItem)
-    );
+    this.getAllrule().forEach(rule -> {
+      rule.getRoute().forEach(RouteItem::initTagItem);
+      if (!CollectionUtils.isEmpty(rule.getFallback())) {
+        rule.getFallback().forEach(RouteItem::initTagItem);
+      }
+    });
   }
 
   public TagItem getNextInvokeVersion(PolicyRuleItem policyRuleItem) {
@@ -44,7 +49,19 @@ public class ServiceInfoCache {
     if (policyRuleItem.getTotal() == null) {
       
policyRuleItem.setTotal(rule.stream().mapToInt(RouteItem::getWeight).sum());
     }
-    rule.stream().forEach(RouteItem::addCurrentWeight);
+    return calculateWeight(rule, policyRuleItem.getTotal());
+  }
+
+  public TagItem getFallbackNextInvokeVersion(PolicyRuleItem policyRuleItem) {
+    List<RouteItem> rule = policyRuleItem.getFallback();
+    if (policyRuleItem.getFallbackTotal() == null) {
+      
policyRuleItem.setFallbackTotal(rule.stream().mapToInt(RouteItem::getWeight).sum());
+    }
+    return calculateWeight(rule, policyRuleItem.getFallbackTotal());
+  }
+
+  private TagItem calculateWeight(List<RouteItem> rule, int total) {
+    rule.forEach(RouteItem::addCurrentWeight);
     int maxIndex = 0, maxWeight = -1;
     for (int i = 0; i < rule.size(); i++) {
       if (maxWeight < rule.get(i).getCurrentWeight()) {
@@ -52,7 +69,7 @@ public class ServiceInfoCache {
         maxWeight = rule.get(i).getCurrentWeight();
       }
     }
-    rule.get(maxIndex).reduceCurrentWeight(policyRuleItem.getTotal());
+    rule.get(maxIndex).reduceCurrentWeight(total);
     return rule.get(maxIndex).getTagitem();
   }
 
diff --git 
a/governance/src/test/java/org/apache/servicecomb/router/RouterDistributorFileWithFallbackTest.java
 
b/governance/src/test/java/org/apache/servicecomb/router/RouterDistributorFileWithFallbackTest.java
new file mode 100644
index 000000000..2d7a10645
--- /dev/null
+++ 
b/governance/src/test/java/org/apache/servicecomb/router/RouterDistributorFileWithFallbackTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.servicecomb.router;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.apache.servicecomb.governance.marker.GovernanceRequest;
+import org.apache.servicecomb.router.distribute.RouterDistributor;
+import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import 
org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(locations = "classpath:META-INF/spring/*.xml", 
initializers = ConfigDataApplicationContextInitializer.class)
+public class RouterDistributorFileWithFallbackTest {
+  private static final String TARGET_SERVICE_NAME_WITHOUT_FALLBACK = 
"test_server3";
+
+  private static final String TARGET_SERVICE_NAME_WITH_FALLBACK = 
"test_server4";
+
+  private static final String TARGET_SERVICE_NAME_ROUTE_FALLBACK = 
"test_server5";
+
+  private RouterFilter routerFilter;
+
+  private RouterDistributor<ServiceIns> routerDistributor;
+
+  @Autowired
+  public void setRouterFilter(RouterFilter routerFilter) {
+    this.routerFilter = routerFilter;
+  }
+
+  @Autowired
+  public void setRouterDistributor(RouterDistributor<ServiceIns> 
routerDistributor) {
+    this.routerDistributor = routerDistributor;
+  }
+
+  @Test
+  public void testDistributeWithoutFallback() {
+    List<ServiceIns> list = 
initServiceList(TARGET_SERVICE_NAME_WITHOUT_FALLBACK);
+    HashMap<String, String> header = new HashMap<>();
+    header.put("canary", "canary");
+    GovernanceRequest governanceRequest = new GovernanceRequest();
+    governanceRequest.setHeaders(header);
+    List<ServiceIns> listOfServers = routerFilter
+        .getFilteredListOfServers(list, TARGET_SERVICE_NAME_WITHOUT_FALLBACK, 
governanceRequest, routerDistributor);
+    Assertions.assertNotNull(listOfServers);
+    for (ServiceIns server : listOfServers) {
+      Assertions.assertEquals(TARGET_SERVICE_NAME_WITHOUT_FALLBACK, 
server.getServerName());
+    }
+    int serverNum1 = 0;
+    int serverNum2 = 0;
+
+    for (int i = 0; i < 10; i++) {
+      List<ServiceIns> serverList = routerFilter
+          .getFilteredListOfServers(list, 
TARGET_SERVICE_NAME_WITHOUT_FALLBACK, governanceRequest, routerDistributor);
+      for (ServiceIns serviceIns : serverList) {
+        if ("01".equals(serviceIns.getId())) {
+          serverNum1++;
+        } else if ("02".equals(serviceIns.getId())) {
+          serverNum2++;
+        }
+      }
+    }
+    Assertions.assertTrue(serverNum2 == serverNum1);
+  }
+
+  @Test
+  public void testDistributeWithFallback() {
+    List<ServiceIns> list = initServiceList(TARGET_SERVICE_NAME_WITH_FALLBACK);
+    HashMap<String, String> header = new HashMap<>();
+    header.put("canary", "canary");
+    GovernanceRequest governanceRequest = new GovernanceRequest();
+    governanceRequest.setHeaders(header);
+    List<ServiceIns> listOfServers = routerFilter
+        .getFilteredListOfServers(list, TARGET_SERVICE_NAME_WITH_FALLBACK, 
governanceRequest, routerDistributor);
+    Assertions.assertNotNull(listOfServers);
+    for (ServiceIns server : listOfServers) {
+      Assertions.assertEquals(TARGET_SERVICE_NAME_WITH_FALLBACK, 
server.getServerName());
+    }
+    int serverNum1 = 0;
+    int serverNum2 = 0;
+
+    for (int i = 0; i < 10; i++) {
+      List<ServiceIns> serverList = routerFilter
+          .getFilteredListOfServers(list, TARGET_SERVICE_NAME_WITH_FALLBACK, 
governanceRequest, routerDistributor);
+      for (ServiceIns serviceIns : serverList) {
+        if ("01".equals(serviceIns.getId())) {
+          serverNum1++;
+        } else if ("02".equals(serviceIns.getId())) {
+          serverNum2++;
+        }
+      }
+    }
+    Assertions.assertTrue((serverNum2 + serverNum1) == serverNum1);
+  }
+
+  @Test
+  public void testDistributeRouteAndFallbackHaveSame() {
+    List<ServiceIns> list = 
initServiceList(TARGET_SERVICE_NAME_ROUTE_FALLBACK);
+    HashMap<String, String> header = new HashMap<>();
+    header.put("canary", "canary");
+    GovernanceRequest governanceRequest = new GovernanceRequest();
+    governanceRequest.setHeaders(header);
+    List<ServiceIns> listOfServers = routerFilter
+        .getFilteredListOfServers(list, TARGET_SERVICE_NAME_ROUTE_FALLBACK, 
governanceRequest, routerDistributor);
+    Assertions.assertNotNull(listOfServers);
+    for (ServiceIns server : listOfServers) {
+      Assertions.assertEquals(TARGET_SERVICE_NAME_ROUTE_FALLBACK, 
server.getServerName());
+    }
+    int serverNum1 = 0;
+    int serverNum2 = 0;
+
+    for (int i = 0; i < 20; i++) {
+      List<ServiceIns> serverList = routerFilter
+          .getFilteredListOfServers(list, TARGET_SERVICE_NAME_ROUTE_FALLBACK, 
governanceRequest, routerDistributor);
+      for (ServiceIns serviceIns : serverList) {
+        if ("01".equals(serviceIns.getId())) {
+          serverNum1++;
+        } else if ("02".equals(serviceIns.getId())) {
+          serverNum2++;
+        }
+      }
+    }
+    boolean flag = false;
+    if (Math.round(serverNum1 * 1.0 / serverNum2) == 3) {
+      flag = true;
+    }
+    Assertions.assertTrue(flag);
+  }
+
+  List<ServiceIns> initServiceList(String serviceName) {
+    ServiceIns serviceIns1 = new ServiceIns("01", serviceName);
+    ServiceIns serviceIns2 = new ServiceIns("02", serviceName);
+    serviceIns1.setVersion("1.0");
+    serviceIns2.setVersion("2.0");
+    serviceIns1.addTags("x-group", "red");
+    serviceIns2.addTags("x-group", "green");
+    List<ServiceIns> list = new ArrayList<>();
+    list.add(serviceIns1);
+    list.add(serviceIns2);
+    return list;
+  }
+}
diff --git a/governance/src/test/resources/application.yaml 
b/governance/src/test/resources/application.yaml
index 96b614075..ad9c68599 100644
--- a/governance/src/test/resources/application.yaml
+++ b/governance/src/test/resources/application.yaml
@@ -223,10 +223,8 @@ servicecomb:
           headers:                           # header 匹配
             region:                          # 如果配置了多个 header,那么所有的 header 
规则都必须和请求匹配
               exact: 'providerRegion'
-              caseInsensitive: false         # 不区分大小写
             type:
               regex: gray_[a-z]+             # java 正则表达式匹配
-              caseInsensitive: true          # 区分大小写
         route:                               # 路由规则
           - weight: 20                       # 权重值
             tags:
@@ -249,4 +247,52 @@ servicecomb:
         route:
           - weight: 20
             tags:
-              x-group: red
\ No newline at end of file
+              x-group: red
+
+    test_server3: |                              # 服务名
+      - precedence: 2
+        match:
+          headers:
+            canary:
+              exact: 'canary'
+        route:
+          - weight: 20
+            tags:
+              version: 1.0.0
+          - weight: 80
+            tags:
+              version: 2.0.0
+    test_server4: |                              # 服务名
+      - precedence: 2
+        match:
+          headers:
+            canary:
+              exact: 'canary'
+        route:
+          - weight: 20
+            tags:
+              version: 1.0.0
+          - weight: 80
+            tags:
+              version: 2.0.0
+        fallback:
+          - weight: 100
+            tags:
+              x-group: red
+    test_server5: |                              # 服务名
+      - precedence: 2
+        match:
+          headers:
+            canary:
+              exact: 'canary'
+        route:
+          - weight: 50
+            tags:
+              x-group: red
+        fallback:
+          - weight: 50
+            tags:
+              x-group: red
+          - weight: 50
+            tags:
+              x-group: green
\ No newline at end of file

Reply via email to