Should handle shutdown signals in app main file or in actual HTTP server file for graceful shutdown?

I am trying to find out if there is any difference between two implementations that currently do same thing - gracefully shutdown application/server, for example when Ctrl+C is hit. Both works fine and are based on the documentation.

What a friend of mine says is that, the example 2 handles shutdown at application level which shuts down all contexts throughout the application. However, the example 1 does it in HTTP server level which doesn’t necessarily shut down all contexts throughout the application. Since I am a beginner I cannot argue back and need your input on this please.

Example 1

The signals are handled in http.go file so the whole graceful shutdown has been handled in a single file.

cmd/app/main.go

    package main
    
    import (
    	"context"
    	"internal/http"
    	"os"
    	"os/signal"
    	"syscall"
    )
    
    func main() {
    	// Bootstrap config, logger, etc
    
    	http.Start()
    }

internal/http/server.go

    package http
    
    import (
    	"context"
    	"github.com/prometheus/common/log"
    	"net/http"
    	"os"
    	"os/signal"
    	"syscall"
    	"time"
    )
    
    func Start() {
    	log.Infof("starting HTTP server")
    
    	srv := &http.Server{Addr: ":8080", Handler: nil}
    
    	idle := make(chan struct{})
    
    	go shutdown(srv, idle)
    
    	if err := srv.ListenAndServe(); err != http.ErrServerClosed {
    		log.Fatalf("failed to start/close HTTP server [%v]", err)
    	}
    
    	<-idle
    
    	log.Info("gracefully shutdown HTTP server")
    }
    
    func shutdown(srv *http.Server, idle chan<- struct{}) {
    	sig := make(chan os.Signal, 1)
    
    	signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
    
    	<-sig
    
    	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
    	defer cancel()
    
    	if err := srv.Shutdown(ctx); err != nil {
    		log.Fatalf("shutdown HTTP server by interrupting idle connections [%v]", err)
    	}
    
    	close(idle)
    }

Example 2

The signals are handled in application’s main.go file so the whole graceful shutdown has been split within two files. The only addition is this example uses WithCancel context.

cmd/app/main.go

    package main
    
    import (
    	"context"
    	"internal/http"
    	"os"
    	"os/signal"
    	"syscall"
    )
    
    func main() {
    	// Bootstrap config, logger, etc
    
    	backgroundCtx := context.Background()
    	withCancelCtx, cancel := context.WithCancel(backgroundCtx)
    	go shutdown(cancel)
    
    	http.Start(withCancelCtx)
    }
    
    func shutdown(cancel context.CancelFunc) {
    	sig := make(chan os.Signal, 1)
    
    	signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
    
    	<-sig
    
    	cancel()
    }

internal/http/server.go

    package http
    
    import (
    	"context"
    	"github.com/prometheus/common/log"
    	"net/http"
    	"time"
    )
    
    func Start(ctx context.Context) {
    	log.Infof("starting HTTP server")
    
    	srv := &http.Server{Addr: ":8080", Handler: nil}
    
    	idle := make(chan struct{})
    
    	go shutdown(ctx, srv, idle)
    
    	if err := srv.ListenAndServe(); err != http.ErrServerClosed {
    		log.Fatalf("failed to start/close HTTP server [%v]", err)
    	}
    
    	<-idle
    
    	log.Info("gracefully shutdown HTTP server")
    }
    
    func shutdown(ctx context.Context, srv *http.Server, idle chan struct{}) {
    	<-ctx.Done()
    
    	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
    	defer cancel()
    
    	if err := srv.Shutdown(ctx); err != nil {
    		log.Fatalf("shutdown HTTP server by interrupting idle connections [%v]", err)
    	}
    
    	close(idle)
    }

It is look like the same just a design decision. If internal/http is your internal package then there isn`t difference.

Hi @heatbr ,

Thanks for the input. Would you please mind explaining more what exactly you mean and if it was you, which one would you go with?

Thanks

I would without hesitation go with the first one because it is simpler. It’s much easier to understand the logic and what is being done from the code. Only 2 methods and groups of instructions are used, and only one context.

@Christophe_Meessen Thanks for the reply.

So the most important part for me is that to understand if having context.WithCancel is unnecessary or not. Sounds like it is unnecessary. Am I getting it right? I mean if the context.WithTimeout times out, every single context throughout the application would be stopped as well?

This depends on your application. The cancel context can be used to notify other components of your program that a graceful shutdown is performed.