Last active
December 31, 2019 01:36
-
-
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]
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ 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