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?
This example has two areas where cancellation can happen:
request cancel
application shutdown
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