From d6cfe3fe5ba4184fface14338a147b0c00cdb88e Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 9 Dec 2021 14:44:03 +0200 Subject: [PATCH] new Timeout, TimeoutMessage configuration fields and apps.OnApplicationRegistered listener --- HISTORY.md | 4 ++++ apps/apps.go | 12 +++++++++++ configuration.go | 44 ++++++++++++++++++++++++++++++++++++++- context/application.go | 19 +++++++++++++++-- context/configuration.go | 4 ++++ context/context.go | 12 +++++++---- core/router/router.go | 20 ++++++++++++++++++ iris.go | 5 +++++ middleware/pprof/pprof.go | 11 ++++++++++ 9 files changed, 124 insertions(+), 7 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index c28820c7..ab402039 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,10 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- New `Configuration.Timeout` and `Configuration.TimeoutMessage` fields. Use it to set HTTP timeouts. Note that your http server's (`Application.ConfigureHost`) Read/Write timeouts should be a bit higher than the `Configuration.Timeout` in order to give some time to http timeout handler to kick in and be able to send the `Configuration.TimeoutMessage` properly. + +- New `apps.OnApplicationRegistered` method which listens on new Iris applications hosted under the same binary. Use it on your `init` functions to configure Iris applications by any spot in your project's files. + - `Context.JSON` respects any object implements the `easyjson.Marshaler` interface and renders the result using the [easyjon](https://github.com/mailru/easyjson)'s writer. - minor: `Context` structure implements the standard go Context interface now (includes: Deadline, Done, Err and Value methods). Handlers can now just pass the `ctx iris.Context` as a shortcut of `ctx.Request().Context()` when needed. diff --git a/apps/apps.go b/apps/apps.go index 5088f180..90a07c5d 100644 --- a/apps/apps.go +++ b/apps/apps.go @@ -48,3 +48,15 @@ func GetAll() []*iris.Application { return apps } + +// OnApplicationRegistered adds a function which fires when a new application +// is registered. +func OnApplicationRegistered(listeners ...func(app *iris.Application)) { + appListeners := make([]func(context.Application), 0, len(listeners)) + for i := range listeners { + appListeners = append(appListeners, func(ctxApp context.Application) { + listeners[i](ctxApp.(*iris.Application)) + }) + } + context.OnApplicationRegistered(appListeners...) +} diff --git a/configuration.go b/configuration.go index 14d77a36..19adcd8a 100644 --- a/configuration.go +++ b/configuration.go @@ -208,6 +208,14 @@ func WithKeepAlive(keepAliveDur time.Duration) Configurator { } } +// WithTimeout sets the `Configuration.Timeout` field to the given duration. +func WithTimeout(timeoutDur time.Duration, htmlBody string) Configurator { + return func(app *Application) { + app.config.Timeout = timeoutDur + app.config.TimeoutMessage = htmlBody + } +} + // WithoutServerError will cause to ignore the matched "errors" // from the main application's `Run/Listen` function. // @@ -498,7 +506,6 @@ func WithSitemap(startURL string) Configurator { } else { href = "/" + langPath + loc } - } else if app.I18n.Subdomain { // then use the subdomain. // e.g. http://el.domain.com/path @@ -627,6 +634,17 @@ type Configuration struct { // // Defaults to 0. KeepAlive time.Duration `ini:"keepalive" json:"keepAlive" yaml:"KeepAlive" toml:"KeepAlive" env:"KEEP_ALIVE"` + // Timeout wraps the application's router with an http timeout handler + // if the value is greater than zero. + // + // The underline response writer supports the Pusher interface but does not support + // the Hijacker or Flusher interfaces when Timeout handler is registered. + // + // Read more at: https://pkg.go.dev/net/http#TimeoutHandler. + Timeout time.Duration `ini:"timeout" json:"timeout" yaml:"Timeout" toml:"Timeout"` + // TimeoutMessage specifies the HTML body when a handler hits its life time based + // on the Timeout configuration field. + TimeoutMessage string `ini:"timeout_message" json:"timeoutMessage" yaml:"TimeoutMessage" toml:"TimeoutMessage"` // Tunneling can be optionally set to enable ngrok http(s) tunneling for this Iris app instance. // See the `WithTunneling` Configurator too. Tunneling TunnelingConfiguration `ini:"tunneling" json:"tunneling,omitempty" yaml:"Tunneling" toml:"Tunneling"` @@ -914,6 +932,16 @@ func (c Configuration) GetKeepAlive() time.Duration { return c.KeepAlive } +// GetKeepAlive returns the Timeout field. +func (c Configuration) GetTimeout() time.Duration { + return c.Timeout +} + +// GetKeepAlive returns the TimeoutMessage field. +func (c Configuration) GetTimeoutMessage() string { + return c.TimeoutMessage +} + // GetDisablePathCorrection returns the DisablePathCorrection field. func (c Configuration) GetDisablePathCorrection() bool { return c.DisablePathCorrection @@ -1088,6 +1116,14 @@ func WithConfiguration(c Configuration) Configurator { main.KeepAlive = v } + if v := c.Timeout; v > 0 { + main.Timeout = v + } + + if v := c.TimeoutMessage; v != "" { + main.TimeoutMessage = v + } + if len(c.Tunneling.Tunnels) > 0 { main.Tunneling = c.Tunneling } @@ -1234,12 +1270,18 @@ func WithConfiguration(c Configuration) Configurator { } } +// DefaultTimeoutMessage is the default timeout message which is rendered +// on expired handlers when timeout handler is registered (see Timeout configuration field). +var DefaultTimeoutMessage = `Timeout

Timeout

Looks like the server is taking too long to respond, this can be caused by either poor connectivity or an error with our servers. Please try again in a while.` + // DefaultConfiguration returns the default configuration for an iris station, fills the main Configuration func DefaultConfiguration() Configuration { return Configuration{ LogLevel: "info", SocketSharding: false, KeepAlive: 0, + Timeout: 0, + TimeoutMessage: DefaultTimeoutMessage, DisableStartupLog: false, DisableInterruptHandler: false, DisablePathCorrection: false, diff --git a/context/application.go b/context/application.go index 81d35413..eddfbff6 100644 --- a/context/application.go +++ b/context/application.go @@ -113,8 +113,9 @@ var ( // It's slice instead of map because if IRIS_APP_NAME env var exists, // by-default all applications running on the same machine // will have the same name unless `Application.SetName` is called. - registeredApps []Application - mu sync.RWMutex + registeredApps []Application + onApplicationRegisteredListeners []func(Application) + mu sync.RWMutex ) // RegisterApplication registers an application to the global shared storage. @@ -126,6 +127,20 @@ func RegisterApplication(app Application) { mu.Lock() registeredApps = append(registeredApps, app) mu.Unlock() + + mu.RLock() + for _, listener := range onApplicationRegisteredListeners { + listener(app) + } + mu.RUnlock() +} + +// OnApplicationRegistered adds a function which fires when a new application +// is registered. +func OnApplicationRegistered(listeners ...func(app Application)) { + mu.Lock() + onApplicationRegisteredListeners = append(onApplicationRegisteredListeners, listeners...) + mu.Unlock() } // GetApplications returns a slice of all the registered Applications. diff --git a/context/configuration.go b/context/configuration.go index 9cc03d37..67df7e55 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -20,6 +20,10 @@ type ConfigurationReadOnly interface { GetSocketSharding() bool // GetKeepAlive returns the KeepAlive field. GetKeepAlive() time.Duration + // GetKeepAlive returns the Timeout field. + GetTimeout() time.Duration + // GetKeepAlive returns the TimeoutMessage field. + GetTimeoutMessage() string // GetDisablePathCorrection returns the DisablePathCorrection field GetDisablePathCorrection() bool // GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field. diff --git a/context/context.go b/context/context.go index 044f7dc1..4e01695d 100644 --- a/context/context.go +++ b/context/context.go @@ -708,7 +708,6 @@ func (ctx *Context) StopExecution() { // And stop. ctx.currentHandlerIndex = stopExecutionIndex } - } // IsStopped reports whether the current position of the context's handlers is -1, @@ -2697,7 +2696,6 @@ func (ctx *Context) ReadMsgPack(ptr interface{}) error { // As a special case if the "ptr" was a pointer to string or []byte // then it will bind it to the request body as it is. func (ctx *Context) ReadBody(ptr interface{}) error { - // If the ptr is string or byte, read the body as it's. switch v := ptr.(type) { case *string: @@ -4871,7 +4869,6 @@ func CookieAllowReclaim(cookieNames ...string) CookieOption { header.Del("Cookie") } } - } // CookieAllowSubdomains set to the Cookie Options @@ -5888,5 +5885,12 @@ func (ctx *Context) GetID() interface{} { // It returns the Context's ID given by a `SetID`call, // followed by the client's IP and the method:uri. func (ctx *Context) String() string { - return fmt.Sprintf("[%s] %s ▶ %s:%s", ctx.GetID(), ctx.RemoteAddr(), ctx.Method(), ctx.Request().RequestURI) + id := ctx.GetID() + if id != nil { + if stringer, ok := id.(fmt.Stringer); ok { + id = stringer.String() + } + } + + return fmt.Sprintf("[%v] %s ▶ %s:%s", id, ctx.RemoteAddr(), ctx.Method(), ctx.Request().RequestURI) } diff --git a/core/router/router.go b/core/router/router.go index 98488135..7c9897c1 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -6,6 +6,7 @@ import ( "sort" "strings" "sync" + "time" "github.com/kataras/iris/v12/context" @@ -271,6 +272,25 @@ func (router *Router) Downgraded() bool { return router.mainHandler != nil && router.requestHandler == nil } +// SetTimeoutHandler overrides the main handler with a timeout handler. +// +// TimeoutHandler supports the Pusher interface but does not support +// the Hijacker or Flusher interfaces. +// +// All previous registered wrappers and middlewares are still executed as expected. +func (router *Router) SetTimeoutHandler(timeout time.Duration, msg string) { + if timeout <= 0 { + return + } + + mainHandler := router.mainHandler + h := func(w http.ResponseWriter, r *http.Request) { + mainHandler(w, r) + } + + router.mainHandler = http.TimeoutHandler(http.HandlerFunc(h), timeout, msg).ServeHTTP +} + // WrapRouter adds a wrapper on the top of the main router. // Usually it's useful for third-party middleware // when need to wrap the entire application with a middleware like CORS. diff --git a/iris.go b/iris.go index 976c99f4..c1035858 100644 --- a/iris.go +++ b/iris.go @@ -640,6 +640,11 @@ func (app *Application) Build() error { return err } app.HTTPErrorHandler = routerHandler + + if app.config.Timeout > 0 { + app.Router.SetTimeoutHandler(app.config.Timeout, app.config.TimeoutMessage) + } + // re-build of the router from outside can be done with // app.RefreshRouter() } diff --git a/middleware/pprof/pprof.go b/middleware/pprof/pprof.go index f8551f33..ec9b83ee 100644 --- a/middleware/pprof/pprof.go +++ b/middleware/pprof/pprof.go @@ -12,6 +12,17 @@ import ( func init() { context.SetHandlerName("iris/middleware/pprof.*", "iris.profiling") + + /* for our readers: + apps.OnApplicationRegistered(func(app *iris.Application) { + app.Any("/debug/pprof/cmdline", iris.FromStd(pprof.Cmdline)) + app.Any("/debug/pprof/profile", iris.FromStd(pprof.Profile)) + app.Any("/debug/pprof/symbol", iris.FromStd(pprof.Symbol)) + app.Any("/debug/pprof/trace", iris.FromStd(pprof.Trace)) + + app.Any("/debug/pprof /debug/pprof/{action:string}", New()) + }) + */ } // net/http/pprof copy: