23/12/2019 - GO
In this example I am going to show you how you can gracefully handle HTTP server shutdown in Go applications. The point here is to wait for certain amount of duration before shutting down the server rather than killing all "live" requests/connections by just shutting it down straight away. Read the code comments to understand what exactly lines and functions do. There are different ways of handling graceful server shutdown so this is just one of those examples which is mainly based on the Golang Shutdown documentation. I am just going to keep the necessary code to keep the example as relevant as possible.
.
├── cmd
│ └── client
│ └── main.go
├── go.mod
└── internal
├── app
│ ├── client.go
│ ├── handler.go
│ └── server.go
└── user
└── get.go
A "known" shutdown signal occurs. e.g. Ctrl-C
, kill PID
, docker stop
or docker down
. If there is no "live" request/connection at that moment, block new requests coming in and shutdown the server without waiting for 10
seconds. However, if there was one or many "live" requests/connection, block new requests coming in and allow them 10
seconds to finish what they are doing before shutting it down. If any "live" request/connection exceeds 10
seconds then it gets killed.
The whole code could be kept in a single file or organised differently so it's up to you to improve/refactor it.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"internal/app"
)
func main() {
// Create the application instance.
application := app.App{
Server: app.Server(app.Handler()),
}
// idleChan channel is dedicated for shutting down all active connections.
// Once actual shutdown occurred by closing this channel, the main goroutine
// is shutdown.
idleChan := make(chan struct{})
go func() {
// signChan channel is used to transmit signal notifications.
signChan := make(chan os.Signal, 1)
// Catch and relay certain signal(s) to signChan channel.
signal.Notify(signChan, os.Interrupt, syscall.SIGTERM)
// Blocking until a signal is sent over signChan channel.
sig := <-signChan
log.Println("shutdown:", sig)
// Create a new context with a timeout duration. It helps allowing
// timeout duration to all active connections in order for them to
// finish their job. Any connections that wont complete within the
// allowed timeout duration gets halted.
ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()
if err := application.Stop(ctx); err == context.DeadlineExceeded {
log.Println("shutdown: halted active connections")
}
// Actual shutdown trigger.
close(idleChan)
}()
if err := application.Start(); err == http.ErrServerClosed {
log.Println("shutdown: started")
}
// Blocking until the shutdown to complete then inform the main goroutine.
<-idleChan
log.Println("shutdown: completed")
}
package app
import (
"context"
"net/http"
)
type App struct {
Server *http.Server
}
func (a App) Start() error {
return a.Server.ListenAndServe()
}
func (a App) Stop(ctx context.Context) error {
return a.Server.Shutdown(ctx)
}
package app
import (
"net/http"
"internal/user"
)
func Handler() http.Handler {
handler := http.NewServeMux()
handler.HandleFunc("/client/api/v1/users", user.Get)
return handler
}
package app
import (
"net/http"
)
func Server(handler http.Handler) *http.Server {
return &http.Server{
Addr: ":8080",
Handler: handler,
}
}
package user
import (
"fmt"
"net/http"
"time"
)
func Get(w http.ResponseWriter, _ *http.Request) {
time.Sleep(15 * time.Second)
_, _ = w.Write([]byte(fmt.Sprintf("Response: %d", 1)))
}
When there is live connections.
^C
2020/03/22 20:54:59 shutdown: interrupt
2020/03/22 20:54:59 shutdown: started
2020/03/22 20:55:04 shutdown: halted active connections
2020/03/22 20:55:04 shutdown: completed
When there isn't any live connections.
^C
2020/03/22 20:54:34 shutdown: interrupt
2020/03/22 20:54:34 shutdown: started
2020/03/22 20:54:39 shutdown: completed