First commit
This commit is contained in:
commit
850aafbacf
|
@ -0,0 +1,6 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- "1.10.x"
|
||||
- "1.11.x"
|
||||
- master
|
|
@ -0,0 +1,11 @@
|
|||
Copyright (c) 2017-2018, Cloudflare. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,62 @@
|
|||
# Graceful process restarts in Go
|
||||
|
||||
[![](https://godoc.org/github.com/cloudflare/tableflip?status.svg)](https://godoc.org/github.com/cloudflare/tableflip)
|
||||
|
||||
It is sometimes useful to update the running code and / or configuration of a
|
||||
network service, without disrupting existing connections. Usually, this is
|
||||
achieved by starting a new process, somehow transferring clients to it and
|
||||
then exiting the old process.
|
||||
|
||||
There are [many ways to implement graceful upgrades](https://blog.cloudflare.com/graceful-upgrades-in-go/).
|
||||
They vary wildly in the trade-offs they make, and how much control they afford the user. This library
|
||||
has the following goals:
|
||||
|
||||
* No old code keeps running after a successful upgrade
|
||||
* The new process has a grace period for performing initialisation
|
||||
* Crashing during initialisation is OK
|
||||
* Only a single upgrade is ever run in parallel
|
||||
|
||||
`tableflip` does not work on Windows.
|
||||
|
||||
It's easy to get started:
|
||||
|
||||
```Go
|
||||
upg, err := tableflip.New(tableflip.Options{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer upg.Stop()
|
||||
|
||||
go func() {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGHUP)
|
||||
for range sig {
|
||||
err := upg.Upgrade()
|
||||
if err != nil {
|
||||
log.Println("Upgrade failed:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("Upgrade succeeded")
|
||||
}
|
||||
}()
|
||||
|
||||
ln, err := upg.Fds.Listen("tcp", "localhost:8080")
|
||||
if err != nil {
|
||||
log.Fatalln("Can't listen:", err)
|
||||
}
|
||||
|
||||
var server http.Server
|
||||
go server.Serve(ln)
|
||||
|
||||
if err := upg.Ready(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
<-upg.Exit()
|
||||
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
os.Exit(1)
|
||||
})
|
||||
|
||||
_ = server.Shutdown(context.Background())
|
||||
```
|
|
@ -0,0 +1,113 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type child struct {
|
||||
*env
|
||||
proc process
|
||||
readyR, namesW *os.File
|
||||
ready <-chan *os.File
|
||||
result <-chan error
|
||||
exited <-chan struct{}
|
||||
}
|
||||
|
||||
func startChild(wdir string, binpath string, env *env, passedFiles map[fileName]*file) (*child, error) {
|
||||
// These pipes are used for communication between parent and child
|
||||
// readyW is passed to the child, readyR stays with the parent
|
||||
readyR, readyW, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "pipe failed")
|
||||
}
|
||||
|
||||
namesR, namesW, err := os.Pipe()
|
||||
if err != nil {
|
||||
readyR.Close()
|
||||
readyW.Close()
|
||||
return nil, errors.Wrap(err, "pipe failed")
|
||||
}
|
||||
|
||||
// Copy passed fds and append the notification pipe
|
||||
fds := []*os.File{readyW, namesR}
|
||||
var fdNames [][]string
|
||||
for name, file := range passedFiles {
|
||||
nameSlice := make([]string, len(name))
|
||||
copy(nameSlice, name[:])
|
||||
fdNames = append(fdNames, nameSlice)
|
||||
fds = append(fds, file.File)
|
||||
}
|
||||
|
||||
// Copy environment and append the notification env vars
|
||||
environ := append([]string(nil), env.environ()...)
|
||||
environ = append(environ,
|
||||
fmt.Sprintf("%s=yes", sentinelEnvVar))
|
||||
|
||||
proc, err := env.newProc(wdir, binpath, os.Args[1:], fds, environ)
|
||||
if err != nil {
|
||||
readyR.Close()
|
||||
readyW.Close()
|
||||
namesR.Close()
|
||||
namesW.Close()
|
||||
return nil, errors.Wrapf(err, "can't start process %s", binpath)
|
||||
}
|
||||
|
||||
exited := make(chan struct{})
|
||||
result := make(chan error, 1)
|
||||
ready := make(chan *os.File, 1)
|
||||
|
||||
c := &child{
|
||||
env,
|
||||
proc,
|
||||
readyR,
|
||||
namesW,
|
||||
ready,
|
||||
result,
|
||||
exited,
|
||||
}
|
||||
go c.writeNames(fdNames)
|
||||
go c.waitExit(result, exited)
|
||||
go c.waitReady(ready)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *child) String() string {
|
||||
return c.proc.String()
|
||||
}
|
||||
|
||||
func (c *child) Kill() {
|
||||
c.proc.Signal(os.Kill)
|
||||
}
|
||||
|
||||
func (c *child) waitExit(result chan<- error, exited chan<- struct{}) {
|
||||
result <- c.proc.Wait()
|
||||
close(exited)
|
||||
// Unblock waitReady and writeNames
|
||||
c.readyR.Close()
|
||||
c.namesW.Close()
|
||||
}
|
||||
|
||||
func (c *child) waitReady(ready chan<- *os.File) {
|
||||
var b [1]byte
|
||||
if n, _ := c.readyR.Read(b[:]); n > 0 && b[0] == notifyReady {
|
||||
// We know that writeNames has exited by this point.
|
||||
// Closing the FD now signals to the child that the parent
|
||||
// has exited.
|
||||
ready <- c.namesW
|
||||
}
|
||||
c.readyR.Close()
|
||||
}
|
||||
|
||||
func (c *child) writeNames(names [][]string) {
|
||||
enc := gob.NewEncoder(c.namesW)
|
||||
if names == nil {
|
||||
// Gob panics on nil
|
||||
_ = enc.Encode([][]string{})
|
||||
return
|
||||
}
|
||||
_ = enc.Encode(names)
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChildExit(t *testing.T) {
|
||||
env, procs := testEnv()
|
||||
|
||||
child, err := startChild(env, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proc := <-procs
|
||||
proc.exit(nil)
|
||||
if err := <-child.result; err != nil {
|
||||
t.Error("Wait returns non-nil error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChildKill(t *testing.T) {
|
||||
env, procs := testEnv()
|
||||
|
||||
child, err := startChild(env, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proc := <-procs
|
||||
|
||||
go child.Kill()
|
||||
if sig := proc.recvSignal(nil); sig != os.Kill {
|
||||
t.Errorf("Received %v instead of os.Kill", sig)
|
||||
}
|
||||
|
||||
proc.exit(nil)
|
||||
}
|
||||
|
||||
func TestChildNotReady(t *testing.T) {
|
||||
env, procs := testEnv()
|
||||
|
||||
child, err := startChild(env, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proc := <-procs
|
||||
proc.exit(nil)
|
||||
<-child.result
|
||||
<-child.exited
|
||||
|
||||
select {
|
||||
case <-child.ready:
|
||||
t.Error("Child signals readiness without pipe being closed")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func TestChildReady(t *testing.T) {
|
||||
env, procs := testEnv()
|
||||
|
||||
child, err := startChild(env, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proc := <-procs
|
||||
if _, _, err := proc.notify(); err != nil {
|
||||
t.Fatal("Can't notify:", err)
|
||||
}
|
||||
<-child.ready
|
||||
proc.exit(nil)
|
||||
}
|
||||
|
||||
func TestChildPassedFds(t *testing.T) {
|
||||
env, procs := testEnv()
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
in := map[fileName]*file{
|
||||
fileName{"r"}: newFile(r.Fd(), fileName{"r"}),
|
||||
fileName{"w"}: newFile(w.Fd(), fileName{"w"}),
|
||||
}
|
||||
|
||||
if _, err := startChild(env, in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proc := <-procs
|
||||
if len(proc.fds) != 2+2 {
|
||||
t.Error("Expected 4 files, got", len(proc.fds))
|
||||
}
|
||||
|
||||
out, _, err := proc.notify()
|
||||
if err != nil {
|
||||
t.Fatal("Notify failed:", err)
|
||||
}
|
||||
|
||||
for name, inFd := range in {
|
||||
if outFd, ok := out[name]; !ok {
|
||||
t.Error(name, "is missing")
|
||||
} else if outFd.Fd() != inFd.Fd() {
|
||||
t.Error(name, "fd mismatch:", outFd.Fd(), inFd.Fd())
|
||||
}
|
||||
}
|
||||
|
||||
proc.exit(nil)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// Package tableflip implements zero downtime upgrades.
|
||||
//
|
||||
// An upgrade spawns a new copy of argv[0] and passes
|
||||
// file descriptors of used listening sockets to the new process. The old process exits
|
||||
// once the new process signals readiness. Thus new code can use sockets allocated
|
||||
// in the old process. This is similar to the approach used by nginx, but
|
||||
// as a library.
|
||||
//
|
||||
// At any point in time there are one or two processes, with at most one of them
|
||||
// in non-ready state. A successful upgrade fully replaces all old configuration
|
||||
// and code.
|
||||
//
|
||||
// To use this library with systemd you need to use the PIDFile option in the service
|
||||
// file.
|
||||
//
|
||||
// [Unit]
|
||||
// Description=Service using tableflip
|
||||
//
|
||||
// [Service]
|
||||
// ExecStart=/path/to/binary -some-flag /path/to/pid-file
|
||||
// ExecReload=/bin/kill -HUP $MAINPID
|
||||
// PIDFile=/path/to/pid-file
|
||||
//
|
||||
// Then pass /path/to/pid-file to New. You can use systemd-run to
|
||||
// test your implementation:
|
||||
//
|
||||
// systemd-run --user -p PIDFile=/path/to/pid-file /path/to/binary
|
||||
//
|
||||
// systemd-run will print a unit name, which you can use with systemctl to
|
||||
// inspect the service.
|
||||
//
|
||||
// NOTES:
|
||||
//
|
||||
// Requires at least Go 1.9, since there is a race condition on the
|
||||
// pipes used for communication between parent and child.
|
||||
//
|
||||
// If you're seeing "can't start process: no such file or directory",
|
||||
// you're probably using "go run main.go", for graceful reloads to work,
|
||||
// you'll need use "go build main.go".
|
||||
//
|
||||
package tableflip
|
|
@ -0,0 +1,12 @@
|
|||
// +build go1.12
|
||||
|
||||
package tableflip
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func dupFile(fh *os.File, name fileName) (*file, error) {
|
||||
// os.File implements syscall.Conn from go 1.12
|
||||
return dupConn(fh, name)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
// +build !go1.12
|
||||
|
||||
package tableflip
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func dupFile(fh *os.File, name fileName) (*file, error) {
|
||||
return dupFd(fh.Fd(), name)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var stdEnv = &env{
|
||||
newProc: newOSProcess,
|
||||
newFile: os.NewFile,
|
||||
environ: os.Environ,
|
||||
getenv: os.Getenv,
|
||||
closeOnExec: syscall.CloseOnExec,
|
||||
}
|
||||
|
||||
type env struct {
|
||||
newProc func(string, string, []string, []*os.File, []string) (process, error)
|
||||
newFile func(fd uintptr, name string) *os.File
|
||||
environ func() []string
|
||||
getenv func(string) string
|
||||
closeOnExec func(fd int)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func testEnv() (*env, chan *testProcess) {
|
||||
procs := make(chan *testProcess, 10)
|
||||
return &env{
|
||||
newProc: func(_ string, _ []string, files []*os.File, env []string) (process, error) {
|
||||
p, err := newTestProcess(files, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
procs <- p
|
||||
return p, nil
|
||||
},
|
||||
environ: func() []string { return nil },
|
||||
getenv: func(string) string { return "" },
|
||||
closeOnExec: func(fd int) {},
|
||||
}, procs
|
||||
}
|
|
@ -0,0 +1,350 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Listener can be shared between processes.
|
||||
type Listener interface {
|
||||
net.Listener
|
||||
syscall.Conn
|
||||
}
|
||||
|
||||
// Conn can be shared between processes.
|
||||
type Conn interface {
|
||||
net.Conn
|
||||
syscall.Conn
|
||||
}
|
||||
|
||||
const (
|
||||
listenKind = "listener"
|
||||
connKind = "conn"
|
||||
fdKind = "fd"
|
||||
)
|
||||
|
||||
type fileName [3]string
|
||||
|
||||
func (name fileName) String() string {
|
||||
return strings.Join(name[:], ":")
|
||||
}
|
||||
|
||||
func (name fileName) isUnixListener() bool {
|
||||
return name[0] == listenKind && (name[1] == "unix" || name[1] == "unixpacket")
|
||||
}
|
||||
|
||||
// file works around the fact that it's not possible
|
||||
// to get the fd from an os.File without putting it into
|
||||
// blocking mode.
|
||||
type file struct {
|
||||
*os.File
|
||||
fd uintptr
|
||||
}
|
||||
|
||||
func newFile(fd uintptr, name fileName) *file {
|
||||
f := os.NewFile(fd, name.String())
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &file{
|
||||
f,
|
||||
fd,
|
||||
}
|
||||
}
|
||||
|
||||
// Fds holds all file descriptors inherited from the
|
||||
// parent process.
|
||||
type Fds struct {
|
||||
mu sync.Mutex
|
||||
// NB: Files in these maps may be in blocking mode.
|
||||
inherited map[fileName]*file
|
||||
used map[fileName]*file
|
||||
}
|
||||
|
||||
func newFds(inherited map[fileName]*file) *Fds {
|
||||
if inherited == nil {
|
||||
inherited = make(map[fileName]*file)
|
||||
}
|
||||
return &Fds{
|
||||
inherited: inherited,
|
||||
used: make(map[fileName]*file),
|
||||
}
|
||||
}
|
||||
|
||||
// Listen returns a listener inherited from the parent process, or creates a new one.
|
||||
func (f *Fds) Listen(network, addr string) (net.Listener, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
ln, err := f.listenerLocked(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ln != nil {
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
ln, err = net.Listen(network, addr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't create new listener")
|
||||
}
|
||||
|
||||
if _, ok := ln.(Listener); !ok {
|
||||
ln.Close()
|
||||
return nil, errors.Errorf("%T doesn't implement tableflip.Listener", ln)
|
||||
}
|
||||
|
||||
err = f.addListenerLocked(network, addr, ln.(Listener))
|
||||
if err != nil {
|
||||
ln.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
// Listener returns an inherited listener or nil.
|
||||
//
|
||||
// It is safe to close the returned listener.
|
||||
func (f *Fds) Listener(network, addr string) (net.Listener, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
return f.listenerLocked(network, addr)
|
||||
}
|
||||
|
||||
func (f *Fds) listenerLocked(network, addr string) (net.Listener, error) {
|
||||
key := fileName{listenKind, network, addr}
|
||||
file := f.inherited[key]
|
||||
if file == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ln, err := net.FileListener(file.File)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "can't inherit listener %s %s", network, addr)
|
||||
}
|
||||
|
||||
delete(f.inherited, key)
|
||||
f.used[key] = file
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
// AddListener adds a listener.
|
||||
//
|
||||
// It is safe to close ln after calling the method.
|
||||
// Any existing listener with the same address is overwitten.
|
||||
func (f *Fds) AddListener(network, addr string, ln Listener) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
return f.addListenerLocked(network, addr, ln)
|
||||
}
|
||||
|
||||
type unlinkOnCloser interface {
|
||||
SetUnlinkOnClose(bool)
|
||||
}
|
||||
|
||||
func (f *Fds) addListenerLocked(network, addr string, ln Listener) error {
|
||||
if ifc, ok := ln.(unlinkOnCloser); ok {
|
||||
ifc.SetUnlinkOnClose(false)
|
||||
}
|
||||
|
||||
return f.addConnLocked(listenKind, network, addr, ln)
|
||||
}
|
||||
|
||||
// Conn returns an inherited connection or nil.
|
||||
//
|
||||
// It is safe to close the returned Conn.
|
||||
func (f *Fds) Conn(network, addr string) (net.Conn, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
key := fileName{connKind, network, addr}
|
||||
file := f.inherited[key]
|
||||
if file == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
conn, err := net.FileConn(file.File)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "can't inherit connection %s %s", network, addr)
|
||||
}
|
||||
|
||||
delete(f.inherited, key)
|
||||
f.used[key] = file
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// AddConn adds a connection.
|
||||
//
|
||||
// It is safe to close conn after calling this method.
|
||||
func (f *Fds) AddConn(network, addr string, conn Conn) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
return f.addConnLocked(connKind, network, addr, conn)
|
||||
}
|
||||
|
||||
func (f *Fds) addConnLocked(kind, network, addr string, conn syscall.Conn) error {
|
||||
key := fileName{kind, network, addr}
|
||||
file, err := dupConn(conn, key)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "can't dup %s (%s %s)", kind, network, addr)
|
||||
}
|
||||
|
||||
delete(f.inherited, key)
|
||||
f.used[key] = file
|
||||
return nil
|
||||
}
|
||||
|
||||
// File returns an inherited file or nil.
|
||||
//
|
||||
// The descriptor may be in blocking mode.
|
||||
func (f *Fds) File(name string) (*os.File, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
key := fileName{fdKind, name}
|
||||
file := f.inherited[key]
|
||||
if file == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Make a copy of the file, since we don't want to
|
||||
// allow the caller to invalidate fds in f.inherited.
|
||||
dup, err := dupFd(file.fd, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
delete(f.inherited, key)
|
||||
f.used[key] = file
|
||||
return dup.File, nil
|
||||
}
|
||||
|
||||
// AddFile adds a file.
|
||||
//
|
||||
// Until Go 1.12, file will be in blocking mode
|
||||
// after this call.
|
||||
func (f *Fds) AddFile(name string, file *os.File) error {
|
||||
key := fileName{fdKind, name}
|
||||
|
||||
dup, err := dupFile(file, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
delete(f.inherited, key)
|
||||
f.used[key] = dup
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fds) copy() map[fileName]*file {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
files := make(map[fileName]*file, len(f.used))
|
||||
for key, file := range f.used {
|
||||
files[key] = file
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func (f *Fds) closeInherited() {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
for key, file := range f.inherited {
|
||||
if key.isUnixListener() {
|
||||
// Remove inherited but unused Unix sockets from the file system.
|
||||
// This undoes the effect of SetUnlinkOnClose(false).
|
||||
_ = unlinkUnixSocket(key[2])
|
||||
}
|
||||
_ = file.Close()
|
||||
}
|
||||
f.inherited = make(map[fileName]*file)
|
||||
}
|
||||
|
||||
func unlinkUnixSocket(path string) error {
|
||||
if strings.HasPrefix(path, "@") {
|
||||
// Don't unlink sockets using the abstract namespace.
|
||||
return nil
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.Mode()&os.ModeSocket == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func (f *Fds) closeUsed() {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
for _, file := range f.used {
|
||||
_ = file.Close()
|
||||
}
|
||||
f.used = make(map[fileName]*file)
|
||||
}
|
||||
|
||||
func (f *Fds) closeAndRemoveUsed() {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
for key, file := range f.used {
|
||||
if key.isUnixListener() {
|
||||
// Remove used Unix Domain Sockets if we are shutting
|
||||
// down without having done an upgrade.
|
||||
// This undoes the effect of SetUnlinkOnClose(false).
|
||||
_ = unlinkUnixSocket(key[2])
|
||||
}
|
||||
_ = file.Close()
|
||||
}
|
||||
f.used = make(map[fileName]*file)
|
||||
}
|
||||
|
||||
func dupConn(conn syscall.Conn, name fileName) (*file, error) {
|
||||
// Use SyscallConn instead of File to avoid making the original
|
||||
// fd non-blocking.
|
||||
raw, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dup *file
|
||||
var duperr error
|
||||
err = raw.Control(func(fd uintptr) {
|
||||
dup, duperr = dupFd(fd, name)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't access fd")
|
||||
}
|
||||
return dup, duperr
|
||||
}
|
||||
|
||||
func dupFd(fd uintptr, name fileName) (*file, error) {
|
||||
dupfd, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_DUPFD_CLOEXEC, 0)
|
||||
if errno != 0 {
|
||||
return nil, errors.Wrap(errno, "can't dup fd using fcntl")
|
||||
}
|
||||
|
||||
return newFile(dupfd, name), nil
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFdsListen(t *testing.T) {
|
||||
addrs := [][2]string{
|
||||
{"unix", ""},
|
||||
{"tcp", "localhost:0"},
|
||||
}
|
||||
|
||||
fds := newFds(nil)
|
||||
|
||||
for _, addr := range addrs {
|
||||
ln, err := fds.Listen(addr[0], addr[1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ln == nil {
|
||||
t.Fatal("Missing listener", addr)
|
||||
}
|
||||
ln.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFdsListener(t *testing.T) {
|
||||
addr := &net.TCPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
Port: 0,
|
||||
}
|
||||
|
||||
tcp, err := net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tcp.Close()
|
||||
|
||||
temp, err := ioutil.TempDir("", "tableflip")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(temp)
|
||||
|
||||
socketPath := filepath.Join(temp, "socket")
|
||||
unix, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer unix.Close()
|
||||
|
||||
parent := newFds(nil)
|
||||
if err := parent.AddListener(addr.Network(), addr.String(), tcp); err != nil {
|
||||
t.Fatal("Can't add listener:", err)
|
||||
}
|
||||
tcp.Close()
|
||||
|
||||
if err := parent.AddListener("unix", socketPath, unix.(Listener)); err != nil {
|
||||
t.Fatal("Can't add listener:", err)
|
||||
}
|
||||
unix.Close()
|
||||
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
t.Error("Unix.Close() unlinked socketPath:", err)
|
||||
}
|
||||
|
||||
abstractUnix, err := parent.Listen("unix", "@tableflip-test-r5N5j")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer abstractUnix.Close()
|
||||
|
||||
child := newFds(parent.copy())
|
||||
ln, err := child.Listener(addr.Network(), addr.String())
|
||||
if err != nil {
|
||||
t.Fatal("Can't get listener:", err)
|
||||
}
|
||||
if ln == nil {
|
||||
t.Fatal("Missing listener")
|
||||
}
|
||||
ln.Close()
|
||||
|
||||
child.closeInherited()
|
||||
if _, err := os.Stat(socketPath); err == nil {
|
||||
t.Error("closeInherited() did not unlink socketPath")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFdsUnixListener(t *testing.T) {
|
||||
temp, err := ioutil.TempDir("", "tableflip")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(temp)
|
||||
|
||||
fds := newFds(nil)
|
||||
|
||||
socketPath := filepath.Join(temp, "socket")
|
||||
unix, err := fds.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unix.Close()
|
||||
|
||||
fds.closeAndRemoveUsed()
|
||||
if _, err := os.Stat(socketPath); err == nil {
|
||||
t.Error("Unix listeners are not removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFdsConn(t *testing.T) {
|
||||
unix, err := net.ListenUnixgram("unixgram", &net.UnixAddr{
|
||||
Net: "unixgram",
|
||||
Name: "",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
parent := newFds(nil)
|
||||
if err := parent.AddConn("unixgram", "", unix); err != nil {
|
||||
t.Fatal("Can't add conn:", err)
|
||||
}
|
||||
unix.Close()
|
||||
|
||||
child := newFds(parent.copy())
|
||||
conn, err := child.Conn("unixgram", "")
|
||||
if err != nil {
|
||||
t.Fatal("Can't get conn:", err)
|
||||
}
|
||||
if conn == nil {
|
||||
t.Fatal("Missing conn")
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestFdsFile(t *testing.T) {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
parent := newFds(nil)
|
||||
if err := parent.AddFile("test", w); err != nil {
|
||||
t.Fatal("Can't add file:", err)
|
||||
}
|
||||
w.Close()
|
||||
|
||||
child := newFds(parent.copy())
|
||||
file, err := child.File("test")
|
||||
if err != nil {
|
||||
t.Fatal("Can't get file:", err)
|
||||
}
|
||||
if file == nil {
|
||||
t.Fatal("Missing file")
|
||||
}
|
||||
file.Close()
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module git.bit5.ru/backend/tableflip
|
||||
|
||||
go 1.12
|
||||
|
||||
require github.com/pkg/errors v0.8.1
|
|
@ -0,0 +1,2 @@
|
|||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
@ -0,0 +1,80 @@
|
|||
package tableflip_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.bit5.ru/backend/tableflip"
|
||||
)
|
||||
|
||||
// This shows how to use the upgrader
|
||||
// with the graceful shutdown facilities of net/http.
|
||||
func Example_httpShutdown() {
|
||||
var (
|
||||
listenAddr = flag.String("listen", "localhost:8080", "`Address` to listen on")
|
||||
pidFile = flag.String("pid-file", "", "`Path` to pid file")
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
log.SetPrefix(fmt.Sprintf("%d ", os.Getpid()))
|
||||
|
||||
upg, err := tableflip.New(tableflip.Options{
|
||||
PIDFile: *pidFile,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer upg.Stop()
|
||||
|
||||
// Do an upgrade on SIGHUP
|
||||
go func() {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGHUP)
|
||||
for range sig {
|
||||
err := upg.Upgrade()
|
||||
if err != nil {
|
||||
log.Println("Upgrade failed:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ln, err := upg.Fds.Listen("tcp", *listenAddr)
|
||||
if err != nil {
|
||||
log.Fatalln("Can't listen:", err)
|
||||
}
|
||||
|
||||
server := http.Server{
|
||||
// Set timeouts, etc.
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.Serve(ln)
|
||||
if err != http.ErrServerClosed {
|
||||
log.Println("HTTP server:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("ready")
|
||||
if err := upg.Ready(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
<-upg.Exit()
|
||||
|
||||
// Make sure to set a deadline on exiting the process
|
||||
// after upg.Exit() is closed. No new upgrades can be
|
||||
// performed if the parent doesn't exit.
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
log.Println("Graceful shutdown timed out")
|
||||
os.Exit(1)
|
||||
})
|
||||
|
||||
// Wait for connections to drain.
|
||||
server.Shutdown(context.Background())
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
sentinelEnvVar = "TABLEFLIP_HAS_PARENT_7DIU3"
|
||||
notifyReady = 42
|
||||
)
|
||||
|
||||
type parent struct {
|
||||
wr *os.File
|
||||
result <-chan error
|
||||
exited <-chan struct{}
|
||||
}
|
||||
|
||||
func newParent(env *env) (*parent, map[fileName]*file, error) {
|
||||
if env.getenv(sentinelEnvVar) == "" {
|
||||
return nil, make(map[fileName]*file), nil
|
||||
}
|
||||
|
||||
wr := env.newFile(3, "write")
|
||||
rd := env.newFile(4, "read")
|
||||
|
||||
var names [][]string
|
||||
dec := gob.NewDecoder(rd)
|
||||
if err := dec.Decode(&names); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "can't decode names from parent process")
|
||||
}
|
||||
|
||||
files := make(map[fileName]*file)
|
||||
for i, parts := range names {
|
||||
var key fileName
|
||||
copy(key[:], parts)
|
||||
|
||||
// Start at 5 to account for stdin, etc. and write
|
||||
// and read pipes.
|
||||
fd := 5 + i
|
||||
env.closeOnExec(fd)
|
||||
files[key] = &file{
|
||||
env.newFile(uintptr(fd), key.String()),
|
||||
uintptr(fd),
|
||||
}
|
||||
}
|
||||
|
||||
result := make(chan error, 1)
|
||||
exited := make(chan struct{})
|
||||
go func() {
|
||||
defer rd.Close()
|
||||
|
||||
n, err := io.Copy(ioutil.Discard, rd)
|
||||
if n != 0 {
|
||||
err = errors.New("unexpected data from parent process")
|
||||
} else if err != nil {
|
||||
err = errors.Wrap(err, "unexpected error while waiting for parent to exit")
|
||||
}
|
||||
result <- err
|
||||
close(exited)
|
||||
}()
|
||||
|
||||
return &parent{
|
||||
wr: wr,
|
||||
result: result,
|
||||
exited: exited,
|
||||
}, files, nil
|
||||
}
|
||||
|
||||
func (ps *parent) sendReady() error {
|
||||
defer ps.wr.Close()
|
||||
if _, err := ps.wr.Write([]byte{notifyReady}); err != nil {
|
||||
return errors.Wrap(err, "can't notify parent process")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParentExit(t *testing.T) {
|
||||
env, procs := testEnv()
|
||||
child, err := startChild(env, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proc := <-procs
|
||||
_, exited, err := proc.notify()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
readyFile := <-child.ready
|
||||
if _, err = readyFile.Write([]byte{1}); err != nil {
|
||||
t.Fatal("Can't inject garbage from parent")
|
||||
}
|
||||
if err := readyFile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = <-exited
|
||||
if err == nil {
|
||||
t.Fatal("Expect child to detect garbage from parent")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
var initialWD, _ = os.Getwd()
|
||||
|
||||
type process interface {
|
||||
fmt.Stringer
|
||||
Signal(sig os.Signal) error
|
||||
Wait() error
|
||||
}
|
||||
|
||||
type osProcess struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func newOSProcess(wdir string, executable string, args []string, files []*os.File, env []string) (process, error) {
|
||||
cmd := exec.Command(executable, args...)
|
||||
cmd.Dir = wdir
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.ExtraFiles = files
|
||||
cmd.Env = env
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &osProcess{cmd}, nil
|
||||
}
|
||||
|
||||
func (osp *osProcess) Signal(sig os.Signal) error {
|
||||
return osp.cmd.Process.Signal(sig)
|
||||
}
|
||||
|
||||
func (osp *osProcess) Wait() error {
|
||||
return osp.cmd.Wait()
|
||||
}
|
||||
|
||||
func (osp *osProcess) String() string {
|
||||
return fmt.Sprintf("pid=%d", osp.cmd.Process.Pid)
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type testProcess struct {
|
||||
fds []*os.File
|
||||
env env
|
||||
signals chan os.Signal
|
||||
sigErr chan error
|
||||
waitErr chan error
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
func newTestProcess(fds []*os.File, envstr []string) (*testProcess, error) {
|
||||
environ := make(map[string]string)
|
||||
for _, entry := range envstr {
|
||||
parts := strings.SplitN(entry, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid env entry: %s", entry)
|
||||
}
|
||||
environ[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
return &testProcess{
|
||||
fds,
|
||||
env{
|
||||
newFile: func(fd uintptr, name string) *os.File {
|
||||
return fds[fd-3]
|
||||
},
|
||||
getenv: func(key string) string {
|
||||
return environ[key]
|
||||
},
|
||||
closeOnExec: func(int) {},
|
||||
},
|
||||
make(chan os.Signal, 1),
|
||||
make(chan error),
|
||||
make(chan error),
|
||||
make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tp *testProcess) Signal(sig os.Signal) error {
|
||||
select {
|
||||
case tp.signals <- sig:
|
||||
return <-tp.sigErr
|
||||
case <-tp.quit:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *testProcess) Wait() error {
|
||||
select {
|
||||
case err := <-tp.waitErr:
|
||||
return err
|
||||
case <-tp.quit:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *testProcess) String() string {
|
||||
return fmt.Sprintf("tp=%p", tp)
|
||||
}
|
||||
|
||||
func (tp *testProcess) exit(err error) {
|
||||
select {
|
||||
case tp.waitErr <- err:
|
||||
close(tp.quit)
|
||||
case <-tp.quit:
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *testProcess) recvSignal(err error) os.Signal {
|
||||
sig := <-tp.signals
|
||||
tp.sigErr <- err
|
||||
return sig
|
||||
}
|
||||
|
||||
func (tp *testProcess) notify() (map[fileName]*file, <-chan error, error) {
|
||||
parent, files, err := newParent(&tp.env)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return files, parent.result, parent.sendReady()
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package tableflip_test
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.bit5.ru/backend/tableflip"
|
||||
)
|
||||
|
||||
// This shows how to use the Upgrader
|
||||
// with a listener based service.
|
||||
func Example_tcpServer() {
|
||||
var (
|
||||
listenAddr = flag.String("listen", "localhost:8080", "`Address` to listen on")
|
||||
pidFile = flag.String("pid-file", "", "`Path` to pid file")
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
log.SetPrefix(fmt.Sprintf("%d ", os.Getpid()))
|
||||
|
||||
upg, err := tableflip.New(tableflip.Options{
|
||||
PIDFile: *pidFile,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer upg.Stop()
|
||||
|
||||
// Do an upgrade on SIGHUP
|
||||
go func() {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGHUP)
|
||||
for range sig {
|
||||
err := upg.Upgrade()
|
||||
if err != nil {
|
||||
log.Println("upgrade failed:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ln, err := upg.Fds.Listen("tcp", *listenAddr)
|
||||
if err != nil {
|
||||
log.Fatalln("Can't listen:", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer ln.Close()
|
||||
|
||||
log.Printf("listening on %s", ln.Addr())
|
||||
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
c.SetDeadline(time.Now().Add(time.Second))
|
||||
c.Write([]byte("It is a mistake to think you can solve any major problems just with potatoes.\n"))
|
||||
c.Close()
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("ready")
|
||||
if err := upg.Ready(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
<-upg.Exit()
|
||||
}
|
|
@ -0,0 +1,307 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// DefaultUpgradeTimeout is the duration before the Upgrader kills the new process if no
|
||||
// readiness notification was received.
|
||||
const DefaultUpgradeTimeout time.Duration = time.Minute
|
||||
|
||||
// Options control the behaviour of the Upgrader.
|
||||
type Options struct {
|
||||
// Time after which an upgrade is considered failed. Defaults to
|
||||
// DefaultUpgradeTimeout.
|
||||
UpgradeTimeout time.Duration
|
||||
// The PID of a ready process is written to this file.
|
||||
PIDFile string
|
||||
WDir string
|
||||
BinFile string
|
||||
}
|
||||
|
||||
// Upgrader handles zero downtime upgrades and passing files between processes.
|
||||
type Upgrader struct {
|
||||
*Fds
|
||||
|
||||
*env
|
||||
opts Options
|
||||
parent *parent
|
||||
parentErr chan error
|
||||
readyOnce sync.Once
|
||||
readyC chan struct{}
|
||||
stopOnce sync.Once
|
||||
stopC chan struct{}
|
||||
upgradeC chan chan<- error
|
||||
exitC chan struct{}
|
||||
exitFd chan neverCloseThisFile
|
||||
}
|
||||
|
||||
var (
|
||||
stdEnvMu sync.Mutex
|
||||
stdEnvUpgrader *Upgrader
|
||||
)
|
||||
|
||||
// New creates a new Upgrader. Files are passed from the parent and may be empty.
|
||||
//
|
||||
// Only the first call to this function will succeed.
|
||||
func New(opts Options) (upg *Upgrader, err error) {
|
||||
stdEnvMu.Lock()
|
||||
defer stdEnvMu.Unlock()
|
||||
|
||||
if stdEnvUpgrader != nil {
|
||||
return nil, errors.New("tableflip: only a single Upgrader allowed")
|
||||
}
|
||||
|
||||
upg, err = newUpgrader(stdEnv, opts)
|
||||
// Store a reference to upg in a private global variable, to prevent
|
||||
// it from being GC'ed and exitFd being closed prematurely.
|
||||
stdEnvUpgrader = upg
|
||||
return
|
||||
}
|
||||
|
||||
func newUpgrader(env *env, opts Options) (*Upgrader, error) {
|
||||
if initialWD == "" {
|
||||
return nil, errors.New("couldn't determine initial working directory")
|
||||
}
|
||||
|
||||
parent, files, err := newParent(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.UpgradeTimeout <= 0 {
|
||||
opts.UpgradeTimeout = DefaultUpgradeTimeout
|
||||
}
|
||||
|
||||
u := &Upgrader{
|
||||
env: env,
|
||||
opts: opts,
|
||||
parent: parent,
|
||||
parentErr: make(chan error, 1),
|
||||
readyC: make(chan struct{}),
|
||||
stopC: make(chan struct{}),
|
||||
upgradeC: make(chan chan<- error),
|
||||
exitC: make(chan struct{}),
|
||||
exitFd: make(chan neverCloseThisFile, 1),
|
||||
Fds: newFds(files),
|
||||
}
|
||||
|
||||
go u.run(opts.WDir, opts.BinFile)
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Ready signals that the current process is ready to accept connections.
|
||||
// It must be called to finish the upgrade.
|
||||
//
|
||||
// All fds which were inherited but not used are closed after the call to Ready.
|
||||
func (u *Upgrader) Ready() error {
|
||||
u.readyOnce.Do(func() {
|
||||
u.Fds.closeInherited()
|
||||
close(u.readyC)
|
||||
})
|
||||
|
||||
if u.opts.PIDFile != "" {
|
||||
if err := writePIDFile(u.opts.PIDFile); err != nil {
|
||||
return errors.Wrap(err, "tableflip: can't write PID file")
|
||||
}
|
||||
}
|
||||
|
||||
if u.parent == nil {
|
||||
return nil
|
||||
}
|
||||
return u.parent.sendReady()
|
||||
}
|
||||
|
||||
// Exit returns a channel which is closed when the process should
|
||||
// exit.
|
||||
func (u *Upgrader) Exit() <-chan struct{} {
|
||||
return u.exitC
|
||||
}
|
||||
|
||||
// Stop prevents any more upgrades from happening, and closes
|
||||
// the exit channel.
|
||||
//
|
||||
// If this function is called before a call to Upgrade() has
|
||||
// succeeded, it is assumed that the process is being shut down
|
||||
// completely. All Unix sockets known to Upgrader.Fds are then
|
||||
// unlinked from the filesystem.
|
||||
func (u *Upgrader) Stop() {
|
||||
u.stopOnce.Do(func() {
|
||||
// Interrupt any running Upgrade(), and
|
||||
// prevent new upgrade from happening.
|
||||
close(u.stopC)
|
||||
})
|
||||
}
|
||||
|
||||
// WaitForParent blocks until the parent has exited.
|
||||
//
|
||||
// Returns an error if the parent misbehaved during shutdown.
|
||||
func (u *Upgrader) WaitForParent(ctx context.Context) error {
|
||||
if u.parent == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
select {
|
||||
case err = <-u.parent.result:
|
||||
case err = <-u.parentErr:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// This is a bit cheeky, since it means that multiple
|
||||
// calls to WaitForParent resolve in sequence, but that
|
||||
// probably doesn't matter.
|
||||
u.parentErr <- err
|
||||
return err
|
||||
}
|
||||
|
||||
// HasParent checks if the current process is an upgrade or the first invocation.
|
||||
func (u *Upgrader) HasParent() bool {
|
||||
return u.parent != nil
|
||||
}
|
||||
|
||||
// Upgrade triggers an upgrade.
|
||||
func (u *Upgrader) Upgrade() error {
|
||||
response := make(chan error, 1)
|
||||
select {
|
||||
case <-u.stopC:
|
||||
return errors.New("terminating")
|
||||
case <-u.exitC:
|
||||
return errors.New("already upgraded")
|
||||
case u.upgradeC <- response:
|
||||
}
|
||||
|
||||
return <-response
|
||||
}
|
||||
|
||||
var errNotReady = errors.New("process is not ready yet")
|
||||
|
||||
func (u *Upgrader) run(wdir string, binpath string) {
|
||||
defer close(u.exitC)
|
||||
|
||||
var (
|
||||
parentExited <-chan struct{}
|
||||
processReady = u.readyC
|
||||
)
|
||||
|
||||
if u.parent != nil {
|
||||
parentExited = u.parent.exited
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-parentExited:
|
||||
parentExited = nil
|
||||
|
||||
case <-processReady:
|
||||
processReady = nil
|
||||
|
||||
case <-u.stopC:
|
||||
u.Fds.closeAndRemoveUsed()
|
||||
return
|
||||
|
||||
case request := <-u.upgradeC:
|
||||
if processReady != nil {
|
||||
request <- errNotReady
|
||||
continue
|
||||
}
|
||||
|
||||
if parentExited != nil {
|
||||
request <- errors.New("parent hasn't exited")
|
||||
continue
|
||||
}
|
||||
|
||||
file, err := u.doUpgrade(wdir, binpath)
|
||||
request <- err
|
||||
|
||||
if err == nil {
|
||||
// Save file in exitFd, so that it's only closed when the process
|
||||
// exits. This signals to the new process that the old process
|
||||
// has exited.
|
||||
u.exitFd <- neverCloseThisFile{file}
|
||||
u.Fds.closeUsed()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Upgrader) doUpgrade(wdir string, binpath string) (*os.File, error) {
|
||||
child, err := startChild(wdir, binpath, u.env, u.Fds.copy())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can't start child")
|
||||
}
|
||||
|
||||
readyTimeout := time.After(u.opts.UpgradeTimeout)
|
||||
for {
|
||||
select {
|
||||
case request := <-u.upgradeC:
|
||||
request <- errors.New("upgrade in progress")
|
||||
|
||||
case err := <-child.result:
|
||||
if err == nil {
|
||||
return nil, errors.Errorf("child %s exited", child)
|
||||
}
|
||||
return nil, errors.Wrapf(err, "child %s exited", child)
|
||||
|
||||
case <-u.stopC:
|
||||
child.Kill()
|
||||
return nil, errors.New("terminating")
|
||||
|
||||
case <-readyTimeout:
|
||||
child.Kill()
|
||||
return nil, errors.Errorf("new child %s timed out", child)
|
||||
|
||||
case file := <-child.ready:
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This file must never be closed by the Go runtime, since its used by the
|
||||
// child to determine when the parent has died. It must only be closed
|
||||
// by the OS.
|
||||
// Hence we make sure that this file can't be garbage collected by referencing
|
||||
// it from an Upgrader.
|
||||
type neverCloseThisFile struct {
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func writePIDFile(path string) error {
|
||||
dir, file := filepath.Split(path)
|
||||
|
||||
// if dir is empty, the user probably specified just the name
|
||||
// of the pid file expecting it to be created in the current work directory
|
||||
if dir == "" {
|
||||
dir = initialWD
|
||||
}
|
||||
|
||||
if dir == "" {
|
||||
return errors.New("empty initial working directory")
|
||||
}
|
||||
|
||||
fh, err := ioutil.TempFile(dir, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
// Remove temporary PID file if something fails
|
||||
defer os.Remove(fh.Name())
|
||||
|
||||
_, err = fh.WriteString(strconv.Itoa(os.Getpid()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(fh.Name(), path)
|
||||
}
|
|
@ -0,0 +1,497 @@
|
|||
package tableflip
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testUpgrader struct {
|
||||
*Upgrader
|
||||
procs chan *testProcess
|
||||
}
|
||||
|
||||
func newTestUpgrader(opts Options) *testUpgrader {
|
||||
env, procs := testEnv()
|
||||
u, err := newUpgrader(env, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = u.Ready()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &testUpgrader{
|
||||
Upgrader: u,
|
||||
procs: procs,
|
||||
}
|
||||
}
|
||||
|
||||
func (tu *testUpgrader) upgradeProc(t *testing.T) (*testProcess, <-chan error) {
|
||||
t.Helper()
|
||||
|
||||
ch := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
err := tu.Upgrade()
|
||||
if err != errNotReady {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
t.Fatal("Upgrade failed:", err)
|
||||
return nil, nil
|
||||
|
||||
case proc := <-tu.procs:
|
||||
return proc, ch
|
||||
}
|
||||
}
|
||||
|
||||
var names = []string{"zaphod", "beeblebrox"}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
upg, err := New(Options{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if upg.parent == nil {
|
||||
// Execute test suite if there is no parent.
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
pid, err := upg.Fds.File("pid")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if pid != nil {
|
||||
buf := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(buf, uint64(os.Getpid()))
|
||||
pid.Write(buf)
|
||||
pid.Close()
|
||||
}
|
||||
|
||||
parent, err := upg.Fds.File("hasParent")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if parent != nil {
|
||||
if _, err := io.WriteString(parent, fmt.Sprint(upg.HasParent())); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
parent.Close()
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
file, err := upg.Fds.File(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if file == nil {
|
||||
continue
|
||||
}
|
||||
if _, err := io.WriteString(file, name); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := upg.Ready(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgraderOnOS(t *testing.T) {
|
||||
u, err := newUpgrader(stdEnv, Options{})
|
||||
if err != nil {
|
||||
t.Fatal("Can't create Upgrader:", err)
|
||||
}
|
||||
defer u.Stop()
|
||||
|
||||
rPid, wPid, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rPid.Close()
|
||||
|
||||
if err := u.Fds.AddFile("pid", wPid); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wPid.Close()
|
||||
|
||||
rHasParent, wHasParent, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rHasParent.Close()
|
||||
|
||||
if err := u.Fds.AddFile("hasParent", wHasParent); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wHasParent.Close()
|
||||
|
||||
var readers []*os.File
|
||||
defer func() {
|
||||
for _, r := range readers {
|
||||
r.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, name := range names {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
readers = append(readers, r)
|
||||
|
||||
if err := u.Fds.AddFile(name, w); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Close()
|
||||
}
|
||||
|
||||
if err := u.Upgrade(); err == nil {
|
||||
t.Error("Upgrade before Ready should return an error")
|
||||
}
|
||||
|
||||
if err := u.Ready(); err != nil {
|
||||
t.Fatal("Ready failed:", err)
|
||||
}
|
||||
|
||||
for {
|
||||
if err := u.Upgrade(); err == nil {
|
||||
break
|
||||
} else if err != errNotReady {
|
||||
t.Fatal("Upgrade failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close copies of write pipes, so that
|
||||
// reads below return EOF.
|
||||
u.Stop()
|
||||
|
||||
buf := make([]byte, 8)
|
||||
if _, err := rPid.Read(buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if int(binary.LittleEndian.Uint64(buf)) == os.Getpid() {
|
||||
t.Error("Child did not execute in new process")
|
||||
}
|
||||
|
||||
hasParentBytes, err := ioutil.ReadAll(rHasParent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(hasParentBytes, []byte("true")) {
|
||||
t.Fatal("Child did not recognize parent")
|
||||
}
|
||||
|
||||
for i, name := range names {
|
||||
nameBytes, err := ioutil.ReadAll(readers[i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(nameBytes, []byte(name)) {
|
||||
t.Fatalf("File %s has name %s in child", name, string(nameBytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgraderCleanExit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newTestUpgrader(Options{})
|
||||
defer u.Stop()
|
||||
|
||||
proc, errs := u.upgradeProc(t)
|
||||
|
||||
proc.exit(nil)
|
||||
if err := <-errs; err == nil {
|
||||
t.Error("Expected Upgrade to return error when new child exits clean")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgraderUncleanExit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newTestUpgrader(Options{})
|
||||
defer u.Stop()
|
||||
|
||||
proc, errs := u.upgradeProc(t)
|
||||
|
||||
proc.exit(errors.New("some error"))
|
||||
if err := <-errs; err == nil {
|
||||
t.Error("Expected Upgrade to return error when new child exits unclean")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgraderTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newTestUpgrader(Options{
|
||||
UpgradeTimeout: 10 * time.Millisecond,
|
||||
})
|
||||
defer u.Stop()
|
||||
|
||||
new, errs := u.upgradeProc(t)
|
||||
|
||||
if sig := new.recvSignal(nil); sig != os.Kill {
|
||||
t.Error("Expected os.Kill, got", sig)
|
||||
}
|
||||
|
||||
if err := <-errs; err == nil {
|
||||
t.Error("Expected Upgrade to return error when new child times out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgraderConcurrentUpgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newTestUpgrader(Options{})
|
||||
defer u.Stop()
|
||||
|
||||
new, _ := u.upgradeProc(t)
|
||||
|
||||
go new.recvSignal(nil)
|
||||
|
||||
if err := u.Upgrade(); err == nil {
|
||||
t.Error("Expected Upgrade to refuse concurrent upgrade")
|
||||
}
|
||||
|
||||
new.exit(nil)
|
||||
}
|
||||
|
||||
func TestHasParent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newTestUpgrader(Options{})
|
||||
defer u.Stop()
|
||||
|
||||
if u.HasParent() {
|
||||
t.Fatal("First process cannot have a parent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgraderWaitForParent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env, procs := testEnv()
|
||||
child, err := startChild(env, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
proc := <-procs
|
||||
u, err := newUpgrader(&proc.env, Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer u.Stop()
|
||||
|
||||
if err := u.Ready(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exited := make(chan error, 1)
|
||||
go func() {
|
||||
exited <- u.WaitForParent(context.Background())
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-exited:
|
||||
t.Fatal("Returned before parent exited")
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
|
||||
readyFile := <-child.ready
|
||||
if err := readyFile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := <-exited; err != nil {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgraderReady(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newTestUpgrader(Options{})
|
||||
defer u.Stop()
|
||||
|
||||
new, errs := u.upgradeProc(t)
|
||||
|
||||
_, exited, err := new.notify()
|
||||
if err != nil {
|
||||
t.Fatal("Can't notify Upgrader:", err)
|
||||
}
|
||||
|
||||
if err := <-errs; err != nil {
|
||||
t.Fatal("Expected Upgrade to return nil when child is ready")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-u.Exit():
|
||||
default:
|
||||
t.Error("Expected Exit() to be closed when upgrade is done")
|
||||
}
|
||||
|
||||
// Simulate the process exiting
|
||||
file := <-u.exitFd
|
||||
file.file.Close()
|
||||
|
||||
select {
|
||||
case err := <-exited:
|
||||
if err != nil {
|
||||
t.Error("exit error", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("Child wasn't notified of parent exiting")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgraderShutdownCancelsUpgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newTestUpgrader(Options{})
|
||||
defer u.Stop()
|
||||
|
||||
new, errs := u.upgradeProc(t)
|
||||
|
||||
go new.recvSignal(nil)
|
||||
|
||||
u.Stop()
|
||||
if err := <-errs; err == nil {
|
||||
t.Error("Upgrade doesn't return an error when Stopp()ed")
|
||||
}
|
||||
|
||||
if err := u.Upgrade(); err == nil {
|
||||
t.Error("Upgrade doesn't return an error after Stop()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadyWritesPIDFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir, err := ioutil.TempDir("", "tableflip")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
file := dir + "/pid"
|
||||
u := newTestUpgrader(Options{
|
||||
PIDFile: file,
|
||||
})
|
||||
defer u.Stop()
|
||||
|
||||
if err := u.Ready(); err != nil {
|
||||
t.Fatal("Ready returned error:", err)
|
||||
}
|
||||
|
||||
fh, err := os.Open(file)
|
||||
if err != nil {
|
||||
t.Fatal("PID file doesn't exist:", err)
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
var pid int
|
||||
if _, err := fmt.Fscan(fh, &pid); err != nil {
|
||||
t.Fatal("Can't read PID:", err)
|
||||
}
|
||||
|
||||
if pid != os.Getpid() {
|
||||
t.Error("PID doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePidFileWithoutPath(t *testing.T) {
|
||||
pidFile := "tableflip-test.pid"
|
||||
|
||||
err := writePIDFile(pidFile)
|
||||
if err != nil {
|
||||
t.Fatal("Could not write pidfile:", err)
|
||||
}
|
||||
defer os.Remove(pidFile)
|
||||
|
||||
// lets see if we are able to read the file back
|
||||
fh, err := os.Open(pidFile)
|
||||
if err != nil {
|
||||
t.Fatal("PID file doesn't exist:", err)
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
// just to be sure: check the pid for correctness
|
||||
// if something failed at a previous run we could be reading
|
||||
// a bogus pidfile
|
||||
var pid int
|
||||
if _, err := fmt.Fscan(fh, &pid); err != nil {
|
||||
t.Fatal("Can't read PID:", err)
|
||||
}
|
||||
|
||||
if pid != os.Getpid() {
|
||||
t.Error("PID doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUpgrade(b *testing.B) {
|
||||
for _, n := range []int{4, 400, 4000} {
|
||||
b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
|
||||
fds := newFds(nil)
|
||||
for i := 0; i < n; i += 2 {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
err = fds.AddFile(strconv.Itoa(n), r)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
r.Close()
|
||||
|
||||
err = fds.AddFile(strconv.Itoa(n), w)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
w.Close()
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u, err := newUpgrader(stdEnv, Options{})
|
||||
if err != nil {
|
||||
b.Fatal("Can't create Upgrader:", err)
|
||||
}
|
||||
if err := u.Ready(); err != nil {
|
||||
b.Fatal("Can't call Ready:", err)
|
||||
}
|
||||
|
||||
u.Fds = fds
|
||||
if err := u.Upgrade(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
|
||||
for _, f := range fds.used {
|
||||
f.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue