Go Pattern: Supervise Multiple Servers

This post describes a simple pattern to supervise multiple http/grpc/tcp servers in a go program.

Packages Used: context, errgroup, os, os/signal

After receiving a termination signal, we wait on multiple servers to shut them down gracefully. This is done using the errgroup package.

$ go run main.go
2019/10/21 11:55:59 server 2 listening on port 8081
2019/10/21 11:55:59 server 1 listening on port 8080
^C2019/10/21 11:56:10 shutting down servers, please wait...
2019/10/21 11:56:14 a graceful bye

To see the delay in shutdown in action, we can mimic a alive open connection by sending a request from the browser i.e. open http://localhost:8080. A curl connection would terminate immediately and you wouldn’t be able to see the shutown delay since there’s nothing to cleanup.

And here’s the code:

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"golang.org/x/sync/errgroup"
)

type helloHandler struct {
	ctx  context.Context
	name string
}

func (h *helloHandler) ServeHTTP(
	w http.ResponseWriter,
	r *http.Request,
) {
	w.Write([]byte(fmt.Sprintf("Hello from %s", h.name)))
}

func newHelloServer(
	ctx context.Context,
	name string,
	port int,
) *http.Server {

	mux := http.NewServeMux()
	handler := &helloHandler{ctx: ctx, name: name}
	mux.Handle("/", handler)
	httpServer := &http.Server{
		Addr:    fmt.Sprintf(":%d", port),
		Handler: mux,
	}

	return httpServer
}

func main() {
	// HERE
	// setup context and signal handling
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
	defer signal.Stop(quit)

	g, ctx := errgroup.WithContext(ctx)

	// start servers
	server1 := newHelloServer(ctx, "server1", 8080)
	g.Go(func() error {
		log.Println("server 1 listening on port 8080")
		if err := server1.ListenAndServe(); 
			err != http.ErrServerClosed {
			return err
		}

		return nil
	})

	server2 := newHelloServer(ctx, "server2", 8081)
	g.Go(func() error {
		log.Println("server 2 listening on port 8081")
		if err := server2.ListenAndServe(); 
			err != http.ErrServerClosed {
			return err
		}

		return nil
	})

	// handle termination
	select {
	case <-quit:
		break
	case <-ctx.Done():
		break
	}

	// gracefully shutdown http servers
	cancel()

	timeoutCtx, timeoutCancel := context.WithTimeout(
		context.Background(),
		10*time.Second,
	)
	defer timeoutCancel()

	log.Println("shutting down servers, please wait...")

	server1.Shutdown(timeoutCtx)
	server2.Shutdown(timeoutCtx)

	// AND THIS
	// wait for shutdown
	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}

	log.Println("a graceful bye")
}