implement a dynamic router handler for #2167

This commit is contained in:
Gerasimos (Makis) Maropoulos 2023-08-04 17:59:00 +03:00
parent 72f9d4ba5c
commit 9d538eabb0
No known key found for this signature in database
GPG Key ID: B9839E9CD30B7B6B
6 changed files with 150 additions and 11 deletions

View File

@ -41,5 +41,5 @@ func main() {
ctx.Exec("GET", "/invisible/iris") ctx.Exec("GET", "/invisible/iris")
}) })
app.Listen(":8080") app.Listen(":8080", iris.WithDynamicHandler)
} }

View File

@ -308,6 +308,14 @@ var WithLowercaseRouting = func(app *Application) {
app.config.ForceLowercaseRouting = true app.config.ForceLowercaseRouting = true
} }
// WithDynamicHandler enables for dynamic routing by
// setting the `EnableDynamicHandler` to true.
//
// See `Configuration`.
var WithDynamicHandler = func(app *Application) {
app.config.EnableDynamicHandler = true
}
// WithOptimizations can force the application to optimize for the best performance where is possible. // WithOptimizations can force the application to optimize for the best performance where is possible.
// //
// See `Configuration`. // See `Configuration`.
@ -737,6 +745,14 @@ type Configuration struct {
// //
// Defaults to false. // Defaults to false.
ForceLowercaseRouting bool `ini:"force_lowercase_routing" json:"forceLowercaseRouting,omitempty" yaml:"ForceLowercaseRouting" toml:"ForceLowercaseRouting"` ForceLowercaseRouting bool `ini:"force_lowercase_routing" json:"forceLowercaseRouting,omitempty" yaml:"ForceLowercaseRouting" toml:"ForceLowercaseRouting"`
// EnableOptimizations enables dynamic request handler.
// It gives the router the feature to add routes while in serve-time,
// when `RefreshRouter` is called.
// If this setting is set to true, the request handler will use a mutex for data(trie routing) protection,
// hence the performance cost.
//
// Defaults to false.
EnableDynamicHandler bool `ini:"enable_dynamic_handler" json:"enableDynamicHandler,omitempty" yaml:"EnableDynamicHandler" toml:"EnableDynamicHandler"`
// FireMethodNotAllowed if it's true router checks for StatusMethodNotAllowed(405) and // FireMethodNotAllowed if it's true router checks for StatusMethodNotAllowed(405) and
// fires the 405 error instead of 404 // fires the 405 error instead of 404
// Defaults to false. // Defaults to false.
@ -1008,6 +1024,11 @@ func (c *Configuration) GetForceLowercaseRouting() bool {
return c.ForceLowercaseRouting return c.ForceLowercaseRouting
} }
// GetEnableDynamicHandler returns the EnableDynamicHandler field.
func (c *Configuration) GetEnableDynamicHandler() bool {
return c.EnableDynamicHandler
}
// GetFireMethodNotAllowed returns the FireMethodNotAllowed field. // GetFireMethodNotAllowed returns the FireMethodNotAllowed field.
func (c *Configuration) GetFireMethodNotAllowed() bool { func (c *Configuration) GetFireMethodNotAllowed() bool {
return c.FireMethodNotAllowed return c.FireMethodNotAllowed

View File

@ -34,6 +34,8 @@ type ConfigurationReadOnly interface {
GetEnablePathEscape() bool GetEnablePathEscape() bool
// GetForceLowercaseRouting returns the ForceLowercaseRouting field. // GetForceLowercaseRouting returns the ForceLowercaseRouting field.
GetForceLowercaseRouting() bool GetForceLowercaseRouting() bool
// GetEnableOptimizations returns the EnableDynamicHandler field.
GetEnableDynamicHandler() bool
// GetFireMethodNotAllowed returns the FireMethodNotAllowed field. // GetFireMethodNotAllowed returns the FireMethodNotAllowed field.
GetFireMethodNotAllowed() bool GetFireMethodNotAllowed() bool
// GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field. // GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field.

View File

@ -1,10 +1,13 @@
package router package router
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"sort" "sort"
"strings" "strings"
"sync"
"sync/atomic"
"time" "time"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
@ -39,8 +42,18 @@ type (
// on the given context's response status code. // on the given context's response status code.
FireErrorCode(ctx *context.Context) FireErrorCode(ctx *context.Context)
} }
// RouteAdder is an optional interface that can be implemented by a `RequestHandler`.
RouteAdder interface {
// AddRoute should add a route to the request handler directly.
AddRoute(*Route) error
}
) )
// ErrNotRouteAdder throws on `AddRouteUnsafe` when a registered `RequestHandler`
// does not implements the optional `AddRoute(*Route) error` method.
var ErrNotRouteAdder = errors.New("request handler does not implement AddRoute method")
type routerHandler struct { type routerHandler struct {
// Config. // Config.
disablePathCorrection bool disablePathCorrection bool
@ -59,8 +72,103 @@ type routerHandler struct {
errorDefaultHandlers context.Handlers // the main handler(s) for default error code handlers, when not registered directly by the end-developer. errorDefaultHandlers context.Handlers // the main handler(s) for default error code handlers, when not registered directly by the end-developer.
} }
var _ RequestHandler = (*routerHandler)(nil) var (
var _ HTTPErrorHandler = (*routerHandler)(nil) _ RequestHandler = (*routerHandler)(nil)
_ HTTPErrorHandler = (*routerHandler)(nil)
)
type routerHandlerDynamic struct {
RequestHandler
rw sync.RWMutex
locked uint32
}
// RouteExists reports whether a particular route exists.
func (h *routerHandlerDynamic) RouteExists(ctx *context.Context, method, path string) (exists bool) {
h.lock(false, func() error {
exists = h.RequestHandler.RouteExists(ctx, method, path)
return nil
})
return
}
func (h *routerHandlerDynamic) AddRoute(r *Route) error {
if v, ok := h.RequestHandler.(RouteAdder); ok {
return h.lock(true, func() error {
return v.AddRoute(r)
})
}
return ErrNotRouteAdder
}
func (h *routerHandlerDynamic) lock(writeAccess bool, fn func() error) error {
if atomic.CompareAndSwapUint32(&h.locked, 0, 1) {
if writeAccess {
h.rw.Lock()
} else {
h.rw.RLock()
}
err := fn()
// check agan because fn may called the unlock method.
if atomic.CompareAndSwapUint32(&h.locked, 1, 0) {
if writeAccess {
h.rw.Unlock()
} else {
h.rw.RUnlock()
}
}
return err
}
return fn()
}
func (h *routerHandlerDynamic) Build(provider RoutesProvider) error {
// Build can be called inside HandleRequest if the route handler
// calls the RefreshRouter method, and it will stuck on the rw.Lock() call,
// so use a custom version of it.
// h.rw.Lock()
// defer h.rw.Unlock()
return h.lock(true, func() error {
return h.RequestHandler.Build(provider)
})
}
func (h *routerHandlerDynamic) HandleRequest(ctx *context.Context) {
h.lock(false, func() error {
h.RequestHandler.HandleRequest(ctx)
return nil
})
}
func (h *routerHandlerDynamic) FireErrorCode(ctx *context.Context) {
h.lock(false, func() error {
h.RequestHandler.FireErrorCode(ctx)
return nil
})
}
// NewDynamicHandler returns a new router handler which is responsible handle each request
// with routes that can be added in serve-time.
// It's a wrapper of the `NewDefaultHandler`.
// It's being used when the `ConfigurationReadOnly.GetEnableDynamicHandler` is true.
func NewDynamicHandler(config context.ConfigurationReadOnly, logger *golog.Logger) RequestHandler /* #2167 */ {
handler := NewDefaultHandler(config, logger)
return wrapDynamicHandler(handler)
}
func wrapDynamicHandler(handler RequestHandler) RequestHandler {
return &routerHandlerDynamic{
RequestHandler: handler,
}
}
// NewDefaultHandler returns the handler which is responsible // NewDefaultHandler returns the handler which is responsible
// to map the request with a route (aka mux implementation). // to map the request with a route (aka mux implementation).
@ -71,6 +179,7 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge
fireMethodNotAllowed bool fireMethodNotAllowed bool
enablePathIntelligence bool enablePathIntelligence bool
forceLowercaseRouting bool forceLowercaseRouting bool
dynamicHandlerEnabled bool
) )
if config != nil { // #2147 if config != nil { // #2147
@ -79,9 +188,10 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge
fireMethodNotAllowed = config.GetFireMethodNotAllowed() fireMethodNotAllowed = config.GetFireMethodNotAllowed()
enablePathIntelligence = config.GetEnablePathIntelligence() enablePathIntelligence = config.GetEnablePathIntelligence()
forceLowercaseRouting = config.GetForceLowercaseRouting() forceLowercaseRouting = config.GetForceLowercaseRouting()
dynamicHandlerEnabled = config.GetEnableDynamicHandler()
} }
return &routerHandler{ handler := &routerHandler{
disablePathCorrection: disablePathCorrection, disablePathCorrection: disablePathCorrection,
disablePathCorrectionRedirection: disablePathCorrectionRedirection, disablePathCorrectionRedirection: disablePathCorrectionRedirection,
fireMethodNotAllowed: fireMethodNotAllowed, fireMethodNotAllowed: fireMethodNotAllowed,
@ -89,6 +199,12 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge
forceLowercaseRouting: forceLowercaseRouting, forceLowercaseRouting: forceLowercaseRouting,
logger: logger, logger: logger,
} }
if dynamicHandlerEnabled {
return wrapDynamicHandler(handler)
}
return handler
} }
func (h *routerHandler) getTree(statusCode int, method, subdomain string) *trie { func (h *routerHandler) getTree(statusCode int, method, subdomain string) *trie {

View File

@ -12,6 +12,8 @@ import (
func TestRegisterRule(t *testing.T) { func TestRegisterRule(t *testing.T) {
app := iris.New() app := iris.New()
app.Configure(iris.WithDynamicHandler)
// collect the error on RouteError rule. // collect the error on RouteError rule.
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
app.Logger().SetTimeFormat("").DisableNewLine().SetOutput(buf) app.Logger().SetTimeFormat("").DisableNewLine().SetOutput(buf)

View File

@ -49,23 +49,21 @@ func NewRouter() *Router {
// RefreshRouter re-builds the router. Should be called when a route's state // RefreshRouter re-builds the router. Should be called when a route's state
// changed (i.e Method changed at serve-time). // changed (i.e Method changed at serve-time).
//
// Note that in order to use RefreshRouter while in serve-time,
// you have to set the `EnableDynamicHandler` Iris Application setting to true,
// e.g. `app.Listen(":8080", iris.WithEnableDynamicHandler)`
func (router *Router) RefreshRouter() error { func (router *Router) RefreshRouter() error {
return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider, true) return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider, true)
} }
// ErrNotRouteAdder throws on `AddRouteUnsafe` when a registered `RequestHandler`
// does not implements the optional `AddRoute(*Route) error` method.
var ErrNotRouteAdder = errors.New("request handler does not implement AddRoute method")
// AddRouteUnsafe adds a route directly to the router's request handler. // AddRouteUnsafe adds a route directly to the router's request handler.
// Works before or after Build state. // Works before or after Build state.
// Mainly used for internal cases like `iris.WithSitemap`. // Mainly used for internal cases like `iris.WithSitemap`.
// Do NOT use it on serve-time. // Do NOT use it on serve-time.
func (router *Router) AddRouteUnsafe(routes ...*Route) error { func (router *Router) AddRouteUnsafe(routes ...*Route) error {
if h := router.requestHandler; h != nil { if h := router.requestHandler; h != nil {
if v, ok := h.(interface { if v, ok := h.(RouteAdder); ok {
AddRoute(*Route) error
}); ok {
for _, r := range routes { for _, r := range routes {
return v.AddRoute(r) return v.AddRoute(r)
} }