在编写 HTTP 处理程序时,我们是否需要监听请求上下文的取消?

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

When writing an http handler, do we have to listen for request context cancellation?

问题

假设我正在编写一个 HTTP 处理程序,在返回响应之前要做一些其他操作,我是否需要设置一个监听器来检查 HTTP 请求上下文是否已被取消?这样它就可以立即返回,或者是否有其他方法在请求上下文被取消时退出处理程序?

你尝试为此编写了一个测试,但是在上下文被取消后,doSomething 函数并没有停止,而是继续在后台运行。

在你的代码中,doSomething 函数没有主动检查上下文是否已被取消,因此它会继续执行。为了在上下文被取消时停止 doSomething 函数的执行,你可以在 doSomething 函数中添加对上下文的检查。

以下是修改后的代码示例:

func doSomething(ctx context.Context) error {
    // 在每次循环迭代之前检查上下文是否已被取消
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // 模拟执行某些操作
            time.Sleep(time.Second)
        }
    }
}

通过在 doSomething 函数中添加上下文的检查,当上下文被取消时,doSomething 函数会立即返回并停止执行。

希望这可以帮助到你!如果你有任何其他问题,请随时问我。

英文:

Supposed that I'm writing an http handler, that do something else before returning a response, do I have to setup a listener to check wether the http request context has been canceled? so that it can return immediately, or is there any other way to exit the handler when the request context cancelled?

func handleSomething(w http.ResponseWriter, r *http.Request) {
	done := make(chan error)

	go func() {
		if err := doSomething(r.Context()); err != nil {
			done &lt;- err
                        return
		}

		done &lt;- nil
	}()

	select {
	case &lt;-r.Context().Done():
		http.Error(w, r.Context().Err().Error(), http.StatusInternalServerError)
		return
	case err := &lt;-done:
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(&quot;ok&quot;))
	}
}

func doSomething(ctx context.Context) error {
	// simulate doing something for 1 second.
	time.Sleep(time.Second)
	return nil
}

I tried making a test for it, but after the context got cancelled, doSomething function didn't stop and still running in the background.

func TestHandler(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc(&quot;/something&quot;, handleSomething)

	srv := http.Server{
		Addr:    &quot;:8989&quot;,
		Handler: mux,
	}

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := srv.ListenAndServe(); err != nil {
			log.Println(err)
		}
	}()

	time.Sleep(time.Second)

	req, err := http.NewRequest(http.MethodGet, &quot;http://localhost:8989/something&quot;, nil)
	if err != nil {
		t.Fatal(err)
	}

	cl := http.Client{
		Timeout: 3 * time.Second,
	}

	res, err := cl.Do(req)
	if err != nil {
		t.Logf(&quot;error: %s&quot;, err.Error())
	} else {
		t.Logf(&quot;request is done with status code %d&quot;, res.StatusCode)
	}

	go func() {
		&lt;-time.After(10 * time.Second)
		shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		srv.Shutdown(shutdown)
	}()

	wg.Wait()
}

func handleSomething(w http.ResponseWriter, r *http.Request) {
	done := make(chan error)

	go func() {
		if err := doSomething(r.Context()); err != nil {
			log.Println(err)
			done &lt;- err
		}

		done &lt;- nil
	}()

	select {
	case &lt;-r.Context().Done():
		log.Println(&quot;context is done!&quot;)
		return
	case err := &lt;-done:
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.WriteHeader(http.StatusOK)
		w.Write([]byte(&quot;ok&quot;))
	}
}

func doSomething(ctx context.Context) error {
	return runInContext(ctx, func() {
		log.Println(&quot;doing something&quot;)
		defer log.Println(&quot;done doing something&quot;)

		time.Sleep(10 * time.Second)
	})
}

func runInContext(ctx context.Context, fn func()) error {
	ch := make(chan struct{})
	go func() {
		defer close(ch)
		fn()
	}()

	select {
	case &lt;-ctx.Done():
		return ctx.Err()
	case &lt;-ch:
		return nil
	}
}

答案1

得分: 1

我刚刚稍微修改了提供的解决方案,现在应该可以工作了。让我引导你了解相关的更改。

doSomething 函数

func doSomething(ctx context.Context) error {
	fmt.Printf("%v - doSomething: start\n", time.Now())
	select {
	case <-ctx.Done():
		fmt.Printf("%v - doSomething: cancelled\n", time.Now())
		return ctx.Err()
	case <-time.After(3 * time.Second):
		fmt.Printf("%v - doSomething: processed\n", time.Now())
		return nil
	}
}

该函数等待取消输入,或者在延迟 3 秒后返回给调用者。它接受一个上下文来监听。

handleSomething 函数

