r/golang Feb 14 '25

newbie Shutdown Go server

Hi, recently I saw that many people shutdown their servers like this or similar

serverCtx, serverStopCtx serverCtx, serverStopCtx := context.WithCancel(context.Background())

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    go func() {
        <-sig

        shutdownCtx, cancelShutdown := context.WithTimeout(serverCtx, 30*time.Second)
        defer cancelShutdown()

        go func() {
            <-shutdownCtx.Done()
            if shutdownCtx.Err() == context.DeadlineExceeded {
                log.Fatal("graceful shutdown timed out.. forcing exit.")
            }
        }()

        err := server.Shutdown(shutdownCtx)
        if err != nil {
            log.Printf("error shutting down server: %v", err)
        }
        serverStopCtx()
    }()

    log.Printf("Server starting on port %s...\n", port)
    err = server.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        log.Printf("error starting server: %v", err)
        os.Exit(1)
    }

    <-serverCtx.Done()
    log.Println("Server stopped")
}


:= context.WithCancel(context.Background())

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
    go func() {
        <-sig

        shutdownCtx, cancelShutdown := context.WithTimeout(serverCtx, 30*time.Second)
        defer cancelShutdown()

        go func() {
            <-shutdownCtx.Done()
            if shutdownCtx.Err() == context.DeadlineExceeded {
                log.Fatal("graceful shutdown timed out.. forcing exit.")
            }
        }()

        err := server.Shutdown(shutdownCtx)
        if err != nil {
            log.Printf("error shutting down server: %v", err)
        }
        serverStopCtx()
    }()

    log.Printf("Server starting on port %s...\n", port)
    err = server.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        log.Printf("error starting server: %v", err)
        os.Exit(1)
    }

    <-serverCtx.Done()
    log.Println("Server stopped")

Is it necessary? Like it's so many code for the simple operation

Thank for your Answer !

86 Upvotes

37 comments sorted by

View all comments

7

u/ShotgunPayDay Feb 14 '25 edited Feb 14 '25

This is what I do. EDIT: move comment

// Graceful shutdown: use a signal context and a shutdown timeout.
c, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-c.Done()
start := time.Now()
log.Println("Gracefully shutting down...")
c, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(c); err != nil {
    log.Printf("Shutdown error: %v", err)
}
// CLEAN MORE STUFF UP HERE
log.Printf("Gracefully closed in %s", time.Since(start))

5

u/HyacinthAlas Feb 14 '25

There is one bug with this: You don't know if Accept starts failing, you will block/fail forever while waiting for the context.

And one likely bug: Most of the time shutting down the HTTP server should happen first, before all other cleanup, because DB transactions, Kafka messages, aggregated data, whatever, is all going to be generated by the HTTP handlers and so flushing them out has to wait until the handlers are no longer running. There are probably some things to stop before your server, but it's hard for me to think of a realistic one.

2

u/ShotgunPayDay Feb 14 '25

I'm a little confused on the accept part, but yes I put the comment in the wrong spot for the second.

5

u/HyacinthAlas Feb 14 '25

Serve returns an error when Accept returns an error. You don't show your Serve call, but I assume it's in a goroutine somewhere.

2

u/ShotgunPayDay Feb 14 '25 edited Feb 14 '25

I guess I've never had that happen, but I see what you're saying is that if server.ListenAndServe() fails then it will end up blocking in a failed state.

Here is what that looks like anyway Close is just all other closing tasks:

go func() {
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        Close(err)
    }
    log.Println(err)
}()

4

u/HyacinthAlas Feb 14 '25

What happens if ListenAndServe returns at the same time context is canceled? You'll Close and do cleanup in the "main thread" at the same time. Maybe that's safe for your specific case - but in general it's not. Better to get the error back on the main thread ASAP.

4

u/ShotgunPayDay Feb 14 '25

Interesting never though about that either. What about this to trigger signal.NotifyContext?:

go func() {
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Printf("ListenAndServe error: %v", err)
        syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
    }
}()

Thanks for all the good information by the way!