Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
caddy
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
caddy
Commits
1f29c521
Commit
1f29c521
authored
May 07, 2016
by
Matt Holt
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #807 from mholt/graceful-inproc-restart
Restart gracefully with in-process restart
parents
db21b031
9705f349
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
27 additions
and
207 deletions
+27
-207
caddy/caddy.go
caddy/caddy.go
+20
-75
caddy/helpers.go
caddy/helpers.go
+1
-39
caddy/restart.go
caddy/restart.go
+4
-86
caddy/restartinproc.go
caddy/restartinproc.go
+2
-6
main.go
main.go
+0
-1
No files found.
caddy/caddy.go
View file @
1f29c521
...
@@ -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
.
Listener
Fds
,
s
.
Addr
)
delete
(
restart
Fds
,
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
,
f
dIndex
:=
range
loadedGob
.
Listener
Fds
{
for
key
,
f
ile
:=
range
restart
Fds
{
os
.
NewFile
(
fdIndex
,
""
)
.
Close
()
file
.
Close
()
delete
(
loadedGob
.
Listener
Fds
,
key
)
delete
(
restart
Fds
,
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
()
...
...
caddy/helpers.go
View file @
1f29c521
...
@@ -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.
...
...
caddy/restart.go
View file @
1f29c521
...
@@ -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
{
...
...
caddy/restartinproc.go
View file @
1f29c521
...
@@ -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
}
}
main.go
View file @
1f29c521
...
@@ -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"
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment