将主 goroutine 的上下文副本传递给子例程的上下文。

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

Pass a copy of main goroutine context to subroutine context

问题

我有一个带有上下文的golang API端点。

由于端点需要在后台执行一些繁重的工作,所以我在主端点内创建了一个新的子例程,并在那里返回响应。

为了处理上下文取消,我创建了一个后台上下文,并将其作为新上下文传递给子例程。

问题是,通过这样做,是的,我可以执行后台子例程,但是主上下文中的值,如请求ID、跨度ID等(大部分键对我来说是未知的),用于跟踪的值将丢失。

我如何在将响应发送给客户端后,将父上下文传递给子例程而不取消执行呢?

编辑

我没有将任何值传递到上下文中。
但最初,我们传递了请求ID、跨度ID等,这些对于跟踪是必要的。
这些信息在上下文中。
这是一个内部库,上下文是我们保存这些信息的地方。

我知道使用上下文传递值是一种反模式,除了对库而言重要的请求ID和其他值之外,没有传递任何值给业务逻辑。

英文:

I have a golang API endpoint with a context associated with it.

The endpoint needs to do some heavy lifting behind the scenes, so I create a new sub routine within the main endpoint and return the response then there itself.

In order to deal with context cancellation, I am creating a background context and pass this as the new context to the subroutine.

The problem is by doing this, yes, I can execute the background subroutine, but the values within the main context such as request id, span id etc. (most of keys are unknown to me), which are being used for tracing will be lost.

How do I pass a parent context to the subroutine without cancelling the the execution even after the response is sent to the client.

EDIT

I am not passing any values into the context.
But initially we are passing request-id, span-id etc. which are needed for tracing.
These informations are in the context.
This is an internal library, and context is the place were we are keeping this.

I know this is an anti-pattern for using context to pass values, and no values is passed, except the request-id and other values that important to the library not to the business logic

答案1

得分: 4

当您取消一个父上下文时,所有从它派生的上下文也将被取消。所以您在为从请求处理程序生成的goroutine创建一个新的上下文是正确的。

当您创建一个新的上下文时,您应该将您感兴趣的所有值从原始上下文复制到新的上下文中。然而,您说您不知道所有的键。所以您仍然可以保留对父上下文的引用,以便您可以查询它的值。类似这样的代码:

type nestedContext struct {
   context.Context
   parent context.Context
}

func (n nestedContext)  Value(key any) any {
   return n.parent.Value(key)
}

...
newContext := nestedContext{
   Context:context.Background(),
   parent: parentContext,
}

这将从context.Background()创建一个新的上下文,该上下文将从已取消的父上下文中查找值。

newContext作为从处理程序创建的goroutine的上下文传递。

英文:

When you cancel a parent context, all contexts derived from it will also cancel. So you are correct in creating a new context for goroutines spawned from a request handler.

When you create a new context, you should copy all the values you are interested in from the original context to the new context. However, you said you don't know all the keys. So you can still keep a reference to the parent context so that you can query it for values. Something like this:

type nestedContext struct {
   context.Context
   parent context.Context
}

func (n nestedContext)  Value(key any) any {
   return n.parent.Value(key)
}

...
newContext := nestedContext{
   Context:context.Background(),
   parent: parentContext,
}

This will create a new context from the context.Background() that will lookup values from the canceled parent context.

Pass newContext as the context to the goroutines created from the handler.

答案2

得分: 1

> 包context定义了Context类型,它在API边界和进程之间传递截止时间、取消信号和其他请求范围的值。context

> 对于传入的服务器请求,当客户端的连接关闭、请求被取消(使用HTTP/2)或ServeHTTP方法返回时,上下文将被取消。net/http

通过上下文发送超出请求本身生命周期的值是否是一种反模式?
我认为是的,但有些人可能会对此提出异议。等待评论...

我创建了一个cantCancelMe结构体,实现了上下文接口并且永远不会被取消。这非常不正统和可怕,但编码过程很有趣,这正是你想要实现的效果。确保你理解了副作用,并在注释中留下一些线索,以防其他人在同一代码库上工作。

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ctx, connClose := context.WithCancel(context.Background())
	handler(ctx)
	connClose() // 在这里取消以模拟连接关闭

	<-time.After(time.Hour)
}

// 定义自己的上下文类型,它不能被取消(字面上的意思)
type cantCancelMe struct {
	// 接收任何实现上下文接口的内容(包括保存你的值并由处理程序提前取消的上下文)
	context.Context
}

