Como fazer graceful shutdown correto em um server HTTP

Embora esse tema não seja abordado em exemplos simples de APIs, ele é muito importante. Encerrar um servidor HTTP de forma abrupta, acaba por fechar todas as conexões que ele tem abertas da mesma forma. Em outras palavras, não tratar desligamentos via SIGINT ou SIGTERM de uma API, pode acabar gerando grandes problemas.

Para evitar tais problemas, vou mostrar nesse post como tratar tais sinais e fazer um graceful shutdown do seu servidor HTTP.

Servidor HTTP

Para começar, vamos escrever uma rota que retornará um “hello world”. Vou utilizar o package go-chi para definir a rota não por ser necessário, mas para facilitar o entendimento em aplicações do mundo real.

r := chi.NewRouter()

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello World"))
})

Dando um pequeno spoiler, como para fazer o graceful shutdown teremos que executar o método Shutdown, ao invés de utilizar a função http.ListenAndServe, precisamos iniciar uma nova struct http.Server manualmente.

Isso por que, embora por debaixo dos panos a função http.ListenAndServe crie uma struct do tipo http.Server e depois execute o método ListenAndServe dessa struct, o fato dela não retornar a struct em si, acaba por impossibilitar sua utilização.

r := chi.NewRouter()

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello World"))
})

server := &http.Server{
	Addr:    ":8080",
	Handler: r,
}

Agora, vamos chamar a função ListenAndServe e validar o erro retornado.

r := chi.NewRouter()

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello World"))
})

server := &http.Server{
	Addr:    ":8080",
	Handler: r,
}

if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
	log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")

Essa validação é necessário pois quando a função ListenAndServe retorna “sem erro”, um erro do tipo http.ErrServerClosed é retornado. Talvez fizesse mais sentido retornar nil. No entanto não foi assim que o time do Go decidiu.

Signals

Nessa parte do código, vamos iniciar um novo channel e colocá-lo para ouvir os sinais do tipo SIGINT e SIGTERM.

sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
<-sc

Agora, vamos criar um context do tipo WithTimeout. Também vamos colocar a chamada da função de cancelamento em um defer. Isso fará com que todos os recursos associados ao context sejam liberados.

sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
<-sc

ctx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()

Por fim, na chamada do método Shutdown, passaremos o context recém criado.

sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
<-sc

ctx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()

if err := server.Shutdown(ctx); err != nil {
	log.Fatalf("HTTP shutdown error: %v", err)
}
log.Println("Graceful shutdown complete.")

Graceful Shutdown

Não sei se você notou, mas ambos os códigos tem um ponto bloqueante. No primeiro, a chamada da função server.ListenAndServe(). Já no segundo, a espera do channel <-sc.

Por isso, a única forma de fazer ambos os códigos funcionarem em conjunto, é colocando parte de um deles em uma goroutine.

Como não queremos que o programe encerre antes do nosso shutdown ser executado e, o retorno da função ListenAndServe faria isso, vamos colocar sua chamada em uma goroutine.

O código completo ficará assim.

package main

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

	"github.com/go-chi/chi/v5"
)

func main() {
	r := chi.NewRouter()

	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello World"))
	})

	server := &http.Server{
		Addr:    ":8080",
		Handler: r,
	}

	go func() {
		if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
			log.Fatalf("HTTP server error: %v", err)
		}
		log.Println("Stopped serving new connections.")
	}()

	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
	<-sc

	ctx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
	defer shutdownRelease()

	if err := server.Shutdown(ctx); err != nil {
		log.Fatalf("HTTP shutdown error: %v", err)
	}
	log.Println("Graceful shutdown complete.")
}

Agora é só fazer o build e testar.

Se ficou alguma dúvida, é só deixar nos comentários.

Espero que tenha ajudado.

Até a próxima!


Se inscreva na nossa newsletter

* indicates required

Um comentário sobre “Como fazer graceful shutdown correto em um server HTTP

Deixe uma resposta