Skip to content

Instantly share code, notes, and snippets.

@mtilson
Last active December 31, 2019 01:36
Show Gist options
  • Save mtilson/4acb20bcc48faf3cb7665a974187a38d to your computer and use it in GitHub Desktop.
Save mtilson/4acb20bcc48faf3cb7665a974187a38d to your computer and use it in GitHub Desktop.
how to use context, channels, and goroutines to create and gracefully shutdown multiple http server (listener) instances [golang]
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/mux"
)
var (
version string = "UNKNOWN" // by default, can be redefined with `-ldflags`
portServ string = "8080" // by default, can be redefined with `-ldflags`
portDiag string = "8081" // by default, can be redefined with `-ldflags`
)
func main() {
router := mux.NewRouter()
// handler for Server's `/hello` request
router.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request '%v' received", r.URL.RequestURI())
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Hello world.")
})
server := http.Server{
Addr: net.JoinHostPort("", portServ),
Handler: router,
}
routerDiag := mux.NewRouter()
// handler for Diagnostics Server's `/ready` request
routerDiag.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request '%v' received", r.URL.RequestURI())
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Version: %s", version)
})
serverDiag := http.Server{
Addr: net.JoinHostPort("", portDiag),
Handler: routerDiag,
}
shutdownChan := make(chan error, 2)
go func() {
log.Print("Starting Server...")
// `ListenAndServe()` listens on the TCP network address and then calls `Serve()` with handler to
// handle requests on incoming connections. It always returns a non-nil error.
// `Serve()` accepts incoming HTTP connections on the listener, creating a new service goroutine for each.
// The service goroutines read requests and then call handler to reply to them.
// `Serve()` always returns a non-nil error.
// `ListenAndServe()` will exit in the following cases:
// - when `server.Shutdown()` is called - `ListenAndServe()` then immediately return `ErrServerClosed`
// - this case is the signal from main.main() goroutine that flow passed through `select {}` block
// due to either unblocking read operation on either signal channel or shutdown channel and we
// don't need to notify `select {}` block (by writing to shutdown channel) as the flow has already
// pass through this block
//
// - when error occurs and returned by Server's underlying Listener(s)
// - in this case we need notify `select {}` block (by writing to shutdown channel) that some error
// occured with the Server and we need to shutdown both Server (its goroutine will be already down)
// and Diagnostics Server
err := server.ListenAndServe()
log.Printf("Server exited with '%v` error", err)
if err != http.ErrServerClosed {
log.Printf("Put '%v' error returned by Server to shutdown channel", err)
shutdownChan <- err
}
}()
go func() {
log.Print("Starting Diagnostics Server...")
// details are the same as for the `server.ListenAndServe()` section above
err := serverDiag.ListenAndServe()
log.Printf("Diagnostics Server exited with '%v` error", err)
if err != http.ErrServerClosed {
log.Printf("Put '%v' error returned by Diagnostics Server to shutdown channel", err)
shutdownChan <- err
}
}()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
select {
// one of registered signal from OS is received and we need to break this block operation and
// `Shutdown()` both servers
case sig := <-signalChan:
log.Printf("Signal '%v' received from signal channel", sig)
// one of Server's goroutine put error to shutdown channel and we need to break this block operation and
// `Shutdown()` another server (by the way we `Shutdown()` both as it doesn't matter if we try to
// `Shutdown()` server which already down)
case err := <-shutdownChan:
log.Printf("Error '%v' received from shutdown channel", err)
}
// channel returned by `ctxTimeout.Done()` is closed when the specified timeout (5*time.Second) expires or
// when `cancelFunc()` function is called whichever happens first
ctxTimeout, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
// `cancelFunc()` is deferred till the moment we return from `main()`
defer cancelFunc()
// `Shutdown()` gracefully shuts down the server without interrupting any active connections.
// If the provided context expires before the shutdown is complete, `Shutdown()` returns the context's error,
// otherwise it returns any error returned from closing the Server's underlying Listener(s).
// When `Shutdown()` is called, `Serve()`, `ListenAndServe()`, and `ListenAndServeTLS()` immediately return
// `ErrServerClosed`.
// Make sure the program doesn't exit and waits instead for `Shutdown()` to return.
// Once `Shutdown()` has been called on a server, it may not be reused; future calls to methods such as
// `Serve()` will return `ErrServerClosed`.
// if we put pause (timeout) here, then there will be that timeout between `Signal 'interrupt' received` event
// and `Diagnostics Server exited` event
err := serverDiag.Shutdown(ctxTimeout)
if err != nil {
log.Print(err)
}
// if we put pause (timeout) here, then there will be that timeout between `Diagnostics Server exited` event
// and `Server exited` event
err = server.Shutdown(ctxTimeout)
if err != nil {
log.Print(err)
}
// if we don't put timeout here, we will not see the log messages put into the output after `ListenAndServe()`
// returned (see code lines 76 and 89), which means this (main.main()) goroutine could exit before those two
// goroutines ... but ... it seems that even if there will be 'goroutine leak' it will not cause any harm as
// `Shutdown()` has already returned at this moment and the main.main() goroutine exiting will exit other
// goroutines which have no any meaningful state (or state chaging)
time.Sleep(1 * time.Nanosecond)
}
$ go run main.go
2019/12/31 04:35:29 Starting Diagnostics Server...
2019/12/31 04:35:29 Starting Server...
2019/12/31 04:35:42 Request '/ready' received
2019/12/31 04:35:50 Request '/hello' received
^C2019/12/31 04:36:02 Signal 'interrupt' received from signal channel
2019/12/31 04:36:02 Diagnostics Server exited with 'http: Server closed` error
2019/12/31 04:36:02 Server exited with 'http: Server closed` error
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment