Home > Mobile >  In Go, what is the proper way to use context with pgx within http handlers?
In Go, what is the proper way to use context with pgx within http handlers?

Time:06-07

Update 1: it seems that using a context tied to the HTTP request may lead to the 'context canceled' error. However, using the context.Background() as the parent seems to work fine.

    // This works, no 'context canceled' errors
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)

    // However, this creates 'context canceled' errors under mild load
    // ctx, cancel := context.WithTimeout(r.Context(), 100*time.Second)

    defer cancel()
    app.Insert(ctx, record)

(updated code sample below to produce a self-contained example for repro)


In go, I have an http handler like the following code. On the first HTTP request to this endpoint I get a context cancelled error. However, the data is actually inserted into the database. On subsequent requests to this endpoint, no such error is given and data is also successfully inserted into the database.

Question: Am I setting up and passing the context correctly between the http handler and pgx QueryRow method? (if not is there a better way?)

If you copy this code into main.go and run go run main.go, go to localhost:4444/create and hold ctrl-R to produce a mild load, you should see some context canceled errors produced.

package main

import (
    "context"
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "time"

    "github.com/jackc/pgx/v4/pgxpool"
)

type application struct {
    DB *pgxpool.Pool
}

type Task struct {
    ID     string
    Name   string
    Status string
}

//HTTP GET /create
func (app *application) create(w http.ResponseWriter, r *http.Request) {
    fmt.Println(r.URL.Path, time.Now())
    task := &Task{Name: fmt.Sprintf("Task #%d", rand.Int()00), Status: "pending"}
    // -------- problem code here ----
    // This line works and does not generate any 'context canceled' errors
    //ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
    // However, this linegenerates 'context canceled' errors under mild load
    ctx, cancel := context.WithTimeout(r.Context(), 100*time.Second)
    // -------- end -------
    defer cancel()
    err := app.insertTask(ctx, task)
    if err != nil {
        fmt.Println("insert error:", err)
        return
    }
    fmt.Fprintf(w, "% v", task)
}
func (app *application) insertTask(ctx context.Context, t *Task) error {
    stmt := `INSERT INTO task (name, status) VALUES ($1, $2) RETURNING ID`
    row := app.DB.QueryRow(ctx, stmt, t.Name, t.Status)
    err := row.Scan(&t.ID)
    if err != nil {
        return err
    }
    return nil
}

func main() {
    rand.Seed(time.Now().UnixNano())
    db, err := pgxpool.Connect(context.Background(), "postgres://test:test123@localhost:5432/test")
    if err != nil {
        log.Fatal(err)
    }
    log.Println("db conn pool created")
    stmt := `CREATE TABLE IF NOT EXISTS public.task (
        id uuid NOT NULL DEFAULT gen_random_uuid(),
        name text NULL,
        status text NULL,
        PRIMARY KEY (id)
     ); `
    _, err = db.Exec(context.Background(), stmt)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("task table created")
    defer db.Close()
    app := &application{
        DB: db,
    }
    mux := http.NewServeMux()
    mux.HandleFunc("/create", app.create)

    log.Println("http server up at localhost:4444")
    err = http.ListenAndServe(":4444", mux)
    if err != nil {
        log.Fatal(err)
    }
}


CodePudding user response:

TLDR: Using r.Context() works fine in production, testing using Browser is a problem.

An HTTP request gets its own context that is cancelled when the request is finished. That is a feature, not a bug. Developers are expected to use it and gracefully shutdown execution when the request is interrupted by client or timeout. For example, a cancelled request can mean that client never see the response (transaction result) and developer can decide to roll back that transaction.

In production, request cancelation does not happen very often for normally design/build APIs. Typically, flow is controlled by the server and the server returns the result before the request is cancelled. Multiple Client requests does not affect each other because they get independent go-routine and context. Again, we are talking about happy path for normally designed/build applications. Your sample app looks good and should work fine.

The problem is how we test the app. Instead of creating multiple independent requests, we use Browser and refresh a single browser session. I did not check what exactly is going on, but assume that the Browser terminates the existing request in order to run a new one when you click ctrl-R. The server sees that request termination and communicates it to your code as context cancelation.

Try to test your code using curl or some other script/utility that creates independent requests. I am sure you will not see cancelations in that case.

  • Related