├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── zerodown.go └── zerodowndemo └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/visualstudiocode,go 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 18 | .glide/ 19 | 20 | ### VisualStudioCode ### 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | .history 27 | 28 | # End of https://www.gitignore.io/api/visualstudiocode,go 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.9 5 | - tip 6 | 7 | sudo: false 8 | 9 | install: 10 | - go install -v ./... 11 | 12 | script: 13 | - go test ./... 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lingchao Xin 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 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | zerodown [![Build Status](https://secure.travis-ci.org/douglarek/zerodown.png)](https://travis-ci.org/douglarek/zerodown) 2 | ===== 3 | 4 | Package zerodown provides a library that makes it easy to build socket 5 | based servers that can be gracefully terminated & restarted (that is, 6 | without dropping any connections). 7 | 8 | Usage 9 | ----- 10 | 11 | Demo HTTP Server with graceful termination and restart: 12 | https://github.com/douglarek/zerodown/blob/master/zerodowndemo/server.go 13 | 14 | 1. Install the demo application 15 | 16 | go get github.com/douglarek/zerodown/zerodowndemo 17 | 18 | 2. Start it in the first terminal 19 | 20 | zerodowndemo 21 | 22 | 3. In a second terminal start a slow HTTP request 23 | 24 | curl 'http://localhost:8080/?duration=20s' 25 | 26 | 4. In a third terminal trigger a graceful server restart: 27 | 28 | kill -USR2 [zerodowndemo pid] 29 | 30 | 5. Trigger another shorter request that finishes before the earlier request: 31 | 32 | curl 'http://localhost:8080/?duration=0s' 33 | 34 | 35 | If done quickly enough, this shows the second quick request will be served by 36 | the new process while the slow first request will be 37 | served by the first server. It shows how the active connection was gracefully 38 | served before the server was shutdown. It is also showing that at one point 39 | both the new as well as the old server was running at the same time. 40 | -------------------------------------------------------------------------------- /zerodown.go: -------------------------------------------------------------------------------- 1 | // Package zerodown provides easy to use graceful restart 2 | // functionality for HTTP server. 3 | package zerodown 4 | 5 | import ( 6 | "context" 7 | "net" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "os/signal" 12 | "strings" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | const ( 18 | defaultTimeout = 20 * time.Second // default timeout is 20s 19 | graceEnv = "ZEROXVLBZGBAICMRAJWWHTHCDOWN=true" // env flag for reload 20 | ) 21 | 22 | // A Grace carries actions for graceful restart or shutdown. 23 | type Grace interface { 24 | Run(*http.Server) error 25 | ListenAndServe(string, http.Handler) error 26 | } 27 | 28 | type grace struct { 29 | srv *http.Server 30 | listener net.Listener 31 | timeout time.Duration 32 | err error 33 | } 34 | 35 | func (g *grace) reload() *grace { 36 | f, err := g.listener.(*net.TCPListener).File() 37 | if err != nil { 38 | g.err = err 39 | return g 40 | } 41 | defer f.Close() 42 | 43 | var args []string 44 | if len(os.Args) > 1 { 45 | args = append(args, os.Args[1:]...) 46 | } 47 | cmd := exec.Command(os.Args[0], args...) 48 | cmd.Stdout = os.Stdout 49 | cmd.Stderr = os.Stderr 50 | cmd.Env = append(os.Environ(), graceEnv) 51 | cmd.ExtraFiles = []*os.File{f} 52 | 53 | g.err = cmd.Start() 54 | return g 55 | } 56 | 57 | func (g *grace) stop() *grace { 58 | if g.err != nil { 59 | return g 60 | } 61 | 62 | ctx, cancel := context.WithTimeout(context.Background(), g.timeout) 63 | defer cancel() 64 | 65 | if err := g.srv.Shutdown(ctx); err != nil { 66 | g.err = err 67 | } 68 | return g 69 | } 70 | 71 | func (g *grace) run() (err error) { 72 | if _, ok := syscall.Getenv(strings.Split(graceEnv, "=")[0]); ok { 73 | f := os.NewFile(3, "") 74 | if g.listener, err = net.FileListener(f); err != nil { 75 | return 76 | } 77 | } else { 78 | if g.listener, err = net.Listen("tcp", g.srv.Addr); err != nil { 79 | return 80 | } 81 | } 82 | 83 | terminate := make(chan error) 84 | go func() { 85 | if err := g.srv.Serve(g.listener); err != nil { 86 | terminate <- err 87 | } 88 | }() 89 | 90 | quit := make(chan os.Signal) 91 | signal.Notify(quit) 92 | 93 | for { 94 | select { 95 | case s := <-quit: 96 | switch s { 97 | case syscall.SIGINT, syscall.SIGTERM: 98 | signal.Stop(quit) 99 | return g.stop().err 100 | case syscall.SIGUSR2: 101 | return g.reload().stop().err 102 | } 103 | case err = <-terminate: 104 | return 105 | } 106 | } 107 | } 108 | 109 | // WithTimeout returns a custom timeout Grace. 110 | func WithTimeout(timeout time.Duration) Grace { 111 | return &grace{timeout: timeout} 112 | } 113 | 114 | func (g *grace) Run(srv *http.Server) error { 115 | g.srv = srv 116 | return g.run() 117 | } 118 | 119 | func (g *grace) ListenAndServe(addr string, handler http.Handler) error { 120 | g.srv = &http.Server{Addr: addr, Handler: handler} 121 | return g.run() 122 | } 123 | 124 | var _ Grace = (*grace)(nil) // assert *grace implements Grace. 125 | 126 | // Run accepts a custom http Server and provice signal magic. 127 | func Run(srv *http.Server) error { 128 | return WithTimeout(defaultTimeout).Run(srv) 129 | } 130 | 131 | // ListenAndServe wraps http.ListenAndServe and provides signal magic. 132 | func ListenAndServe(addr string, handler http.Handler) error { 133 | return WithTimeout(defaultTimeout).ListenAndServe(addr, handler) 134 | } 135 | -------------------------------------------------------------------------------- /zerodowndemo/server.go: -------------------------------------------------------------------------------- 1 | // +build go1.8 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/douglarek/zerodown" 12 | ) 13 | 14 | func main() { 15 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 16 | d := r.URL.Query().Get("duration") 17 | if len(d) != 0 { 18 | t, _ := time.ParseDuration(d) 19 | time.Sleep(t) 20 | } 21 | fmt.Fprintln(w, "Hello, World!") 22 | }) 23 | log.Fatalln(zerodown.ListenAndServe(":8080", nil)) 24 | } 25 | --------------------------------------------------------------------------------