mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +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.
|
||||
|
||||
- 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.
|
||||
|
||||
# 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"
|
||||
// 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
|
||||
// on the Timeout configuration field.
|
||||
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.
|
||||
// See the `WithTunneling` Configurator too.
|
||||
Tunneling TunnelingConfiguration `ini:"tunneling" json:"tunneling,omitempty" yaml:"Tunneling" toml:"Tunneling"`
|
||||
|
@ -994,6 +1005,11 @@ func (c *Configuration) GetTimeout() time.Duration {
|
|||
return c.Timeout
|
||||
}
|
||||
|
||||
// GetNonBlocking returns the NonBlocking field.
|
||||
func (c *Configuration) GetNonBlocking() bool {
|
||||
return c.NonBlocking
|
||||
}
|
||||
|
||||
// GetTimeoutMessage returns the TimeoutMessage field.
|
||||
func (c *Configuration) GetTimeoutMessage() string {
|
||||
return c.TimeoutMessage
|
||||
|
@ -1201,6 +1217,10 @@ func WithConfiguration(c Configuration) Configurator {
|
|||
main.TimeoutMessage = v
|
||||
}
|
||||
|
||||
if v := c.NonBlocking; v {
|
||||
main.NonBlocking = v
|
||||
}
|
||||
|
||||
if len(c.Tunneling.Tunnels) > 0 {
|
||||
main.Tunneling = c.Tunneling
|
||||
}
|
||||
|
@ -1375,6 +1395,7 @@ func DefaultConfiguration() Configuration {
|
|||
KeepAlive: 0,
|
||||
Timeout: 0,
|
||||
TimeoutMessage: DefaultTimeoutMessage,
|
||||
NonBlocking: false,
|
||||
DisableStartupLog: false,
|
||||
DisableInterruptHandler: false,
|
||||
DisablePathCorrection: false,
|
||||
|
|
|
@ -24,6 +24,8 @@ type ConfigurationReadOnly interface {
|
|||
GetTimeout() time.Duration
|
||||
// GetTimeoutMessage returns the TimeoutMessage field.
|
||||
GetTimeoutMessage() string
|
||||
// GetNonBlocking returns the NonBlocking field.
|
||||
GetNonBlocking() bool
|
||||
// GetDisablePathCorrection returns the DisablePathCorrection field
|
||||
GetDisablePathCorrection() bool
|
||||
// GetDisablePathCorrectionRedirection returns the DisablePathCorrectionRedirection field.
|
||||
|
|
|
@ -184,7 +184,7 @@ func ResolveVHost(addr string) string {
|
|||
}
|
||||
|
||||
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:]
|
||||
} 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" {
|
||||
|
|
131
iris.go
131
iris.go
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -109,6 +110,8 @@ type Application struct {
|
|||
// Hosts field is available after `Run` or `NewHost`.
|
||||
Hosts []*host.Supervisor
|
||||
hostConfigurators []host.Configurator
|
||||
runError error
|
||||
runErrorMu sync.RWMutex
|
||||
}
|
||||
|
||||
// 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()
|
||||
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 {
|
||||
app.logger.Debugf("Host[%d]: Shutdown now", i)
|
||||
if err := su.Shutdown(ctx); err != nil {
|
||||
|
@ -1006,7 +1013,8 @@ var (
|
|||
// on the TCP network address "host:port" which
|
||||
// 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)`
|
||||
// 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 */)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
app.logger.Error(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
|
||||
func (app *Application) tryStartTunneling() {
|
||||
if len(app.config.Tunneling.Tunnels) == 0 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user