Skip to content

Commit 9ea7651

Browse files
luk3skyw4lkergabycoderabbitai[bot]
authored
✨ feat: Add support for RebuildTree (gofiber#3074)
* feat: add rebuild tree method * docs: add newline at the end of app.md * docs: add an example of dynamic defined routes * docs: remove tabs from example code on app.md * Update docs/api/app.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update app.md * docs: add RebuildTree to what's new documentation * fix: markdown errors in documentation Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: add mutex lock to the addRoute function * refactor: remove mutex lock from addRoute * refactor: fix mutex deadlock in addRoute --------- Co-authored-by: Juan Calderon-Perez <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 4f1dc49 commit 9ea7651

File tree

4 files changed

+99
-2
lines changed

4 files changed

+99
-2
lines changed

docs/api/app.md

+28
Original file line numberDiff line numberDiff line change
@@ -573,3 +573,31 @@ Hooks is a method to return [hooks](./hooks.md) property.
573573
```go title="Signature"
574574
func (app *App) Hooks() *Hooks
575575
```
576+
577+
## RebuildTree
578+
579+
The RebuildTree method is designed to rebuild the route tree and enable dynamic route registration. It returns a pointer to the App instance.
580+
581+
```go title="Signature"
582+
func (app *App) RebuildTree() *App
583+
```
584+
585+
**Note:** Use this method with caution. It is **not** thread-safe and calling it can be very performance-intensive, so it should be used sparingly and only in development mode. Avoid using it concurrently.
586+
587+
### Example Usage
588+
589+
Here’s an example of how to define and register routes dynamically:
590+
591+
```go
592+
app.Get("/define", func(c Ctx) error { // Define a new route dynamically
593+
app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route
594+
return c.SendStatus(http.StatusOK)
595+
})
596+
597+
app.RebuildTree() // Rebuild the route tree to register the new route
598+
599+
return c.SendStatus(http.StatusOK)
600+
})
601+
```
602+
603+
In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available.

docs/whats_new.md

+25
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,31 @@ app.Route("/api").Route("/user/:id?")
437437
});
438438
```
439439

440+
### 🗺 RebuildTree
441+
442+
We have added a new method that allows the route tree stack to be rebuilt in runtime, with it, you can add a route while your application is running and rebuild the route tree stack to make it registered and available for calls.
443+
444+
You can find more reference on it in the [app](./api/app.md#rebuildtree):
445+
446+
#### Example Usage
447+
448+
```go
449+
app.Get("/define", func(c Ctx) error { // Define a new route dynamically
450+
app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route
451+
return c.SendStatus(http.StatusOK)
452+
})
453+
454+
app.RebuildTree() // Rebuild the route tree to register the new route
455+
456+
return c.SendStatus(http.StatusOK)
457+
})
458+
```
459+
460+
In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available.
461+
462+
**Note:** Use this method with caution. It is **not** thread-safe and calling it can be very performance-intensive, so it should be used sparingly and only in
463+
development mode. Avoid using it concurrently.
464+
440465
### 🧠 Context
441466

442467
### 📎 Parser

router.go

+18-2
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,9 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
375375
}
376376

377377
func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
378+
app.mutex.Lock()
379+
defer app.mutex.Unlock()
380+
378381
// Check mounted routes
379382
var mounted bool
380383
if len(isMounted) > 0 {
@@ -400,15 +403,28 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
400403

401404
// Execute onRoute hooks & change latestRoute if not adding mounted route
402405
if !mounted {
403-
app.mutex.Lock()
404406
app.latestRoute = route
405407
if err := app.hooks.executeOnRouteHooks(*route); err != nil {
406408
panic(err)
407409
}
408-
app.mutex.Unlock()
409410
}
410411
}
411412

413+
// BuildTree rebuilds the prefix tree from the previously registered routes.
414+
// This method is useful when you want to register routes dynamically after the app has started.
415+
// It is not recommended to use this method on production environments because rebuilding
416+
// the tree is performance-intensive and not thread-safe in runtime. Since building the tree
417+
// is only done in the startupProcess of the app, this method does not makes sure that the
418+
// routeTree is being safely changed, as it would add a great deal of overhead in the request.
419+
// Latest benchmark results showed a degradation from 82.79 ns/op to 94.48 ns/op and can be found in:
420+
// https://github.com/gofiber/fiber/issues/2769#issuecomment-2227385283
421+
func (app *App) RebuildTree() *App {
422+
app.mutex.Lock()
423+
defer app.mutex.Unlock()
424+
425+
return app.buildTree()
426+
}
427+
412428
// buildTree build the prefix tree from the previously registered routes
413429
func (app *App) buildTree() *App {
414430
if !app.routesRefreshed {

router_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"errors"
1010
"fmt"
1111
"io"
12+
"net/http"
1213
"net/http/httptest"
1314
"os"
1415
"testing"
@@ -368,6 +369,33 @@ func Test_Router_NotFound_HTML_Inject(t *testing.T) {
368369
require.Equal(t, "Cannot DELETE /does/not/exist&lt;script&gt;alert(&#39;foo&#39;);&lt;/script&gt;", string(c.Response.Body()))
369370
}
370371

372+
func Test_App_Rebuild_Tree(t *testing.T) {
373+
t.Parallel()
374+
app := New()
375+
376+
app.Get("/test", func(c Ctx) error {
377+
app.Get("/dynamically-defined", func(c Ctx) error {
378+
return c.SendStatus(http.StatusOK)
379+
})
380+
381+
app.RebuildTree()
382+
383+
return c.SendStatus(http.StatusOK)
384+
})
385+
386+
resp, err := app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil))
387+
require.NoError(t, err, "app.Test(req)")
388+
require.Equal(t, http.StatusNotFound, resp.StatusCode, "Status code")
389+
390+
resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil))
391+
require.NoError(t, err, "app.Test(req)")
392+
require.Equal(t, http.StatusOK, resp.StatusCode, "Status code")
393+
394+
resp, err = app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil))
395+
require.NoError(t, err, "app.Test(req)")
396+
require.Equal(t, http.StatusOK, resp.StatusCode, "Status code")
397+
}
398+
371399
//////////////////////////////////////////////
372400
///////////////// BENCHMARKS /////////////////
373401
//////////////////////////////////////////////

0 commit comments

Comments
 (0)