Commit 9705f349 authored by Benny Ng's avatar Benny Ng

Restart gracefully for in-process restart

parent db21b031
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
// To use this package, follow a few simple steps: // To use this package, follow a few simple steps:
// //
// 1. Set the AppName and AppVersion variables. // 1. Set the AppName and AppVersion variables.
// 2. Call LoadCaddyfile() to get the Caddyfile (it // 2. Call LoadCaddyfile() to get the Caddyfile.
// might have been piped in as part of a restart).
// You should pass in your own Caddyfile loader. // You should pass in your own Caddyfile loader.
// 3. Call caddy.Start() to start Caddy, caddy.Stop() // 3. Call caddy.Start() to start Caddy, caddy.Stop()
// to stop it, or caddy.Restart() to restart it. // to stop it, or caddy.Restart() to restart it.
...@@ -16,7 +15,6 @@ package caddy ...@@ -16,7 +15,6 @@ package caddy
import ( import (
"bytes" "bytes"
"encoding/gob"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
...@@ -26,7 +24,6 @@ import ( ...@@ -26,7 +24,6 @@ import (
"path" "path"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/mholt/caddy/caddy/https" "github.com/mholt/caddy/caddy/https"
...@@ -52,11 +49,6 @@ var ( ...@@ -52,11 +49,6 @@ var (
// GracefulTimeout is the maximum duration of a graceful shutdown. // GracefulTimeout is the maximum duration of a graceful shutdown.
GracefulTimeout time.Duration GracefulTimeout time.Duration
// RestartMode is the mode used for restart,
// "inproc" will restart in process,
// otherwise default behavior is used (inproc on Windows, fork on Linux).
RestartMode = ""
) )
var ( var (
...@@ -66,10 +58,6 @@ var ( ...@@ -66,10 +58,6 @@ var (
// caddyfileMu protects caddyfile during changes // caddyfileMu protects caddyfile during changes
caddyfileMu sync.Mutex caddyfileMu sync.Mutex
// errIncompleteRestart occurs if this process is a fork
// of the parent but no Caddyfile was piped in
errIncompleteRestart = errors.New("incomplete restart")
// servers is a list of all the currently-listening servers // servers is a list of all the currently-listening servers
servers []*server.Server servers []*server.Server
...@@ -79,11 +67,8 @@ var ( ...@@ -79,11 +67,8 @@ var (
// wg is used to wait for all servers to shut down // wg is used to wait for all servers to shut down
wg sync.WaitGroup wg sync.WaitGroup
// loadedGob is used if this is a child process as part of // restartFds keeps the servers' sockets for graceful in-process restart
// a graceful restart; it is used to map listeners to their restartFds = make(map[string]*os.File)
// index in the list of inherited file descriptors. This
// variable is not safe for concurrent access.
loadedGob caddyfileGob
// startedBefore should be set to true if caddy has been started // startedBefore should be set to true if caddy has been started
// at least once (does not indicate whether currently running). // at least once (does not indicate whether currently running).
...@@ -104,31 +89,7 @@ const ( ...@@ -104,31 +89,7 @@ const (
// one. // one.
// //
// This function blocks until all the servers are listening. // This function blocks until all the servers are listening.
//
// Note (POSIX): If Start is called in the child process of a
// restart more than once within the duration of the graceful
// cutoff (i.e. the child process called Start a first time,
// then called Stop, then Start again within the first 5 seconds
// or however long GracefulTimeout is) and the Caddyfiles have
// at least one listener address in common, the second Start
// may fail with "address already in use" as there's no
// guarantee that the parent process has relinquished the
// address before the grace period ends.
func Start(cdyfile Input) (err error) { func Start(cdyfile Input) (err error) {
// If we return with no errors, we must do two things: tell the
// parent that we succeeded and write to the pidfile.
defer func() {
if err == nil {
signalSuccessToParent() // TODO: Is doing this more than once per process a bad idea? Start could get called more than once in other apps.
if PidFile != "" {
err := writePidFile()
if err != nil {
log.Printf("[ERROR] Could not write pidfile: %v", err)
}
}
}
}()
// Input must never be nil; try to load something // Input must never be nil; try to load something
if cdyfile == nil { if cdyfile == nil {
cdyfile, err = LoadCaddyfile(nil) cdyfile, err = LoadCaddyfile(nil)
...@@ -158,10 +119,11 @@ func Start(cdyfile Input) (err error) { ...@@ -158,10 +119,11 @@ func Start(cdyfile Input) (err error) {
if err != nil { if err != nil {
return err return err
} }
startedBefore = true
showInitializationOutput(groupings) showInitializationOutput(groupings)
startedBefore = true
return nil return nil
} }
...@@ -193,8 +155,8 @@ func showInitializationOutput(groupings bindingGroup) { ...@@ -193,8 +155,8 @@ func showInitializationOutput(groupings bindingGroup) {
// startServers starts all the servers in groupings, // startServers starts all the servers in groupings,
// taking into account whether or not this process is // taking into account whether or not this process is
// a child from a graceful restart or not. It blocks // from a graceful restart or not. It blocks until
// until the servers are listening. // the servers are listening.
func startServers(groupings bindingGroup) error { func startServers(groupings bindingGroup) error {
var startupWg sync.WaitGroup var startupWg sync.WaitGroup
errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later
...@@ -213,12 +175,9 @@ func startServers(groupings bindingGroup) error { ...@@ -213,12 +175,9 @@ func startServers(groupings bindingGroup) error {
} }
var ln server.ListenerFile var ln server.ListenerFile
if IsRestart() { if len(restartFds) > 0 {
// Look up this server's listener in the map of inherited file descriptors; // Reuse the listeners for in-process restart
// if we don't have one, we must make a new one (later). if file, ok := restartFds[s.Addr]; ok {
if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok {
file := os.NewFile(fdIndex, "")
fln, err := net.FileListener(file) fln, err := net.FileListener(file)
if err != nil { if err != nil {
return err return err
...@@ -230,7 +189,7 @@ func startServers(groupings bindingGroup) error { ...@@ -230,7 +189,7 @@ func startServers(groupings bindingGroup) error {
} }
file.Close() file.Close()
delete(loadedGob.ListenerFds, s.Addr) delete(restartFds, s.Addr)
} }
} }
...@@ -240,7 +199,7 @@ func startServers(groupings bindingGroup) error { ...@@ -240,7 +199,7 @@ func startServers(groupings bindingGroup) error {
// run startup functions that should only execute when // run startup functions that should only execute when
// the original parent process is starting. // the original parent process is starting.
if !IsRestart() && !startedBefore { if !startedBefore {
err := s.RunFirstStartupFuncs() err := s.RunFirstStartupFuncs()
if err != nil { if err != nil {
errChan <- err errChan <- err
...@@ -268,10 +227,10 @@ func startServers(groupings bindingGroup) error { ...@@ -268,10 +227,10 @@ func startServers(groupings bindingGroup) error {
} }
// Close the remaining (unused) file descriptors to free up resources // Close the remaining (unused) file descriptors to free up resources
if IsRestart() { if len(restartFds) > 0 {
for key, fdIndex := range loadedGob.ListenerFds { for key, file := range restartFds {
os.NewFile(fdIndex, "").Close() file.Close()
delete(loadedGob.ListenerFds, key) delete(restartFds, key)
} }
} }
...@@ -314,25 +273,11 @@ func Wait() { ...@@ -314,25 +273,11 @@ func Wait() {
wg.Wait() wg.Wait()
} }
// LoadCaddyfile loads a Caddyfile, prioritizing a Caddyfile // LoadCaddyfile loads a Caddyfile by calling the user's loader function,
// piped from stdin as part of a restart (only happens on first call // and if that returns nil, then this function resorts to the default
// to LoadCaddyfile). If it is not a restart, this function tries // configuration. Thus, if there are no other errors, this function
// calling the user's loader function, and if that returns nil, then // always returns at least the default Caddyfile.
// this function resorts to the default configuration. Thus, if there
// are no other errors, this function always returns at least the
// default Caddyfile.
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) { func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
// If we are a fork, finishing the restart is highest priority;
// piped input is required in this case.
if IsRestart() {
err := gob.NewDecoder(os.Stdin).Decode(&loadedGob)
if err != nil {
return nil, err
}
cdyfile = loadedGob.Caddyfile
atomic.StoreInt32(https.OnDemandIssuedCount, loadedGob.OnDemandTLSCertsIssued)
}
// Try user's loader // Try user's loader
if cdyfile == nil && loader != nil { if cdyfile == nil && loader != nil {
cdyfile, err = loader() cdyfile, err = loader()
......
...@@ -4,13 +4,11 @@ import ( ...@@ -4,13 +4,11 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
) )
// isLocalhost returns true if host looks explicitly like a localhost address. // isLocalhost returns true if host looks explicitly like a localhost address.
...@@ -35,46 +33,10 @@ func checkFdlimit() { ...@@ -35,46 +33,10 @@ func checkFdlimit() {
} }
} }
// signalSuccessToParent tells the parent our status using pipe at index 3.
// If this process is not a restart, this function does nothing.
// Calling this function once this process has successfully initialized
// is vital so that the parent process can unblock and kill itself.
// This function is idempotent; it executes at most once per process.
func signalSuccessToParent() {
signalParentOnce.Do(func() {
if IsRestart() {
ppipe := os.NewFile(3, "") // parent is reading from pipe at index 3
_, err := ppipe.Write([]byte("success")) // we must send some bytes to the parent
if err != nil {
log.Printf("[ERROR] Communicating successful init to parent: %v", err)
}
ppipe.Close()
}
})
}
// signalParentOnce is used to make sure that the parent is only
// signaled once; doing so more than once breaks whatever socket is
// at fd 4 (the reason for this is still unclear - to reproduce,
// call Stop() and Start() in succession at least once after a
// restart, then try loading first host of Caddyfile in the browser).
// Do not use this directly - call signalSuccessToParent instead.
var signalParentOnce sync.Once
// caddyfileGob maps bind address to index of the file descriptor
// in the Files array passed to the child process. It also contains
// the caddyfile contents and other state needed by the new process.
// Used only during graceful restarts where a new process is spawned.
type caddyfileGob struct {
ListenerFds map[string]uintptr
Caddyfile Input
OnDemandTLSCertsIssued int32
}
// IsRestart returns whether this process is, according // IsRestart returns whether this process is, according
// to env variables, a fork as part of a graceful restart. // to env variables, a fork as part of a graceful restart.
func IsRestart() bool { func IsRestart() bool {
return os.Getenv("CADDY_RESTART") == "true" return startedBefore
} }
// writePidFile writes the process ID to the file at PidFile, if specified. // writePidFile writes the process ID to the file at PidFile, if specified.
......
...@@ -4,23 +4,14 @@ package caddy ...@@ -4,23 +4,14 @@ package caddy
import ( import (
"bytes" "bytes"
"encoding/gob"
"errors" "errors"
"io/ioutil"
"log" "log"
"net" "net"
"os"
"os/exec"
"path/filepath" "path/filepath"
"sync/atomic"
"github.com/mholt/caddy/caddy/https" "github.com/mholt/caddy/caddy/https"
) )
func init() {
gob.Register(CaddyfileInput{})
}
// Restart restarts the entire application; gracefully with zero // Restart restarts the entire application; gracefully with zero
// downtime if on a POSIX-compatible system, or forcefully if on // downtime if on a POSIX-compatible system, or forcefully if on
// Windows but with imperceptibly-short downtime. // Windows but with imperceptibly-short downtime.
...@@ -52,87 +43,14 @@ func Restart(newCaddyfile Input) error { ...@@ -52,87 +43,14 @@ func Restart(newCaddyfile Input) error {
return errors.New("TLS preload: " + err.Error()) return errors.New("TLS preload: " + err.Error())
} }
if RestartMode == "inproc" { // Add file descriptors of all the sockets for new instance
return restartInProc(newCaddyfile)
}
if len(os.Args) == 0 { // this should never happen, but...
os.Args = []string{""}
}
// Tell the child that it's a restart
os.Setenv("CADDY_RESTART", "true")
// Prepare our payload to the child process
cdyfileGob := caddyfileGob{
ListenerFds: make(map[string]uintptr),
Caddyfile: newCaddyfile,
OnDemandTLSCertsIssued: atomic.LoadInt32(https.OnDemandIssuedCount),
}
// Prepare a pipe to the fork's stdin so it can get the Caddyfile
rpipe, wpipe, err := os.Pipe()
if err != nil {
return err
}
// Prepare a pipe that the child process will use to communicate
// its success with us by sending > 0 bytes
sigrpipe, sigwpipe, err := os.Pipe()
if err != nil {
return err
}
// Pass along relevant file descriptors to child process; ordering
// is very important since we rely on these being in certain positions.
extraFiles := []*os.File{sigwpipe} // fd 3
// Add file descriptors of all the sockets
serversMu.Lock() serversMu.Lock()
for i, s := range servers { for _, s := range servers {
extraFiles = append(extraFiles, s.ListenerFd()) restartFds[s.Addr] = s.ListenerFd()
cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners
} }
serversMu.Unlock() serversMu.Unlock()
// Set up the command return restartInProc(newCaddyfile)
cmd := exec.Command(os.Args[0], os.Args[1:]...)
cmd.Stdin = rpipe // fd 0
cmd.Stdout = os.Stdout // fd 1
cmd.Stderr = os.Stderr // fd 2
cmd.ExtraFiles = extraFiles
// Spawn the child process
err = cmd.Start()
if err != nil {
return err
}
// Immediately close our dup'ed fds and the write end of our signal pipe
for _, f := range extraFiles {
f.Close()
}
// Feed Caddyfile to the child
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
if err != nil {
return err
}
wpipe.Close()
// Determine whether child startup succeeded
answer, readErr := ioutil.ReadAll(sigrpipe)
if answer == nil || len(answer) == 0 {
cmdErr := cmd.Wait() // get exit status
log.Printf("[ERROR] Restart: child failed to initialize (%v) - changes not applied", cmdErr)
if readErr != nil {
log.Printf("[ERROR] Restart: additionally, error communicating with child process: %v", readErr)
}
return errIncompleteRestart
}
// Looks like child is successful; we can exit gracefully.
return Stop()
} }
func getCertsForNewCaddyfile(newCaddyfile Input) error { func getCertsForNewCaddyfile(newCaddyfile Input) error {
......
...@@ -5,6 +5,7 @@ import "log" ...@@ -5,6 +5,7 @@ import "log"
// restartInProc restarts Caddy forcefully in process using newCaddyfile. // restartInProc restarts Caddy forcefully in process using newCaddyfile.
func restartInProc(newCaddyfile Input) error { func restartInProc(newCaddyfile Input) error {
wg.Add(1) // barrier so Wait() doesn't unblock wg.Add(1) // barrier so Wait() doesn't unblock
defer wg.Done()
err := Stop() err := Stop()
if err != nil { if err != nil {
...@@ -20,13 +21,8 @@ func restartInProc(newCaddyfile Input) error { ...@@ -20,13 +21,8 @@ func restartInProc(newCaddyfile Input) error {
// revert to old Caddyfile // revert to old Caddyfile
if oldErr := Start(oldCaddyfile); oldErr != nil { if oldErr := Start(oldCaddyfile); oldErr != nil {
log.Printf("[ERROR] Restart: in-process restart failed and cannot revert to old Caddyfile: %v", oldErr) log.Printf("[ERROR] Restart: in-process restart failed and cannot revert to old Caddyfile: %v", oldErr)
} else {
wg.Done() // take down our barrier
} }
return err
} }
wg.Done() // take down our barrier return err
return nil
} }
...@@ -33,7 +33,6 @@ func init() { ...@@ -33,7 +33,6 @@ func init() {
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file") flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port") flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port")
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
flag.StringVar(&caddy.RestartMode, "restart", "", "Restart mode (inproc for in process restart)")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site") flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site")
flag.BoolVar(&version, "version", false, "Show version") flag.BoolVar(&version, "version", false, "Show version")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment