diff --git a/_examples/routing/route-state/main.go b/_examples/routing/route-state/main.go index 6c362c1159..d59b8c8104 100644 --- a/_examples/routing/route-state/main.go +++ b/_examples/routing/route-state/main.go @@ -41,5 +41,5 @@ func main() { ctx.Exec("GET", "/invisible/iris") }) - app.Listen(":8080") + app.Listen(":8080", iris.WithDynamicHandler) } diff --git a/configuration.go b/configuration.go index 931a5a442d..81377bcaa5 100644 --- a/configuration.go +++ b/configuration.go @@ -308,6 +308,14 @@ var WithLowercaseRouting = func(app *Application) { 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. // // See `Configuration`. @@ -737,6 +745,14 @@ type Configuration struct { // // Defaults to false. 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 // fires the 405 error instead of 404 // Defaults to false. @@ -1008,6 +1024,11 @@ func (c *Configuration) GetForceLowercaseRouting() bool { return c.ForceLowercaseRouting } +// GetEnableDynamicHandler returns the EnableDynamicHandler field. +func (c *Configuration) GetEnableDynamicHandler() bool { + return c.EnableDynamicHandler +} + // GetFireMethodNotAllowed returns the FireMethodNotAllowed field. func (c *Configuration) GetFireMethodNotAllowed() bool { return c.FireMethodNotAllowed diff --git a/context/configuration.go b/context/configuration.go index 255aab4dad..0e7554d8d6 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -34,6 +34,8 @@ type ConfigurationReadOnly interface { GetEnablePathEscape() bool // GetForceLowercaseRouting returns the ForceLowercaseRouting field. GetForceLowercaseRouting() bool + // GetEnableOptimizations returns the EnableDynamicHandler field. + GetEnableDynamicHandler() bool // GetFireMethodNotAllowed returns the FireMethodNotAllowed field. GetFireMethodNotAllowed() bool // GetDisableAutoFireStatusCode returns the DisableAutoFireStatusCode field. diff --git a/core/router/handler.go b/core/router/handler.go index 34c449312d..024d0f5292 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -1,10 +1,13 @@ package router import ( + "errors" "fmt" "net/http" "sort" "strings" + "sync" + "sync/atomic" "time" "github.com/kataras/iris/v12/context" @@ -39,8 +42,18 @@ type ( // on the given context's response status code. 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 { // Config. 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. } -var _ RequestHandler = (*routerHandler)(nil) -var _ HTTPErrorHandler = (*routerHandler)(nil) +var ( + _ 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 // to map the request with a route (aka mux implementation). @@ -71,6 +179,7 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge fireMethodNotAllowed bool enablePathIntelligence bool forceLowercaseRouting bool + dynamicHandlerEnabled bool ) if config != nil { // #2147 @@ -79,9 +188,10 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge fireMethodNotAllowed = config.GetFireMethodNotAllowed() enablePathIntelligence = config.GetEnablePathIntelligence() forceLowercaseRouting = config.GetForceLowercaseRouting() + dynamicHandlerEnabled = config.GetEnableDynamicHandler() } - return &routerHandler{ + handler := &routerHandler{ disablePathCorrection: disablePathCorrection, disablePathCorrectionRedirection: disablePathCorrectionRedirection, fireMethodNotAllowed: fireMethodNotAllowed, @@ -89,6 +199,12 @@ func NewDefaultHandler(config context.ConfigurationReadOnly, logger *golog.Logge forceLowercaseRouting: forceLowercaseRouting, logger: logger, } + + if dynamicHandlerEnabled { + return wrapDynamicHandler(handler) + } + + return handler } func (h *routerHandler) getTree(statusCode int, method, subdomain string) *trie { diff --git a/core/router/route_register_rule_test.go b/core/router/route_register_rule_test.go index 9fd8ce1aae..68ff259541 100644 --- a/core/router/route_register_rule_test.go +++ b/core/router/route_register_rule_test.go @@ -12,6 +12,8 @@ import ( func TestRegisterRule(t *testing.T) { app := iris.New() + app.Configure(iris.WithDynamicHandler) + // collect the error on RouteError rule. buf := new(bytes.Buffer) app.Logger().SetTimeFormat("").DisableNewLine().SetOutput(buf) diff --git a/core/router/router.go b/core/router/router.go index e3b33671c2..25d84ad444 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -49,23 +49,21 @@ func NewRouter() *Router { // RefreshRouter re-builds the router. Should be called when a route's state // 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 { 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. // Works before or after Build state. // Mainly used for internal cases like `iris.WithSitemap`. // Do NOT use it on serve-time. func (router *Router) AddRouteUnsafe(routes ...*Route) error { if h := router.requestHandler; h != nil { - if v, ok := h.(interface { - AddRoute(*Route) error - }); ok { + if v, ok := h.(RouteAdder); ok { for _, r := range routes { return v.AddRoute(r) }