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