new NonBlocking option and Wait method on Application. See HISTORY.md for more

This commit is contained in:
Gerasimos (Makis) Maropoulos 2024-01-14 06:58:17 +02:00
parent 5ef854d835
commit 70882914d4
No known key found for this signature in database
GPG Key ID: B9839E9CD30B7B6B
5 changed files with 174 additions and 4 deletions

View File

@ -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

View File

@ -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,

View File

@ -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.

View File

@ -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
View File

@ -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 {