Skip to content

Commit 6f9b71c

Browse files
aldasstffabi
andauthored
Poc router stack backtracking (#1791)
Router: PoC stack based backtracking Co-authored-by: stffabi <[email protected]>
1 parent b2444d8 commit 6f9b71c

File tree

2 files changed

+243
-127
lines changed

2 files changed

+243
-127
lines changed

router.go

+77-117
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package echo
22

33
import (
44
"net/http"
5-
"strings"
65
)
76

87
type (
@@ -334,21 +333,48 @@ func (r *Router) Find(method, path string, c Context) {
334333
cn := r.tree // Current node as root
335334

336335
var (
337-
search = path
338-
child *node // Child node
339-
n int // Param counter
340-
nk kind // Next kind
341-
nn *node // Next node
342-
ns string // Next search
343-
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
336+
search = path
337+
searchIndex = 0
338+
n int // Param counter
339+
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
344340
)
345341

346-
// Search order static > param > any
347-
for {
348-
if search == "" {
349-
break
342+
// Backtracking is needed when a dead end (leaf node) is reached in the router tree.
343+
// To backtrack the current node will be changed to the parent node and the next kind for the
344+
// router logic will be returned based on fromKind or kind of the dead end node (static > param > any).
345+
// For example if there is no static node match we should check parent next sibling by kind (param).
346+
// Backtracking itself does not check if there is a next sibling, this is done by the router logic.
347+
backtrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) {
348+
previous := cn
349+
cn = previous.parent
350+
valid = cn != nil
351+
352+
// Next node type by priority
353+
// NOTE: With the current implementation we never backtrack from an `any` route, so `previous.kind` is
354+
// always `static` or `any`
355+
// If this is changed then for any route next kind would be `static` and this statement should be changed
356+
nextNodeKind = previous.kind + 1
357+
358+
if fromKind == skind {
359+
// when backtracking is done from static kind block we did not change search so nothing to restore
360+
return
350361
}
351362

363+
// restore search to value it was before we move to current node we are backtracking from.
364+
if previous.kind == skind {
365+
searchIndex -= len(previous.prefix)
366+
} else {
367+
n--
368+
// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
369+
// for that index as it would also contain part of path we cut off before moving into node we are backtracking from
370+
searchIndex -= len(pvalues[n])
371+
}
372+
search = path[searchIndex:]
373+
return
374+
}
375+
376+
// Search order static > param > any
377+
for {
352378
pl := 0 // Prefix length
353379
l := 0 // LCP length
354380

@@ -365,148 +391,82 @@ func (r *Router) Find(method, path string, c Context) {
365391
}
366392
}
367393

368-
if l == pl {
369-
// Continue search
370-
search = search[l:]
371-
// Finish routing if no remaining search and we are on an leaf node
372-
if search == "" && (nn == nil || cn.parent == nil || cn.ppath != "") {
373-
break
374-
}
375-
// Handle special case of trailing slash route with existing any route (see #1526)
376-
if search == "" && path[len(path)-1] == '/' && cn.anyChildren != nil {
377-
goto Any
394+
if l != pl {
395+
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
396+
nk, ok := backtrackToNextNodeKind(skind)
397+
if !ok {
398+
return // No other possibilities on the decision path
399+
} else if nk == pkind {
400+
goto Param
401+
// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently
402+
//} else if nk == akind {
403+
// goto Any
404+
} else {
405+
// Not found (this should never be possible for static node we are looking currently)
406+
return
378407
}
379408
}
380409

381-
// Attempt to go back up the tree on no matching prefix or no remaining search
382-
if l != pl || search == "" {
383-
if nn == nil { // Issue #1348
384-
return // Not found
385-
}
386-
cn = nn
387-
search = ns
388-
if nk == pkind {
389-
goto Param
390-
} else if nk == akind {
391-
goto Any
392-
}
410+
// The full prefix has matched, remove the prefix from the remaining search
411+
search = search[l:]
412+
searchIndex = searchIndex + l
413+
414+
// Finish routing if no remaining search and we are on an leaf node
415+
if search == "" && cn.ppath != "" {
416+
break
393417
}
394418

395419
// Static node
396-
if child = cn.findStaticChild(search[0]); child != nil {
397-
// Save next
398-
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
399-
nk = pkind
400-
nn = cn
401-
ns = search
420+
if search != "" {
421+
if child := cn.findStaticChild(search[0]); child != nil {
422+
cn = child
423+
continue
402424
}
403-
cn = child
404-
continue
405425
}
406426

407427
Param:
408428
// Param node
409-
if child = cn.paramChildren; child != nil {
410-
// Issue #378
411-
if len(pvalues) == n {
412-
continue
413-
}
414-
415-
// Save next
416-
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
417-
nk = akind
418-
nn = cn
419-
ns = search
420-
}
421-
429+
if child := cn.paramChildren; search != "" && child != nil {
422430
cn = child
423431
i, l := 0, len(search)
424432
for ; i < l && search[i] != '/'; i++ {
425433
}
426434
pvalues[n] = search[:i]
427435
n++
428436
search = search[i:]
437+
searchIndex = searchIndex + i
429438
continue
430439
}
431440

432441
Any:
433442
// Any node
434-
if cn = cn.anyChildren; cn != nil {
443+
if child := cn.anyChildren; child != nil {
435444
// If any node is found, use remaining path for pvalues
445+
cn = child
436446
pvalues[len(cn.pnames)-1] = search
437447
break
438448
}
439449

440-
// No node found, continue at stored next node
441-
// or find nearest "any" route
442-
if nn != nil {
443-
// No next node to go down in routing (issue #954)
444-
// Find nearest "any" route going up the routing tree
445-
search = ns
446-
np := nn.parent
447-
// Consider param route one level up only
448-
if cn = nn.paramChildren; cn != nil {
449-
pos := strings.IndexByte(ns, '/')
450-
if pos == -1 {
451-
// If no slash is remaining in search string set param value
452-
if len(cn.pnames) > 0 {
453-
pvalues[len(cn.pnames)-1] = search
454-
}
455-
break
456-
} else if pos > 0 {
457-
// Otherwise continue route processing with restored next node
458-
cn = nn
459-
nn = nil
460-
ns = ""
461-
goto Param
462-
}
463-
}
464-
// No param route found, try to resolve nearest any route
465-
for {
466-
np = nn.parent
467-
if cn = nn.anyChildren; cn != nil {
468-
break
469-
}
470-
if np == nil {
471-
break // no further parent nodes in tree, abort
472-
}
473-
var str strings.Builder
474-
str.WriteString(nn.prefix)
475-
str.WriteString(search)
476-
search = str.String()
477-
nn = np
478-
}
479-
if cn != nil { // use the found "any" route and update path
480-
pvalues[len(cn.pnames)-1] = search
481-
break
482-
}
450+
// Let's backtrack to the first possible alternative node of the decision path
451+
nk, ok := backtrackToNextNodeKind(akind)
452+
if !ok {
453+
return // No other possibilities on the decision path
454+
} else if nk == pkind {
455+
goto Param
456+
} else if nk == akind {
457+
goto Any
458+
} else {
459+
// Not found
460+
return
483461
}
484-
return // Not found
485-
486462
}
487463

488464
ctx.handler = cn.findHandler(method)
489465
ctx.path = cn.ppath
490466
ctx.pnames = cn.pnames
491467

492-
// NOTE: Slow zone...
493468
if ctx.handler == nil {
494469
ctx.handler = cn.checkMethodNotAllowed()
495-
496-
// Dig further for any, might have an empty value for *, e.g.
497-
// serving a directory. Issue #207.
498-
if cn = cn.anyChildren; cn == nil {
499-
return
500-
}
501-
if h := cn.findHandler(method); h != nil {
502-
ctx.handler = h
503-
} else {
504-
ctx.handler = cn.checkMethodNotAllowed()
505-
}
506-
ctx.path = cn.ppath
507-
ctx.pnames = cn.pnames
508-
pvalues[len(cn.pnames)-1] = ""
509470
}
510-
511471
return
512472
}

0 commit comments

Comments
 (0)