Skip to content

Commit

Permalink
✨ feat: Add support for RebuildTree (#3074)
Browse files Browse the repository at this point in the history
* 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 <835733+gaby@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Jul 18, 2024
1 parent 4f1dc49 commit 9ea7651
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 2 deletions.
28 changes: 28 additions & 0 deletions docs/api/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,3 +573,31 @@ Hooks is a method to return [hooks](./hooks.md) property.
```go title="Signature"
func (app *App) Hooks() *Hooks
```

## RebuildTree

The RebuildTree method is designed to rebuild the route tree and enable dynamic route registration. It returns a pointer to the App instance.

```go title="Signature"
func (app *App) RebuildTree() *App
```

**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.

### Example Usage

Here’s an example of how to define and register routes dynamically:

```go
app.Get("/define", func(c Ctx) error { // Define a new route dynamically
app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route
return c.SendStatus(http.StatusOK)
})

app.RebuildTree() // Rebuild the route tree to register the new route

This comment has been minimized.

Copy link
@ad3n

ad3n Jul 19, 2024

Contributor

Finally


return c.SendStatus(http.StatusOK)
})
```

In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available.
25 changes: 25 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,31 @@ app.Route("/api").Route("/user/:id?")
});
```

### 🗺 RebuildTree

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.

You can find more reference on it in the [app](./api/app.md#rebuildtree):

#### Example Usage

```go
app.Get("/define", func(c Ctx) error { // Define a new route dynamically
app.Get("/dynamically-defined", func(c Ctx) error { // Adding a dynamically defined route
return c.SendStatus(http.StatusOK)
})

app.RebuildTree() // Rebuild the route tree to register the new route

return c.SendStatus(http.StatusOK)
})
```

In this example, a new route is defined and then `RebuildTree()` is called to make sure the new route is registered and available.

**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.

### 🧠 Context

### 📎 Parser
Expand Down
20 changes: 18 additions & 2 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ func (app *App) register(methods []string, pathRaw string, group *Group, handler
}

func (app *App) addRoute(method string, route *Route, isMounted ...bool) {
app.mutex.Lock()
defer app.mutex.Unlock()

// Check mounted routes
var mounted bool
if len(isMounted) > 0 {
Expand All @@ -400,15 +403,28 @@ func (app *App) addRoute(method string, route *Route, isMounted ...bool) {

// Execute onRoute hooks & change latestRoute if not adding mounted route
if !mounted {
app.mutex.Lock()
app.latestRoute = route
if err := app.hooks.executeOnRouteHooks(*route); err != nil {
panic(err)
}
app.mutex.Unlock()
}
}

// BuildTree rebuilds the prefix tree from the previously registered routes.
// This method is useful when you want to register routes dynamically after the app has started.
// It is not recommended to use this method on production environments because rebuilding
// the tree is performance-intensive and not thread-safe in runtime. Since building the tree
// is only done in the startupProcess of the app, this method does not makes sure that the
// routeTree is being safely changed, as it would add a great deal of overhead in the request.
// Latest benchmark results showed a degradation from 82.79 ns/op to 94.48 ns/op and can be found in:
// https://github.com/gofiber/fiber/issues/2769#issuecomment-2227385283
func (app *App) RebuildTree() *App {
app.mutex.Lock()
defer app.mutex.Unlock()

return app.buildTree()
}

// buildTree build the prefix tree from the previously registered routes
func (app *App) buildTree() *App {
if !app.routesRefreshed {
Expand Down
28 changes: 28 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
Expand Down Expand Up @@ -368,6 +369,33 @@ func Test_Router_NotFound_HTML_Inject(t *testing.T) {
require.Equal(t, "Cannot DELETE /does/not/exist&lt;script&gt;alert(&#39;foo&#39;);&lt;/script&gt;", string(c.Response.Body()))
}

func Test_App_Rebuild_Tree(t *testing.T) {
t.Parallel()
app := New()

app.Get("/test", func(c Ctx) error {
app.Get("/dynamically-defined", func(c Ctx) error {
return c.SendStatus(http.StatusOK)
})

app.RebuildTree()

return c.SendStatus(http.StatusOK)
})

resp, err := app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, http.StatusNotFound, resp.StatusCode, "Status code")

resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, http.StatusOK, resp.StatusCode, "Status code")

resp, err = app.Test(httptest.NewRequest(MethodGet, "/dynamically-defined", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, http.StatusOK, resp.StatusCode, "Status code")
}

//////////////////////////////////////////////
///////////////// BENCHMARKS /////////////////
//////////////////////////////////////////////
Expand Down

0 comments on commit 9ea7651

Please sign in to comment.