mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 07:20:35 +01:00
new NonBlocking option and Wait method on Application. See HISTORY.md for more
This commit is contained in:
parent
5ef854d835
commit
70882914d4
22
HISTORY.md
22
HISTORY.md
|
@ -23,6 +23,28 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene
|
||||||
|
|
||||||
Changes apply to `main` branch.
|
Changes apply to `main` branch.
|
||||||
|
|
||||||
|
- New `iris.NonBlocking()` configuration option to run the server without blocking the main routine, `Application.Wait(context.Context) error` method can be used to block and wait for the server to be up and running. Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
app.Get("/", func(ctx iris.Context) {
|
||||||
|
ctx.Writef("Hello, %s!", "World")
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Listen(":8080", iris.NonBlocking())
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := app.Wait(ctx); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Server is up and running now, you may continue with other functions below].
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- Add `x/mathx.RoundToInteger` math helper function.
|
- Add `x/mathx.RoundToInteger` math helper function.
|
||||||
|
|
||||||
# Wed, 10 Jan 2024 | v12.2.9
|
# Wed, 10 Jan 2024 | v12.2.9
|
||||||
|
|
|
@ -216,6 +216,13 @@ func WithTimeout(timeoutDur time.Duration, htmlBody ...string) Configurator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NonBlocking sets the `Configuration.NonBlocking` field to true.
|
||||||
|
func NonBlocking() Configurator {
|
||||||
|
return func(app *Application) {
|
||||||
|
app.config.NonBlocking = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithoutServerError will cause to ignore the matched "errors"
|
// WithoutServerError will cause to ignore the matched "errors"
|
||||||
// from the main application's `Run/Listen` function.
|
// from the main application's `Run/Listen` function.
|
||||||
//
|
//
|
||||||
|
@ -677,6 +684,10 @@ type Configuration struct {
|
||||||
// TimeoutMessage specifies the HTML body when a handler hits its life time based
|
// TimeoutMessage specifies the HTML body when a handler hits its life time based
|
||||||
// on the Timeout configuration field.
|
// on the Timeout configuration field.
|
||||||
TimeoutMessage string `ini:"timeout_message" json:"timeoutMessage" yaml:"TimeoutMessage" toml:"TimeoutMessage"`
|
TimeoutMessage string `ini:"timeout_message" json:"timeoutMessage" yaml:"TimeoutMessage" toml:"TimeoutMessage"`
|
||||||
|
// NonBlocking, if set to true then the server will start listening for incoming connections
|
||||||
|
// without blocking the main goroutine. Use the Application.Wait method to block and wait for the server to be up and running.
|
||||||
|
NonBlocking bool `ini:"non_blocking" json:"nonBlocking" yaml:"NonBlocking" toml:"NonBlocking"`
|
||||||
|
|
||||||
// Tunneling can be optionally set to enable ngrok http(s) tunneling for this Iris app instance.
|
// Tunneling can be optionally set to enable ngrok http(s) tunneling for this Iris app instance.
|
||||||
// See the `WithTunneling` Configurator too.
|
// See the `WithTunneling` Configurator too.
|
||||||
Tunneling TunnelingConfiguration `ini:"tunneling" json:"tunneling,omitempty" yaml:"Tunneling" toml:"Tunneling"`
|
Tunneling TunnelingConfiguration `ini:"tunneling" json:"tunneling,omitempty" yaml:"Tunneling" toml:"Tunneling"`
|
||||||
|
@ -994,6 +1005,11 @@ func (c *Configuration) GetTimeout() time.Duration {
|
||||||
return c.Timeout
|
return c.Timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNonBlocking returns the NonBlocking field.
|
||||||
|
func (c *Configuration) GetNonBlocking() bool {
|
||||||
|
return c.NonBlocking
|
||||||
|
}
|
||||||
|
|
||||||
// GetTimeoutMessage returns the TimeoutMessage field.
|
// GetTimeoutMessage returns the TimeoutMessage field.
|
||||||
func (c *Configuration) GetTimeoutMessage() string {
|
func (c *Configuration) GetTimeoutMessage() string {
|
||||||
return c.TimeoutMessage
|
return c.TimeoutMessage
|
||||||
|
@ -1201,6 +1217,10 @@ func WithConfiguration(c Configuration) Configurator {
|
||||||
main.TimeoutMessage = v
|
main.TimeoutMessage = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v := c.NonBlocking; v {
|
||||||
|
main.NonBlocking = v
|
||||||
|
}
|
||||||
|
|
||||||
if len(c.Tunneling.Tunnels) > 0 {
|
if len(c.Tunneling.Tunnels) > 0 {
|
||||||
main.Tunneling = c.Tunneling
|
main.Tunneling = c.Tunneling
|
||||||
}
|
}
|
||||||
|
@ -1375,6 +1395,7 @@ func DefaultConfiguration() Configuration {
|
||||||
KeepAlive: 0,
|
KeepAlive: 0,
|
||||||
Timeout: 0,
|
Timeout: 0,
|
||||||
TimeoutMessage: DefaultTimeoutMessage,
|
TimeoutMessage: DefaultTimeoutMessage,
|
||||||
|
NonBlocking: false,
|
||||||
DisableStartupLog: false,
|
DisableStartupLog: false,
|
||||||
DisableInterruptHandler: false,
|
DisableInterruptHandler: false,
|
||||||
DisablePathCorrection: false,
|
DisablePathCorrection: false,
|
||||||
|
|
|
@ -24,6 +24,8 @@ type ConfigurationReadOnly interface {
|
||||||
GetTimeout() time.Duration
|
GetTimeout() time.Duration
|
||||||
// GetTimeoutMessage returns the TimeoutMessage field.
|
// GetTimeoutMessage returns the TimeoutMessage field.
|
||||||
GetTimeoutMessage() string
|
GetTimeoutMessage() string
|
||||||
|
// GetNonBlocking returns the NonBlocking field.
|
||||||
|
GetNonBlocking() bool
|
||||||
// GetDisablePathCorrection returns the DisablePathCorrection field
|
// GetDisablePathCorrection returns the DisablePathCorrection field
|
||||||
GetDisablePathCorrection() bool
|
GetDisablePathCorrection() bool
|
||||||
// GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field.
|
// GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field.
|
||||||
|
|
|
@ -184,7 +184,7 @@ func ResolveVHost(addr string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx := strings.IndexByte(addr, ':'); idx == 0 {
|
if idx := strings.IndexByte(addr, ':'); idx == 0 {
|
||||||
// only port, then return the 0.0.0.0
|
// only port, then return the 0.0.0.0:PORT
|
||||||
return /* "0.0.0.0" */ "localhost" + addr[idx:]
|
return /* "0.0.0.0" */ "localhost" + addr[idx:]
|
||||||
} else if idx > 0 { // if 0.0.0.0:80 let's just convert it to localhost.
|
} else if idx > 0 { // if 0.0.0.0:80 let's just convert it to localhost.
|
||||||
if addr[0:idx] == "0.0.0.0" {
|
if addr[0:idx] == "0.0.0.0" {
|
||||||
|
|
131
iris.go
131
iris.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -109,6 +110,8 @@ type Application struct {
|
||||||
// Hosts field is available after `Run` or `NewHost`.
|
// Hosts field is available after `Run` or `NewHost`.
|
||||||
Hosts []*host.Supervisor
|
Hosts []*host.Supervisor
|
||||||
hostConfigurators []host.Configurator
|
hostConfigurators []host.Configurator
|
||||||
|
runError error
|
||||||
|
runErrorMu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates and returns a fresh empty iris *Application instance.
|
// New creates and returns a fresh empty iris *Application instance.
|
||||||
|
@ -627,6 +630,10 @@ func (app *Application) Shutdown(ctx stdContext.Context) error {
|
||||||
app.mu.Lock()
|
app.mu.Lock()
|
||||||
defer app.mu.Unlock()
|
defer app.mu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
app.setRunError(ErrServerClosed) // make sure to set the error so any .Wait calls return.
|
||||||
|
}()
|
||||||
|
|
||||||
for i, su := range app.Hosts {
|
for i, su := range app.Hosts {
|
||||||
app.logger.Debugf("Host[%d]: Shutdown now", i)
|
app.logger.Debugf("Host[%d]: Shutdown now", i)
|
||||||
if err := su.Shutdown(ctx); err != nil {
|
if err := su.Shutdown(ctx); err != nil {
|
||||||
|
@ -1006,7 +1013,8 @@ var (
|
||||||
// on the TCP network address "host:port" which
|
// on the TCP network address "host:port" which
|
||||||
// handles requests on incoming connections.
|
// handles requests on incoming connections.
|
||||||
//
|
//
|
||||||
// Listen always returns a non-nil error.
|
// Listen always returns a non-nil error except
|
||||||
|
// when NonBlocking option is being passed, so the error goes to the Wait method.
|
||||||
// Ignore specific errors by using an `iris.WithoutServerError(iris.ErrServerClosed)`
|
// Ignore specific errors by using an `iris.WithoutServerError(iris.ErrServerClosed)`
|
||||||
// as a second input argument.
|
// as a second input argument.
|
||||||
//
|
//
|
||||||
|
@ -1048,15 +1056,132 @@ func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error {
|
||||||
app.logger.Debugf("Application: running using %d host(s)", len(app.Hosts)+1 /* +1 the current */)
|
app.logger.Debugf("Application: running using %d host(s)", len(app.Hosts)+1 /* +1 the current */)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this will block until an error(unless supervisor's DeferFlow called from a Task).
|
if app.config.NonBlocking {
|
||||||
|
go func() {
|
||||||
|
err := app.serve(serve)
|
||||||
|
if err != nil {
|
||||||
|
app.setRunError(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will block until an error(unless supervisor's DeferFlow called from a Task)
|
||||||
|
// or NonBlocking was passed (see above).
|
||||||
|
return app.serve(serve)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) serve(serve Runner) error {
|
||||||
err := serve(app)
|
err := serve(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.logger.Error(err)
|
app.logger.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *Application) setRunError(err error) {
|
||||||
|
app.runErrorMu.Lock()
|
||||||
|
app.runError = err
|
||||||
|
app.runErrorMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) getRunError() error {
|
||||||
|
app.runErrorMu.RLock()
|
||||||
|
err := app.runError
|
||||||
|
app.runErrorMu.RUnlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait blocks the main goroutine until the server application is up and running.
|
||||||
|
// Useful only when `Run` is called with `iris.NonBlocking()` option.
|
||||||
|
func (app *Application) Wait(ctx stdContext.Context) error {
|
||||||
|
if !app.config.NonBlocking {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// First check if there is an error already from the app.Run.
|
||||||
|
if err := app.getRunError(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the base for exponential backoff.
|
||||||
|
base := 2.0
|
||||||
|
|
||||||
|
// Get the maximum number of retries by context or force to 7 retries.
|
||||||
|
var maxRetries int
|
||||||
|
// Get the deadline of the context.
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
now := time.Now()
|
||||||
|
timeout := deadline.Sub(now)
|
||||||
|
|
||||||
|
maxRetries = getMaxRetries(timeout, base)
|
||||||
|
} else {
|
||||||
|
maxRetries = 7 // 256 seconds max.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the initial retry interval.
|
||||||
|
retryInterval := time.Second
|
||||||
|
|
||||||
|
return app.tryConnect(ctx, maxRetries, retryInterval, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMaxRetries calculates the maximum number of retries from the retry interval and the base.
|
||||||
|
func getMaxRetries(retryInterval time.Duration, base float64) int {
|
||||||
|
// Convert the retry interval to seconds.
|
||||||
|
seconds := retryInterval.Seconds()
|
||||||
|
// Apply the inverse formula.
|
||||||
|
retries := math.Log(seconds)/math.Log(base) - 1
|
||||||
|
return int(math.Round(retries))
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryConnect tries to connect to the server with the given context and retry parameters.
|
||||||
|
func (app *Application) tryConnect(ctx stdContext.Context, maxRetries int, retryInterval time.Duration, base float64) error {
|
||||||
|
address := app.config.GetVHost() // Get this server's listening address.
|
||||||
|
|
||||||
|
// Try to connect to the server in a loop.
|
||||||
|
for i := 0; i < maxRetries; i++ {
|
||||||
|
// Check the context before each attempt.
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Context is canceled, return the context error.
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
// Context is not canceled, proceed with the attempt.
|
||||||
|
conn, err := net.Dial("tcp", address)
|
||||||
|
if err == nil {
|
||||||
|
// Connection successful, close the connection and return nil.
|
||||||
|
conn.Close()
|
||||||
|
return nil // exit.
|
||||||
|
} // ignore error.
|
||||||
|
|
||||||
|
// Connection failed, wait for the retry interval and try again.
|
||||||
|
time.Sleep(retryInterval)
|
||||||
|
// After each failed attempt, check the server Run's error again.
|
||||||
|
if err := app.getRunError(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase the retry interval by the base raised to the power of the number of attempts.
|
||||||
|
/*
|
||||||
|
0 2 seconds
|
||||||
|
1 4 seconds
|
||||||
|
2 8 seconds
|
||||||
|
3 ~16 seconds
|
||||||
|
4 ~32 seconds
|
||||||
|
5 ~64 seconds
|
||||||
|
6 ~128 seconds
|
||||||
|
7 ~256 seconds
|
||||||
|
8 ~512 seconds
|
||||||
|
...
|
||||||
|
*/
|
||||||
|
retryInterval = time.Duration(math.Pow(base, float64(i+1))) * time.Second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All attempts failed, return an error.
|
||||||
|
return fmt.Errorf("failed to connect to the server after %d retries", maxRetries)
|
||||||
|
}
|
||||||
|
|
||||||
// https://ngrok.com/docs
|
// https://ngrok.com/docs
|
||||||
func (app *Application) tryStartTunneling() {
|
func (app *Application) tryStartTunneling() {
|
||||||
if len(app.config.Tunneling.Tunnels) == 0 {
|
if len(app.config.Tunneling.Tunnels) == 0 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user