Tasks That Should Never Be Interrupted
There are instances when a task should never be interrupted once initiated by a user.
For example, when a user presses the transfer button and then immediately closes the webpage, or when storing a large amount of data in a database.
While typical users might not frequently perform such actions, requests being canceled due to internet disconnections or power outages happen more often than you might think.
What Happens When an API Is Interrupted?
In Go, a context
object is commonly used to control the flow of tasks within a server.
This context
helps to stop tasks on the server when a user’s request is canceled.
For instance, in net/http
, you can obtain the context for a request via the r.Context()
function as shown below:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// ...
}
Consider the following setup: What happens if the task is interrupted?
func SomeHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Second)
slog.InfoContext(r.Context(), "Request received", "method", r.Method, "url", r.URL)
w.Write([]byte("Hello World"))
})
}
2025/03/19 15:48:42 INFO Starting server port=:8080
2025/03/19 15:49:03 INFO Request received method=GET url=/
Surprisingly, no issue arises. The context is used to control various flows, but it doesn’t inherently possess the feature to forcibly terminate an API midway. This would likely cause issues if it were a default feature.
So, how can we effectively manage the context to ensure tasks are interrupted when needed? Consider the following code:
func SomeHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
result := make(chan string)
go func() {
time.Sleep(10 * time.Second)
result <- "DONE!!"
}()
select {
case res := <-result:
slog.Info("Request completed")
w.Write([]byte(res))
return
case <-r.Context().Done():
slog.Info("Request cancelled", "err", r.Context().Err())
return
}
})
}
If the user waits for 10 seconds allowing the task to complete normally, the following log will be produced:
2025/03/19 16:34:06 INFO Request completed
On the other hand, if the context is cut before 10 seconds, the following log is generated:
2025/03/19 16:35:20 INFO Request cancelled err="context canceled"
In this way, context can be used to appropriately handle most tasks needing interruption upon request cancellation.
In addition, in the database/sql
package, methods like QueryContext
utilize ctx.Done()
to return an error if the context is cancelled, as shown below:
func ctxDriverQuery(ctx context.Context, queryerCtx driver.QueryerContext, queryer driver.Queryer, query string, nvdargs []driver.NamedValue) (driver.Rows, error) {
if queryerCtx != nil {
return queryerCtx.QueryContext(ctx, query, nvdargs)
}
dargs, err := namedValueToValue(nvdargs)
if err != nil {
return nil, err
}
select {
default:
case <-ctx.Done():
return nil, ctx.Err()
}
return queryer.Query(query, dargs)
}
Many library codes frequently present methods with ..Context(ctx)
suffixes or forms like WithContext(ctx)
. This is intended either for leveraging parameters within the context, as discussed in Logging Request ID in Go API Server, or for preventing function termination through Done()
channel, as shown in the ctxDriverQuery
method above.
How to Ensure Uninterrupted API Continuation
While the explanation might be lengthy, the upcoming example may not seem significant if you’re unfamiliar with the principle of Context. Now, let’s implement a setup where the task continues even if the API request is interrupted.
package main
import (
"context"
"log"
"log/slog"
"net/http"
"time"
)
func SomeHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
errCh := make(chan error)
go func() {
defer cancel()
errCh <- someFuncNeedContext(ctx)
}()
select {
case err := <-errCh:
if err != nil {
slog.Error("Failed to do something", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
case <-r.Context().Done():
slog.Info("Request cancelled", "err", r.Context().Err())
return
}
})
}
func someFuncNeedContext(ctx context.Context) error {
time.Sleep(3 * time.Second)
slog.InfoContext(ctx, "Doing something")
return nil
}
func main() {
mux := http.NewServeMux()
mux.Handle("GET /", SomeHandler())
port := ":8080"
slog.Info("Starting server", "port", port)
if err := http.ListenAndServe(port, mux); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
The function someFuncNeedContext
is imagined to be one that requires a ctx
(considering possible mid-execution cancellation processing). It performs a task that takes 3 seconds.
A key point is that the ctx
passed to this function is separate from r.Context()
, created as context.WithTimeout(context.Background(), 5*time.Second)
.
This means even if r.Context()
related to the API request is canceled and the Done()
signal is received, the ongoing task can continue due to the separately created ctx
.
Upon executing this code and disconnecting immediately thereafter, the following logs are produced:
2025/03/19 17:06:39 INFO Starting server port=:8080
2025/03/19 17:06:45 INFO Request cancelled err="context canceled"
2025/03/19 17:06:46 INFO Doing something
By using a separate Context, you can safely protect tasks that should not be interrupted, ensuring they are completed even if the API request is canceled.
Asynchronous API
Actually, this method is mainly used to keep the API itself synchronous. Another method is to design the API to be asynchronous.
In this case, the handler only returns status codes like 202 (Accepted), meaning additional data design and notification systems are required to inform whether the client has terminated successfully or unsuccessfully.
Due to the extensive nature of this content, it will be covered in a future post.