/*
然后覆盖可能取消它的所有内容,同时保持Values()不变:
*/

// 当没有设置截止时间时,Deadline返回ok==false。
func (cantCancelMe) Deadline() (deadline time.Time, ok bool) {
	return time.Time{}, false
}

// 如果此上下文永远不会被取消,Done可能返回nil。
func (cantCancelMe) Done() <-chan struct{} {
	return nil
}

// 如果Done尚未关闭,Err返回nil
func (cantCancelMe) Err() error {
	return nil
}

/*
一个简化的处理程序,将常规上下文注入到第一个例程中,
并将“cantCancelMe”上下文注入到第二个例程中:
- 第一个例程将立即接收到取消信号
- 第二个例程将永远不会接收到取消信号(听起来很可怕)
*/
func handler(ctx context.Context) {
	regularCtx := context.WithValue(ctx, "reqID", "regular")
	notCancellableCtx := cantCancelMe{
		context.WithValue(ctx, "reqID", "nerver-cancel-ctx"),
	}
	go longRunning(regularCtx)
	go longRunning(notCancellableCtx)
}

func longRunning(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf(`handler ctx was cancelled, while I was still working on 
			reqID=%s\n`, ctx.Value("reqID"))
			return
		case <-time.After(time.Second):
			fmt.Printf("heavy duty for reqID=%s\n", ctx.Value("reqID"))
		}
	}
}
$ go run main.go
handler ctx was cancelled, while I was still working on reqID=regular
heavy duty for reqID=nerver-cancel-ctx
heavy duty for reqID=nerver-cancel-ctx
[...]
英文:

> Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes. context

> For incoming server requests, the context is canceled when the client's connection closes, the request is canceled (with HTTP/2), or when the ServeHTTP method returns. net/http

Is it an anti-pattern to send values through a context, that lives longer than the request itself?
I do believe it is, but some could argue about this. Waiting for comments...

I created a cantCancelMe struct that implements the context interface and can never be cancelled. Pretty unorthodox and scary but it was fun to code and that's exactly what you want to achieve. Make sure you understand the side effects and leave some clues in the comments if there's other people working on the same codebase.

package main

import (
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;time&quot;
)

func main() {
	ctx, connClose := context.WithCancel(context.Background())
	handler(ctx)
	connClose() // cancel here to simulate a conn close

	&lt;-time.After(time.Hour)
}

// Define your own type of context, that can&#39;t be cancelled(literally:)
type cantCancelMe struct {
	// Receive anyting that implements Context interface(including the context
	// which holds your values and gets cancelled early by the handler)
	context.Context
}

/*
Then override everything that could cancel it, while leaving Values()untouched:
*/

// Deadline returns ok==false when no deadline is set.
func (cantCancelMe) Deadline() (deadline time.Time, ok bool) {
	return time.Time{}, false
}

// Done may return nil if this context can never be canceled.
func (cantCancelMe) Done() &lt;-chan struct{} {
	return nil
}

// If Done is not yet closed, Err returns nil
func (cantCancelMe) Err() error {
	return nil
}

/*
A simplified handler that injects a regular context into the first routine
and a &quot;cantCancelMe&quot; context into the second routine:
- the first routine will receive the cancel signal immediately
- the second routine will never receive a cancel signal (sounds scarry)
*/
func handler(ctx context.Context) {
	regularCtx := context.WithValue(ctx, &quot;reqID&quot;, &quot;regular&quot;)
	notCancellableCtx := cantCancelMe{
		context.WithValue(ctx, &quot;reqID&quot;, &quot;nerver-cancel-ctx&quot;),
	}
	go longRunning(regularCtx)
	go longRunning(notCancellableCtx)
}

func longRunning(ctx context.Context) {
	for {
		select {
		case &lt;-ctx.Done():
			fmt.Printf(`handler ctx was cancelled, while I was still working on 
			reqID=%s\n`, ctx.Value(&quot;reqID&quot;))
			return
		case &lt;-time.After(time.Second):
			fmt.Printf(&quot;heavy duty for reqID=%s\n&quot;, ctx.Value(&quot;reqID&quot;))
		}
	}
}
$ go run main.go
handler ctx was cancelled, while I was still working on reqID=regular
heavy duty for reqID=nerver-cancel-ctx
heavy duty for reqID=nerver-cancel-ctx
[...]

huangapple
  • 本文由 发表于 2023年3月31日 02:13:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/75891641.html
匿名

发表评论

匿名网友

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

确定