Skip to content

Instantly share code, notes, and snippets.

@rivo
Created November 26, 2017 12:28
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save rivo/f96ad8710b54a49180a314ec4d68dbfb to your computer and use it in GitHub Desktop.
Save rivo/f96ad8710b54a49180a314ec4d68dbfb to your computer and use it in GitHub Desktop.
Graceful stop and restart for HTTP servers in Go
package main
import (
"context"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"sync"
"syscall"
"time"
)
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted connections.
// It's used by ListenAndServe and ListenAndServeTLS so dead TCP connections
// (e.g. closing laptop mid-download) eventually go away. This is code from
// net/http/server.go.
type tcpKeepAliveListener struct {
*net.TCPListener
}
// Accept accepts a TCP connection while setting keep-alive timeouts.
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
tc, err := ln.AcceptTCP()
if err != nil {
return
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}
// Serve starts an HTTP server using the DefaultServeMux. This function does
// not return until the server has been stopped and all requests have finished.
//
// The server is controlled using Unix signals:
//
// kill -s SIGINT <pid> (or Ctrl-C) stops the server, waiting for any active
// requests to finish.
//
// kill -s SIGUSR1 <pid> restarts the server with no downtime, using its (new)
// binary. Current requests will finish before the old server exits and while
// the new server already processes new requests.
//
// kill -s SIGTERM <pid> stops the server without waiting for active requests
// to finish.
func Serve(address string) error {
// The general idea for this is described here:
// https://grisha.org/blog/2014/06/03/graceful-restart-in-golang/
// We need to shut down gracefully when the user hits Ctrl-C.
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGTERM)
// Create a server.
server := &http.Server{Addr: address}
// Start the server.
var (
listener *net.TCPListener
wg sync.WaitGroup
)
wg.Add(1)
go func() {
defer wg.Done()
// If this is a forked child process, we'll use its connection.
isFork := os.Getenv("FORKED_SERVER") != ""
var (
ln net.Listener
err error
)
if isFork {
// It's a fork. Get the file that was handed over.
fmt.Printf("%d Getting existing listener for %s\n", pid, address)
file := os.NewFile(3, "")
ln, err = net.FileListener(file)
if err != nil {
fmt.Printf("%d Cannot use existing listener: %s\n", pid, err)
sig <- syscall.SIGTERM
return
}
// Tell the parent to stop the server now.
parent := syscall.Getppid()
fmt.Printf("%d Telling parent process (%d) to stop server\n", pid, parent)
syscall.Kill(parent, syscall.SIGTERM)
// Give the parent some time.
time.Sleep(100 * time.Millisecond)
} else {
// It's a new server.
fmt.Printf("%d Starting web server on %s\n", pid, address)
ln, err = net.Listen("tcp", address)
if err != nil {
fmt.Printf("%d Cannot listen to %s: %s\n", pid, address, err)
sig <- syscall.SIGTERM
return
}
}
// We can start the server now.
fmt.Println(pid, "Serving requests...")
listener = ln.(*net.TCPListener)
if err = server.Serve(tcpKeepAliveListener{listener}); err != nil {
fmt.Printf("%d Web server was shut down: %s\n", pid, err)
}
fmt.Println(pid, "Web server has finished")
}()
// Wait for the interrupt signal.
s := <-sig
switch s {
case syscall.SIGTERM:
// Go for the program exit. Don't wait for the server to finish.
fmt.Println(pid, "Received SIGTERM, exiting without waiting for the web server to shut down")
return nil
case syscall.SIGINT:
// Stop the server gracefully.
fmt.Println(pid, "Received SIGINT")
case syscall.SIGUSR1:
// Spawn a child process.
fmt.Println(pid, "Received SIGUSR1")
var args []string
if len(os.Args) > 1 {
args = os.Args[1:]
}
file, err := listener.File()
if err != nil {
fmt.Printf("%d Listener did not return file, not forking: %s\n", pid, err)
} else {
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = []*os.File{file}
cmd.Env = append(os.Environ(), "FORKED_SERVER=1")
if err := cmd.Start(); err != nil {
fmt.Printf("%d Fork did not succeed: %s\n", pid, err)
}
fmt.Printf("%d Started child process %d, waiting for its ready signal\n", pid, cmd.Process.Pid)
// We have a child process. A SIGTERM means the child process is ready to
// start its server.
<-sig
}
}
// Force the server to shut down.
fmt.Println(pid, "Shutting down web server and waiting for requests to finish...")
defer fmt.Println(pid, "Requests have finished")
if err := server.Shutdown(context.Background()); err != nil {
return fmt.Errorf("Shutdown failed: %s", err)
}
wg.Wait()
return nil
}
@piusalfred
Copy link

This is great but I am missing the initialization of "pid"

@rivo
Copy link
Author

rivo commented Jul 28, 2020

Right. This was a global variable elsewhere. You can simply initialize it with os.Getpid():

pid := os.Getpid()

@piusalfred
Copy link

Thanks for the reply @rivo

@pseidemann
Copy link

hi,
I created a module which wraps this functionality: https://github.com/pseidemann/finish

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment