commit
4519b7ee56
@ -0,0 +1 @@
|
|||||||
|
/go-graceful-restart-example
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
Copyright (c) 2022, EdisonLiu
|
||||||
|
Copyright (c) 2014, Scalingo
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
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,37 @@
|
|||||||
|
# Server graceful restart with Go
|
||||||
|
|
||||||
|
## Install and run the server
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get github.com/Scalingo/go-graceful-restart-example
|
||||||
|
$ go-graceful-restart-example
|
||||||
|
2014/12/14 20:26:42 [Server - 4301] Listen on [::]:12345
|
||||||
|
[...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connect with the client
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd $GOPATH/src/github.com/Scalingo/go-graceful-restart-example/client
|
||||||
|
$ go run pong.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graceful restart
|
||||||
|
|
||||||
|
```
|
||||||
|
# The server pid is included in its log, in the example: 4301
|
||||||
|
|
||||||
|
$ kill -HUP <server pid>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stop with timeout
|
||||||
|
|
||||||
|
Let 10 seconds for the current requests to finish.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ kill -TERM <server pid>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gist of output
|
||||||
|
|
||||||
|
https://gist.github.com/Soulou/7ca6a2d4f475f8e2345e
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sock, err := net.Dial("tcp", ":12345")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("fail to dial ':12345':", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := make([]byte, 64)
|
||||||
|
for {
|
||||||
|
n, err := sock.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("fail to read socket:", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(string(buffer))
|
||||||
|
fmt.Printf("[Client] Received %d bytes, '%s'\n", n, string(buffer[:n]))
|
||||||
|
_, err = sock.Write([]byte("pong"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("fail to write 'pong' to the socket:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[Client] Sent 'ping'\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
module github.com/Scalingo/go-graceful-restart-example
|
||||||
|
|
||||||
|
go 1.17
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
logger *log.Logger
|
||||||
|
prefix string
|
||||||
|
pid int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(prefix string) *Logger {
|
||||||
|
l := &Logger{logger: log.New(os.Stdout, "", log.LstdFlags)}
|
||||||
|
l.prefix = fmt.Sprintf("[%s - %d]", prefix, os.Getpid())
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Println(args ...interface{}) {
|
||||||
|
l.logger.Println(append([]interface{}{l.prefix}, args...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Printf(format string, args ...interface{}) {
|
||||||
|
l.logger.Printf(l.prefix+" "+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Fatalln(args ...interface{}) {
|
||||||
|
l.Println(args...)
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Scalingo/go-graceful-restart-example/logger"
|
||||||
|
"github.com/Scalingo/go-graceful-restart-example/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log := logger.New("Server")
|
||||||
|
|
||||||
|
var s *server.Server
|
||||||
|
var err error
|
||||||
|
if os.Getenv("_GRACEFUL_RESTART") == "true" {
|
||||||
|
s, err = server.NewFromFD(log, 4)
|
||||||
|
} else {
|
||||||
|
s, err = server.New(log, 12345)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("fail to init server:", err)
|
||||||
|
}
|
||||||
|
log.Println("Listen on", s.Addr())
|
||||||
|
|
||||||
|
go s.StartAcceptLoop()
|
||||||
|
|
||||||
|
signals := make(chan os.Signal)
|
||||||
|
signal.Notify(signals, syscall.SIGHUP, syscall.SIGTERM)
|
||||||
|
for sig := range signals {
|
||||||
|
if sig == syscall.SIGTERM {
|
||||||
|
// Stop accepting new connections
|
||||||
|
s.Stop()
|
||||||
|
// Wait a maximum of 10 seconds for existing connections to finish
|
||||||
|
err := s.WaitWithTimeout(10 * time.Second)
|
||||||
|
if err == server.WaitTimeoutError {
|
||||||
|
log.Printf("Timeout when stopping server, %d active connections will be cut.\n", s.ConnectionsCounter())
|
||||||
|
os.Exit(-127)
|
||||||
|
}
|
||||||
|
// Then the program exists
|
||||||
|
log.Println("Server shutdown successful")
|
||||||
|
os.Exit(0)
|
||||||
|
} else if sig == syscall.SIGHUP {
|
||||||
|
// Stop accepting requests
|
||||||
|
s.Stop()
|
||||||
|
// Get socket file descriptor to pass it to fork
|
||||||
|
listenerFD, err := s.ListenerFD()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Fail to get socket file descriptor:", err)
|
||||||
|
}
|
||||||
|
// Set a flag for the new process start process
|
||||||
|
os.Setenv("_GRACEFUL_RESTART", "true")
|
||||||
|
execSpec := &syscall.ProcAttr{
|
||||||
|
Env: os.Environ(),
|
||||||
|
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listenerFD},
|
||||||
|
}
|
||||||
|
// Fork exec the new version of your server
|
||||||
|
fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Fail to fork1", err)
|
||||||
|
}
|
||||||
|
log.Println("SIGHUP received: fork-exec to", fork)
|
||||||
|
// Wait for all conections to be finished
|
||||||
|
s.Wait()
|
||||||
|
log.Println(os.Getpid(), "Server gracefully shutdown")
|
||||||
|
|
||||||
|
// Stop the old server, all the connections have been closed and the new one is running
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type ConnectionManager struct {
|
||||||
|
*sync.WaitGroup
|
||||||
|
Counter int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnectionManager() *ConnectionManager {
|
||||||
|
cm := &ConnectionManager{}
|
||||||
|
cm.WaitGroup = &sync.WaitGroup{}
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConnectionManager) Add(delta int) {
|
||||||
|
cm.Counter += delta
|
||||||
|
cm.WaitGroup.Add(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConnectionManager) Done() {
|
||||||
|
cm.Counter--
|
||||||
|
cm.WaitGroup.Done()
|
||||||
|
}
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Scalingo/go-graceful-restart-example/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
cm *ConnectionManager
|
||||||
|
socket *net.TCPListener
|
||||||
|
logger *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger *logger.Logger, port int) (*Server, error) {
|
||||||
|
s := &Server{cm: NewConnectionManager(), logger: logger}
|
||||||
|
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fail to resolve addr: %v", err)
|
||||||
|
}
|
||||||
|
sock, err := net.ListenTCP("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fail to listen tcp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.socket = sock
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromFD(logger *logger.Logger, fd uintptr) (*Server, error) {
|
||||||
|
s := &Server{cm: NewConnectionManager(), logger: logger}
|
||||||
|
file := os.NewFile(3, "")
|
||||||
|
// file := os.NewFile(fd, "/tmp/sock-go-graceful-restart")
|
||||||
|
listener, err := net.FileListener(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("File to recover socket from file descriptor: " + err.Error())
|
||||||
|
}
|
||||||
|
listenerTCP, ok := listener.(*net.TCPListener)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("File descriptor %d is not a valid TCP socket", fd)
|
||||||
|
}
|
||||||
|
s.socket = listenerTCP
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Stop() {
|
||||||
|
// Accept will instantly return a timeout error
|
||||||
|
s.socket.SetDeadline(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ListenerFD() (uintptr, error) {
|
||||||
|
file, err := s.socket.File()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return file.Fd(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Wait() {
|
||||||
|
s.cm.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
var WaitTimeoutError = errors.New("timeout")
|
||||||
|
|
||||||
|
func (s *Server) WaitWithTimeout(duration time.Duration) error {
|
||||||
|
timeout := time.NewTimer(duration)
|
||||||
|
wait := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
s.Wait()
|
||||||
|
wait <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-timeout.C:
|
||||||
|
return WaitTimeoutError
|
||||||
|
case <-wait:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) StartAcceptLoop() {
|
||||||
|
for {
|
||||||
|
conn, err := s.socket.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||||
|
s.logger.Println("Stop accepting connections")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.Println("[Error] fail to accept:", err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
s.cm.Add(1)
|
||||||
|
s.handleConn(conn)
|
||||||
|
s.cm.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleConn(conn net.Conn) {
|
||||||
|
tick := time.NewTicker(time.Second)
|
||||||
|
buffer := make([]byte, 64)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
_, err := conn.Write([]byte("ping6"))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Println("[Error] fail to write 'ping':", err)
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.Printf("[Server] Sent 'ping'\n")
|
||||||
|
|
||||||
|
n, err := conn.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Println("[Error] fail to read from socket:", err)
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Printf("[Server] OK: read %d bytes: '%s'\n", n, string(buffer[:n]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Addr() net.Addr {
|
||||||
|
return s.socket.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ConnectionsCounter() int {
|
||||||
|
return s.cm.Counter
|
||||||
|
}
|
||||||
Loading…
Reference in new issue