Skip to content

Commit 9d538ea

Browse files
committed
implement a dynamic router handler for #2167
1 parent 72f9d4b commit 9d538ea

File tree

6 files changed

+150
-11
lines changed

6 files changed

+150
-11
lines changed

_examples/routing/route-state/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,5 @@ func main() {
4141
ctx.Exec("GET", "/invisible/iris")
4242
})
4343

44-
app.Listen(":8080")
44+
app.Listen(":8080", iris.WithDynamicHandler)
4545
}

configuration.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,14 @@ var WithLowercaseRouting = func(app *Application) {
308308
app.config.ForceLowercaseRouting = true
309309
}
310310

311+
// WithDynamicHandler enables for dynamic routing by
312+
// setting the `EnableDynamicHandler` to true.
313+
//
314+
// See `Configuration`.
315+
var WithDynamicHandler = func(app *Application) {
316+
app.config.EnableDynamicHandler = true
317+
}
318+
311319
// WithOptimizations can force the application to optimize for the best performance where is possible.
312320
//
313321
// See `Configuration`.
@@ -737,6 +745,14 @@ type Configuration struct {
737745
//
738746
// Defaults to false.
739747
ForceLowercaseRouting bool `ini:"force_lowercase_routing" json:"forceLowercaseRouting,omitempty" yaml:"ForceLowercaseRouting" toml:"ForceLowercaseRouting"`
748+
// EnableOptimizations enables dynamic request handler.
749+
// It gives the router the feature to add routes while in serve-time,
750+
// when `RefreshRouter` is called.
751+
// If this setting is set to true, the request handler will use a mutex for data(trie routing) protection,
752+
// hence the performance cost.
753+
//
754+
// Defaults to false.
755+
EnableDynamicHandler bool `ini:"enable_dynamic_handler" json:"enableDynamicHandler,omitempty" yaml:"EnableDynamicHandler" toml:"EnableDynamicHandler"`
740756
// FireMethodNotAllowed if it's true router checks for StatusMethodNotAllowed(405) and
741757
// fires the 405 error instead of 404
742758
// Defaults to false.
@@ -1008,6 +1024,11 @@ func (c *Configuration) GetForceLowercaseRouting() bool {
10081024
return c.ForceLowercaseRouting
10091025
}
10101026

1027+
// GetEnableDynamicHandler returns the EnableDynamicHandler field.
1028+
func (c *Configuration) GetEnableDynamicHandler() bool {
1029+
return c.EnableDynamicHandler
1030+
}
1031+
10111032
// GetFireMethodNotAllowed returns the FireMethodNotAllowed field.
10121033
func (c *Configuration) GetFireMethodNotAllowed() bool {
10131034
return c.FireMethodNotAllowed

context/configuration.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ type ConfigurationReadOnly interface {
3434
GetEnablePathEscape() bool
3535
// GetForceLowercaseRouting returns the ForceLowercaseRouting field.
3636
GetForceLowercaseRouting() bool
37+
// GetEnableOptimizations returns the EnableDynamicHandler field.
38+
GetEnableDynamicHandler() bool
3739
// GetFireMethodNotAllowed returns the FireMethodNotAllowed field.
3840
GetFireMethodNotAllowed() bool
3941
// GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field.

core/router/handler.go

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package router
22

33
import (
4+
"errors"
45
"fmt"
56
"net/http"
67
"sort"
78
"strings"
9+
"sync"
10+
"sync/atomic"
811
"time"
912

1013
"github.com/kataras/iris/v12/context"
@@ -39,8 +42,18 @@ type (
3942
// on the given context's response status code.
4043
FireErrorCode(ctx *context.Context)
4144
}
45+
46+
// RouteAdder is an optional interface that can be implemented by a `RequestHandler`.
47+
RouteAdder interface {
48+
// AddRoute should add a route to the request handler directly.
49+
AddRoute(*Route) error
50+
}
4251
)
4352

53+
// ErrNotRouteAdder throws on `AddRouteUnsafe` when a registered `RequestHandler`
54+
// does not implements the optional `AddRoute(*Route) error` method.
55+
var ErrNotRouteAdder = errors.New("request handler does not implement AddRoute method")
56+
4457
type routerHandler struct {
4558
// Config.
4659
disablePathCorrection bool
@@ -59,8 +72,103 @@ type routerHandler struct {
5972
errorDefaultHandlers context.Handlers // the main handler(s) for default error code handlers, when not registered directly by the end-developer.
6073
}
6174

62-
var _ RequestHandler = (*routerHandler)(nil)
63-
var _ HTTPErrorHandler = (*routerHandler)(nil)
75+
var (
76+
_ RequestHandler = (*routerHandler)(nil)
77+
_ HTTPErrorHandler = (*routerHandler)(nil)
78+
)
79+
80+
type routerHandlerDynamic struct {
81+
RequestHandler
82+
rw sync.RWMutex
83+
84+
locked uint32
85+
}
86+
87+
// RouteExists reports whether a particular route exists.
88+
func (h *routerHandlerDynamic) RouteExists(ctx *context.Context, method, path string) (exists bool) {
89+
h.lock(false, func() error {
90+
exists = h.RequestHandler.RouteExists(ctx, method, path)
91+
return nil
92+
})
93+
94+
return
95+
}
96+
97+
func (h *routerHandlerDynamic) AddRoute(r *Route) error {
98+
if v, ok := h.RequestHandler.(RouteAdder); ok {
99+
return h.lock(true, func() error {
100+
return v.AddRoute(r)
101+
})
102+
}
103+
104+
return ErrNotRouteAdder
105+
}
106+
107+
func (h *routerHandlerDynamic) lock(writeAccess bool, fn func() error) error {
108+
if atomic.CompareAndSwapUint32(&h.locked, 0, 1) {
109+
if writeAccess {
110+
h.rw.Lock()
111+
} else {
112+
h.rw.RLock()
113+
}
114+
115+
err := fn()
116+
117+
// check agan because fn may called the unlock method.
118+
if atomic.CompareAndSwapUint32(&h.locked, 1, 0) {
119+
if writeAccess {
120+
h.rw.Unlock()
121+
} else {
122+
h.rw.RUnlock()
123+
}
124+
}
125+
126+
return err
127+
}
128+
129+
return fn()
130+
}
131+
132+
func (h *routerHandlerDynamic) Build(provider RoutesProvider) error {
133+
// Build can be called inside HandleRequest if the route handler
134+
// calls the RefreshRouter method, and it will stuck on the rw.Lock() call,
135+
// so use a custom version of it.
136+
// h.rw.Lock()
137+
// defer h.rw.Unlock()
138+
139+
return h.lock(true, func() error {
140+
return h.RequestHandler.Build(provider)
141+
})
142+
}
143+
144+
func (h *routerHandlerDynamic) HandleRequest(ctx *context.Context) {
145+
h.lock(false, func() error {
146+
h.RequestHandler.HandleRequest(ctx)
147+
return nil
148+
})
149+
}
150+
151+
func (h *routerHandlerDynamic) FireErrorCode(ctx *context.Context) {
152+
h.lock(false, func() error {
153+
h.RequestHandler.FireErrorCode(ctx)
154+
return nil
155+
})
156+
}
157+
158+
// NewDynamicHandler returns a new router handler which is responsible handle each request
159+
// with routes that can be added in serve-time.
160+
// It's a wrapper of the `NewDefaultHandler`.
161+
// It's being used when the `ConfigurationReadOnly.GetEnableDynamicHandler` is true.
162+
func NewDynamicHandler(config context.ConfigurationReadOnly, logger *golog.Logger) RequestHandler /* #2167 */ {
163+
handler := NewDefaultHandler(config, logger)
164+
return wrapDynamicHandler(handler)
165+
}
166+
167+
func wrapDynamicHandler(handler RequestHandler) RequestHandler {
168+
return &routerHandlerDynamic{
169+
RequestHandler: handler,
170+
}
171+
}
64172

65173
// NewDefaultHandler returns the handler which is responsible
66174
// to map the request with a route (aka mux implementation).
@@ -71,6 +179,7 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge
71179
fireMethodNotAllowed bool
72180
enablePathIntelligence bool
73181
forceLowercaseRouting bool
182+
dynamicHandlerEnabled bool
74183
)
75184

76185
if config != nil { // #2147
@@ -79,16 +188,23 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge
79188
fireMethodNotAllowed = config.GetFireMethodNotAllowed()
80189
enablePathIntelligence = config.GetEnablePathIntelligence()
81190
forceLowercaseRouting = config.GetForceLowercaseRouting()
191+
dynamicHandlerEnabled = config.GetEnableDynamicHandler()
82192
}
83193

84-
return &routerHandler{
194+
handler := &routerHandler{
85195
disablePathCorrection: disablePathCorrection,
86196
disablePathCorrectionRedirection: disablePathCorrectionRedirection,
87197
fireMethodNotAllowed: fireMethodNotAllowed,
88198
enablePathIntelligence: enablePathIntelligence,
89199
forceLowercaseRouting: forceLowercaseRouting,
90200
logger: logger,
91201
}
202+
203+
if dynamicHandlerEnabled {
204+
return wrapDynamicHandler(handler)
205+
}
206+
207+
return handler
92208
}
93209

94210
func (h *routerHandler) getTree(statusCode int, method, subdomain string) *trie {

core/router/route_register_rule_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212

1313
func TestRegisterRule(t *testing.T) {
1414
app := iris.New()
15+
app.Configure(iris.WithDynamicHandler)
16+
1517
// collect the error on RouteError rule.
1618
buf := new(bytes.Buffer)
1719
app.Logger().SetTimeFormat("").DisableNewLine().SetOutput(buf)

core/router/router.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,21 @@ func NewRouter() *Router {
4949

5050
// RefreshRouter re-builds the router. Should be called when a route's state
5151
// changed (i.e Method changed at serve-time).
52+
//
53+
// Note that in order to use RefreshRouter while in serve-time,
54+
// you have to set the `EnableDynamicHandler` Iris Application setting to true,
55+
// e.g. `app.Listen(":8080", iris.WithEnableDynamicHandler)`
5256
func (router *Router) RefreshRouter() error {
5357
return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider, true)
5458
}
5559

56-
// ErrNotRouteAdder throws on `AddRouteUnsafe` when a registered `RequestHandler`
57-
// does not implements the optional `AddRoute(*Route) error` method.
58-
var ErrNotRouteAdder = errors.New("request handler does not implement AddRoute method")
59-
6060
// AddRouteUnsafe adds a route directly to the router's request handler.
6161
// Works before or after Build state.
6262
// Mainly used for internal cases like `iris.WithSitemap`.
6363
// Do NOT use it on serve-time.
6464
func (router *Router) AddRouteUnsafe(routes ...*Route) error {
6565
if h := router.requestHandler; h != nil {
66-
if v, ok := h.(interface {
67-
AddRoute(*Route) error
68-
}); ok {
66+
if v, ok := h.(RouteAdder); ok {
6967
for _, r := range routes {
7068
return v.AddRoute(r)
7169
}

0 commit comments

Comments
 (0)