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

xuetaoli pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git


The following commit(s) were added to refs/heads/develop by this push:
     new c618a5ac feat: update route mechanism (#777)
c618a5ac is described below

commit c618a5acdbad09397d342e7f95f1188865bc951f
Author: Xuetao Li <[email protected]>
AuthorDate: Sun Dec 7 20:39:58 2025 +0800

    feat: update route mechanism (#777)
    
    * feat(router): enhance routing capabilities with snapshot management and 
header matching
---
 pkg/common/constant/http.go              |   3 +-
 pkg/common/http/manager_test.go          |  52 +-
 pkg/common/router/{ => mock}/router.go   |   7 +-
 pkg/common/router/router.go              | 241 +++++++---
 pkg/common/router/router_bench_test.go   | 504 +++++++++++++++++++
 pkg/common/router/router_test.go         | 802 +++++++++++++++++++++++++++++--
 pkg/common/util/stringutil/stringutil.go |  27 +-
 pkg/model/router.go                      |   9 +-
 pkg/model/router_snapshot.go             | 181 +++++++
 9 files changed, 1693 insertions(+), 133 deletions(-)

diff --git a/pkg/common/constant/http.go b/pkg/common/constant/http.go
index 2156ab90..1ec456b8 100644
--- a/pkg/common/constant/http.go
+++ b/pkg/common/constant/http.go
@@ -57,7 +57,8 @@ const (
 
        HeaderValueAuthorization = "Authorization"
 
-       HeaderValueAll = "*"
+       HeaderValueAll       = "*"
+       HeaderValueAllLevels = "**"
 
        PathSlash           = "/"
        ProtocolSlash       = "://"
diff --git a/pkg/common/http/manager_test.go b/pkg/common/http/manager_test.go
index 33080f38..69cb4458 100644
--- a/pkg/common/http/manager_test.go
+++ b/pkg/common/http/manager_test.go
@@ -39,7 +39,6 @@ import (
        "github.com/apache/dubbo-go-pixiu/pkg/common/constant"
        "github.com/apache/dubbo-go-pixiu/pkg/common/extension/filter"
        commonmock "github.com/apache/dubbo-go-pixiu/pkg/common/mock"
-       "github.com/apache/dubbo-go-pixiu/pkg/common/router/trie"
        contexthttp "github.com/apache/dubbo-go-pixiu/pkg/context/http"
        "github.com/apache/dubbo-go-pixiu/pkg/context/mock"
        "github.com/apache/dubbo-go-pixiu/pkg/logger"
@@ -121,10 +120,19 @@ func TestCreateHttpConnectionManager(t *testing.T) {
 
        hcmc := model.HttpConnectionManagerConfig{
                RouteConfig: model.RouteConfiguration{
-                       RouteTrie: trie.NewTrieWithDefault("POST/api/v1/**", 
model.RouteAction{
-                               Cluster:                     "test_dubbo",
-                               ClusterNotFoundResponseCode: 505,
-                       }),
+                       Routes: []*model.Router{
+                               {
+                                       ID: "1",
+                                       Match: model.RouterMatch{
+                                               Path:    "/api/v1/**",
+                                               Methods: []string{"POST"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
"test_dubbo",
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                       },
                        Dynamic: false,
                },
                HTTPFilters: []*model.HTTPFilter{
@@ -157,9 +165,20 @@ func TestCreateHttpConnectionManager(t *testing.T) {
 func TestStreamingResponse(t *testing.T) {
        hcmc := model.HttpConnectionManagerConfig{
                RouteConfig: model.RouteConfiguration{
-                       RouteTrie: trie.NewTrieWithDefault("GET/api/sse", 
model.RouteAction{
-                               Cluster: "mock_stream_cluster",
-                       }),
+                       Routes: []*model.Router{
+                               {
+                                       ID: "1",
+                                       Match: model.RouterMatch{
+                                               Path:    "/api/sse",
+                                               Methods: []string{"GET"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
"mock_stream_cluster",
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                       },
+                       Dynamic: false,
                },
                HTTPFilters: []*model.HTTPFilter{
                        {
@@ -360,9 +379,20 @@ func TestStreamableHTTPResponse(t *testing.T) {
 func testStreamableResponse(t *testing.T, contentType string) {
        hcmc := model.HttpConnectionManagerConfig{
                RouteConfig: model.RouteConfiguration{
-                       RouteTrie: trie.NewTrieWithDefault("GET/api/stream", 
model.RouteAction{
-                               Cluster: "mock_stream_cluster",
-                       }),
+                       Routes: []*model.Router{
+                               {
+                                       ID: "1",
+                                       Match: model.RouterMatch{
+                                               Path:    "/api/stream",
+                                               Methods: []string{"GET"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
"mock_stream_cluster",
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                       },
+                       Dynamic: false,
                },
                HTTPFilters: []*model.HTTPFilter{
                        {
diff --git a/pkg/common/router/router.go b/pkg/common/router/mock/router.go
similarity index 94%
copy from pkg/common/router/router.go
copy to pkg/common/router/mock/router.go
index cd175eb2..317f37e4 100644
--- a/pkg/common/router/router.go
+++ b/pkg/common/router/mock/router.go
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package router
+package mock
 
 import (
        stdHttp "net/http"
@@ -37,6 +37,11 @@ import (
        "github.com/apache/dubbo-go-pixiu/pkg/server"
 )
 
+// * =================================
+// This is a old implementation before updating to the new router framework 
(#777)
+// It is a reserved for function verification only
+// DO NOT USE IT IN ANYWHERE BUT TEST IN pkg/common/router/router_test.go
+// * =================================
 type (
        // RouterCoordinator the router coordinator for http connection manager
        RouterCoordinator struct {
diff --git a/pkg/common/router/router.go b/pkg/common/router/router.go
index cd175eb2..6a8393c5 100644
--- a/pkg/common/router/router.go
+++ b/pkg/common/router/router.go
@@ -19,8 +19,10 @@ package router
 
 import (
        stdHttp "net/http"
-       "strings"
+       "slices"
        "sync"
+       "sync/atomic"
+       "time"
 )
 
 import (
@@ -28,7 +30,6 @@ import (
 )
 
 import (
-       "github.com/apache/dubbo-go-pixiu/pkg/common/constant"
        "github.com/apache/dubbo-go-pixiu/pkg/common/router/trie"
        "github.com/apache/dubbo-go-pixiu/pkg/common/util/stringutil"
        "github.com/apache/dubbo-go-pixiu/pkg/context/http"
@@ -37,136 +38,216 @@ import (
        "github.com/apache/dubbo-go-pixiu/pkg/server"
 )
 
-type (
-       // RouterCoordinator the router coordinator for http connection manager
-       RouterCoordinator struct {
-               activeConfig *model.RouteConfiguration
-               rw           sync.RWMutex
-       }
-)
+// RouterCoordinator the router coordinator for http connection manager
+type RouterCoordinator struct {
+       mainSnapshot atomic.Pointer[model.RouteSnapshot] // atomic snapshot
+       mu           sync.Mutex
+
+       nextSnapshot []*model.Router // temp store for dynamic update, DO NOT 
read directly
+
+       timer    *time.Timer   // debounce timer
+       debounce time.Duration // merge window, default 50ms
+}
 
 // CreateRouterCoordinator create coordinator for http connection manager
 func CreateRouterCoordinator(routeConfig *model.RouteConfiguration) 
*RouterCoordinator {
-       rc := &RouterCoordinator{activeConfig: routeConfig}
+       rc := &RouterCoordinator{
+               nextSnapshot: make([]*model.Router, 0, len(routeConfig.Routes)),
+               debounce:     50 * time.Millisecond, // merge window
+       }
        if routeConfig.Dynamic {
                server.GetRouterManager().AddRouterListener(rc)
        }
-       rc.initTrie()
-       rc.initRegex()
+       // build initial config and store snapshot
+       
rc.mainSnapshot.Store(model.ToSnapshot(buildRouteConfiguration(routeConfig.Routes).Routes))
+       // copy initial routes to store, keep origin order
+       rc.nextSnapshot = append(rc.nextSnapshot, routeConfig.Routes...)
        return rc
 }
 
-// Route find routeAction for request
 func (rm *RouterCoordinator) Route(hc *http.HttpContext) (*model.RouteAction, 
error) {
-       rm.rw.RLock()
-       defer rm.rw.RUnlock()
-
        return rm.route(hc.Request)
 }
 
 func (rm *RouterCoordinator) RouteByPathAndName(path, method string) 
(*model.RouteAction, error) {
-       rm.rw.RLock()
-       defer rm.rw.RUnlock()
-
-       return rm.activeConfig.RouteByPathAndMethod(path, method)
+       s := rm.mainSnapshot.Load()
+       if s == nil {
+               return nil, errors.New("router configuration is empty")
+       }
+       t := s.MethodTries[method]
+       if t == nil {
+               return nil, errors.Errorf("route failed for %s, no rules 
matched", stringutil.GetTrieKey(method, path))
+       }
+       node, _, ok := t.Match(stringutil.GetTrieKey(method, path))
+       if !ok || node == nil || node.GetBizInfo() == nil {
+               return nil, errors.Errorf("route failed for %s, no rules 
matched", stringutil.GetTrieKey(method, path))
+       }
+       act, ok := node.GetBizInfo().(model.RouteAction)
+       if !ok {
+               return nil, errors.Errorf("route failed for %s, invalid route 
action type", stringutil.GetTrieKey(method, path))
+       }
+       return &act, nil
 }
 
 func (rm *RouterCoordinator) route(req *stdHttp.Request) (*model.RouteAction, 
error) {
-       // match those route that only contains headers first
-       var matched []*model.Router
-       for _, route := range rm.activeConfig.Routes {
-               if len(route.Match.Prefix) > 0 {
+       s := rm.mainSnapshot.Load()
+       if s == nil {
+               return nil, errors.New("router configuration is empty")
+       }
+
+       // header-only first
+       for _, hr := range s.HeaderOnly {
+               if !model.MethodAllowed(hr.Methods, req.Method) {
                        continue
                }
-               if route.Match.MatchHeader(req) {
-                       matched = append(matched, route)
+               if matchHeaders(hr.Headers, req) {
+                       if len(hr.Action.Cluster) == 0 {
+                               return nil, errors.New("action is nil. please 
check your configuration.")
+                       }
+                       return &hr.Action, nil
                }
        }
+       // Trie
+       t := s.MethodTries[req.Method]
+       if t == nil {
+               return nil, errors.Errorf("route failed for %s, no rules 
matched", stringutil.GetTrieKey(req.Method, req.URL.Path))
 
-       // always return the first match of header if got any
-       if len(matched) > 0 {
-               if len(matched[0].Route.Cluster) == 0 {
-                       return nil, errors.New("action is nil. please check 
your configuration.")
-               }
-               return &matched[0].Route, nil
        }
 
-       // match those route that only contains prefix
-       // TODO: may consider implementing both prefix and header in the future
-       return rm.activeConfig.Route(req)
+       node, _, ok := t.Match(stringutil.GetTrieKey(req.Method, req.URL.Path))
+       if !ok || node == nil || node.GetBizInfo() == nil {
+               return nil, errors.Errorf("route failed for %s, no rules 
matched", stringutil.GetTrieKey(req.Method, req.URL.Path))
+       }
+       act, ok := node.GetBizInfo().(model.RouteAction)
+       if !ok {
+               return nil, errors.Errorf("route failed for %s, invalid route 
action type", stringutil.GetTrieKey(req.Method, req.URL.Path))
+       }
+       return &act, nil
 }
 
-func getTrieKey(method string, path string, isPrefix bool) string {
-       if isPrefix {
-               if !strings.HasSuffix(path, constant.PathSlash) {
-                       path = path + constant.PathSlash
+// reset timer or publish directly
+func (rm *RouterCoordinator) schedulePublishLocked() {
+       if rm.debounce <= 0 {
+               // fallback: immediate
+               rm.publishLocked()
+               return
+       }
+       if rm.timer == nil {
+               rm.timer = time.NewTimer(rm.debounce)
+               go rm.awaitAndPublish()
+               return
+       }
+       // clear timer channel
+       if !rm.timer.Stop() {
+               select {
+               case <-rm.timer.C:
+               default:
                }
-               path = path + "**"
        }
-       return stringutil.GetTrieKey(method, path)
+       rm.timer.Reset(rm.debounce)
 }
 
-func (rm *RouterCoordinator) initTrie() {
-       if rm.activeConfig.RouteTrie.IsEmpty() {
-               rm.activeConfig.RouteTrie = trie.NewTrie()
-       }
-       for _, router := range rm.activeConfig.Routes {
-               rm.OnAddRouter(router)
+// wait for timer and publish
+func (rm *RouterCoordinator) awaitAndPublish() {
+       <-rm.timer.C
+       rm.mu.Lock()
+       defer rm.mu.Unlock()
+       rm.publishLocked()
+       rm.timer = nil
+}
+
+// publish: clone from store -> build new config -> atomic switch
+func (rm *RouterCoordinator) publishLocked() {
+       // 1) clone routes
+       next := make([]*model.Router, len(rm.nextSnapshot))
+       copy(next, rm.nextSnapshot)
+       // 2) build new config
+       cfg := buildRouteConfiguration(next)
+       // 3) atomic switch
+       rm.mainSnapshot.Store(model.ToSnapshot(cfg.Routes))
+}
+
+func buildRouteConfiguration(routes []*model.Router) *model.RouteConfiguration 
{
+       cfg := &model.RouteConfiguration{
+               RouteTrie: trie.NewTrie(),
+               Routes:    make([]*model.Router, 0, len(routes)),
+               Dynamic:   false,
        }
+       cfg.Routes = append(cfg.Routes, routes...)
+       initRegex(cfg)
+       fillTrieFromRoutes(cfg)
+       return cfg
 }
 
-func (rm *RouterCoordinator) initRegex() {
-       for _, router := range rm.activeConfig.Routes {
+func initRegex(cfg *model.RouteConfiguration) {
+       for _, router := range cfg.Routes {
                headers := router.Match.Headers
                for i := range headers {
                        if headers[i].Regex && len(headers[i].Values) > 0 {
-                               // regexp always use first value of header
-                               err := 
headers[i].SetValueRegex(headers[i].Values[0])
-                               if err != nil {
-                                       logger.Errorf("invalid regexp in 
headers[%d]: %v", i, err)
-                                       panic(err)
+                               if err := 
headers[i].SetValueRegex(headers[i].Values[0]); err != nil {
+                                       logger.Warnf("invalid regexp in 
headers[%d]: %v", i, err)
                                }
                        }
                }
        }
 }
 
-// OnAddRouter add router
+// OnAddRouter add router, every call of OnAdd will ADD a new rule, instead of 
OVERWRITE the same rule
 func (rm *RouterCoordinator) OnAddRouter(r *model.Router) {
-       //TODO: lock move to trie node
-       rm.rw.Lock()
-       defer rm.rw.Unlock()
-       if r.Match.Methods == nil {
-               r.Match.Methods = []string{constant.Get, constant.Put, 
constant.Delete, constant.Post, constant.Options}
-       }
-       isPrefix := r.Match.Prefix != ""
-       for _, method := range r.Match.Methods {
-               var key string
-               if isPrefix {
-                       key = getTrieKey(method, r.Match.Prefix, isPrefix)
-               } else {
-                       key = getTrieKey(method, r.Match.Path, isPrefix)
+       rm.mu.Lock()
+       defer rm.mu.Unlock()
+       rm.nextSnapshot = append(rm.nextSnapshot, r)
+       rm.schedulePublishLocked()
+}
+
+func fillTrieFromRoutes(cfg *model.RouteConfiguration) {
+       for _, r := range cfg.Routes {
+               methods := r.Match.Methods
+               if len(methods) == 0 {
+                       methods = []string{"GET", "POST", "PUT", "DELETE", 
"PATCH", "OPTIONS", "HEAD"}
+               }
+               for _, m := range methods {
+                       key := stringutil.GetTrieKeyWithPrefix(m, r.Match.Path, 
r.Match.Prefix, r.Match.Prefix != "")
+                       _, _ = cfg.RouteTrie.Put(key, r.Route)
                }
-               _, _ = rm.activeConfig.RouteTrie.Put(key, r.Route)
        }
 }
 
 // OnDeleteRouter delete router
 func (rm *RouterCoordinator) OnDeleteRouter(r *model.Router) {
-       rm.rw.Lock()
-       defer rm.rw.Unlock()
+       rm.mu.Lock()
+       defer rm.mu.Unlock()
 
-       if r.Match.Methods == nil {
-               r.Match.Methods = []string{constant.Get, constant.Put, 
constant.Delete, constant.Post}
+       if len(rm.nextSnapshot) == 0 {
+               return
        }
-       isPrefix := r.Match.Prefix != ""
-       for _, method := range r.Match.Methods {
-               var key string
-               if isPrefix {
-                       key = getTrieKey(method, r.Match.Prefix, isPrefix)
+       out := rm.nextSnapshot[:0]
+       for _, rr := range rm.nextSnapshot {
+               if rr.ID == r.ID {
+                       continue
+               }
+               out = append(out, rr)
+       }
+       rm.nextSnapshot = out
+       rm.schedulePublishLocked()
+}
+
+func matchHeaders(chs []model.CompiledHeader, r *stdHttp.Request) bool {
+       for _, ch := range chs {
+               if val := r.Header.Get(ch.Name); len(val) > 0 {
+                       if ch.Regex != nil {
+                               if ok := ch.Regex.MatchString(val); !ok {
+                                       return false
+                               }
+                               continue
+                       }
+
+                       if !slices.Contains(ch.Values, val) {
+                               return false
+                       }
                } else {
-                       key = getTrieKey(method, r.Match.Path, isPrefix)
+                       return false
                }
-               _, _ = rm.activeConfig.RouteTrie.Remove(key)
        }
+       return true
 }
diff --git a/pkg/common/router/router_bench_test.go 
b/pkg/common/router/router_bench_test.go
new file mode 100644
index 00000000..e44599d8
--- /dev/null
+++ b/pkg/common/router/router_bench_test.go
@@ -0,0 +1,504 @@
+/*
+ * 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 router
+
+import (
+       "math/rand"
+       stdHttp "net/http"
+       "reflect"
+       "strconv"
+       "testing"
+       "time"
+)
+
+import (
+       oldrouter "github.com/apache/dubbo-go-pixiu/pkg/common/router/mock"
+       "github.com/apache/dubbo-go-pixiu/pkg/context/http"
+       "github.com/apache/dubbo-go-pixiu/pkg/model"
+)
+
+/*
+==============================
+this is the benchmark for router
+contrast oldrouter and newrouter
+oldrouter: "github.com/apache/dubbo-go-pixiu/pkg/common/router/mock"
+newrouter: "github.com/apache/dubbo-go-pixiu/pkg/common/router"
+==============================
+*/
+type benchShape struct {
+       NRoutes         int     // router number
+       PrefixRatio     float64 // prefix router ratio(others are accurate path)
+       HeaderOnlyRatio float64 // header only router ratio(without path/prefix)
+       Methods         []string
+}
+
+func buildOldCoordinator(routes []*model.Router) *oldrouter.RouterCoordinator {
+       cfg := &model.RouteConfiguration{
+               Routes:  routes,
+               Dynamic: false,
+       }
+       return oldrouter.CreateRouterCoordinator(cfg)
+}
+
+func genRoutes(sh benchShape) []*model.Router {
+       routes := make([]*model.Router, 0, sh.NRoutes)
+       if len(sh.Methods) == 0 {
+               sh.Methods = []string{"GET", "POST"}
+       }
+       nHeader := int(float64(sh.NRoutes) * sh.HeaderOnlyRatio)
+       nPrefix := int(float64(sh.NRoutes-nHeader) * sh.PrefixRatio)
+       nPath := sh.NRoutes - nHeader - nPrefix
+
+       // 1) Header-only
+       for i := 0; i < nHeader; i++ {
+               id := "hdr-" + strconv.Itoa(i)
+               r := &model.Router{
+                       ID: id,
+                       Match: model.RouterMatch{
+                               Methods: sh.Methods,
+                               Headers: []model.HeaderMatcher{
+                                       {Name: "X-Env", Values: 
[]string{"prod"}, Regex: false},
+                               },
+                       },
+                       Route: model.RouteAction{Cluster: "c-h-" + id},
+               }
+               routes = append(routes, r)
+       }
+       // 2) Prefix routes
+       for i := 0; i < nPrefix; i++ {
+               id := "pre-" + strconv.Itoa(i)
+               p := "/api/v1/service" + strconv.Itoa(i%50) + "/"
+               r := &model.Router{
+                       ID: id,
+                       Match: model.RouterMatch{
+                               Methods: sh.Methods,
+                               Prefix:  p,
+                       },
+                       Route: model.RouteAction{Cluster: "c-p-" + id},
+               }
+               routes = append(routes, r)
+       }
+       // 3) Exact path
+       for i := 0; i < nPath; i++ {
+               id := "pth-" + strconv.Itoa(i)
+               pp := "/api/v1/item/" + strconv.Itoa(i)
+               r := &model.Router{
+                       ID: id,
+                       Match: model.RouterMatch{
+                               Methods: sh.Methods,
+                               Path:    pp,
+                       },
+                       Route: model.RouteAction{Cluster: "c-x-" + id},
+               }
+               routes = append(routes, r)
+       }
+       return routes
+}
+
+func buildNewCoordinator(routes []*model.Router) *RouterCoordinator {
+       cfg := &model.RouteConfiguration{
+               Routes:  routes,
+               Dynamic: false,
+       }
+       return CreateRouterCoordinator(cfg)
+}
+
+func buildDelta(base []*model.Router, seed int64) []*model.Router {
+       cp := make([]*model.Router, len(base))
+       copy(cp, base)
+       rnd := rand.New(rand.NewSource(seed))
+       k := len(cp) / 100 // 1%
+       out := make([]*model.Router, 0, k)
+       for i := 0; i < k; i++ {
+               idx := rnd.Intn(len(cp)) // NOSONAR
+               old := cp[idx]
+               newPath := "/api/v1/item/" + strconv.Itoa(rnd.Intn(100000)) // 
NOSONAR
+               nr := &model.Router{
+                       ID: old.ID,
+                       Match: model.RouterMatch{
+                               Methods: old.Match.Methods,
+                               Path:    newPath,
+                               Headers: old.Match.Headers,
+                       },
+                       Route: old.Route,
+               }
+               out = append(out, nr)
+       }
+       return out
+}
+
+func genRequests(n int) []*stdHttp.Request {
+       reqs := make([]*stdHttp.Request, 0, n)
+       methods := []string{"GET", "POST"}
+       for i := 0; i < n; i++ {
+               var path string
+               switch i % 3 {
+               case 0:
+                       path = "/api/v1/item/" + strconv.Itoa(i%10000)
+               case 1:
+                       path = "/api/v1/service" + strconv.Itoa(i%50) + 
"/foo/bar"
+               default:
+                       path = "/unknown/" + strconv.Itoa(i)
+               }
+               req, _ := stdHttp.NewRequest(methods[i%len(methods)], path, nil)
+               if i%5 == 0 { // trigger header-only route
+                       req.Header.Set("X-Env", "prod")
+               }
+               reqs = append(reqs, req)
+       }
+       return reqs
+}
+
+// helper: assert Route behavior of old/new is the same on a set of requests
+func assertRouteSame(b testing.TB, oldc *oldrouter.RouterCoordinator, newc 
*RouterCoordinator, reqs []*stdHttp.Request) {
+       b.Helper()
+       for i, r := range reqs {
+               ctxOld := http.HttpContext{Request: r}
+               ctxNew := http.HttpContext{Request: r}
+
+               oldRes, oldErr := oldc.Route(&ctxOld)
+               newRes, newErr := newc.Route(&ctxNew)
+
+               if (oldErr != nil && newErr == nil) || (oldErr == nil && newErr 
!= nil) {
+                       b.Fatalf("route error text mismatch on #%d path=%s 
method=%s: oldErr=%v newErr=%v",
+                               i, r.URL.Path, r.Method, oldErr, newErr)
+               }
+               if !reflect.DeepEqual(oldRes, newRes) {
+                       b.Fatalf("route result mismatch on #%d path=%s 
method=%s: old=%#v new=%#v",
+                               i, r.URL.Path, r.Method, oldRes, newRes)
+               }
+       }
+}
+
+// helper: assert RouteByPathAndName behavior is the same
+func assertRouteByPathAndNameSame(b testing.TB, oldc 
*oldrouter.RouterCoordinator, newc *RouterCoordinator, paths []string, method 
string) {
+       b.Helper()
+       for i, p := range paths {
+               oldRes, oldErr := oldc.RouteByPathAndName(p, method)
+               newRes, newErr := newc.RouteByPathAndName(p, method)
+
+               if (oldErr != nil && newErr == nil) || (oldErr == nil && newErr 
!= nil) {
+                       b.Fatalf("route error text mismatch on #%d path=%s 
method=%s: oldRes=%v oldErr=%v  newRes=%v newErr=%v",
+                               i, p, method, oldRes, oldErr, newRes, newErr)
+               }
+               if !reflect.DeepEqual(oldRes, newRes) {
+                       b.Fatalf("RouteByPathAndName result mismatch on #%d 
path=%s method=%s: old=%#v new=%#v",
+                               i, p, method, oldRes, newRes)
+               }
+       }
+}
+
+// ============= Bench 1:read throughput (one goroutine) =============
+
+func BenchmarkRouteReadThroughput(b *testing.B) {
+       shape := benchShape{NRoutes: 30000, PrefixRatio: 0.4, HeaderOnlyRatio: 
0.1, Methods: []string{"GET", "POST"}}
+
+       oldRoutes := genRoutes(shape)
+       newRoutes := genRoutes(shape)
+       reqs := genRequests(4096)
+
+       oldc := buildOldCoordinator(oldRoutes)
+       newc := buildNewCoordinator(newRoutes)
+
+       assertRouteSame(b, oldc, newc, reqs)
+
+       b.Run("old/locked-read-30k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       r := reqs[i%len(reqs)]
+                       httpContext := http.HttpContext{
+                               Request: r,
+                       }
+                       _, _ = oldc.Route(&httpContext)
+               }
+       })
+
+       b.Run("new/rcu-read-30k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       r := reqs[i%len(reqs)]
+                       httpContext := http.HttpContext{
+                               Request: r,
+                       }
+                       _, _ = newc.Route(&httpContext)
+               }
+       })
+}
+
+// ============= Bench 2:read throughput (parallel) =============
+
+func BenchmarkRouteReadParallel(b *testing.B) {
+       shape := benchShape{NRoutes: 30000, PrefixRatio: 0.4, HeaderOnlyRatio: 
0.1, Methods: []string{"GET", "POST"}}
+
+       oldRoutes := genRoutes(shape)
+       newRoutes := genRoutes(shape)
+       reqs := genRequests(8192)
+
+       oldc := buildOldCoordinator(oldRoutes)
+       newc := buildNewCoordinator(newRoutes)
+
+       assertRouteSame(b, oldc, newc, reqs)
+
+       b.Run("old/parallel-30k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.SetParallelism(40)
+               b.ResetTimer()
+               b.RunParallel(func(pb *testing.PB) {
+                       i := rand.Int() // NOSONAR
+                       for pb.Next() {
+                               r := reqs[i%len(reqs)]
+                               httpContext := http.HttpContext{
+                                       Request: r,
+                               }
+                               _, _ = oldc.Route(&httpContext)
+                               i++
+                       }
+               })
+       })
+
+       b.Run("new/parallel-30k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.SetParallelism(40)
+               b.ResetTimer()
+               b.RunParallel(func(pb *testing.PB) {
+                       i := rand.Int() // NOSONAR
+                       for pb.Next() {
+                               r := reqs[i%len(reqs)]
+                               httpContext := http.HttpContext{
+                                       Request: r,
+                               }
+                               _, _ = newc.Route(&httpContext)
+                               i++
+                       }
+               })
+       })
+}
+
+// ============= Bench 3:read and write(1% write) =============
+
+func BenchmarkReloadLatency(b *testing.B) {
+       shape := benchShape{NRoutes: 30000, PrefixRatio: 0.4, HeaderOnlyRatio: 
0.1, Methods: []string{"GET", "POST"}}
+       base := genRoutes(shape)
+
+       oldc := buildOldCoordinator(base)
+       newc := buildNewCoordinator(base)
+
+       {
+               checkOld := buildOldCoordinator(base)
+               checkNew := buildNewCoordinator(base)
+               deltaOld := buildDelta(base, 1)
+               for i := range deltaOld {
+                       checkOld.OnAddRouter(deltaOld[i])
+                       checkNew.OnAddRouter(deltaOld[i])
+               }
+               reqs := genRequests(1024)
+               time.Sleep(100 * time.Millisecond)
+               assertRouteSame(b, checkOld, checkNew, reqs)
+       }
+
+       b.Run("old/reload-1percent-30k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       for _, r := range buildDelta(base, int64(i)) {
+                               oldc.OnAddRouter(r)
+                       }
+               }
+       })
+
+       b.Run("new/reload-1percent-30k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       for _, r := range buildDelta(base, int64(i)) {
+                               newc.OnAddRouter(r)
+                       }
+               }
+       })
+}
+
+func BenchmarkRoute100kReadThroughput(b *testing.B) {
+       shape := benchShape{
+               NRoutes:         100_000,
+               PrefixRatio:     0.4,
+               HeaderOnlyRatio: 0.1,
+               Methods:         []string{"GET", "POST"},
+       }
+       reqs := genRequests(16_384)
+
+       oldc := buildOldCoordinator(genRoutes(shape))
+       newc := buildNewCoordinator(genRoutes(shape))
+
+       assertRouteSame(b, oldc, newc, reqs)
+
+       b.Run("old/locked-read-100k", func(b *testing.B) {
+               b.ReportAllocs()
+               for i := 0; i < b.N; i++ {
+                       r := reqs[i%len(reqs)]
+                       httpContext := http.HttpContext{
+                               Request: r,
+                       }
+                       _, _ = oldc.Route(&httpContext)
+               }
+       })
+       b.Run("new/rcu-read-100k", func(b *testing.B) {
+               b.ReportAllocs()
+               for i := 0; i < b.N; i++ {
+                       r := reqs[i%len(reqs)]
+                       httpContext := http.HttpContext{
+                               Request: r,
+                       }
+                       _, _ = newc.Route(&httpContext)
+               }
+       })
+}
+
+func BenchmarkRoute100kReadParallel(b *testing.B) {
+       shape := benchShape{
+               NRoutes:         100_000,
+               PrefixRatio:     0.4,
+               HeaderOnlyRatio: 0.1,
+               Methods:         []string{"GET", "POST"},
+       }
+       reqs := genRequests(32_768)
+
+       oldc := buildOldCoordinator(genRoutes(shape))
+       newc := buildNewCoordinator(genRoutes(shape))
+
+       assertRouteSame(b, oldc, newc, reqs)
+
+       b.Run("old/parallel-100k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.SetParallelism(4)
+               b.RunParallel(func(pb *testing.PB) {
+                       i := 0
+                       for pb.Next() {
+                               r := reqs[i&(len(reqs)-1)]
+                               httpContext := http.HttpContext{
+                                       Request: r,
+                               }
+                               _, _ = oldc.Route(&httpContext)
+                               i++
+                       }
+               })
+       })
+       b.Run("new/parallel-100k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.SetParallelism(4)
+               b.RunParallel(func(pb *testing.PB) {
+                       i := 0
+                       for pb.Next() {
+                               r := reqs[i&(len(reqs)-1)]
+                               httpContext := http.HttpContext{
+                                       Request: r,
+                               }
+                               _, _ = newc.Route(&httpContext)
+                               i++
+                       }
+               })
+       })
+}
+
+func BenchmarkReload100kLatency1Percent(b *testing.B) {
+       shape := benchShape{
+               NRoutes:         100_000,
+               PrefixRatio:     0.4,
+               HeaderOnlyRatio: 0.1,
+               Methods:         []string{"GET", "POST"},
+       }
+       base := genRoutes(shape)
+
+       oldc := buildOldCoordinator(genRoutes(shape))
+       newc := buildNewCoordinator(genRoutes(shape))
+
+       {
+               checkOld := buildOldCoordinator(base)
+               checkNew := buildNewCoordinator(base)
+               deltaOld := buildDelta(base, 1)
+               for i := range deltaOld {
+                       checkOld.OnAddRouter(deltaOld[i])
+                       checkNew.OnAddRouter(deltaOld[i])
+               }
+               reqs := genRequests(2048)
+               time.Sleep(100 * time.Millisecond)
+               assertRouteSame(b, checkOld, checkNew, reqs)
+       }
+
+       b.Run("old/reload-1percent-100k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       for _, r := range buildDelta(base, int64(i)) {
+                               oldc.OnAddRouter(r)
+                       }
+               }
+       })
+
+       b.Run("new/reload-1percent-100k", func(b *testing.B) {
+               b.ReportAllocs()
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       for _, r := range buildDelta(base, int64(i)) {
+                               newc.OnAddRouter(r)
+                       }
+               }
+       })
+}
+
+// ============= Bench 4:RouteByPathAndName(API behavior must same) 
=============
+
+func BenchmarkRouteByPathAndName(b *testing.B) {
+       shape := benchShape{
+               NRoutes:         20000,
+               PrefixRatio:     0.5,
+               HeaderOnlyRatio: 0.0,
+               Methods:         []string{"GET"},
+       }
+
+       oldc := buildOldCoordinator(genRoutes(shape))
+       newc := buildNewCoordinator(genRoutes(shape))
+
+       paths := []string{
+               "/api/v1/item/12345",
+               "/api/v1/service7/xxx/yyy",
+               "/no/match/path",
+       }
+       method := "GET"
+
+       assertRouteByPathAndNameSame(b, oldc, newc, paths, method)
+
+       b.Run("old/RouteByPathAndName", func(b *testing.B) {
+               b.ReportAllocs()
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       path := paths[i%len(paths)]
+                       _, _ = oldc.RouteByPathAndName(path, method)
+               }
+       })
+
+       b.Run("new/RouteByPathAndName", func(b *testing.B) {
+               b.ReportAllocs()
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       path := paths[i%len(paths)]
+                       _, _ = newc.RouteByPathAndName(path, method)
+               }
+       })
+}
diff --git a/pkg/common/router/router_test.go b/pkg/common/router/router_test.go
index 46441514..f3063a00 100644
--- a/pkg/common/router/router_test.go
+++ b/pkg/common/router/router_test.go
@@ -19,7 +19,9 @@ package router
 
 import (
        "bytes"
-       "net/http"
+       "math/rand"
+       stdHttp "net/http"
+       "strconv"
        "testing"
 )
 
@@ -28,36 +30,24 @@ import (
 )
 
 import (
-       "github.com/apache/dubbo-go-pixiu/pkg/common/router/trie"
+       oldrouter "github.com/apache/dubbo-go-pixiu/pkg/common/router/mock"
+       "github.com/apache/dubbo-go-pixiu/pkg/context/http"
        "github.com/apache/dubbo-go-pixiu/pkg/context/mock"
        "github.com/apache/dubbo-go-pixiu/pkg/model"
 )
 
 func TestCreateRouterCoordinator(t *testing.T) {
-       hcmc := model.HttpConnectionManagerConfig{
-               RouteConfig: model.RouteConfiguration{
-                       RouteTrie: trie.NewTrieWithDefault("POST/api/v1/**", 
model.RouteAction{
-                               Cluster:                     "test_dubbo",
-                               ClusterNotFoundResponseCode: 505,
-                       }),
-                       Dynamic: false,
-               },
-               HTTPFilters: []*model.HTTPFilter{
-                       {
-                               Name:   "test",
-                               Config: nil,
-                       },
-               },
-               ServerName:        "test_http_dubbo",
-               GenerateRequestID: false,
-               IdleTimeoutStr:    "100",
+       specs := []RouteSpec{
+               // exact
+               {ID: "test", Methods: []string{"POST"}, Path: "/api/v1/**", 
Cluster: "test_dubbo"},
        }
 
-       r := CreateRouterCoordinator(&hcmc.RouteConfig)
-       request, err := http.NewRequest("POST", 
"http://www.dubbogopixiu.com/api/v1?name=tc";, 
bytes.NewReader([]byte("{\"id\":\"12345\"}")))
+       coordinator := BuildNew(specs)
+
+       request, err := stdHttp.NewRequest("POST", 
"http://www.dubbogopixiu.com/api/v1?name=tc";, 
bytes.NewReader([]byte("{\"id\":\"12345\"}")))
        assert.NoError(t, err)
        c := mock.GetMockHTTPContext(request)
-       a, err := r.Route(c)
+       a, err := coordinator.Route(c)
        assert.NoError(t, err)
        assert.Equal(t, a.Cluster, "test_dubbo")
 
@@ -71,13 +61,19 @@ func TestCreateRouterCoordinator(t *testing.T) {
                },
        }
 
-       r.OnAddRouter(router)
-       r.OnDeleteRouter(router)
+       coordinator.OnAddRouter(router)
+       coordinator.OnDeleteRouter(router)
 }
 
 func TestRoute(t *testing.T) {
        const (
                Cluster1 = "test-cluster-1"
+               Cluster2 = "test-cluster-2"
+               Cluster3 = "test-cluster-3"
+               Cluster4 = "test-cluster-4"
+               Cluster5 = "test-cluster-5"
+               Cluster6 = "test-cluster-6"
+               Cluster7 = "test-cluster-7"
        )
 
        hcmc := model.HttpConnectionManagerConfig{
@@ -89,16 +85,52 @@ func TestRoute(t *testing.T) {
                                                Headers: []model.HeaderMatcher{
                                                        {
                                                                Name:   "A",
-                                                               Values: 
[]string{"1", "2", "3"},
+                                                               Values: 
[]string{"1", "2", "0"},
                                                        },
+                                               },
+                                               Methods: []string{"GET", 
"POST"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
Cluster1,
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                               {
+                                       ID: "2",
+                                       Match: model.RouterMatch{
+                                               Headers: []model.HeaderMatcher{
                                                        {
                                                                Name:   "A",
                                                                Values: 
[]string{"3", "4", "5"},
                                                        },
+                                               },
+                                               Methods: []string{"GET", 
"POST"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
Cluster2,
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                               {
+                                       ID: "3",
+                                       Match: model.RouterMatch{
+                                               Headers: []model.HeaderMatcher{
                                                        {
                                                                Name:   "B",
                                                                Values: 
[]string{"1"},
                                                        },
+                                               },
+                                               Methods: []string{"GET", 
"POST"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
Cluster3,
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                               {
+                                       ID: "4",
+                                       Match: model.RouterMatch{
+                                               Headers: []model.HeaderMatcher{
                                                        {
                                                                Name:   
"normal-regex",
                                                                Values: 
[]string{"(k){2}"},
@@ -113,7 +145,65 @@ func TestRoute(t *testing.T) {
                                                Methods: []string{"GET", 
"POST"},
                                        },
                                        Route: model.RouteAction{
-                                               Cluster:                     
Cluster1,
+                                               Cluster:                     
Cluster4,
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                               {
+                                       ID: "5",
+                                       Match: model.RouterMatch{
+                                               Headers: []model.HeaderMatcher{
+                                                       {
+                                                               Name:   
"broken-regex",
+                                                               Values: 
[]string{"(t){2]]"},
+                                                               Regex:  true,
+                                                       },
+                                               },
+                                               Methods: []string{"GET", 
"POST"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
Cluster5,
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                               {
+                                       ID: "6",
+                                       Match: model.RouterMatch{
+                                               Headers: []model.HeaderMatcher{
+                                                       {
+                                                               Name:   "C",
+                                                               Values: 
[]string{"1", "2", "0"},
+                                                       },
+                                                       {
+                                                               Name:   "D",
+                                                               Values: 
[]string{"3", "4", "5"},
+                                                       },
+                                               },
+                                               Methods: []string{"GET", 
"POST"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
Cluster6,
+                                               ClusterNotFoundResponseCode: 
505,
+                                       },
+                               },
+                               {
+                                       ID: "7",
+                                       Match: model.RouterMatch{
+                                               Headers: []model.HeaderMatcher{
+                                                       {
+                                                               Name:   "E",
+                                                               Values: 
[]string{"1", "2", "0"},
+                                                       },
+                                                       {
+                                                               Name:   
"normal-regex",
+                                                               Values: 
[]string{"(k){2}"},
+                                                               Regex:  true,
+                                                       },
+                                               },
+                                               Methods: []string{"GET", 
"POST"},
+                                       },
+                                       Route: model.RouteAction{
+                                               Cluster:                     
Cluster7,
                                                ClusterNotFoundResponseCode: 
505,
                                        },
                                },
@@ -144,7 +234,7 @@ func TestRoute(t *testing.T) {
                        Header: map[string]string{
                                "A": "1",
                        },
-                       Expect: "test-cluster-1",
+                       Expect: Cluster1,
                },
                {
                        Name: "one header matched",
@@ -152,7 +242,7 @@ func TestRoute(t *testing.T) {
                        Header: map[string]string{
                                "A": "3",
                        },
-                       Expect: Cluster1,
+                       Expect: Cluster2,
                },
                {
                        Name: "more header with one regex matched",
@@ -161,16 +251,16 @@ func TestRoute(t *testing.T) {
                                "A":            "5",
                                "normal-regex": "kkkk",
                        },
-                       Expect: Cluster1,
+                       Expect: Cluster2,
                },
                {
                        Name:   "one header but wrong method",
                        URL:    "/user",
                        Method: "PUT",
                        Header: map[string]string{
-                               "A": "3",
+                               "A": "0",
                        },
-                       Expect: "route failed for PUT/user, no rules matched.",
+                       Expect: "route failed for PUT/user, no rules matched",
                },
                {
                        Name: "one broken regex header",
@@ -178,14 +268,22 @@ func TestRoute(t *testing.T) {
                        Header: map[string]string{
                                "broken-regex": "tt",
                        },
-                       Expect: "route failed for GET/user, no rules matched.",
+                       Expect: "route failed for GET/user, no rules matched",
                },
                {
                        Name: "one matched header 2",
                        Header: map[string]string{
                                "B": "1",
                        },
-                       Expect: Cluster1,
+                       Expect: Cluster3,
+               },
+               {
+                       Name:   "only header but wrong method",
+                       Method: "DELETE",
+                       Header: map[string]string{
+                               "B": "1",
+                       },
+                       Expect: "route failed for DELETE, no rules matched",
                },
                {
                        Name:   "only header but wrong method",
@@ -193,7 +291,25 @@ func TestRoute(t *testing.T) {
                        Header: map[string]string{
                                "B": "1",
                        },
-                       Expect: "route failed for DELETE, no rules matched.",
+                       Expect: "route failed for DELETE, no rules matched",
+               },
+               {
+                       Name: "regex AND normal",
+                       URL:  "/user",
+                       Header: map[string]string{
+                               "E":            "0",
+                               "normal-regex": "kk",
+                       },
+                       Expect: Cluster7,
+               },
+               {
+                       Name: "normal AND normal",
+                       URL:  "/user",
+                       Header: map[string]string{
+                               "C": "1",
+                               "D": "3",
+                       },
+                       Expect: Cluster6,
                },
        }
 
@@ -205,7 +321,7 @@ func TestRoute(t *testing.T) {
                        if len(tc.Method) > 0 {
                                method = tc.Method
                        }
-                       request, err := http.NewRequest(method, tc.URL, nil)
+                       request, err := stdHttp.NewRequest(method, tc.URL, nil)
                        assert.NoError(t, err)
 
                        if tc.Header != nil {
@@ -224,3 +340,619 @@ func TestRoute(t *testing.T) {
                })
        }
 }
+
+/* ==============================
+   below are parity test between old and new router
+   ============================== */
+
+type HeaderSpec struct {
+       Name   string
+       Values []string
+       Regex  bool
+}
+
+type RouteSpec struct {
+       ID      string
+       Methods []string
+       Path    string
+       Prefix  string
+       Headers []HeaderSpec
+       Cluster string
+}
+
+func (s RouteSpec) toRouter() *model.Router {
+       h := make([]model.HeaderMatcher, 0, len(s.Headers))
+       for _, x := range s.Headers {
+               h = append(h, model.HeaderMatcher{Name: x.Name, Values: 
append([]string(nil), x.Values...), Regex: x.Regex})
+       }
+       return &model.Router{
+               ID: s.ID,
+               Match: model.RouterMatch{
+                       Methods: append([]string(nil), s.Methods...),
+                       Path:    s.Path,
+                       Prefix:  s.Prefix,
+                       Headers: h,
+               },
+               Route: model.RouteAction{Cluster: s.Cluster},
+       }
+}
+
+func buildOld(specs []RouteSpec) *oldrouter.RouterCoordinator {
+       rs := make([]*model.Router, 0, len(specs))
+       for _, s := range specs {
+               rs = append(rs, s.toRouter())
+       }
+       cfg := &model.RouteConfiguration{Routes: rs, Dynamic: false}
+       return oldrouter.CreateRouterCoordinator(cfg)
+}
+
+func BuildNew(specs []RouteSpec) *RouterCoordinator {
+       rs := make([]*model.Router, 0, len(specs))
+       for _, s := range specs {
+               rs = append(rs, s.toRouter())
+       }
+       cfg := &model.RouteConfiguration{Routes: rs, Dynamic: false}
+       return CreateRouterCoordinator(cfg)
+}
+
+type res struct {
+       ok      bool
+       cluster string
+       err     string
+}
+
+func call(cOld *oldrouter.RouterCoordinator, cNew *RouterCoordinator, method, 
path string, hdr map[string]string) (res, res) {
+       req, _ := stdHttp.NewRequest(method, path, nil)
+       httpContext := http.HttpContext{
+               Request: req,
+       }
+       for k, v := range hdr {
+               req.Header.Set(k, v)
+       }
+       oa, oe := cOld.Route(&httpContext)
+       na, ne := cNew.Route(&httpContext)
+
+       or := res{}
+       if oe != nil || oa == nil {
+               if oe != nil {
+                       or.err = oe.Error()
+               }
+       } else {
+               or.ok = true
+               or.cluster = oa.Cluster
+       }
+
+       nr := res{}
+       if ne != nil || na == nil {
+               if ne != nil {
+                       nr.err = ne.Error()
+               }
+       } else {
+               nr.ok = true
+               nr.cluster = na.Cluster
+       }
+       return or, nr
+}
+
+func assertSame(t *testing.T, oldc *oldrouter.RouterCoordinator, newc 
*RouterCoordinator,
+       method, path string, hdr map[string]string, wantOK bool, wantCluster 
string) {
+
+       ro, rn := call(oldc, newc, method, path, hdr)
+       if ro.ok != rn.ok || ro.cluster != rn.cluster {
+               t.Fatalf("mismatch: %s %s hdr=%v\n old={ok:%v cluster:%q 
err:%q}\n new={ok:%v cluster:%q err:%q}",
+                       method, path, hdr, ro.ok, ro.cluster, ro.err, rn.ok, 
rn.cluster, rn.err)
+       }
+       if ro.ok != wantOK || rn.ok != wantOK {
+               t.Fatalf("ok mismatch: %s %s hdr=%v wantOK=%v oldOK=%v 
newOK=%v", method, path, hdr, wantOK, ro.ok, rn.ok)
+       }
+       if wantOK && wantCluster != "" && (ro.cluster != wantCluster || 
rn.cluster != wantCluster) {
+               t.Fatalf("cluster mismatch: %s %s hdr=%v want=%q old=%q 
new=%q", method, path, hdr, wantCluster, ro.cluster, rn.cluster)
+       }
+}
+
+type varSyntax struct {
+       simplePattern func(seg string) string         // /users/:id
+       digitsPattern func(seg string) (string, bool) // /users/:id(\d+)
+       multiPattern  func(a, b string) string        // /shops/:a/orders/:b
+}
+
+func colonSyntax() varSyntax {
+       return varSyntax{
+               simplePattern: func(seg string) string {
+                       return "/users/:" + seg
+               },
+               digitsPattern: func(seg string) (string, bool) {
+                       return "/users/:" + seg + "(\\d+)", true
+               },
+               multiPattern: func(a, b string) string {
+                       return "/shops/:" + a + "/orders/:" + b
+               },
+       }
+}
+
+/* ==============================
+   test cases (var/regex/priority/header/)
+   ============================== */
+
+func TestParitySimpleCases(t *testing.T) {
+       syntax = colonSyntax()
+
+       specs := []RouteSpec{
+               // exact
+               {ID: "exact", Methods: []string{"GET"}, Path: 
"/api/v1/item/100", Cluster: "c-exact"},
+               // prefix(/**)
+               {ID: "pre", Methods: []string{"GET"}, Prefix: "/api/v1/svc/", 
Cluster: "c-pre"},
+               // var
+               {ID: "var", Methods: []string{"GET"}, Path: 
syntax.simplePattern("id"), Cluster: "c-var"},
+               // multi
+               {ID: "multi", Methods: []string{"GET", "POST"}, Path: "/multi", 
Cluster: "c-multi"},
+               // Header regex
+               {ID: "hdr", Methods: []string{"GET"}, Headers: 
[]HeaderSpec{{Name: "X-Env", Values: []string{"^prod|staging$"}, Regex: true}}, 
Cluster: "c-hdr"},
+       }
+
+       oldc := buildOld(specs)
+       newc := BuildNew(specs)
+
+       cases := []struct {
+               name    string
+               method  string
+               path    string
+               hdr     map[string]string
+               ok      bool
+               cluster string
+       }{
+               {"exact", "GET", "/api/v1/item/100", nil, true, "c-exact"},
+               {"prefix.deep", "GET", "/api/v1/svc/a/b", nil, true, "c-pre"},
+               {"var.hit", "GET", "/users/42", nil, true, "c-var"},
+               {"var.not_deeper", "GET", "/users/42/extra", nil, false, ""},
+               {"multi.get", "GET", "/multi", nil, true, "c-multi"},
+               {"multi.post", "POST", "/multi", nil, true, "c-multi"},
+               {"hdr.regex", "GET", "/whatever", map[string]string{"X-Env": 
"prod"}, true, "c-hdr"},
+               {"miss", "GET", "/no/match", nil, false, ""},
+       }
+
+       for _, tc := range cases {
+               t.Run(tc.name, func(t *testing.T) {
+                       assertSame(t, oldc, newc, tc.method, tc.path, tc.hdr, 
tc.ok, tc.cluster)
+               })
+       }
+}
+
+func TestPrioritySpecificOverWildcard(t *testing.T) {
+       syntax = colonSyntax()
+
+       specs := []RouteSpec{
+               {ID: "wild", Methods: []string{"GET"}, Prefix: "/api/v1/**", 
Cluster: "c-wild"},
+               {ID: "spec", Methods: []string{"GET"}, Path: 
"/api/v1/test-dubbo/user/name/" + 
syntax.simplePattern("name")[len("/users/"):], Cluster: "c-spec"},
+               // equals to /api/v1/test-dubbo/user/name/:name
+       }
+       oldc := buildOld(specs)
+       newc := BuildNew(specs)
+
+       assertSame(t, oldc, newc, "GET",
+               "/api/v1/test-dubbo/user/name/yqxu", nil, true, "c-spec")
+}
+
+func TestPriorityDeeperWins(t *testing.T) {
+       specs := []RouteSpec{
+               {ID: "shallow", Methods: []string{"GET"}, Prefix: "/api/v1/", 
Cluster: "c-shallow"},
+               {ID: "deeper", Methods: []string{"GET"}, Prefix: 
"/api/v1/test-dubbo/", Cluster: "c-deeper"},
+       }
+       oldc := buildOld(specs)
+       newc := BuildNew(specs)
+
+       assertSame(t, oldc, newc, "GET",
+               "/api/v1/test-dubbo/user/name/abc", nil, true, "c-deeper")
+}
+
+func TestPrioritySingleStarOverDoubleStar(t *testing.T) {
+       // use var to express "/*"
+       syntax = colonSyntax()
+       specs := []RouteSpec{
+               {ID: "multi", Methods: []string{"GET"}, Prefix: "/api/", 
Cluster: "c-**"},
+               {ID: "single", Methods: []string{"GET"}, Path: "/api/" + 
syntax.simplePattern("seg")[len("/users/"):] + "/users", Cluster: "c-*"},
+               // equals to /api/:seg/users
+       }
+       oldc := buildOld(specs)
+       newc := BuildNew(specs)
+
+       assertSame(t, oldc, newc, "GET", "/api/v1/users", nil, true, "c-*")
+       assertSame(t, oldc, newc, "GET", "/api/v1/x/users", nil, true, "c-**")
+}
+
+func TestVariablesSingleAndMulti(t *testing.T) {
+       syntax = colonSyntax()
+       specs := []RouteSpec{
+               {ID: "one", Methods: []string{"GET"}, Path: 
syntax.simplePattern("id"), Cluster: "c-one"},
+               {ID: "two", Methods: []string{"GET"}, Path: 
syntax.multiPattern("shopId", "orderId"), Cluster: "c-two"},
+               {ID: "pre", Methods: []string{"GET"}, Prefix: "/shops/", 
Cluster: "c-pre"},
+       }
+       oldc := buildOld(specs)
+       newc := BuildNew(specs)
+
+       assertSame(t, oldc, newc, "GET", "/users/777", nil, true, "c-one")
+       assertSame(t, oldc, newc, "GET", syntax.multiPattern("12", "34"), nil, 
true, "c-two")
+       assertSame(t, oldc, newc, "GET", syntax.multiPattern("12", 
"34")+"/extra", nil, true, "c-pre")
+}
+
+func TestHeaderRegexWithRoutes(t *testing.T) {
+       specs := []RouteSpec{
+               {ID: "hdr", Methods: []string{"GET"}, Headers: 
[]HeaderSpec{{Name: "X-Env", Values: []string{"^prod|staging$"}, Regex: true}}, 
Cluster: "c-hdr"},
+               {ID: "pre", Methods: []string{"GET"}, Prefix: "/api/", Cluster: 
"c-pre"},
+       }
+       oldc := buildOld(specs)
+       newc := BuildNew(specs)
+
+       assertSame(t, oldc, newc, "GET", "/whatever", 
map[string]string{"X-Env": "prod"}, true, "c-hdr")
+       assertSame(t, oldc, newc, "GET", "/api/foo", map[string]string{"X-Env": 
"dev"}, true, "c-pre")
+}
+
+/* ==============================
+   random data fuzz test
+   ============================== */
+
+func TestParityRandomized(t *testing.T) {
+       syntax = colonSyntax()
+       const (
+               nRoutes           = 20000
+               nRequests         = 10000
+               prefixRatio       = 0.40
+               headerRatio       = 0.10
+               seed        int64 = 20250929
+       )
+
+       specs := genRandomSpecsWithVars(syntax, nRoutes, prefixRatio, 
headerRatio, seed)
+       oldc := buildOld(specs)
+       newc := BuildNew(specs)
+
+       reqs := genRandomRequests(nRequests, seed+1)
+       for i, req := range reqs {
+               ro, rn := call(oldc, newc, req.Method, req.URL.Path, 
headerFromReq(req))
+               if ro.ok != rn.ok || ro.cluster != rn.cluster {
+                       t.Fatalf("Randomized mismatch at #%d: %s %s old={ok:%v 
cluster:%q err:%q} new={ok:%v cluster:%q err:%q}",
+                               i, req.Method, req.URL.Path, ro.ok, ro.cluster, 
ro.err, rn.ok, rn.cluster, rn.err)
+               }
+       }
+}
+
+func headerFromReq(r *stdHttp.Request) map[string]string {
+       if len(r.Header) == 0 {
+               return nil
+       }
+       out := make(map[string]string)
+       for k, vs := range r.Header {
+               if len(vs) > 0 {
+                       out[k] = vs[0]
+               }
+       }
+       return out
+}
+
+/* ==============================
+   random data generation tools
+   ============================== */
+
+var syntax varSyntax
+
+func genRandomSpecsWithVars(s varSyntax, n int, prefixRatio, headerOnlyRatio 
float64, seed int64) []RouteSpec {
+       rnd := rand.New(rand.NewSource(seed))
+       out := make([]RouteSpec, 0, n)
+
+       nHeader := int(float64(n) * headerOnlyRatio)
+       nPrefix := int(float64(n-nHeader) * prefixRatio)
+       // preserve 20% for "variable path", the rest for exact path
+       nVars := int(float64(n) * 0.20)
+       nPath := n - nHeader - nPrefix - nVars
+       if nPath < 0 {
+               nPath = 0
+       }
+
+       // Header-only (regex + normal)
+       for i := 0; i < nHeader; i++ {
+               if i%5 == 0 {
+                       out = append(out, RouteSpec{
+                               ID:      "hdrx-" + strconv.Itoa(i),
+                               Methods: []string{"GET", "POST"},
+                               Headers: []HeaderSpec{{Name: "X-Trace", Values: 
[]string{"^pixiu-[0-9a-f]{8}$"}, Regex: true}},
+                               Cluster: "c-hx-" + strconv.Itoa(i),
+                       })
+               } else {
+                       out = append(out, RouteSpec{
+                               ID:      "hdr-" + strconv.Itoa(i),
+                               Methods: []string{"GET", "POST"},
+                               Headers: []HeaderSpec{{Name: "X-Env", Values: 
[]string{"prod"}, Regex: false}},
+                               Cluster: "c-h-" + strconv.Itoa(i),
+                       })
+               }
+       }
+
+       // Prefix
+       for i := 0; i < nPrefix; i++ {
+               base := "/api/v" + strconv.Itoa(1+rnd.Intn(3)) + "/svc" + 
strconv.Itoa(rnd.Intn(50)) + "/" // NOSONAR
+               out = append(out, RouteSpec{
+                       ID:      "pre-" + strconv.Itoa(i),
+                       Methods: []string{"GET", "POST"},
+                       Prefix:  base,
+                       Cluster: "c-p-" + strconv.Itoa(i),
+               })
+       }
+
+       // Variables
+       for i := 0; i < nVars; i++ {
+               if i%3 == 0 {
+                       out = append(out, RouteSpec{
+                               ID:      "var-" + strconv.Itoa(i),
+                               Methods: []string{"GET"},
+                               Path:    s.simplePattern("id"),
+                               Cluster: "c-v-" + strconv.Itoa(i),
+                       })
+               } else {
+                       out = append(out, RouteSpec{
+                               ID:      "var2-" + strconv.Itoa(i),
+                               Methods: []string{"GET"},
+                               Path:    s.multiPattern("a", "b"),
+                               Cluster: "c-v2-" + strconv.Itoa(i),
+                       })
+               }
+       }
+
+       // Exact Path
+       for i := 0; i < nPath; i++ {
+               out = append(out, RouteSpec{
+                       ID:      "pth-" + strconv.Itoa(i),
+                       Methods: []string{"GET"},
+                       Path:    "/api/v1/item/" + strconv.Itoa(i),
+                       Cluster: "c-x-" + strconv.Itoa(i),
+               })
+       }
+       return out
+}
+
+func genRandomRequests(n int, seed int64) []*stdHttp.Request {
+       rnd := rand.New(rand.NewSource(seed))
+       reqs := make([]*stdHttp.Request, 0, n)
+       methods := []string{"GET", "POST"}
+
+       for i := 0; i < n; i++ {
+               var path string
+               switch rnd.Intn(5) { // NOSONAR
+               case 0: // exact style
+                       path = "/api/v1/item/" + strconv.Itoa(rnd.Intn(50000)) 
// NOSONAR
+               case 1: // prefix style
+                       path = "/api/v" + strconv.Itoa(1+rnd.Intn(3)) + "/svc" 
+ strconv.Itoa(rnd.Intn(50)) + "/foo/bar" // NOSONAR
+               case 2: // var
+                       path = "/users/" + strconv.Itoa(1000+rnd.Intn(9000)) // 
NOSONAR
+               case 3: // var
+                       path = "/shops/" + strconv.Itoa(rnd.Intn(100)) + 
"/orders/" + strconv.Itoa(rnd.Intn(1000)) // NOSONAR
+               default:
+                       path = "/unknown/" + strconv.Itoa(rnd.Intn(100000)) // 
NOSONAR
+               }
+               req, _ := stdHttp.NewRequest(methods[rnd.Intn(len(methods))], 
path, nil) // NOSONAR
+               // header-only
+               switch rnd.Intn(7) { // NOSONAR
+               case 0:
+                       req.Header.Set("X-Env", "prod")
+               case 1:
+                       req.Header.Set("X-Trace", 
"pixiu-"+strconv.FormatInt(rnd.Int63()&0xffffffff, 16)) // NOSONAR
+               }
+               reqs = append(reqs, req)
+       }
+       return reqs
+}
+
+func newTestRouter(id, method, path, cluster string) *model.Router {
+       return &model.Router{
+               ID: id,
+               Match: model.RouterMatch{
+                       Methods: []string{method},
+                       Path:    path,
+               },
+               Route: model.RouteAction{
+                       Cluster: cluster,
+               },
+       }
+}
+
+func mustRouteCluster(t *testing.T, rm *RouterCoordinator, method, path 
string, want string) {
+       t.Helper()
+       req, err := stdHttp.NewRequest(method, path, nil)
+       if err != nil {
+               t.Fatalf("failed to build request: %v", err)
+       }
+       ctx := http.HttpContext{Request: req}
+       act, err := rm.Route(&ctx)
+       if err != nil {
+               t.Fatalf("unexpected route error for %s %s: %v", method, path, 
err)
+       }
+       if act == nil {
+               t.Fatalf("route action is nil for %s %s", method, path)
+       }
+       if act.Cluster != want {
+               t.Fatalf("unexpected cluster for %s %s, want %q, got %q", 
method, path, want, act.Cluster)
+       }
+}
+
+func mustRouteNotFound(t *testing.T, rm *RouterCoordinator, method, path 
string) {
+       t.Helper()
+       req, err := stdHttp.NewRequest(method, path, nil)
+       if err != nil {
+               t.Fatalf("failed to build request: %v", err)
+       }
+       ctx := http.HttpContext{Request: req}
+       act, err := rm.Route(&ctx)
+       if err == nil {
+               t.Fatalf("expected error for %s %s, got nil (action=%#v)", 
method, path, act)
+       }
+}
+
+func mustRouteByPathAndNameCluster(t *testing.T, rm *RouterCoordinator, 
method, path string, want string) {
+       t.Helper()
+       act, err := rm.RouteByPathAndName(path, method)
+       if err != nil {
+               t.Fatalf("unexpected RouteByPathAndName error for %s %s: %v", 
method, path, err)
+       }
+       if act == nil {
+               t.Fatalf("route action is nil for %s %s", method, path)
+       }
+       if act.Cluster != want {
+               t.Fatalf("unexpected cluster for %s %s, want %q, got %q", 
method, path, want, act.Cluster)
+       }
+}
+
+func mustRouteByPathAndNameNotFound(t *testing.T, rm *RouterCoordinator, 
method, path string) {
+       t.Helper()
+       act, err := rm.RouteByPathAndName(path, method)
+       if err == nil {
+               t.Fatalf("expected RouteByPathAndName error for %s %s, got nil 
(action=%#v)", method, path, act)
+       }
+}
+
+func TestCreateRouterCoordinatorInitialSnapshot(t *testing.T) {
+       r1 := newTestRouter("r1", "GET", "/foo", "cluster-foo")
+       r2 := newTestRouter("r2", "GET", "/bar", "cluster-bar")
+
+       cfg := &model.RouteConfiguration{
+               Routes:  []*model.Router{r1, r2},
+               Dynamic: false,
+       }
+
+       rm := CreateRouterCoordinator(cfg)
+       rm.debounce = 0
+
+       mustRouteCluster(t, rm, "GET", "/foo", "cluster-foo")
+       mustRouteCluster(t, rm, "GET", "/bar", "cluster-bar")
+
+       mustRouteByPathAndNameCluster(t, rm, "GET", "/foo", "cluster-foo")
+       mustRouteByPathAndNameCluster(t, rm, "GET", "/bar", "cluster-bar")
+}
+
+// TestRouterCoordinatorOnAddRouterUpdatesSnapshot 验证 Add 后新路由生效,旧路由保持
+func TestRouterCoordinatorOnAddRouterUpdatesSnapshot(t *testing.T) {
+       r1 := newTestRouter("r1", "GET", "/foo", "cluster-foo")
+
+       cfg := &model.RouteConfiguration{
+               Routes:  []*model.Router{r1},
+               Dynamic: false,
+       }
+
+       rm := CreateRouterCoordinator(cfg)
+       rm.debounce = 0
+
+       // only /foo
+       mustRouteCluster(t, rm, "GET", "/foo", "cluster-foo")
+       mustRouteNotFound(t, rm, "GET", "/bar")
+       mustRouteByPathAndNameCluster(t, rm, "GET", "/foo", "cluster-foo")
+       mustRouteByPathAndNameNotFound(t, rm, "GET", "/bar")
+
+       // Add /bar
+       r2 := newTestRouter("r2", "GET", "/bar", "cluster-bar")
+       rm.OnAddRouter(r2)
+
+       // /foo available
+       mustRouteCluster(t, rm, "GET", "/foo", "cluster-foo")
+       mustRouteByPathAndNameCluster(t, rm, "GET", "/foo", "cluster-foo")
+
+       // /bar available
+       mustRouteCluster(t, rm, "GET", "/bar", "cluster-bar")
+       mustRouteByPathAndNameCluster(t, rm, "GET", "/bar", "cluster-bar")
+}
+
+// TestRouterCoordinatorOnDeleteRouterUpdatesSnapshot 验证 Delete 后路由失效
+func TestRouterCoordinatorOnDeleteRouterUpdatesSnapshot(t *testing.T) {
+       r1 := newTestRouter("r1", "GET", "/foo", "cluster-foo")
+       r2 := newTestRouter("r2", "GET", "/bar", "cluster-bar")
+
+       cfg := &model.RouteConfiguration{
+               Routes:  []*model.Router{r1, r2},
+               Dynamic: false,
+       }
+
+       rm := CreateRouterCoordinator(cfg)
+       rm.debounce = 0
+
+       // /foo、/bar available
+       mustRouteCluster(t, rm, "GET", "/foo", "cluster-foo")
+       mustRouteCluster(t, rm, "GET", "/bar", "cluster-bar")
+       mustRouteByPathAndNameCluster(t, rm, "GET", "/foo", "cluster-foo")
+       mustRouteByPathAndNameCluster(t, rm, "GET", "/bar", "cluster-bar")
+
+       // delete /bar
+       rm.OnDeleteRouter(r2)
+
+       // /foo available
+       mustRouteCluster(t, rm, "GET", "/foo", "cluster-foo")
+       mustRouteByPathAndNameCluster(t, rm, "GET", "/foo", "cluster-foo")
+
+       // /bar fail
+       mustRouteNotFound(t, rm, "GET", "/bar")
+       mustRouteByPathAndNameNotFound(t, rm, "GET", "/bar")
+}
+
+func TestOldVsNew_OnAddRouterWithSameID(t *testing.T) {
+       base := []*model.Router{
+               {
+                       ID: "r1",
+                       Match: model.RouterMatch{
+                               Methods: []string{"POST"},
+                               Path:    "/api/v1/item/711",
+                       },
+                       Route: model.RouteAction{Cluster: "c-x-r1"},
+               },
+       }
+
+       oldc := buildOldCoordinator(base)
+       newc := buildNewCoordinator(base)
+       newc.debounce = 0
+
+       {
+               req, _ := stdHttp.NewRequest("POST", "/api/v1/item/711", nil)
+               ctxOld := http.HttpContext{Request: req}
+               ctxNew := http.HttpContext{Request: req}
+
+               oldRes, oldErr := oldc.Route(&ctxOld)
+               newRes, newErr := newc.Route(&ctxNew)
+
+               if oldErr != nil || newErr != nil {
+                       t.Fatalf("initial route error: oldErr=%v newErr=%v", 
oldErr, newErr)
+               }
+               if oldRes.Cluster != "c-x-r1" || newRes.Cluster != "c-x-r1" {
+                       t.Fatalf("initial cluster mismatch: old=%v new=%v", 
oldRes, newRes)
+               }
+       }
+
+       delta := &model.Router{
+               ID: "r1",
+               Match: model.RouterMatch{
+                       Methods: []string{"POST"},
+                       Path:    "/api/v1/item/999999",
+               },
+               Route: model.RouteAction{Cluster: "c-x-r1"},
+       }
+
+       oldc.OnAddRouter(delta)
+       newc.OnAddRouter(delta)
+
+       {
+               req, _ := stdHttp.NewRequest("POST", "/api/v1/item/711", nil)
+               ctxOld := http.HttpContext{Request: req}
+               ctxNew := http.HttpContext{Request: req}
+
+               oldRes, oldErr := oldc.Route(&ctxOld)
+               newRes, newErr := newc.Route(&ctxNew)
+
+               assert.Equal(t, oldRes, newRes)
+               assert.Equal(t, oldErr, newErr)
+       }
+
+       {
+               req, _ := stdHttp.NewRequest("POST", "/api/v1/item/999999", nil)
+               ctxOld := http.HttpContext{Request: req}
+               ctxNew := http.HttpContext{Request: req}
+
+               oldRes, oldErr := oldc.Route(&ctxOld)
+               newRes, newErr := newc.Route(&ctxNew)
+
+               assert.Equal(t, oldRes, newRes)
+               assert.Equal(t, oldErr, newErr)
+       }
+}
diff --git a/pkg/common/util/stringutil/stringutil.go 
b/pkg/common/util/stringutil/stringutil.go
index 23ba5eaf..405d0dec 100644
--- a/pkg/common/util/stringutil/stringutil.go
+++ b/pkg/common/util/stringutil/stringutil.go
@@ -59,32 +59,53 @@ func IsPathVariableOrWildcard(key string) bool {
 
 // IsWildcard return if is *
 func IsWildcard(key string) bool {
-       return key == "*"
+       return key == constant.HeaderValueAll
 }
 
+// IsMatchAll return if is **
 func IsMatchAll(key string) bool {
-       return key == "**"
+       return key == constant.HeaderValueAllLevels
 }
 
 func GetTrieKey(method string, path string) string {
+       // "http://localhost:8882/api/v1/test-dubbo/user?name=tc/";
        ret := ""
-       //"http://localhost:8882/api/v1/test-dubbo/user?name=tc";
+
        if strings.Contains(path, constant.ProtocolSlash) {
                path = path[strings.Index(path, 
constant.ProtocolSlash)+len(constant.ProtocolSlash):]
                path = path[strings.Index(path, constant.PathSlash)+1:]
        }
+       // "api/v1/test-dubbo/user?name=tc/"
+
        if strings.HasPrefix(path, constant.PathSlash) {
                ret = method + path
        } else {
                ret = method + constant.PathSlash + path
        }
+       // "METHOD/api/v1/test-dubbo/user?name=tc/"
+
        if strings.HasSuffix(ret, constant.PathSlash) {
                ret = ret[0 : len(ret)-1]
        }
+       // "METHOD/api/v1/test-dubbo/user?name=tc"
+
        ret = strings.Split(ret, "?")[0]
+       // "METHOD/api/v1/test-dubbo/user"
+
        return ret
 }
 
+func GetTrieKeyWithPrefix(method, path, prefix string, isPrefix bool) string {
+       if isPrefix {
+               if prefix != "" && prefix[len(prefix)-1] != '/' {
+                       prefix += constant.PathSlash
+               }
+               prefix += constant.HeaderValueAllLevels
+               return GetTrieKey(method, prefix)
+       }
+       return GetTrieKey(method, path)
+}
+
 func GetIPAndPort(address string) ([]*net.TCPAddr, error) {
        if len(address) <= 0 {
                return nil, errors.Errorf("invalid address, %s", address)
diff --git a/pkg/model/router.go b/pkg/model/router.go
index d3e7fadb..ff4f6a9e 100644
--- a/pkg/model/router.go
+++ b/pkg/model/router.go
@@ -97,7 +97,7 @@ func (rc *RouteConfiguration) Route(req *stdHttp.Request) 
(*RouteAction, error)
        return rc.RouteByPathAndMethod(req.URL.Path, req.Method)
 }
 
-// MatchHeader used when there's only headers to match
+// MatchHeader used when there are only headers to match
 func (rm *RouterMatch) MatchHeader(req *stdHttp.Request) bool {
        if len(rm.Methods) > 0 {
                for _, method := range rm.Methods {
@@ -146,7 +146,12 @@ func (hm *HeaderMatcher) SetValueRegex(regex string) error 
{
 func (r *Router) String() string {
        var builder strings.Builder
        builder.WriteString("[" + strings.Join(r.Match.Methods, ",") + "] ")
-       if r.Match.Prefix != "" {
+       if r.Match.Path == "" && r.Match.Prefix == "" && len(r.Match.Headers) > 
0 {
+               builder.WriteString("headers ")
+               for _, h := range r.Match.Headers {
+                       builder.WriteString(h.Name + "=" + 
strings.Join(h.Values, "|"))
+               }
+       } else if r.Match.Prefix != "" {
                builder.WriteString("prefix " + r.Match.Prefix)
        } else {
                builder.WriteString("path " + r.Match.Path)
diff --git a/pkg/model/router_snapshot.go b/pkg/model/router_snapshot.go
new file mode 100644
index 00000000..bae890cd
--- /dev/null
+++ b/pkg/model/router_snapshot.go
@@ -0,0 +1,181 @@
+/*
+ * 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 model
+
+import (
+       "regexp"
+       "sync"
+)
+
+import (
+       "github.com/apache/dubbo-go-pixiu/pkg/common/router/trie"
+       "github.com/apache/dubbo-go-pixiu/pkg/common/util/stringutil"
+       "github.com/apache/dubbo-go-pixiu/pkg/logger"
+)
+
+var (
+       constMethods = []string{"GET", "POST", "PUT", "DELETE", "PATCH", 
"OPTIONS", "HEAD"}
+)
+
+// RouteSnapshot Read-only snapshot for routing
+type RouteSnapshot struct {
+       // multi-trie for each method, built once and read-only
+       MethodTries map[string]*trie.Trie
+
+       // precompiled regex for header-only routes
+       HeaderOnly []HeaderRoute
+}
+
+type HeaderRoute struct {
+       Methods []string
+       Headers []CompiledHeader
+       Action  RouteAction
+}
+
+type CompiledHeader struct {
+       Name   string
+       Regex  *regexp.Regexp
+       Values []string
+}
+
+func MethodAllowed(methods []string, m string) bool {
+       if len(methods) == 0 {
+               return true
+       }
+       for _, x := range methods {
+               if x == m {
+                       return true
+               }
+       }
+       return false
+}
+
+var regexCache sync.Map // map[string]*regexp.Regexp
+
+func getRegexpWithCache(pat string) *regexp.Regexp {
+       if v, ok := regexCache.Load(pat); ok {
+               return v.(*regexp.Regexp)
+       }
+       // Compile fail return nil (caller will ignore this regex)
+       re, err := regexp.Compile(pat)
+       if err != nil {
+               return nil
+       }
+       if v, ok := regexCache.LoadOrStore(pat, re); ok {
+               return v.(*regexp.Regexp)
+       }
+       return re
+}
+
+// compiledHeaderSlicePool is a pool for temporary []CompiledHeader slices 
during snapshot building
+var compiledHeaderSlicePool = sync.Pool{
+       New: func() any {
+               s := make([]CompiledHeader, 0, 4) // start with small capacity, 
grow as needed
+               return &s
+       },
+}
+
+func ToSnapshot(routes []*Router) *RouteSnapshot {
+       s := &RouteSnapshot{
+               MethodTries: make(map[string]*trie.Trie, 8),
+       }
+
+       // pre-scan header-only routes count
+       headerOnlyCount := 0
+       for _, r := range routes {
+               if r.Match.Path == "" && r.Match.Prefix == "" && 
len(r.Match.Headers) > 0 {
+                       headerOnlyCount++
+               }
+       }
+
+       if headerOnlyCount > 0 {
+               s.HeaderOnly = make([]HeaderRoute, 0, headerOnlyCount)
+       }
+
+       // part to get or create trie for a method
+       getTrie := func(m string) *trie.Trie {
+               if t := s.MethodTries[m]; t != nil {
+                       return t
+               }
+               nt := trie.NewTrie()
+               s.MethodTries[m] = &nt
+               return &nt
+       }
+
+       for _, r := range routes {
+               // A) header-only:with Headers, without Path / Prefix
+               if r.Match.Path == "" && r.Match.Prefix == "" && 
len(r.Match.Headers) > 0 {
+                       hr := HeaderRoute{
+                               Methods: r.Match.Methods,
+                               Action:  r.Route,
+                       }
+
+                       // use temporary slice from pool to build compiled 
headers
+                       chPtr := 
compiledHeaderSlicePool.Get().(*[]CompiledHeader)
+                       ch := (*chPtr)[:0] // reset
+
+                       for _, h := range r.Match.Headers {
+                               c := CompiledHeader{Name: h.Name}
+                               if h.Regex {
+                                       // 1) the model already has compiled 
regex (if any) → use it directly
+                                       if h.valueRE != nil {
+                                               c.Regex = h.valueRE
+                                       } else if len(h.Values) > 0 && 
h.Values[0] != "" {
+                                               // 2) else use global 
cache/compile (cross-snapshot reuse)
+                                               if re := 
getRegexpWithCache(h.Values[0]); re != nil {
+                                                       c.Regex = re
+                                               } else {
+                                                       // invalid regex → skip 
this header matcher
+                                                       logger.Errorf("Header 
regex compiled fail for %v", h.Values[0])
+                                                       continue
+                                               }
+                                       }
+                               } else {
+                                       // not regex → copy values directly (if 
any)
+                                       if len(h.Values) > 0 {
+                                               // direct assignment is ok here 
(string slice)
+                                               c.Values = append(c.Values, 
h.Values...)
+                                       }
+                               }
+                               ch = append(ch, c)
+                       }
+
+                       // move the temporary slice content to snapshot 
(ownership transferred)
+                       hr.Headers = make([]CompiledHeader, len(ch))
+                       copy(hr.Headers, ch)
+
+                       // reset and put back the temporary slice to pool
+                       *chPtr = (*chPtr)[:0]
+                       compiledHeaderSlicePool.Put(chPtr)
+
+                       s.HeaderOnly = append(s.HeaderOnly, hr)
+                       continue
+               }
+
+               // B) Trie
+               methods := r.Match.Methods
+               if len(methods) == 0 {
+                       methods = constMethods
+               }
+               for _, m := range methods {
+                       t := getTrie(m)
+                       t.Put(stringutil.GetTrieKeyWithPrefix(m, r.Match.Path, 
r.Match.Prefix, r.Match.Prefix != ""), r.Route)
+               }
+       }
+       return s
+}

Reply via email to