Skip to content

Commit

Permalink
Switch to Cages for dynamic routing
Browse files Browse the repository at this point in the history
  • Loading branch information
kalverra committed Jan 29, 2025
1 parent 526aa33 commit d7086e6
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 144 deletions.
17 changes: 15 additions & 2 deletions parrot/.changeset/v0.2.0.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Added wildcard routing support
- Added wildcard routing support
- Improved routing performance

### Bench before

Expand All @@ -14,4 +15,16 @@ BenchmarkSave-14 6358 178122 ns/op
BenchmarkLoad-14 1411 852377 ns/op
```

### Bench After
### Bench After

```sh
go test -testLogLevel="" -bench=. -run=^$ ./...
goos: darwin
goarch: arm64
pkg: github.com/smartcontractkit/chainlink-testing-framework/parrot
cpu: Apple M3 Max
BenchmarkRegisterRoute-14 3647503 313.8 ns/op
BenchmarkRouteResponse-14 19143 62011 ns/op
BenchmarkSave-14 5244 218697 ns/op
BenchmarkLoad-14 1101 1049399 ns/op
```
245 changes: 159 additions & 86 deletions parrot/cage.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,85 +9,94 @@ import (

// Cage is a container for all routes and sub-cages to handle routing and wildcard matching for a parrot
// Note: Should only be used internally by the parrot.
type Cage struct {
*CageLevel
type cage struct {
*cageLevel
}

// MethodAny will match to any other method
const MethodAny = "ANY"

// CageLevel holds a single level of routes and further sub cages
// Note: Should only be used internally by the parrot.
type CageLevel struct {
// Routes contains all of the plain routes at this current cage level
// path -> route
Routes map[string]*Route `json:"routes"`
routesRWMu sync.RWMutex // sync.Map might be better here, but eh
// WildCardRoutes contains all the wildcard routes at this current cage level
// path -> route
WildCardRoutes map[string]*Route `json:"wild_card_routes"`
wildCardRoutesRWMu sync.RWMutex
// SubCages contains sub cages at this current cage level
type cageLevel struct {
// cagePath is the path to this cage level
cagePath string
// rwMu is a read write mutex for the cage level
rwMu sync.RWMutex
// TODO: Make all lowercase
// routes contains all of the plain routes at this current cage level
// route.path -> route.method -> route
routes map[string]map[string]*Route
// wildCardRoutes contains all the wildcard routes at this current cage level
// route.path -> route.method -> route
wildCardRoutes map[string]map[string]*Route
// subCages contains sub cages at this current cage level
// cage name -> cage level
SubCages map[string]*CageLevel `json:"sub_cages"`
subCagesRWMu sync.RWMutex
// WildCardSubCages contains wildcard sub cages at this current cage level
subCages map[string]*cageLevel
// wildCardSubCages contains wildcard sub cages at this current cage level
// cage name -> cage level
WildCardSubCages map[string]*CageLevel `json:"wild_card_sub_cages"`
wildCardSubCagesRWMu sync.RWMutex
wildCardSubCages map[string]*cageLevel
}

// newCage creates a new cage with an empty cage level for a new parrot instance
func newCage() *Cage {
return &Cage{
CageLevel: newCageLevel(),
func newCage() *cage {
return &cage{
cageLevel: newCageLevel("/"),
}
}

// newCageLevel creates a new cageLevel with empty maps
func newCageLevel() *CageLevel {
return &CageLevel{
Routes: make(map[string]*Route),
WildCardRoutes: make(map[string]*Route),
SubCages: make(map[string]*CageLevel),
WildCardSubCages: make(map[string]*CageLevel),
func newCageLevel(cagePath string) *cageLevel {
return &cageLevel{
cagePath: cagePath,
routes: make(map[string]map[string]*Route),
wildCardRoutes: make(map[string]map[string]*Route),
subCages: make(map[string]*cageLevel),
wildCardSubCages: make(map[string]*cageLevel),
}
}

// cageLevel searches for a cage level based on the path provided
// If createMode is true, it will create any cage levels if they don't exist
func (c *Cage) cageLevel(path string, createMode bool) (cageLevel *CageLevel, routeSegment string, err error) {
func (c *cage) getCageLevel(path string, createMode bool) (cageLevel *cageLevel, err error) {
splitPath := strings.Split(path, "/")
routeSegment = splitPath[len(splitPath)-1] // The final path segment is the route
splitPath = splitPath[:len(splitPath)-1] // Only looking for the cage level, exclude the route
splitPath = splitPath[:len(splitPath)-1] // Only looking for the cage level, exclude the route
if splitPath[0] == "" {
splitPath = splitPath[1:] // Remove the empty string at the beginning of the split
}
currentCageLevel := c.CageLevel
currentCageLevel := c.cageLevel

for _, pathSegment := range splitPath { // Iterate through each path segment to look for matches
cageLevel, found, err := currentCageLevel.subCageLevel(pathSegment, createMode)
if err != nil {
return nil, routeSegment, err
return nil, err
}
if found {
currentCageLevel = cageLevel
continue
}

if !found {
return nil, routeSegment, newDynamicError(ErrCageNotFound, fmt.Sprintf("path: '%s'", path))
return nil, newDynamicError(ErrCageNotFound, fmt.Sprintf("path: '%s'", path))
}
}

return currentCageLevel, routeSegment, nil
return currentCageLevel, nil
}

// getRoute searches for a route based on the path provided
func (c *Cage) getRoute(path string) (*Route, error) {
cageLevel, routeSegment, err := c.cageLevel(path, false)
func (c *cage) getRoute(routePath, routeMethod string) (*Route, error) {
cageLevel, err := c.getCageLevel(routePath, false)
if err != nil {
return nil, err
}
routeSegments := strings.Split(routePath, "/")
if len(routeSegments) == 0 {
return nil, ErrRouteNotFound
}
routeSegment := routeSegments[len(routeSegments)-1]

route, found, err := cageLevel.route(routeSegment)
route, found, err := cageLevel.route(routeSegment, routeMethod)
if err != nil {
return nil, err
}
Expand All @@ -99,65 +108,116 @@ func (c *Cage) getRoute(path string) (*Route, error) {
}

// newRoute creates a new route, creating new cages if necessary
func (c *Cage) newRoute(route *Route) error {
cageLevel, routeSegment, err := c.cageLevel(route.Path, true)
func (c *cage) newRoute(route *Route) error {
cageLevel, err := c.getCageLevel(route.Path, true)
if err != nil {
return err
}

if strings.Contains(routeSegment, "*") {
cageLevel.wildCardRoutesRWMu.Lock()
defer cageLevel.wildCardRoutesRWMu.Unlock()
cageLevel.WildCardRoutes[routeSegment] = route
} else {
cageLevel.routesRWMu.Lock()
defer cageLevel.routesRWMu.Unlock()
cageLevel.Routes[routeSegment] = route
}
cageLevel.newRoute(route)

return nil
}

// deleteRoute deletes a route based on the path provided
func (c *Cage) deleteRoute(route *Route) error {
cageLevel, routeSegment, err := c.cageLevel(route.Path, true)
// deleteRoute deletes a route
func (c *cage) deleteRoute(route *Route) error {
cageLevel, err := c.getCageLevel(route.Path, false)
if err != nil {
return err
}

if strings.Contains(routeSegment, "*") {
cageLevel.wildCardRoutesRWMu.Lock()
defer cageLevel.wildCardRoutesRWMu.Unlock()
delete(cageLevel.WildCardRoutes, routeSegment)
if strings.Contains(route.Segment(), "*") {
cageLevel.rwMu.RLock()
if _, found := cageLevel.wildCardRoutes[route.Segment()][route.Method]; !found {
cageLevel.rwMu.RUnlock()
return ErrRouteNotFound
}
cageLevel.rwMu.RUnlock()

cageLevel.rwMu.Lock()
delete(cageLevel.wildCardRoutes[route.Segment()], route.Method)
cageLevel.rwMu.Unlock()
} else {
cageLevel.routesRWMu.Lock()
defer cageLevel.routesRWMu.Unlock()
delete(cageLevel.Routes, routeSegment)
cageLevel.rwMu.RLock()
if _, found := cageLevel.routes[route.Segment()][route.Method]; !found {
cageLevel.rwMu.RUnlock()
return ErrRouteNotFound
}
cageLevel.rwMu.RUnlock()

cageLevel.rwMu.Lock()
delete(cageLevel.routes[route.Segment()], route.Method)
cageLevel.rwMu.Unlock()
}

return nil
}

// routes returns all the routes in the cage
func (c *cage) routes() []*Route {
return c.routesRecursive()
}

// routesRecursive returns all the routes in the cage recursively.
// Should only be used internally by the cage. Use routes() instead.
func (cl *cageLevel) routesRecursive() (routes []*Route) {
// Add all the routes at this level
cl.rwMu.RLock()
for _, routePath := range cl.routes {
for _, route := range routePath {
routes = append(routes, route)
}
}

// Add all the wildcard routes at this level
for _, routePath := range cl.wildCardRoutes {
for _, route := range routePath {
routes = append(routes, route)
}
}
cl.rwMu.RUnlock()

for _, subCage := range cl.subCages {
routes = append(routes, subCage.routesRecursive()...)
}
for _, subCage := range cl.wildCardSubCages {
routes = append(routes, subCage.routesRecursive()...)
}

return routes
}

// route searches for a route based on the route segment provided
func (cl *CageLevel) route(routeSegment string) (route *Route, found bool, err error) {
func (cl *cageLevel) route(routeSegment, routeMethod string) (route *Route, found bool, err error) {
// First check for an exact match
cl.routesRWMu.Lock()
if route, found = cl.Routes[routeSegment]; found {
defer cl.routesRWMu.Unlock()
return route, true, nil
cl.rwMu.RLock()
defer cl.rwMu.RUnlock()

if _, ok := cl.routes[routeSegment]; ok {
if route, found = cl.routes[routeSegment][routeMethod]; found {
return route, true, nil
}
}
if _, ok := cl.wildCardRoutes[routeSegment]; ok {
if route, found = cl.wildCardRoutes[routeSegment][MethodAny]; found {
return route, true, nil
}
}
cl.routesRWMu.Unlock()

// if not, look for wildcard routes
cl.wildCardRoutesRWMu.Lock()
defer cl.wildCardRoutesRWMu.Unlock()
for wildCardPattern, route := range cl.WildCardRoutes {
match, err := filepath.Match(wildCardPattern, routeSegment)
for wildCardPattern, routePath := range cl.wildCardRoutes {
pathMatch, err := filepath.Match(wildCardPattern, routeSegment)
if err != nil {
return nil, false, newDynamicError(ErrInvalidPath, err.Error())
}
if match {
return route, true, nil
if pathMatch {
// Found a path match, now check for the method
if route, found = routePath[routeMethod]; found {
return route, true, nil
}
if route, found = routePath[MethodAny]; found {
return route, true, nil
}
}
}

Expand All @@ -166,44 +226,57 @@ func (cl *CageLevel) route(routeSegment string) (route *Route, found bool, err e

// subCageLevel searches for a sub cage level based on the segment provided
// if createMode is true, it will create the cage level if it doesn't exist
func (cl *CageLevel) subCageLevel(subCageSegment string, createMode bool) (cageLevel *CageLevel, found bool, err error) {
func (cl *cageLevel) subCageLevel(subCageSegment string, createMode bool) (cageLevel *cageLevel, found bool, err error) {
// First check for an exact match
cl.subCagesRWMu.RLock()
if cageLevel, exists := cl.SubCages[subCageSegment]; exists {
defer cl.subCagesRWMu.RUnlock()
cl.rwMu.RLock()
if cageLevel, exists := cl.subCages[subCageSegment]; exists {
cl.rwMu.RUnlock()
return cageLevel, true, nil
}
cl.subCagesRWMu.RUnlock()

// if not, look for wildcard cages
cl.wildCardSubCagesRWMu.RLock()
for wildCardPattern, cageLevel := range cl.WildCardSubCages {
for wildCardPattern, cageLevel := range cl.wildCardSubCages {
match, err := filepath.Match(wildCardPattern, subCageSegment)
if err != nil {
cl.wildCardSubCagesRWMu.RUnlock()
cl.rwMu.RUnlock()
return nil, false, newDynamicError(ErrInvalidPath, err.Error())
}
if match {
cl.wildCardSubCagesRWMu.RUnlock()
cl.rwMu.RUnlock()
return cageLevel, true, nil
}
}
cl.wildCardSubCagesRWMu.RUnlock()
cl.rwMu.RUnlock()

// We didn't find a match, so we'll create a new cage level if we're in create mode
if createMode {
newCage := newCageLevel()
newCage := newCageLevel(filepath.Join(cl.cagePath, subCageSegment))
cl.rwMu.Lock()
defer cl.rwMu.Unlock()
if strings.Contains(subCageSegment, "*") {
cl.wildCardSubCagesRWMu.Lock()
defer cl.wildCardSubCagesRWMu.Unlock()
cl.WildCardSubCages[subCageSegment] = newCage
cl.wildCardSubCages[subCageSegment] = newCage
} else {
cl.subCagesRWMu.Lock()
defer cl.subCagesRWMu.Unlock()
cl.SubCages[subCageSegment] = newCage
cl.subCages[subCageSegment] = newCage
}
return newCage, true, nil
}

return nil, false, nil
}

// newRoute creates a new route in the cage level
func (cl *cageLevel) newRoute(route *Route) {
cl.rwMu.Lock()
defer cl.rwMu.Unlock()
if strings.Contains(route.Segment(), "*") {
if _, found := cl.wildCardRoutes[route.Segment()]; !found {
cl.wildCardRoutes[route.Segment()] = make(map[string]*Route)
}
cl.wildCardRoutes[route.Segment()][route.Method] = route
} else {
if _, found := cl.routes[route.Segment()]; !found {
cl.routes[route.Segment()] = make(map[string]*Route)
}
cl.routes[route.Segment()][route.Method] = route
}
}
1 change: 1 addition & 0 deletions parrot/cage_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package parrot
Loading

0 comments on commit d7086e6

Please sign in to comment.