func handleSomething(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	fmt.Printf("%v - handleRequestCtx: start\n", time.Now())

	done := make(chan error)
	go func() {
		if err := doSomething(ctx); err != nil {
			fmt.Printf("%v - handleRequestCtx: error %v\n", time.Now(), err)
			done <- err
		}

		done <- nil
	}()

	select {
	case <-ctx.Done():
		fmt.Printf("%v - handleRequestCtx: cancelled\n", time.Now())
		return
	case err := <-done:
		if err != nil {
			fmt.Printf("%v - handleRequestCtx: error: %v\n", time.Now(), err)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		fmt.Printf("%v - handleRequestCtx: processed\n", time.Now())
	}
}

在这里,逻辑与你的逻辑非常相似。在 select 语句中,我们检查接收到的错误是否为 nil,并根据此返回适当的 HTTP 状态码给调用者。如果我们收到取消输入,我们将取消所有的上下文链。

TestHandler 函数

func TestHandler(t *testing.T) {
	r := mux.NewRouter()
	r.HandleFunc("/demo", handleSomething)

	srv := http.Server{
		Addr:    ":8000",
		Handler: r,
	}

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := srv.ListenAndServe(); err != nil {
			fmt.Println(err.Error())
		}
	}()

	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 1*time.Second) // 请求被取消
	// ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // 请求被处理
	defer cancel()

	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8000/demo", nil)

	client := http.Client{}
	res, err := client.Do(req)
	if err != nil {
		fmt.Println(err.Error())
	} else {
		fmt.Printf("res status code: %d\n", res.StatusCode)
	}
	srv.Shutdown(ctx)

	wg.Wait()
}

在这里,我们启动了一个 HTTP 服务器,并通过 http.Client 发出了一个 HTTP 请求。你可以看到有两个设置上下文超时的语句。如果你使用带有注释 // 请求被取消 的语句,所有的请求都将被取消,否则,如果你使用另一个语句,请求将被处理。

希望这样可以解答你的问题!

英文:

I've just refactored the solution provided a little bit and now it should work. Let me guide you through the relevant changes.

The doSomething function

func doSomething(ctx context.Context) error {
	fmt.Printf(&quot;%v - doSomething: start\n&quot;, time.Now())
	select {
	case &lt;-ctx.Done():
		fmt.Printf(&quot;%v - doSomething: cancelled\n&quot;, time.Now())
		return ctx.Err()
	case &lt;-time.After(3 * time.Second):
		fmt.Printf(&quot;%v - doSomething: processed\n&quot;, time.Now())
		return nil
	}
}

It waits for a cancellation input or after a delay of 3 seconds it returns to the caller. It accepts a context to listen for.

The handleSomething function

func handleSomething(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	fmt.Printf(&quot;%v - handleRequestCtx: start\n&quot;, time.Now())

	done := make(chan error)
	go func() {
		if err := doSomething(ctx); err != nil {
			fmt.Printf(&quot;%v - handleRequestCtx: error %v\n&quot;, time.Now(), err)
			done &lt;- err
		}

		done &lt;- nil
	}()

	select {
	case &lt;-ctx.Done():
		fmt.Printf(&quot;%v - handleRequestCtx: cancelled\n&quot;, time.Now())
		return
	case err := &lt;-done:
		if err != nil {
			fmt.Printf(&quot;%v - handleRequestCtx: error: %v\n&quot;, time.Now(), err)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		fmt.Printf(&quot;%v - handleRequestCtx: processed\n&quot;, time.Now())
	}
}

Here, the logic is very similar to yours. In the select, we check whether the received error is nil or not, and based on this we return to the proper HTTP status code to the caller. If we receive a cancellation input, we cancel all the context chain.

The TestHandler function

func TestHandler(t *testing.T) {
	r := mux.NewRouter()
	r.HandleFunc(&quot;/demo&quot;, handleSomething)

	srv := http.Server{
		Addr:    &quot;:8000&quot;,
		Handler: r,
	}

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := srv.ListenAndServe(); err != nil {
			fmt.Println(err.Error())
		}
	}()

	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 1*time.Second) // request canceled
	// ctx, cancel := context.WithTimeout(ctx, 5*time.Second) // request processed
	defer cancel()

	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, &quot;http://localhost:8000/demo&quot;, nil)

	client := http.Client{}
	res, err := client.Do(req)
	if err != nil {
		fmt.Println(err.Error())
	} else {
		fmt.Printf(&quot;res status code: %d\n&quot;, res.StatusCode)
	}
	srv.Shutdown(ctx)

	wg.Wait()
}

Here, we spin up an HTTP server and issue an HTTP request to it through an http.Client. You can see that there are two statements to set the context timeout. If you use the one with the comment // request canceled, everything will be canceled, otherwise, if you use the other the request will be processed.
I Hope that this clarifies your question!

huangapple
  • 本文由 发表于 2022年11月25日 12:19:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/74568315.html
匿名

发表评论

匿名网友

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

确定