Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 0109e7230a
Fetching contributors…

Cannot retrieve contributors at this time

file 129 lines (119 sloc) 3.15 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
// Zero-downtime restarts in Go.
package goagain

import (
"errors"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
)

// Block this goroutine awaiting signals. With the exception of SIGTERM
// taking the place of SIGQUIT, signals are handled exactly as in Nginx
// and Unicorn: <http://unicorn.bogomips.org/SIGNALS.html>.
func AwaitSignals(l *net.TCPListener) error {
ch := make(chan os.Signal, 2)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGUSR2)
for {
sig := <-ch
log.Println(sig.String())
switch sig {

// TODO SIGHUP should reload configuration.

// SIGQUIT should exit gracefully. However, Go doesn't seem
// to like handling SIGQUIT (or any signal which dumps core by
// default) at all so SIGTERM takes its place. How graceful
// this exit is depends on what the program does after this
// function returns control.
case syscall.SIGTERM:
return nil

// TODO SIGUSR1 should reopen logs.

// SIGUSR2 begins the process of restarting without dropping
// the listener passed to this function.
case syscall.SIGUSR2:
err := Relaunch(l)
if nil != err {
return err
}

}
}
return nil // It'll never get here.
}

// Convert and validate the GOAGAIN_FD and GOAGAIN_PPID environment
// variables. If both are present and in order, this is a child process
// that may pick up where the parent left off.
func GetEnvs() (*net.TCPListener, int, error) {
envFd := os.Getenv("GOAGAIN_FD")
if "" == envFd {
return nil, 0, errors.New("GOAGAIN_FD not set")
}
var fd uintptr
_, err := fmt.Sscan(envFd, &fd)
if nil != err {
return nil, 0, err
}
tmp, err := net.FileListener(os.NewFile(fd, "listener"))
if nil != err {
return nil, 0, err
}
l := tmp.(*net.TCPListener)
envPpid := os.Getenv("GOAGAIN_PPID")
if "" == envPpid {
return l, 0, errors.New("GOAGAIN_PPID not set")
}
var ppid int
_, err = fmt.Sscan(envPpid, &ppid)
if nil != err {
return l, 0, err
}
if syscall.Getppid() != ppid {
return l, ppid, errors.New(fmt.Sprintf(
"GOAGAIN_PPID is %d but parent is %d\n", ppid, syscall.Getppid()))
}
return l, ppid, nil
}

// Send SIGQUIT (but really SIGTERM since Go can't handle SIGQUIT) to the
// given ppid in order to complete the handoff to the child process.
func KillParent(ppid int) error {
err := syscall.Kill(ppid, syscall.SIGTERM)
if nil != err {
return err
}
return nil
}

// Re-exec this image without dropping the listener passed to this function.
func Relaunch(l *net.TCPListener) error {
f, err := l.File()
if nil != err {
return err
}
argv0, err := exec.LookPath(os.Args[0])
if nil != err {
return err
}
wd, err := os.Getwd()
if nil != err {
return err
}
err = os.Setenv("GOAGAIN_FD", fmt.Sprint(f.Fd()))
if nil != err {
return err
}
err = os.Setenv("GOAGAIN_PPID", strconv.Itoa(syscall.Getpid()))
if nil != err {
return err
}
p, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{
Dir: wd,
Env: os.Environ(),
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr, f},
Sys: &syscall.SysProcAttr{},
})
if nil != err {
return err
}
log.Printf("spawned child %d\n", p.Pid)
return nil
}
Something went wrong with that request. Please try again.