在Go语言中,在HTTP处理程序中正确使用pgx的上下文的方法是什么?

huangapple go评论81阅读模式
英文:

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

问题

更新 1:似乎使用与HTTP请求相关联的上下文可能会导致“上下文取消”错误。然而,使用context.Background()作为父上下文似乎可以正常工作。

    // 这样做没有“上下文取消”错误
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)

    // 然而,在轻负载下,这样会产生“上下文取消”错误
	// ctx, cancel := context.WithTimeout(r.Context(), 100*time.Second)

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

(下面的更新代码示例可用于复现)


在Go语言中,我有一个类似以下代码的HTTP处理程序。在对此端点的第一个HTTP请求中,我会收到“上下文取消”错误。然而,数据实际上已插入到数据库中。在对此端点的后续请求中,不会出现此类错误,并且数据也成功插入到数据库中。

问题:我是否正确设置和传递了HTTP处理程序和pgx QueryRow方法之间的上下文?(如果没有,是否有更好的方法?)

如果将此代码复制到main.go中并运行go run main.go,然后转到localhost:4444/create并按住ctrl-R以产生轻负载,您应该会看到一些“上下文取消”错误。

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()%1000), Status: "pending"}
	// -------- 问题代码在这里 ----
	// 这行代码可以正常工作,不会生成任何“上下文取消”错误
	//ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
	// 然而,在轻负载下,这行代码会生成“上下文取消”错误
	ctx, cancel := context.WithTimeout(r.Context(), 100*time.Second)
	// -------- 结束 -------
	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)
	}
}
英文:

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()%1000), 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)
	}
}


答案1

得分: 2

TLDR:在生产环境中使用r.Context()是没有问题的,但在使用浏览器进行测试时会出现问题。

HTTP请求会有自己的上下文,当请求完成时会被取消。这是一种特性,而不是错误。开发人员应该使用它,并在请求被客户端或超时中断时优雅地关闭执行。例如,取消的请求可能意味着客户端永远不会看到响应(事务结果),开发人员可以决定回滚该事务。

在生产环境中,请求的取消并不经常发生在正常设计/构建的API中。通常,流程由服务器控制,并且服务器在请求被取消之前返回结果。
多个客户端请求不会相互影响,因为它们会获得独立的go协程和上下文。再次强调,我们正在讨论正常设计/构建应用程序的情况。您的示例应用程序看起来很好,应该可以正常工作。

问题出在我们如何测试应用程序上。我们不是创建多个独立的请求,而是使用浏览器并刷新单个浏览器会话。我没有检查具体发生了什么,但假设当您点击ctrl-R时,浏览器终止了现有的请求以运行新的请求。服务器会将请求终止的信息传递给您的代码,作为上下文取消。

尝试使用curl或其他创建独立请求的脚本/工具来测试您的代码。我相信在那种情况下,您不会看到取消请求的情况。

英文:

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.

huangapple
  • 本文由 发表于 2022年6月6日 21:55:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/72518769.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定