Skip to content

Commit d7086e6

Browse files
committed
Switch to Cages for dynamic routing
1 parent 526aa33 commit d7086e6

File tree

7 files changed

+239
-144
lines changed

7 files changed

+239
-144
lines changed

parrot/.changeset/v0.2.0.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
## Added wildcard routing support
1+
- Added wildcard routing support
2+
- Improved routing performance
23

34
### Bench before
45

@@ -14,4 +15,16 @@ BenchmarkSave-14 6358 178122 ns/op
1415
BenchmarkLoad-14 1411 852377 ns/op
1516
```
1617

17-
### Bench After
18+
### Bench After
19+
20+
```sh
21+
go test -testLogLevel="" -bench=. -run=^$ ./...
22+
goos: darwin
23+
goarch: arm64
24+
pkg: github.com/smartcontractkit/chainlink-testing-framework/parrot
25+
cpu: Apple M3 Max
26+
BenchmarkRegisterRoute-14 3647503 313.8 ns/op
27+
BenchmarkRouteResponse-14 19143 62011 ns/op
28+
BenchmarkSave-14 5244 218697 ns/op
29+
BenchmarkLoad-14 1101 1049399 ns/op
30+
```

parrot/cage.go

Lines changed: 159 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,85 +9,94 @@ import (
99

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

16+
// MethodAny will match to any other method
17+
const MethodAny = "ANY"
18+
1619
// CageLevel holds a single level of routes and further sub cages
1720
// Note: Should only be used internally by the parrot.
18-
type CageLevel struct {
19-
// Routes contains all of the plain routes at this current cage level
20-
// path -> route
21-
Routes map[string]*Route `json:"routes"`
22-
routesRWMu sync.RWMutex // sync.Map might be better here, but eh
23-
// WildCardRoutes contains all the wildcard routes at this current cage level
24-
// path -> route
25-
WildCardRoutes map[string]*Route `json:"wild_card_routes"`
26-
wildCardRoutesRWMu sync.RWMutex
27-
// SubCages contains sub cages at this current cage level
21+
type cageLevel struct {
22+
// cagePath is the path to this cage level
23+
cagePath string
24+
// rwMu is a read write mutex for the cage level
25+
rwMu sync.RWMutex
26+
// TODO: Make all lowercase
27+
// routes contains all of the plain routes at this current cage level
28+
// route.path -> route.method -> route
29+
routes map[string]map[string]*Route
30+
// wildCardRoutes contains all the wildcard routes at this current cage level
31+
// route.path -> route.method -> route
32+
wildCardRoutes map[string]map[string]*Route
33+
// subCages contains sub cages at this current cage level
2834
// cage name -> cage level
29-
SubCages map[string]*CageLevel `json:"sub_cages"`
30-
subCagesRWMu sync.RWMutex
31-
// WildCardSubCages contains wildcard sub cages at this current cage level
35+
subCages map[string]*cageLevel
36+
// wildCardSubCages contains wildcard sub cages at this current cage level
3237
// cage name -> cage level
33-
WildCardSubCages map[string]*CageLevel `json:"wild_card_sub_cages"`
34-
wildCardSubCagesRWMu sync.RWMutex
38+
wildCardSubCages map[string]*cageLevel
3539
}
3640

3741
// newCage creates a new cage with an empty cage level for a new parrot instance
38-
func newCage() *Cage {
39-
return &Cage{
40-
CageLevel: newCageLevel(),
42+
func newCage() *cage {
43+
return &cage{
44+
cageLevel: newCageLevel("/"),
4145
}
4246
}
4347

4448
// newCageLevel creates a new cageLevel with empty maps
45-
func newCageLevel() *CageLevel {
46-
return &CageLevel{
47-
Routes: make(map[string]*Route),
48-
WildCardRoutes: make(map[string]*Route),
49-
SubCages: make(map[string]*CageLevel),
50-
WildCardSubCages: make(map[string]*CageLevel),
49+
func newCageLevel(cagePath string) *cageLevel {
50+
return &cageLevel{
51+
cagePath: cagePath,
52+
routes: make(map[string]map[string]*Route),
53+
wildCardRoutes: make(map[string]map[string]*Route),
54+
subCages: make(map[string]*cageLevel),
55+
wildCardSubCages: make(map[string]*cageLevel),
5156
}
5257
}
5358

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

6569
for _, pathSegment := range splitPath { // Iterate through each path segment to look for matches
6670
cageLevel, found, err := currentCageLevel.subCageLevel(pathSegment, createMode)
6771
if err != nil {
68-
return nil, routeSegment, err
72+
return nil, err
6973
}
7074
if found {
7175
currentCageLevel = cageLevel
7276
continue
7377
}
7478

7579
if !found {
76-
return nil, routeSegment, newDynamicError(ErrCageNotFound, fmt.Sprintf("path: '%s'", path))
80+
return nil, newDynamicError(ErrCageNotFound, fmt.Sprintf("path: '%s'", path))
7781
}
7882
}
7983

80-
return currentCageLevel, routeSegment, nil
84+
return currentCageLevel, nil
8185
}
8286

8387
// getRoute searches for a route based on the path provided
84-
func (c *Cage) getRoute(path string) (*Route, error) {
85-
cageLevel, routeSegment, err := c.cageLevel(path, false)
88+
func (c *cage) getRoute(routePath, routeMethod string) (*Route, error) {
89+
cageLevel, err := c.getCageLevel(routePath, false)
8690
if err != nil {
8791
return nil, err
8892
}
93+
routeSegments := strings.Split(routePath, "/")
94+
if len(routeSegments) == 0 {
95+
return nil, ErrRouteNotFound
96+
}
97+
routeSegment := routeSegments[len(routeSegments)-1]
8998

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

101110
// newRoute creates a new route, creating new cages if necessary
102-
func (c *Cage) newRoute(route *Route) error {
103-
cageLevel, routeSegment, err := c.cageLevel(route.Path, true)
111+
func (c *cage) newRoute(route *Route) error {
112+
cageLevel, err := c.getCageLevel(route.Path, true)
104113
if err != nil {
105114
return err
106115
}
107116

108-
if strings.Contains(routeSegment, "*") {
109-
cageLevel.wildCardRoutesRWMu.Lock()
110-
defer cageLevel.wildCardRoutesRWMu.Unlock()
111-
cageLevel.WildCardRoutes[routeSegment] = route
112-
} else {
113-
cageLevel.routesRWMu.Lock()
114-
defer cageLevel.routesRWMu.Unlock()
115-
cageLevel.Routes[routeSegment] = route
116-
}
117+
cageLevel.newRoute(route)
117118

118119
return nil
119120
}
120121

121-
// deleteRoute deletes a route based on the path provided
122-
func (c *Cage) deleteRoute(route *Route) error {
123-
cageLevel, routeSegment, err := c.cageLevel(route.Path, true)
122+
// deleteRoute deletes a route
123+
func (c *cage) deleteRoute(route *Route) error {
124+
cageLevel, err := c.getCageLevel(route.Path, false)
124125
if err != nil {
125126
return err
126127
}
127128

128-
if strings.Contains(routeSegment, "*") {
129-
cageLevel.wildCardRoutesRWMu.Lock()
130-
defer cageLevel.wildCardRoutesRWMu.Unlock()
131-
delete(cageLevel.WildCardRoutes, routeSegment)
129+
if strings.Contains(route.Segment(), "*") {
130+
cageLevel.rwMu.RLock()
131+
if _, found := cageLevel.wildCardRoutes[route.Segment()][route.Method]; !found {
132+
cageLevel.rwMu.RUnlock()
133+
return ErrRouteNotFound
134+
}
135+
cageLevel.rwMu.RUnlock()
136+
137+
cageLevel.rwMu.Lock()
138+
delete(cageLevel.wildCardRoutes[route.Segment()], route.Method)
139+
cageLevel.rwMu.Unlock()
132140
} else {
133-
cageLevel.routesRWMu.Lock()
134-
defer cageLevel.routesRWMu.Unlock()
135-
delete(cageLevel.Routes, routeSegment)
141+
cageLevel.rwMu.RLock()
142+
if _, found := cageLevel.routes[route.Segment()][route.Method]; !found {
143+
cageLevel.rwMu.RUnlock()
144+
return ErrRouteNotFound
145+
}
146+
cageLevel.rwMu.RUnlock()
147+
148+
cageLevel.rwMu.Lock()
149+
delete(cageLevel.routes[route.Segment()], route.Method)
150+
cageLevel.rwMu.Unlock()
136151
}
137152

138153
return nil
139154
}
140155

156+
// routes returns all the routes in the cage
157+
func (c *cage) routes() []*Route {
158+
return c.routesRecursive()
159+
}
160+
161+
// routesRecursive returns all the routes in the cage recursively.
162+
// Should only be used internally by the cage. Use routes() instead.
163+
func (cl *cageLevel) routesRecursive() (routes []*Route) {
164+
// Add all the routes at this level
165+
cl.rwMu.RLock()
166+
for _, routePath := range cl.routes {
167+
for _, route := range routePath {
168+
routes = append(routes, route)
169+
}
170+
}
171+
172+
// Add all the wildcard routes at this level
173+
for _, routePath := range cl.wildCardRoutes {
174+
for _, route := range routePath {
175+
routes = append(routes, route)
176+
}
177+
}
178+
cl.rwMu.RUnlock()
179+
180+
for _, subCage := range cl.subCages {
181+
routes = append(routes, subCage.routesRecursive()...)
182+
}
183+
for _, subCage := range cl.wildCardSubCages {
184+
routes = append(routes, subCage.routesRecursive()...)
185+
}
186+
187+
return routes
188+
}
189+
141190
// route searches for a route based on the route segment provided
142-
func (cl *CageLevel) route(routeSegment string) (route *Route, found bool, err error) {
191+
func (cl *cageLevel) route(routeSegment, routeMethod string) (route *Route, found bool, err error) {
143192
// First check for an exact match
144-
cl.routesRWMu.Lock()
145-
if route, found = cl.Routes[routeSegment]; found {
146-
defer cl.routesRWMu.Unlock()
147-
return route, true, nil
193+
cl.rwMu.RLock()
194+
defer cl.rwMu.RUnlock()
195+
196+
if _, ok := cl.routes[routeSegment]; ok {
197+
if route, found = cl.routes[routeSegment][routeMethod]; found {
198+
return route, true, nil
199+
}
200+
}
201+
if _, ok := cl.wildCardRoutes[routeSegment]; ok {
202+
if route, found = cl.wildCardRoutes[routeSegment][MethodAny]; found {
203+
return route, true, nil
204+
}
148205
}
149-
cl.routesRWMu.Unlock()
150206

151207
// if not, look for wildcard routes
152-
cl.wildCardRoutesRWMu.Lock()
153-
defer cl.wildCardRoutesRWMu.Unlock()
154-
for wildCardPattern, route := range cl.WildCardRoutes {
155-
match, err := filepath.Match(wildCardPattern, routeSegment)
208+
for wildCardPattern, routePath := range cl.wildCardRoutes {
209+
pathMatch, err := filepath.Match(wildCardPattern, routeSegment)
156210
if err != nil {
157211
return nil, false, newDynamicError(ErrInvalidPath, err.Error())
158212
}
159-
if match {
160-
return route, true, nil
213+
if pathMatch {
214+
// Found a path match, now check for the method
215+
if route, found = routePath[routeMethod]; found {
216+
return route, true, nil
217+
}
218+
if route, found = routePath[MethodAny]; found {
219+
return route, true, nil
220+
}
161221
}
162222
}
163223

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

167227
// subCageLevel searches for a sub cage level based on the segment provided
168228
// if createMode is true, it will create the cage level if it doesn't exist
169-
func (cl *CageLevel) subCageLevel(subCageSegment string, createMode bool) (cageLevel *CageLevel, found bool, err error) {
229+
func (cl *cageLevel) subCageLevel(subCageSegment string, createMode bool) (cageLevel *cageLevel, found bool, err error) {
170230
// First check for an exact match
171-
cl.subCagesRWMu.RLock()
172-
if cageLevel, exists := cl.SubCages[subCageSegment]; exists {
173-
defer cl.subCagesRWMu.RUnlock()
231+
cl.rwMu.RLock()
232+
if cageLevel, exists := cl.subCages[subCageSegment]; exists {
233+
cl.rwMu.RUnlock()
174234
return cageLevel, true, nil
175235
}
176-
cl.subCagesRWMu.RUnlock()
177236

178237
// if not, look for wildcard cages
179-
cl.wildCardSubCagesRWMu.RLock()
180-
for wildCardPattern, cageLevel := range cl.WildCardSubCages {
238+
for wildCardPattern, cageLevel := range cl.wildCardSubCages {
181239
match, err := filepath.Match(wildCardPattern, subCageSegment)
182240
if err != nil {
183-
cl.wildCardSubCagesRWMu.RUnlock()
241+
cl.rwMu.RUnlock()
184242
return nil, false, newDynamicError(ErrInvalidPath, err.Error())
185243
}
186244
if match {
187-
cl.wildCardSubCagesRWMu.RUnlock()
245+
cl.rwMu.RUnlock()
188246
return cageLevel, true, nil
189247
}
190248
}
191-
cl.wildCardSubCagesRWMu.RUnlock()
249+
cl.rwMu.RUnlock()
192250

193251
// We didn't find a match, so we'll create a new cage level if we're in create mode
194252
if createMode {
195-
newCage := newCageLevel()
253+
newCage := newCageLevel(filepath.Join(cl.cagePath, subCageSegment))
254+
cl.rwMu.Lock()
255+
defer cl.rwMu.Unlock()
196256
if strings.Contains(subCageSegment, "*") {
197-
cl.wildCardSubCagesRWMu.Lock()
198-
defer cl.wildCardSubCagesRWMu.Unlock()
199-
cl.WildCardSubCages[subCageSegment] = newCage
257+
cl.wildCardSubCages[subCageSegment] = newCage
200258
} else {
201-
cl.subCagesRWMu.Lock()
202-
defer cl.subCagesRWMu.Unlock()
203-
cl.SubCages[subCageSegment] = newCage
259+
cl.subCages[subCageSegment] = newCage
204260
}
205261
return newCage, true, nil
206262
}
207263

208264
return nil, false, nil
209265
}
266+
267+
// newRoute creates a new route in the cage level
268+
func (cl *cageLevel) newRoute(route *Route) {
269+
cl.rwMu.Lock()
270+
defer cl.rwMu.Unlock()
271+
if strings.Contains(route.Segment(), "*") {
272+
if _, found := cl.wildCardRoutes[route.Segment()]; !found {
273+
cl.wildCardRoutes[route.Segment()] = make(map[string]*Route)
274+
}
275+
cl.wildCardRoutes[route.Segment()][route.Method] = route
276+
} else {
277+
if _, found := cl.routes[route.Segment()]; !found {
278+
cl.routes[route.Segment()] = make(map[string]*Route)
279+
}
280+
cl.routes[route.Segment()][route.Method] = route
281+
}
282+
}

parrot/cage_benchmark_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package parrot

0 commit comments

Comments
 (0)