fix: streamline route matcher backtracking

Avoid rebuilding skipped-node state during wildcard fallback so the matcher no longer loops on the same static branch and stops allocating on the hot path. Add focused route benchmarks and regression coverage to keep the optimized path stable.
This commit is contained in:
wjqserver 2026-04-07 08:27:00 +08:00
parent b1ce4d584e
commit 6acac9edce
5 changed files with 240 additions and 41 deletions

View file

@ -11,6 +11,7 @@ import (
"regexp"
"strings"
"testing"
"time"
)
// Used as a workaround since we can't compare functions or their addresses
@ -39,6 +40,23 @@ func getSkippedNodes() *[]skippedNode {
return &ps
}
func getValueWithTimeout(t *testing.T, tree *node, path string, unescape bool) nodeValue {
t.Helper()
resultCh := make(chan nodeValue, 1)
go func() {
resultCh <- tree.getValue(path, getParams(), getSkippedNodes(), unescape)
}()
select {
case value := <-resultCh:
return value
case <-time.After(2 * time.Second):
t.Fatalf("lookup for path %q timed out, likely stuck in backtracking", path)
return nodeValue{}
}
}
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
unescape := false
if len(unescapes) >= 1 {
@ -1104,3 +1122,51 @@ func TestComplexBacktrackingWithCatchAll(t *testing.T) {
t.Errorf("处理路径 '%s' 时参数不匹配: \n 得到: %v\n 想要: %v", reqPath, *value.params, wantParams)
}
}
func TestBacktrackingFallsThroughToWildcardBranch(t *testing.T) {
tests := []struct {
name string
routes []string
requestPath string
wantFullPath string
wantParams Params
}{
{
name: "param route after static dead end",
routes: []string{"/foo/bar", "/foo/:id/details"},
requestPath: "/foo/bar/details",
wantFullPath: "/foo/:id/details",
wantParams: Params{{Key: "id", Value: "bar"}},
},
{
name: "catch-all route after static dead end",
routes: []string{"/foo/bar", "/foo/:id/*rest"},
requestPath: "/foo/bar/baz.txt",
wantFullPath: "/foo/:id/*rest",
wantParams: Params{
{Key: "id", Value: "bar"},
{Key: "rest", Value: "/baz.txt"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tree := &node{}
for _, route := range tt.routes {
tree.addRoute(route, fakeHandler(route))
}
value := getValueWithTimeout(t, tree, tt.requestPath, false)
if value.handlers == nil {
t.Fatalf("expected handlers for %q", tt.requestPath)
}
if value.fullPath != tt.wantFullPath {
t.Fatalf("expected full path %q for %q, got %q", tt.wantFullPath, tt.requestPath, value.fullPath)
}
if value.params == nil || !reflect.DeepEqual(*value.params, tt.wantParams) {
t.Fatalf("expected params %v for %q, got %v", tt.wantParams, tt.requestPath, value.params)
}
})
}
}