├── .travis.yml ├── LICENSE ├── static.go ├── README.md ├── gracedown.go ├── gracedown_test.go ├── gracedown_fallback_test.go └── gracedown_fallback.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.4" 5 | - "1.5" 6 | - "1.6" 7 | - "1.7" 8 | - tip 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ichinose Shogo 2 | 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | 27 | -------------------------------------------------------------------------------- /static.go: -------------------------------------------------------------------------------- 1 | package gracedown 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | ) 7 | 8 | var defaultServer *Server 9 | 10 | // ListenAndServe provides a graceful version of the function provided by the 11 | // net/http package. Call Close() to stop the server. 12 | func ListenAndServe(addr string, handler http.Handler) error { 13 | defaultServer = NewWithServer(&http.Server{Addr: addr, Handler: handler}) 14 | return defaultServer.ListenAndServe() 15 | } 16 | 17 | // ListenAndServeTLS provides a graceful version of the function provided by the 18 | // net/http package. Call Close() to stop the server. 19 | func ListenAndServeTLS(addr string, certFile string, keyFile string, handler http.Handler) error { 20 | defaultServer = NewWithServer(&http.Server{Addr: addr, Handler: handler}) 21 | return defaultServer.ListenAndServeTLS(certFile, keyFile) 22 | } 23 | 24 | // Serve provides a graceful version of the function provided by the net/http 25 | // package. Call Close() to stop the server. 26 | func Serve(l net.Listener, handler http.Handler) error { 27 | defaultServer = NewWithServer(&http.Server{Handler: handler}) 28 | return defaultServer.Serve(l) 29 | } 30 | 31 | // Close shuts down the default server used by ListenAndServe, ListenAndServeTLS and 32 | // Serve. It returns true if it's the first time Close is called. 33 | func Close() bool { 34 | return defaultServer.Close() 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-gracedown 2 | ===== 3 | 4 | Package go-gracedown provides a library that makes it easy to build a HTTP server that can be shutdown gracefully (that is, without dropping any connections). 5 | 6 | ## ARCHIVED! 7 | 8 | This project is no longer maintained. 9 | Use https://pkg.go.dev/net/http#Server.Shutdown instead it. 10 | 11 | ## SYNOPSIS 12 | 13 | ``` go 14 | import ( 15 | "os" 16 | "os/signal" 17 | 18 | "github.com/shogo82148/go-gracedown" 19 | ) 20 | 21 | func main() { 22 | go func() { 23 | for { 24 | s := <-signal_chan 25 | if s == syscall.SIGTERM { 26 | gracedown.Close() // trigger graceful shutdown 27 | } 28 | } 29 | }() 30 | 31 | handler := MyHTTPHandler() 32 | gracedown.ListenAndServe(":7000", handler) 33 | } 34 | ``` 35 | 36 | ## built-in graceful shutdown support (from Go version 1.8 onward) 37 | 38 | From Go version 1.8 onward, the HTTP Server has support for graceful shutdown. 39 | ([HTTP Server Graceful Shutdown](https://beta.golang.org/doc/go1.8#http_shutdown)) 40 | The go-gracedown package is just a wrapper of the net/http package to maintain interface compatibility. 41 | So you should use the ["net/http".Server.Shutdown](https://golang.org/pkg/net/http/#Server.Shutdown) method 42 | and ["net/http".Server.Close](https://golang.org/pkg/net/http/#Server.Close) method directly. 43 | 44 | 45 | ### Changes 46 | 47 | - Go version 1.7 or less: The grace.Server.Close method keeps currently-open connections. 48 | - From Go version 1.8 onward: The grace.Server.Close method drops currently-open connections. 49 | 50 | 51 | ## GODOC 52 | 53 | See [godoc](https://godoc.org/github.com/shogo82148/go-gracedown) for more information. 54 | 55 | ## SEE ALSO 56 | 57 | - [braintree/manners](https://github.com/braintree/manners) 58 | - [facebookgo](https://github.com/facebookgo/httpdown) 59 | - [HTTP Server Graceful Shutdown](https://beta.golang.org/doc/go1.8#http_shutdown) 60 | -------------------------------------------------------------------------------- /gracedown.go: -------------------------------------------------------------------------------- 1 | // +build go1.8 2 | 3 | package gracedown 4 | 5 | import ( 6 | "context" 7 | "net" 8 | "net/http" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | ) 13 | 14 | // Server provides a graceful equivalent of net/http.Server. 15 | type Server struct { 16 | *http.Server 17 | 18 | KillTimeOut time.Duration 19 | 20 | mu sync.Mutex 21 | closed int32 // accessed atomically. 22 | doneChan chan struct{} 23 | } 24 | 25 | // NewWithServer wraps an existing http.Server. 26 | func NewWithServer(s *http.Server) *Server { 27 | return &Server{ 28 | Server: s, 29 | KillTimeOut: 10 * time.Second, 30 | } 31 | } 32 | 33 | func (srv *Server) Serve(l net.Listener) error { 34 | err := srv.Server.Serve(l) 35 | 36 | // Wait for closing all connections. 37 | if err == http.ErrServerClosed && atomic.LoadInt32(&srv.closed) != 0 { 38 | ch := srv.getDoneChan() 39 | <-ch 40 | return nil 41 | } 42 | return err 43 | } 44 | 45 | func (srv *Server) getDoneChan() <-chan struct{} { 46 | srv.mu.Lock() 47 | defer srv.mu.Unlock() 48 | return srv.getDoneChanLocked() 49 | } 50 | 51 | func (srv *Server) getDoneChanLocked() chan struct{} { 52 | if srv.doneChan == nil { 53 | srv.doneChan = make(chan struct{}) 54 | } 55 | return srv.doneChan 56 | } 57 | 58 | func (srv *Server) closeDoneChanLocked() { 59 | ch := srv.getDoneChanLocked() 60 | select { 61 | case <-ch: 62 | // Already closed. Don't close again. 63 | default: 64 | // Safe to close here. We're the only closer, guarded 65 | // by s.mu. 66 | close(ch) 67 | } 68 | } 69 | 70 | // Close shuts down the default server used by ListenAndServe, ListenAndServeTLS and 71 | // Serve. It returns true if it's the first time Close is called. 72 | func (srv *Server) Close() bool { 73 | if !atomic.CompareAndSwapInt32(&srv.closed, 0, 1) { 74 | return false 75 | } 76 | 77 | // immediately closes all connection. 78 | if srv.KillTimeOut == 0 { 79 | srv.Server.Close() 80 | 81 | srv.mu.Lock() 82 | defer srv.mu.Unlock() 83 | srv.closeDoneChanLocked() 84 | return true 85 | } 86 | 87 | // graceful shutdown 88 | go func() { 89 | ctx, cancel := context.WithTimeout(context.Background(), srv.KillTimeOut) 90 | defer cancel() 91 | srv.Shutdown(ctx) 92 | 93 | srv.mu.Lock() 94 | defer srv.mu.Unlock() 95 | srv.closeDoneChanLocked() 96 | }() 97 | 98 | return true 99 | } 100 | -------------------------------------------------------------------------------- /gracedown_test.go: -------------------------------------------------------------------------------- 1 | package gracedown 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func newLocalListener() net.Listener { 13 | // this code from net/http/httptest 14 | l, err := net.Listen("tcp", "127.0.0.1:0") 15 | if err != nil { 16 | if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { 17 | panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) 18 | } 19 | } 20 | return l 21 | } 22 | 23 | func TestShutdown_NoKeepAlive(t *testing.T) { 24 | const expectedBody = "test response body" 25 | 26 | // prepare test server 27 | chHandlerRequest := make(chan *http.Request) 28 | chHandlerResponse := make(chan []byte) 29 | handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 30 | chHandlerRequest <- req 31 | data := <-chHandlerResponse 32 | w.Write(data) 33 | }) 34 | ts := NewWithServer(&http.Server{ 35 | Handler: handler, 36 | }) 37 | 38 | // start server 39 | l := newLocalListener() 40 | chServe := make(chan error) 41 | go func() { 42 | chServe <- ts.Serve(l) 43 | }() 44 | url := "http://" + l.Addr().String() 45 | 46 | client := &http.Client{ 47 | Transport: &http.Transport{ 48 | Proxy: http.ProxyFromEnvironment, 49 | Dial: (&net.Dialer{ 50 | Timeout: 5 * time.Second, 51 | KeepAlive: 5 * time.Second, 52 | }).Dial, 53 | TLSHandshakeTimeout: 10 * time.Second, 54 | DisableKeepAlives: true, // keep-alives are DISABLE!! 55 | MaxIdleConnsPerHost: 1, 56 | }, 57 | } 58 | 59 | // first request will be success 60 | chRequest := make(chan []byte) 61 | go func() { 62 | t.Log("request 1st GET") 63 | resp, err := client.Get(url) 64 | if err != nil { 65 | t.Errorf("unexpected error: %v", err) 66 | chRequest <- nil 67 | return 68 | } 69 | data, _ := ioutil.ReadAll(resp.Body) 70 | t.Logf("response is '%s'", string(data)) 71 | resp.Body.Close() 72 | chRequest <- data 73 | }() 74 | 75 | // close server 76 | <-chHandlerRequest // wait for receiving the request... 77 | t.Log("call sever.Close()") 78 | if ts.Close() != true { 79 | t.Errorf("first call to Close returned false") 80 | } 81 | if ts.Close() != false { 82 | t.Fatal("second call to Close returned true") 83 | } 84 | time.Sleep(1 * time.Second) // make sure closing the test server has started 85 | 86 | // second request will be failure, because the server starts shutting down process 87 | _, err := client.Get(url) 88 | t.Logf("request 2nd GET: %v", err) 89 | if err == nil { 90 | t.Errorf("want some error, but not") 91 | } 92 | 93 | select { 94 | case <-chServe: 95 | t.Error("Serve() returned too early") 96 | default: 97 | } 98 | 99 | // test the response 100 | select { 101 | case <-chRequest: 102 | t.Error("the response returned too early") 103 | default: 104 | } 105 | chHandlerResponse <- []byte(expectedBody) 106 | select { 107 | case data := <-chRequest: 108 | if string(data) != expectedBody { 109 | t.Errorf("want %s, got %s", expectedBody, string(data)) 110 | } 111 | case <-time.After(5 * time.Second): 112 | t.Errorf("timeout") 113 | } 114 | 115 | select { 116 | case err := <-chServe: 117 | if err != nil { 118 | t.Errorf("unexpeted error: %v", err) 119 | } 120 | case <-time.After(10 * time.Second): 121 | t.Errorf("timeout") 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /gracedown_fallback_test.go: -------------------------------------------------------------------------------- 1 | // +build !go1.8 2 | 3 | package gracedown 4 | 5 | import ( 6 | "net" 7 | "net/http" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestShutdown_KeepAlive(t *testing.T) { 13 | // prepare test server 14 | handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 15 | }) 16 | ts := NewWithServer(&http.Server{ 17 | Handler: handler, 18 | }) 19 | 20 | // start server 21 | l := newLocalListener() 22 | go func() { 23 | ts.Serve(l) 24 | }() 25 | url := "http://" + l.Addr().String() 26 | 27 | client := &http.Client{ 28 | Transport: &http.Transport{ 29 | Proxy: http.ProxyFromEnvironment, 30 | Dial: (&net.Dialer{ 31 | Timeout: 5 * time.Second, 32 | KeepAlive: 5 * time.Second, 33 | }).Dial, 34 | TLSHandshakeTimeout: 10 * time.Second, 35 | DisableKeepAlives: false, // keep-alives are ENABLE!! 36 | MaxIdleConnsPerHost: 1, 37 | }, 38 | } 39 | 40 | // 1st request will be success 41 | resp, err := client.Get(url) 42 | if err != nil { 43 | t.Errorf("unexpected error: %v", err) 44 | } else { 45 | resp.Body.Close() 46 | } 47 | 48 | // start shutting down process 49 | ts.Close() 50 | time.Sleep(1 * time.Second) // make sure closing the test server has started 51 | 52 | // 2nd request will be success, because this request uses the Keep-Alive connection 53 | resp, err = client.Get(url) 54 | if err != nil { 55 | t.Errorf("unexpected error: %v", err) 56 | } else { 57 | resp.Body.Close() 58 | } 59 | 60 | // 3rd request will be failure, because the Keep-Alive connection is closed 61 | resp, err = client.Get(url) 62 | if err == nil { 63 | t.Error("want error, but not") 64 | } 65 | } 66 | 67 | func TestShutdown_KillKeepAlive(t *testing.T) { 68 | // prepare test server 69 | handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 70 | }) 71 | ts := NewWithServer(&http.Server{ 72 | Handler: handler, 73 | }) 74 | ts.KillTimeOut = time.Second // force close after a second 75 | 76 | // start server 77 | done := make(chan error, 1) 78 | l := newLocalListener() 79 | go func() { 80 | done <- ts.Serve(l) 81 | }() 82 | url := "http://" + l.Addr().String() 83 | 84 | client := &http.Client{ 85 | Transport: &http.Transport{ 86 | Proxy: http.ProxyFromEnvironment, 87 | Dial: (&net.Dialer{ 88 | Timeout: 5 * time.Second, 89 | KeepAlive: 5 * time.Second, 90 | }).Dial, 91 | TLSHandshakeTimeout: 10 * time.Second, 92 | DisableKeepAlives: false, // keep-alives are ENABLE!! 93 | MaxIdleConnsPerHost: 1, 94 | }, 95 | } 96 | 97 | // 1st request will be success 98 | resp, err := client.Get(url) 99 | if err != nil { 100 | t.Errorf("unexpected error: %v", err) 101 | } 102 | resp.Body.Close() 103 | 104 | // start shutting down process 105 | start := time.Now() 106 | ts.Close() 107 | time.Sleep(1 * time.Second) // make sure closing the test server has started 108 | 109 | select { 110 | case err := <-done: 111 | end := time.Now() 112 | dt := end.Sub(start) 113 | t.Logf("kill timeout: %v", dt) 114 | if dt < ts.KillTimeOut { 115 | t.Errorf("too fast kill timeout") 116 | } 117 | if err != nil { 118 | t.Errorf("unexpected err: %v", err) 119 | } 120 | case <-time.After(ts.KillTimeOut + 5*time.Second): 121 | t.Errorf("timeout") 122 | } 123 | 124 | // 2nd request will be failure, because the server has already shut down 125 | resp, err = client.Get(url) 126 | if err == nil { 127 | t.Error("want error, but not") 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /gracedown_fallback.go: -------------------------------------------------------------------------------- 1 | // +build !go1.8 2 | 3 | package gracedown 4 | 5 | import ( 6 | "crypto/tls" 7 | "net" 8 | "net/http" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | ) 13 | 14 | // Server provides a graceful equivalent of net/http.Server. 15 | type Server struct { 16 | *http.Server 17 | 18 | KillTimeOut time.Duration 19 | 20 | wg sync.WaitGroup 21 | mu sync.Mutex 22 | originalConnState func(conn net.Conn, newState http.ConnState) 23 | connStateOnce sync.Once 24 | closed int32 // accessed atomically. 25 | idlePool map[net.Conn]struct{} 26 | listeners map[net.Listener]struct{} 27 | } 28 | 29 | // NewWithServer wraps an existing http.Server. 30 | func NewWithServer(s *http.Server) *Server { 31 | return &Server{ 32 | Server: s, 33 | KillTimeOut: 10 * time.Second, 34 | idlePool: map[net.Conn]struct{}{}, 35 | listeners: map[net.Listener]struct{}{}, 36 | } 37 | } 38 | 39 | // ListenAndServe provides a graceful equivalent of net/http.Server.ListenAndServe 40 | func (srv *Server) ListenAndServe() error { 41 | addr := srv.Server.Addr 42 | if addr == "" { 43 | addr = ":http" 44 | } 45 | ln, err := net.Listen("tcp", addr) 46 | if err != nil { 47 | return err 48 | } 49 | return srv.Serve(ln) 50 | } 51 | 52 | // ListenAndServeTLS provides a graceful equivalent of net/http.Server.ListenAndServeTLS 53 | func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error { 54 | // direct lift from net/http/server.go 55 | addr := srv.Addr 56 | if addr == "" { 57 | addr = ":https" 58 | } 59 | 60 | config := cloneTLSConfig(srv.TLSConfig) 61 | if !strSliceContains(config.NextProtos, "http/1.1") { 62 | config.NextProtos = append(config.NextProtos, "http/1.1") 63 | } 64 | 65 | configHasCert := len(config.Certificates) > 0 || config.GetCertificate != nil 66 | if !configHasCert || certFile != "" || keyFile != "" { 67 | var err error 68 | config.Certificates = make([]tls.Certificate, 1) 69 | config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | 75 | ln, err := net.Listen("tcp", addr) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return srv.Serve(tls.NewListener(ln, config)) 81 | } 82 | 83 | // cloneTLSConfig returns a shallow clone of the exported 84 | // fields of cfg, ignoring the unexported sync.Once, which 85 | // contains a mutex and must not be copied. 86 | // 87 | // The cfg must not be in active use by tls.Server, or else 88 | // there can still be a race with tls.Server updating SessionTicketKey 89 | // and our copying it, and also a race with the server setting 90 | // SessionTicketsDisabled=false on failure to set the random 91 | // ticket key. 92 | // 93 | // If cfg is nil, a new zero tls.Config is returned. 94 | // 95 | // Direct lift from net/http/transport.go 96 | func cloneTLSConfig(cfg *tls.Config) *tls.Config { 97 | if cfg == nil { 98 | return &tls.Config{} 99 | } 100 | return &tls.Config{ 101 | Rand: cfg.Rand, 102 | Time: cfg.Time, 103 | Certificates: cfg.Certificates, 104 | NameToCertificate: cfg.NameToCertificate, 105 | GetCertificate: cfg.GetCertificate, 106 | RootCAs: cfg.RootCAs, 107 | NextProtos: cfg.NextProtos, 108 | ServerName: cfg.ServerName, 109 | ClientAuth: cfg.ClientAuth, 110 | ClientCAs: cfg.ClientCAs, 111 | InsecureSkipVerify: cfg.InsecureSkipVerify, 112 | CipherSuites: cfg.CipherSuites, 113 | PreferServerCipherSuites: cfg.PreferServerCipherSuites, 114 | SessionTicketsDisabled: cfg.SessionTicketsDisabled, 115 | SessionTicketKey: cfg.SessionTicketKey, 116 | ClientSessionCache: cfg.ClientSessionCache, 117 | MinVersion: cfg.MinVersion, 118 | MaxVersion: cfg.MaxVersion, 119 | CurvePreferences: cfg.CurvePreferences, 120 | } 121 | } 122 | 123 | func strSliceContains(ss []string, s string) bool { 124 | for _, v := range ss { 125 | if v == s { 126 | return true 127 | } 128 | } 129 | return false 130 | } 131 | 132 | // Serve provides a graceful equivalent of net/http.Server.Serve 133 | func (srv *Server) Serve(l net.Listener) error { 134 | // remember net.Listener 135 | srv.mu.Lock() 136 | srv.listeners[l] = struct{}{} 137 | srv.mu.Unlock() 138 | defer func() { 139 | srv.mu.Lock() 140 | delete(srv.listeners, l) 141 | srv.mu.Unlock() 142 | }() 143 | 144 | // replace ConnState 145 | srv.connStateOnce.Do(func() { 146 | srv.originalConnState = srv.Server.ConnState 147 | srv.Server.ConnState = srv.connState 148 | }) 149 | 150 | err := srv.Server.Serve(l) 151 | 152 | go func() { 153 | // wait for closing keep-alive connection by sending `Connection: Close` header. 154 | time.Sleep(srv.KillTimeOut) 155 | 156 | // time out, close all idle connections 157 | srv.mu.Lock() 158 | for conn := range srv.idlePool { 159 | conn.Close() 160 | } 161 | srv.mu.Unlock() 162 | }() 163 | 164 | // wait all connections have done 165 | srv.wg.Wait() 166 | 167 | if atomic.LoadInt32(&srv.closed) != 0 { 168 | // ignore closed network error when srv.Close() is called 169 | return nil 170 | } 171 | return err 172 | } 173 | 174 | // Close shuts down the default server used by ListenAndServe, ListenAndServeTLS and 175 | // Serve. It returns true if it's the first time Close is called. 176 | func (srv *Server) Close() bool { 177 | if atomic.CompareAndSwapInt32(&srv.closed, 0, 1) { 178 | srv.Server.SetKeepAlivesEnabled(false) 179 | srv.mu.Lock() 180 | listeners := srv.listeners 181 | srv.listeners = map[net.Listener]struct{}{} 182 | srv.mu.Unlock() 183 | for l := range listeners { 184 | l.Close() 185 | } 186 | return true 187 | } 188 | return false 189 | } 190 | 191 | func (srv *Server) connState(conn net.Conn, newState http.ConnState) { 192 | srv.mu.Lock() 193 | switch newState { 194 | case http.StateNew: 195 | srv.wg.Add(1) 196 | case http.StateActive: 197 | delete(srv.idlePool, conn) 198 | case http.StateIdle: 199 | srv.idlePool[conn] = struct{}{} 200 | case http.StateClosed, http.StateHijacked: 201 | delete(srv.idlePool, conn) 202 | srv.wg.Done() 203 | } 204 | srv.mu.Unlock() 205 | if srv.originalConnState != nil { 206 | srv.originalConnState(conn, newState) 207 | } 208 | } 209 | --------------------------------------------------------------------------------