gohttpservershutdowngraceful-shutdown

How to cancel long-running tasks when a Go HTTP server is shut down?


I'm writing an HTTP server in Go that performs long-running tasks (e.g. data processing or computation). When the server shuts down via srv.Shutdown(ctx), I want in-progress handlers to stop, even if they haven’t returned a response or written anything to the client.

I tried using r.Context().Done() from the incoming *http.Request to detect shutdown, as shown in the example below:

package main

import (
    "context"
    "fmt"
    "math"
    "net/http"
    "time"
)

func main() {
    handler := func(w http.ResponseWriter, r *http.Request) {
        select {
        case <-r.Context().Done():
            fmt.Println("Context cancelled:", r.Context().Err())
            return
        case <-longTask():
            fmt.Println("Handler completed normally")
        }
    }

    server := &http.Server{Addr: ":8080", Handler: http.HandlerFunc(handler)}

    go func() {
        time.Sleep(1 * time.Second)
        http.Get("http://localhost:8080")
    }()

    go func() {
        time.Sleep(8 * time.Second)
        fmt.Println("Shutting down server")
        server.Shutdown(context.Background())
    }()

    fmt.Println("Server starting on :8080")
    err := server.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        panic(err)
    }
    fmt.Println("Server exited")

    time.Sleep(5 * time.Second)
}

func longTask() <-chan bool {
    ch := make(chan bool)
    go func() {
        for i := range math.MaxInt {
            time.Sleep(1 * time.Second)
            fmt.Println("Working on long task ", i)
        }
        ch <- true
    }()
    return ch
}

However, it seems that r.Context().Done() is never triggered. Here's the output I get:

Server starting on :8080
Working on long task  0
Working on long task  1
Working on long task  2
Working on long task  3
Working on long task  4
Working on long task  5
Shutting down server
Server exited
Working on long task  6
Working on long task  7
Working on long task  8
Working on long task  9
Working on long task 10

As you can see, the background task continues running even after the server shuts down. What is the idiomatic way to structure long-running request handlers in Go so that they are reliably interrupted when the server shuts down?


Solution

  • This example has two areas where cancellation can happen:

    In the handler, you will only trigger <-r.Context().Done() when the request context is done. If this were a server without the client.Get block, then you could simulate in the browser. In this case you can add a timeout:

        go func() {
            time.Sleep(1 * time.Second)
            client := &http.Client{Timeout: time.Second * 2}
            client.Get("http://localhost:8080")
        }()
    

    But that doesn't stop the longTask go routine. You need to pass that context into the long-running function and have a way to check for done/cancel. That's why you often see context passed into database and http clients, because that check is done for you. If you're doing your own work, you need to have a mechanism to check the context:

    func longTask(ctx context.Context) <-chan bool {
        ch := make(chan bool)
        go func() {
            for i := range math.MaxInt {
                if ctx.Err() != nil {
                    return
                }
                time.Sleep(1 * time.Second)
                fmt.Println("Working on long task ", i)
            }
            ch <- true
        }()
        return ch
    }
    

    A separate issue is the application shutdown. There are different ways to do this, often by using signal.Notify and signal.NotifyContext , as mentioned here and here.

    In this example you could simply have a main context and add a case for that:

        mainCtx, cancel := context.WithCancel(context.Background())
    
        handler := func(w http.ResponseWriter, r *http.Request) {
            select {
            case <-r.Context().Done():
                fmt.Println("Context cancelled:", r.Context().Err())
            case <-longTask(r.Context()):
                fmt.Println("Handler completed normally")
            case <-mainCtx.Done():
                fmt.Println("Handler shutting down")
            }
        }
    ...
        go func() {
            time.Sleep(8 * time.Second)
            fmt.Println("Shutting down server")
            cancel()
            time.Sleep(time.Second * 3) // allow routines to close
            server.Shutdown(mainCtx)
        }()
    

    Realize that a handler gets the request context, which is not the same as the server or mainCtx in this example. Unless you pass it in, or use global vars, etc.

    This is not an ideal pattern for real-world applications, but should help with this example