Add the new Go 1.8 Shutdown | Remove DisableBanner, is controlled by LoggerPolicy now.

Former-commit-id: 6ef71a4b9f5a79160a42d1111dd924e244ce1f4e
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-02-17 04:46:33 +02:00
parent 2a4997cadf
commit 21a18d0990
10 changed files with 77 additions and 2104 deletions

2015
HISTORY.md

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@ Iris is an efficient and well-designed toolbox with robust set of features.<br/>
<p> <p>
<h1># Not YAWF<!-- hashtag, no title. --></h1> <h1># Not YAWF<!-- hashtag, no title. --></h1>
<a href="https://github.com/kataras/iris/blob/v6/HISTORY.md"><img src="https://img.shields.io/badge/codename-√Next%20-blue.svg?style=flat-square" alt="CHANGELOG/HISTORY"></a> <a href="https://github.com/kataras/iris/blob/v6/HISTORY.md"><img src="https://img.shields.io/badge/codename-√Νεxτ%20-blue.svg?style=flat-square" alt="CHANGELOG/HISTORY"></a>
<a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/examples-%20repository-3362c2.svg?style=flat-square" alt="Examples"></a> <a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/examples-%20repository-3362c2.svg?style=flat-square" alt="Examples"></a>
@ -264,7 +264,7 @@ Besides the fact that we have a [community chat][Chat] for questions or reports
Versioning Versioning
------------ ------------
Current: **v6**, code-named as "√Next" Current: **v6**, code-named as "√Νεxτ"
v5: https://github.com/kataras/iris/tree/5.0.0 v5: https://github.com/kataras/iris/tree/5.0.0

View File

