diff --git a/beego.go b/beego.go index 23c48133..54bbecc4 100644 --- a/beego.go +++ b/beego.go @@ -29,10 +29,10 @@ var ( ViewsPath string RunMode string //"dev" or "prod" AppConfig *Config - //related to session + //related to session GlobalSessions *session.Manager //GlobalSessions SessionOn bool // wheather auto start session,default is false - SessionProvider string // default session provider memory mysql redis + SessionProvider string // default session provider memory mysql redis SessionName string // sessionName cookie's name SessionGCMaxLifetime int64 // session's gc maxlifetime SessionSavePath string // session savepath if use mysql/redis/file this set to the connectinfo @@ -81,15 +81,27 @@ func NewApp() *App { func (app *App) Run() { addr := fmt.Sprintf("%s:%d", HttpAddr, HttpPort) - var err error + var ( + err error + l net.Listener + ) if UseFcgi { - l, e := net.Listen("tcp", addr) - if e != nil { - BeeLogger.Fatal("Listen: ", e) + l, err = net.Listen("tcp", addr) + if err != nil { + BeeLogger.Fatal("Listen: ", err) } err = fcgi.Serve(l, app.Handlers) } else { - err = http.ListenAndServe(addr, app.Handlers) + server := &http.Server{Handler: app.Handlers} + laddr, err := net.ResolveTCPAddr("tcp", addr) + if nil != err { + BeeLogger.Fatal("ResolveTCPAddr:", err) + } + l, err = GetInitListner(laddr) + theStoppable = newStoppable(l) + err = server.Serve(theStoppable) + theStoppable.wg.Wait() + CloseSelf() } if err != nil { BeeLogger.Fatal("ListenAndServe: ", err) diff --git a/reload.go b/reload.go new file mode 100644 index 00000000..961ccd75 --- /dev/null +++ b/reload.go @@ -0,0 +1,152 @@ +// Zero-downtime restarts in Go. +package beego + +import ( + "errors" + "fmt" + "log" + "net" + "os" + "os/exec" + "os/signal" + "reflect" + "strconv" + "sync" + "syscall" +) + +const ( + FDKey = "BEEGO_HOT_FD" +) + +// Export an error equivalent to net.errClosing for use with Accept during +// a graceful exit. +var ErrClosing = errors.New("use of closed network connection") +var ErrInitStart = errors.New("init from") + +// Allows for us to notice when the connection is closed. +type conn struct { + net.Conn + wg *sync.WaitGroup +} + +func (c conn) Close() error { + err := c.Conn.Close() + c.wg.Done() + return err +} + +type stoppableListener struct { + net.Listener + count int64 + stopped bool + wg sync.WaitGroup +} + +var theStoppable *stoppableListener + +func newStoppable(l net.Listener) (sl *stoppableListener) { + sl = &stoppableListener{Listener: l} + + // this goroutine monitors the channel. Can't do this in + // Accept (below) because once it enters sl.Listener.Accept() + // it blocks. We unblock it by closing the fd it is trying to + // accept(2) on. + go func() { + WaitSignal(l) + sl.stopped = true + sl.Listener.Close() + }() + return +} + +func (sl *stoppableListener) Accept() (c net.Conn, err error) { + c, err = sl.Listener.Accept() + if err != nil { + return + } + sl.wg.Add(1) + // Wrap the returned connection, so that we can observe when + // it is closed. + c = conn{Conn: c, wg: &sl.wg} + + return +} + +func WaitSignal(l net.Listener) error { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTERM, syscall.SIGHUP) + for { + sig := <-ch + log.Println(sig.String()) + switch sig { + + case syscall.SIGTERM: + return nil + case syscall.SIGHUP: + err := Restart(l) + if nil != err { + return err + } + return nil + } + } + return nil // It'll never get here. +} + +func CloseSelf() error { + ppid := os.Getpid() + if ppid == 1 { // init provided sockets, for example systemd + return nil + } + p, err := os.FindProcess(ppid) + if err != nil { + return err + } + return p.Kill() +} + +// Re-exec this image without dropping the listener passed to this function. +func Restart(l net.Listener) error { + argv0, err := exec.LookPath(os.Args[0]) + if nil != err { + return err + } + wd, err := os.Getwd() + if nil != err { + return err + } + v := reflect.ValueOf(l).Elem().FieldByName("fd").Elem() + fd := uintptr(v.FieldByName("sysfd").Int()) + allFiles := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, + os.NewFile(fd, string(v.FieldByName("sysfile").String()))) + + p, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{ + Dir: wd, + Env: append(os.Environ(), fmt.Sprintf("%s=%d", FDKey, fd)), + Files: allFiles, + }) + if nil != err { + return err + } + log.Printf("spawned child %d\n", p.Pid) + return nil +} + +func GetInitListner(tcpaddr *net.TCPAddr) (l net.Listener, err error) { + countStr := os.Getenv(FDKey) + if countStr == "" { + return net.ListenTCP("tcp", tcpaddr) + } else { + count, err := strconv.Atoi(countStr) + if err != nil { + return nil, err + } + f := os.NewFile(uintptr(count), "listen socket") + l, err = net.FileListener(f) + if err != nil { + return nil, err + } + return l, nil + } +}