@ -1,6 +1,7 @@
package iris package iris
import ( import (
"context"
"crypto/tls" "crypto/tls"
"net" "net"
"net/http" "net/http"
@ -282,13 +283,12 @@ var ProxyHandler = func(redirectSchemeAndHost string) http.HandlerFunc {
// nothing special, use it only when you want to start a secondary server which its only work is to redirect from one requested path to another // nothing special, use it only when you want to start a secondary server which its only work is to redirect from one requested path to another
// //
// returns a close function // returns a close function
func Proxy(proxyAddr string, redirectSchemeAndHost string) func() error { func Proxy(proxyAddr string, redirectSchemeAndHost string) func(context.Context) error {
proxyAddr = ParseHost(proxyAddr) proxyAddr = ParseHost(proxyAddr)
// override the handler and redirect all requests to this addr // override the handler and redirect all requests to this addr
h := ProxyHandler(redirectSchemeAndHost) h := ProxyHandler(redirectSchemeAndHost)
prx := New(OptionDisableBanner(true)) prx := New()
prx.Adapt(DevLogger())
prx.Adapt(RouterBuilderPolicy(func(RouteRepository, ContextPool) http.Handler { prx.Adapt(RouterBuilderPolicy(func(RouteRepository, ContextPool) http.Handler {
return h return h
@ -297,5 +297,5 @@ func Proxy(proxyAddr string, redirectSchemeAndHost string) func() error {
go prx.Listen(proxyAddr) go prx.Listen(proxyAddr)
time.Sleep(150 * time.Millisecond) time.Sleep(150 * time.Millisecond)
return func() error { return prx.Close() } return prx.Shutdown
} }

View File

@ -2,6 +2,7 @@
package iris_test package iris_test
import ( import (
"context"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
@ -152,8 +153,6 @@ func getRandomNumber(min int, max int) int {
// works as // works as
// defer listenTLS(iris.Default, hostTLS)() // defer listenTLS(iris.Default, hostTLS)()
func listenTLS(app *iris.Framework, hostTLS string) func() { func listenTLS(app *iris.Framework, hostTLS string) func() {
app.Close() // close any prev listener
app.Config.DisableBanner = true
// create the key and cert files on the fly, and delete them when this test finished // create the key and cert files on the fly, and delete them when this test finished
certFile, ferr := ioutil.TempFile("", "cert") certFile, ferr := ioutil.TempFile("", "cert")
@ -173,6 +172,8 @@ func listenTLS(app *iris.Framework, hostTLS string) func() {
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
return func() { return func() {
app.Shutdown(context.Background())
certFile.Close() certFile.Close()
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
os.Remove(certFile.Name()) os.Remove(certFile.Name())

View File

@ -158,11 +158,6 @@ type Configuration struct {
// Defaults to false. // Defaults to false.
FireMethodNotAllowed bool `yaml:"FireMethodNotAllowed"` FireMethodNotAllowed bool `yaml:"FireMethodNotAllowed"`
// DisableBanner outputs the iris banner at startup
//
// Defaults to false.
DisableBanner bool `yaml:"DisableBanner"`
// DisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders. // DisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders.
// If setted to true then it // If setted to true then it
// disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`. // disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`.
@ -315,15 +310,6 @@ var (
} }
} }
// OptionDisableBanner outputs the iris banner at startup.
//
// Defaults to false.
OptionDisableBanner = func(val bool) OptionSet {
return func(c *Configuration) {
c.DisableBanner = val
}
}
// OptionDisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders. // OptionDisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders.
// If setted to true then it // If setted to true then it
// disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`. // disables the body consumption by the `context.UnmarshalBody/ReadJSON/ReadXML`.
@ -416,7 +402,6 @@ func DefaultConfiguration() Configuration {
DisablePathCorrection: DefaultDisablePathCorrection, DisablePathCorrection: DefaultDisablePathCorrection,
EnablePathEscape: DefaultEnablePathEscape, EnablePathEscape: DefaultEnablePathEscape,
FireMethodNotAllowed: false, FireMethodNotAllowed: false,
DisableBanner: false,
DisableBodyConsumptionOnUnmarshal: false, DisableBodyConsumptionOnUnmarshal: false,
TimeFormat: DefaultTimeFormat, TimeFormat: DefaultTimeFormat,
Charset: DefaultCharset, Charset: DefaultCharset,

View File

@ -29,12 +29,12 @@ func TestConfigurationStatic(t *testing.T) {
t.Fatalf("Configuration should be not equal, got: %#v", afterNew) t.Fatalf("Configuration should be not equal, got: %#v", afterNew)
} }
app = New(Configuration{DisableBanner: true}) app = New(Configuration{DisableBodyConsumptionOnUnmarshal: true})
afterNew = *app.Config afterNew = *app.Config
if app.Config.DisableBanner == false { if app.Config.DisableBodyConsumptionOnUnmarshal == false {
t.Fatalf("Passing a Configuration field as Option fails, expected DisableBanner to be true but was false") t.Fatalf("Passing a Configuration field as Option fails, expected DisableBodyConsumptionOnUnmarshal to be true but was false")
} }
app = New() // empty , means defaults so app = New() // empty , means defaults so
@ -47,21 +47,21 @@ func TestConfigurationOptions(t *testing.T) {
charset := "MYCHARSET" charset := "MYCHARSET"
disableBanner := true disableBanner := true
app := New(OptionCharset(charset), OptionDisableBanner(disableBanner)) app := New(OptionCharset(charset), OptionDisableBodyConsumptionOnUnmarshal(disableBanner))
if got := app.Config.Charset; got != charset { if got := app.Config.Charset; got != charset {
t.Fatalf("Expected configuration Charset to be: %s but got: %s", charset, got) t.Fatalf("Expected configuration Charset to be: %s but got: %s", charset, got)
} }
if got := app.Config.DisableBanner; got != disableBanner { if got := app.Config.DisableBodyConsumptionOnUnmarshal; got != disableBanner {
t.Fatalf("Expected configuration DisableBanner to be: %#v but got: %#v", disableBanner, got) t.Fatalf("Expected configuration DisableBodyConsumptionOnUnmarshal to be: %#v but got: %#v", disableBanner, got)
} }
// now check if other default values are setted (should be setted automatically) // now check if other default values are setted (should be setted automatically)
expected := DefaultConfiguration() expected := DefaultConfiguration()
expected.Charset = charset expected.Charset = charset
expected.DisableBanner = disableBanner expected.DisableBodyConsumptionOnUnmarshal = disableBanner
has := *app.Config has := *app.Config
if !reflect.DeepEqual(has, expected) { if !reflect.DeepEqual(has, expected) {
@ -74,11 +74,11 @@ func TestConfigurationOptionsDeep(t *testing.T) {
disableBanner := true disableBanner := true
vhost := "mydomain.com" vhost := "mydomain.com"
// first charset,disableBanner and profilepath, no canonical order. // first charset,disableBanner and profilepath, no canonical order.
app := New(OptionCharset(charset), OptionDisableBanner(disableBanner), OptionVHost(vhost)) app := New(OptionCharset(charset), OptionDisableBodyConsumptionOnUnmarshal(disableBanner), OptionVHost(vhost))
expected := DefaultConfiguration() expected := DefaultConfiguration()
expected.Charset = charset expected.Charset = charset
expected.DisableBanner = disableBanner expected.DisableBodyConsumptionOnUnmarshal = disableBanner
expected.VHost = vhost expected.VHost = vhost
has := *app.Config has := *app.Config
@ -112,7 +112,7 @@ func TestConfigurationYAML(t *testing.T) {
DisablePathCorrection: false DisablePathCorrection: false
EnablePathEscape: false EnablePathEscape: false
FireMethodNotAllowed: true FireMethodNotAllowed: true
DisableBanner: true DisableBodyConsumptionOnUnmarshal: true
DisableBodyConsumptionOnUnmarshal: true DisableBodyConsumptionOnUnmarshal: true
TimeFormat: Mon, 01 Jan 2006 15:04:05 GMT TimeFormat: Mon, 01 Jan 2006 15:04:05 GMT
Charset: UTF-8 Charset: UTF-8
@ -161,8 +161,8 @@ func TestConfigurationYAML(t *testing.T) {
t.Fatalf("error on TestConfigurationYAML: Expected FireMethodNotAllowed %v but got %v", expected, c.FireMethodNotAllowed) t.Fatalf("error on TestConfigurationYAML: Expected FireMethodNotAllowed %v but got %v", expected, c.FireMethodNotAllowed)
} }
if expected := true; c.DisableBanner != expected { if expected := true; c.DisableBodyConsumptionOnUnmarshal != expected {
t.Fatalf("error on TestConfigurationYAML: Expected DisableBanner %v but got %v", expected, c.DisableBanner) t.Fatalf("error on TestConfigurationYAML: Expected DisableBodyConsumptionOnUnmarshal %v but got %v", expected, c.DisableBodyConsumptionOnUnmarshal)
} }
if expected := true; c.DisableBodyConsumptionOnUnmarshal != expected { if expected := true; c.DisableBodyConsumptionOnUnmarshal != expected {

View File

@ -78,8 +78,6 @@ func New(app *iris.Framework, t *testing.T, setters ...OptionSetter) *httpexpect
setter.Set(conf) setter.Set(conf)
} }
app.Set(iris.OptionDisableBanner(true))
app.Adapt(iris.DevLogger())
baseURL := "" baseURL := ""
app.Boot() app.Boot()

100
iris.go
View File

@ -9,6 +9,7 @@
package iris package iris
import ( import (
"context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
@ -33,12 +34,14 @@ const (
// Version is the current version number of the Iris web framework // Version is the current version number of the Iris web framework
Version = "6.2.0" Version = "6.2.0"
codeName = `Νεxτ`
banner = ` _____ _ banner = ` _____ _
|_ _| (_) |_ _| (_)
| | ____ _ ___ | | ____ _ ___
| | | __|| |/ __| | | | __|| |/ __|
_| |_| | | |\__ \ _| |_| | | |\__ \
|_____|_| |_||___/ ` + Version + ` ` |_____|_| |_||___/ ` + codeName
) )
// Default is the field which keeps an empty `Framework` // Default is the field which keeps an empty `Framework`
@ -114,12 +117,25 @@ type Framework struct {
// ConnState type and associated constants for details. // ConnState type and associated constants for details.
ConnState func(net.Conn, http.ConnState) // same as http.Server.ConnState ConnState func(net.Conn, http.ConnState) // same as http.Server.ConnState
closedManually bool // true if closed via .Close, used to not throw an error when closing the app's server // Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners, then closing all idle connections, and then waiting
// indefinitely for connections to return to idle and then shut down.
// If the provided context expires before the shutdown is complete,
// then the context's error is returned.
//
// Shutdown does not attempt to close nor wait for hijacked
// connections such as WebSockets. The caller of Shutdown should
// separately notify such long-lived connections of shutdown and wait
// for them to close, if desired.
Shutdown func(context.Context) error
closedManually bool // true if closed via .Shutdown, used to not throw a panic on s.handlePanic when closing the app's server
once sync.Once // used to 'Boot' once once sync.Once // used to 'Boot' once
} }
var defaultGlobalLoggerOuput = log.New(os.Stdout, "[iris] ", log.LstdFlags) var defaultGlobalLoggerOuput = log.New(os.Stdout, "[Iris] ", log.LstdFlags)
// DevLogger returns a new Logger which prints both ProdMode and DevMode messages // DevLogger returns a new Logger which prints both ProdMode and DevMode messages
// to the default global logger printer. // to the default global logger printer.
@ -377,6 +393,17 @@ func (s *Framework) Must(err error) {
} }
func (s *Framework) handlePanic(err error) { func (s *Framework) handlePanic(err error) {
// if x, ok := err.(*net.OpError); ok && x.Op == "accept" {
// return
// }
if err.Error() == http.ErrServerClosed.Error() && s.closedManually {
//.Shutdown was called, log to dev not in prod (prod is only for critical errors.)
// also do not try to recover from this error, remember, Shutdown was called manually here.
s.Log(DevMode, "HTTP Server closed manually")
return
}
if recoveryHandler := s.policies.EventPolicy.Recover; recoveryHandler != nil { if recoveryHandler := s.policies.EventPolicy.Recover; recoveryHandler != nil {
recoveryHandler(s, err) recoveryHandler(s, err)
return return
@ -413,8 +440,8 @@ func (s *Framework) Boot() (firstTime bool) {
// //
// Serve blocks until the given listener returns permanent error. // Serve blocks until the given listener returns permanent error.
func (s *Framework) Serve(ln net.Listener) error { func (s *Framework) Serve(ln net.Listener) error {
if s.isRunning() { if s.ln != nil {
return errors.New("Server is already started and listening") return errors.New("server is already started and listening")
} }
// maybe a 'race' here but user should not call .Serve more than one time especially in more than one go routines... // maybe a 'race' here but user should not call .Serve more than one time especially in more than one go routines...
s.ln = ln s.ln = ln
@ -423,18 +450,6 @@ func (s *Framework) Serve(ln net.Listener) error {
// post any panics to the user defined logger. // post any panics to the user defined logger.
defer func() { defer func() {
if rerr := recover(); rerr != nil { if rerr := recover(); rerr != nil {
if x, ok := rerr.(*net.OpError); ok && x.Op == "accept" && s.closedManually {
///TODO:
// here we don't report it back because the user called .Close manually.
// NOTES:
//
// I know that the best option to actual Close a server is
// by using a custom net.Listener and do it via channels on its Accept.
// BUT I am not doing this right now because as I'm learning the new go v1.8 will have a shutdown
// options by-default and we will use that instead.
// println("iris.go:355:recover but closed manually so we don't run the handler")
return
}
if err, ok := rerr.(error); ok { if err, ok := rerr.(error); ok {
s.handlePanic(err) s.handlePanic(err)
} }
@ -451,6 +466,15 @@ func (s *Framework) Serve(ln net.Listener) error {
ErrorLog: s.policies.LoggerPolicy.ToLogger(log.LstdFlags), ErrorLog: s.policies.LoggerPolicy.ToLogger(log.LstdFlags),
Handler: s.Router, Handler: s.Router,
} }
// Set the grace shutdown, it's just a func no need to make things complicated
// all are managed by net/http now.
s.Shutdown = func(ctx context.Context) error {
// order matters, look s.handlePanic
s.closedManually = true
err := srv.Shutdown(ctx)
s.ln = nil
return err
}
// print the banner and wait for system channel interrupt // print the banner and wait for system channel interrupt
go s.postServe() go s.postServe()
@ -460,14 +484,8 @@ func (s *Framework) Serve(ln net.Listener) error {
} }
func (s *Framework) postServe() { func (s *Framework) postServe() {
if !s.Config.DisableBanner { bannerMessage := fmt.Sprintf("| Running at %s\n\n%s\n", s.Config.VHost, banner)
bannerMessage := fmt.Sprintf("%s: Running at %s", time.Now().Format(s.Config.TimeFormat), s.Config.VHost) s.Log(DevMode, bannerMessage)
// we don't print it via Logger because:
// 1. The banner is only 'useful' when the developer logs to terminal and no file
// 2. Prefix & LstdFlags options of the default s.Logger
fmt.Printf("%s\n\n%s\n", banner, bannerMessage)
}
ch := make(chan os.Signal, 1) ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt) signal.Notify(ch, os.Interrupt)
@ -476,8 +494,8 @@ func (s *Framework) postServe() {
// fire any custom interrupted events and at the end close and exit // fire any custom interrupted events and at the end close and exit
// if the custom event blocks then it decides what to do next. // if the custom event blocks then it decides what to do next.
s.policies.Fire(s.policies.Interrupted, s) s.policies.Fire(s.policies.Interrupted, s)
// .Close doesn't really closes but it releases the ip:port, wait for go1.8 and see comments on IsRunning
s.Close() s.Shutdown(context.Background())
os.Exit(1) os.Exit(1)
} }
@ -576,34 +594,6 @@ func (s *Framework) ListenUNIX(addr string, mode os.FileMode) {
s.Must(s.Serve(ln)) s.Must(s.Serve(ln))
} }
// IsRunning returns true if server is running
func (s *Framework) isRunning() bool {
///TODO: this will change on gov1.8,
// Reseve or Restart and Close will be re-added again when 1.8 final release.
return s != nil && s.ln != nil && s.ln.Addr() != nil && s.ln.Addr().String() != ""
}
// Close is not working propetly but it releases the host:port.
func (s *Framework) Close() error {
if s.isRunning() {
s.closedManually = true
///TODO:
// This code below doesn't works without custom net listener which will work in a stop channel which will cost us performance.
// This will work on go v1.8 BUT FOR NOW make unexported reserve/reboot/restart in order to be non confusual for the user.
// Close need to be exported because whitebox tests are using this method to release the port.
return s.ln.Close()
}
return nil
}
// restart re-starts the server using the last .Serve's listener
// func (s *Framework) restart() error {
// ///TODO: See .close() notes
// return s.Serve(s.ln)
// }
func (s *Framework) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Framework) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.Router.ServeHTTP(w, r) s.Router.ServeHTTP(w, r)
} }

View File

@ -143,12 +143,10 @@ func (l LoggerPolicy) Adapt(frame *Policies) {
// //
// Implementations must not retain p. // Implementations must not retain p.
// //
// Note: this Write writes as the Production Env, so the default logger should be able to log this messages // Note: this Write writes as the DevMode.
// coming from internal http.Server (mostly)
// you can change this behavior too.
func (l LoggerPolicy) Write(p []byte) (n int, err error) { func (l LoggerPolicy) Write(p []byte) (n int, err error) {
log := string(p) log := string(p)
l(ProdMode, log) l(DevMode, log)
return len(log), nil return len(log), nil
} }

View File

@ -132,7 +132,7 @@ func TestResponseRecorderStatusCodeContentTypeBody(t *testing.T) {
} }
func ExampleResponseWriter_WriteHeader() { func ExampleResponseWriter_WriteHeader() {
app := iris.New(iris.OptionDisableBanner(true)) app := iris.New()
app.Adapt(newTestNativeRouter()) app.Adapt(newTestNativeRouter())
expectedOutput := "Hey" expectedOutput := "Hey"