-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
context: add WithoutCancel #40221
Comments
I can't help but feel that this request is a direct by product of context's conflation of cancellation and a skiplist of values. I agree with the rationale for this, but also feel that this is pushing the use case of context as a bag of values beyond its intention. |
Yes, without said conflation this proposal wouldn't be needed.
While my primary intent here (that is much more immediate) wasn't to move that discussion forward... in a way, this proposal could also be seen as a first step in sidestepping (undoing?) the conflation, since it effectively turns cancellation into a regular copy-on-write value like all other values carried by the |
I also have seen a It might however be better to encapsulate the specifc use case of detached background tasks by having a Task struct and a This might also avoid adding more exposed API to context itself. |
@martisch i wouldn't add goroutines to the mix. There are legitimate use cases (rollback/cleanup operations that can run in a Also, your counterproposal seem to be way more complex in terms of number of new exposed APIs (one new package, one new struct/interface and at least one new method) compared to the one proposed here. You're right though it wouldn't need to be in To be 100% clear though, I completely agree that the current design of But, as mentioned above, my intent here was not really to propose a new design for It's worth noting that a new |
Directly related: |
Note that the To summarize: values stored in a |
Do we have any actual example of this need? I am thinking at all middlewares we are using and can't think of any that would need that (including tracing and logging). Furthermore, even if it was really needed, middleware (again, thinking mostly of logging and tracing) will normally already happily let you override the current values (e.g. a logger or trace/span), so I'm not really convinced about the need for a specific, more complicated to cover it. Maybe a concrete example may help understand. |
...and presumably others: anything with a local buffer of logs or traces, or a local tree of spans or regions, that needs the buffer or parts of the tree to be flushed to a disk or a remote log server when they are “complete”. |
You can only override values that you know about. The point of In practice, that means one of two things:
|
Thanks for the list. I am familiar with some, but not all, of these... can you help me understand exactly what you wouldn't be able to do with the current proposal? My current impression is that flushing a logger when the triggering request completes is not a very convincing example, as whatever middleware added the logger to the context could trivially flush the logger when the request terminates; similarly the asynchronous task knows which logger it is using, so it can flush it when the task completes (I am setting aside for a second the fact that flushing logs at every request is probably not such a common requirement; all loggers and tracers I worked with normally flush based on time or buffer size, and at process shutdown). The other use-case I read somewhere is IIUC ensuring that values attached to the context that are needed before cancellation but not after are not retained while the derived child context stays alive. I must say I never faced this problem... maybe some concrete examples of where this was needed could help me understand better.
Clarified I did not specifically argue for a specific name, thanks! (our internal impl is not called
Doesn't this apply in reverse to #19643 as well? The code that registers a value as "preservable" should be aware of all possible uses of a certain context value. Also, in that design, what would happen if different code paths were to require different values to be preserved?
I'm not sure about the point here; the use case I'm trying to cover is the non-synchronous one (w.r.t. parent cancellation; if the parent has already been cancelled because the client connection broke there's no real difference between running in a separate goroutine or not)
I understand that there's a pre-existing design in #19643, but I think it's worth noting that it was declined because of lack of evidence it was the correct one. Has this changed? |
Depends on the library, but in general you end up with some form of use-after- |
Different code paths cannot require different values. (Values should be preserved by intersecting the set of preservable keys with the set of keys found in the parent context.) The preservation semantics are based on the meaning of the context value, not any detail about its usage, so the “preserver” code is independent of the point of use. For example, if you have a tracing library, “detaching” for an asynchronous operation should be an event in the parent trace that results in its own (related but distinct) tracing span: that is the only coherent way to “preserve” continuity of the trace, and it should be correct to do any time a context is used asynchronously. |
I think we have pretty solid evidence that using a context asynchronously is incorrect, or at least error-prone, but I'm not sure that we have any more evidence than we did about whether the API proposed in #19643 is ideal. |
FWIW, speaking for the Frameworks Go team within Google, having a way to detach from the parent context would be enormously beneficial for us. We want the trace context, request IDs and some other values preserved in all work that is started from a request handler, or from the framework-specific In fact, we have static analysis that flags the use of |
To advance the conversation it would be ideal to have the evidence about being "error-prone" be available for discussion. As for incorrect, I am not sure where this may come from, as I see nothing in the
I'm assuming that "request-scoped" can't be interpreted to mean that as soon as the request is "over" the values in the
If we were to agree on the previous point, then this would be an issue with the specific library - as it's relying on unspecified behavior, no? True, if you have already flushed something it's unrealistic to pretend to unflush it so in some cases you won't be able to satisfy the call (I don't really consider the other two cases, error and panic, as valid; in the former, it would be on the user that ignores the returned error, in the latter it would be a bug in the library for relying on unspecified behavior). But, crucially, this is not something that is allowed today because anyway all values are lost when people are forced to use That being said, I agree it we may at least consider the possibility of having values attached to a context opt-in to (or opt-out of) propagation to a detached context (e.g. via an optional interface on the context key? or with yet another value in the context?) |
I have not seen a particularly cogent demonstration of these to date (at least in defect reports), which is not to say they could exist. From my own experience, however, I have seen many cases of folks attempting to "work around" cancellation and simulate detachment by using But generally speaking even the loss of context metadata for (not exclusively for but rather citing it because it is concrete) tracing is particularly bad when using func (b *BankService) TransferMoney(ctx context.Context, in *transferpb.Request) (out *transferpb.Response, error) {
// Validate request: sufficient funds, source and destination accounts are correct.
// Low latency and internally batched for later if failures arise.
err := b.moveMoney(ctx, in.GetSourceIBAN(), in.GetDestIBAN(), in.GetDate(), in.GetAmount())
if err != nil {
return nil, err
}
// Keep data store tidy as a way to minimize transfer kiting (https://en.wikipedia.org/wiki/Check_kiting).
//
// It is slow and requires a partial data set audit and compaction of records and should not be done
// directly in the critical path. Execution is best-effort, so we do not care about errors. If it fails,
// periodic data store reconciliation/cleansing efforts find, report, and act on problems. This is merely
// to provide a more realistic view of account standing.
//
// The operation itself is derived from the request TransferMoney receives, so it should still receive its
// authorization credentials that were attached to the ctx as well as any trace spans for purposes of
// operational forensics.
//
// But because the data store reconciliation can be slow and is run at batch priority (don't want to starve)
// user-interactive requests, its execution often outlives the remaining bits of TransferMoney.
if in.GetAmount() > outstandingLimit {
// Dilemma:
// 1. use context.Background() and lose any sense of metadata and have to do a bespoke re-attachment dance for
// all relevant data
// 2. use local ctx and run the risk of compaction not running to completion
go b.compactRecords(ctx, in.GetSourceIBAN(), in.GetDestIBAN())
}
// Skim the millicents from transfers involving Initech clients.
} In short, I am a proponent of either an API in the /x/ hierarchy or something that is a first-class companion of
I am glad you raised that excerpt. This has been something I have wanted to correct in the documentation, at least propose. In short, that it is an overspecification. My bread and butter are servers, so you'll see that bias what I write. Typical servers deal with two scopes (possibly three):
Contexts remain valid for all of these situations, and the documentation indicating otherwise does it a disservice. |
I'd like to raise another use-case for detaching a context from cancellation and most values that I don't think has been raised yet, and that would be security context -- authentication, for example. As a framework author, I very explicitly do not want users to have to think about this, but code running under the scope of the context should carry and propagate this information, even if they need to do an opportunistic roll-back when a context is cancelled. To be more concrete with the example: an RPC services a user request. The RPC framework translates the user closing the connection into context cancellation. The RPC framework propagates security credentials (incoming and outgoing) to/from out of band RPC metadata and internally via context. To service the request, a transaction is opened, and a rollback operation (which is another RPC, and thus requires a context) is deferred. The backend server in question enforces credentials from both our server (available to all contexts generated by the framework) and the calling user (available within the request handler). Not every language has the cancellation semantics Go has, so the API presumes you will still send these when rolling back, as opposed to giving you a token or something that can be used for rollback without the out of band RPC auth metadata. Now, in Go, I've seen multiple approaches to handling this:
The other option, which I tend to prefer, is to have a "detach" library that knows how to copy only opted-in context values, and they can register a function to copy themselves. This would be ideal: it works both for this use-case, but also for the long-running task use case. Unfortunately, this only works if there is a single registry for such values. Otherwise you can't propagate values outside of a single ecosystem. So, I'd propose something like: package detach
func FromContext(context.Context) context.Context
func RegisterValue[T any](key interface{}, derive func(orig T) T) This addresses the core need to be able to copy, and possibly derive, detachable values like security contexts. This doesn't address logging (that buffers), traces (that want a new span), etc. One way to handle this would be to add APIs that let them spin op a goroutine when the sub-context is done: func WithValuesFrom(ctx, donor context.Context) context.Context
func RegisterCopy(copy func(ctx, orig context.Context) context.Context) This means extra goroutines though, which doesn't excite me. Maybe something like this would be better: type Task func(context.Context) error
func Run(ctx context.Context, f Task, additional ...Wrapper) error
type Wrapper func(ctx, orig context.Context, next Task) Task
func RegisterWrap(wrap Wrapper) (orig would be a derived context from the original, probably pre-cancelled to avoid confusion) This would allow cleaning up when the task completes, which is more useful for span-style context values. It would also allow for adding local wrappers to apply timeouts, etc on a per-call basis. So FromContext would be the API for in-band detach, and Run would be the API for long running tasks. |
I ran into this today while discussing on the Gophers slack. Having the ability to detach a context so that you can keep, for example, trace and other user information in the context that is used by downstream calls would be useful. We have our own implementation that we use in a reasonably large number of places in our code base, but it would be nice if the standard library supported this directly. |
No change in consensus, so accepted. 🎉 |
CL ready for review: https://go-review.googlesource.com/c/go/+/459016 |
Change https://go.dev/cl/479918 mentions this issue: |
Change https://go.dev/cl/486535 mentions this issue: |
For #40221 For #56661 For #57928 Change-Id: Iaf7425bb26eeb9c23235d13c786d5bb572159481 Reviewed-on: https://go-review.googlesource.com/c/go/+/486535 Run-TryBot: Damien Neil <dneil@google.com> Reviewed-by: Sameer Ajmani <sameer@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org>
https://stackoverflow.com/a/75883438/1901067 Tx @CAFxX ! |
WithoutCancel returns a copy of parent that is not canceled when parent is canceled. The returned context returns no Deadline or Err, and its Done channel is nil. Calling Cause on the returned context returns nil. API changes: +pkg context, func WithoutCancel(Context) Context Fixes golang#40221 Change-Id: Ide29631c08881176a2c2a58409fed9ca6072e65d Reviewed-on: https://go-review.googlesource.com/c/go/+/479918 Run-TryBot: Sameer Ajmani <sameer@golang.org> Reviewed-by: Ian Lance Taylor <iant@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
What
I propose to add a
WithoutCancel
function incontext
that, given a parentContext
returns a new childContext
with the same values of the parent, but that is not canceled when the parent is canceled. If needed, although it seems not entirely warranted given the widespread use of similar functions (see section below), this new function could initially live inx/net/context
and be migrated tocontext
later.In addition to the above, we should clarify a point that is only laid out implicitly in the
context
API docs, i.e., that values attached to theContext
can also be used after theContext
has been canceled.https://go.dev/cl/459016 contains the proposed implementation.
Why
This is useful in multiple frequently recurring and important scenarios:
This is doable today by not propagating the triggering event's context and replacing it instead with a new context obtained from
context.Background()
. This is problematic though, as the new context does not contain any of the values attached to the context of the triggering event, and these values are important to e.g., ensure correct authentication/logging/tracing/error recovery functionality (a common scenario when using a middleware-based approach to request/event handling).As noted below with @davecheney, a nice consequence that naturally falls out of this approach is that it effectively turns cancellation into a regular context value that can be overridden in children contexts. It doesn't solve the problem of cancellation being conflated with the intent of context being just a "bag of values" (that would almost certainly require breaking changes to solve) but it's an effective step into alleviating the situation, and it's backward compatible.
As noted further below with @martisch the benefit of this approach is that it's pretty much as minimal, composable, and in line with the current design of the
context
package as possible, requiring a single new public API and eschewing conflating additional mechanisms (goroutines).An important point to be made is that all existing implementations of this (see below) rely on an internal/undocumented guarantee of
cancelCtx
. If this proposal is shot down at least that guarantee should be explicitly documented in the exported API.Existing implementations
Looking around it is possible to find multiple reimplementations of this proposal, almost identical but with different names. I'm not advocating for a specific name here.
Value()
can be implemented by embedding the parentContext
) - this implementation is also duplicated at https://godoc.org/golang.org/x/pkgsite/internal/xcontext#Detachcontextutil.WithoutCancellation
, with the package name chosen only to avoid clashes with thecontext
package from the standard library)detach.FromContext
implementationNewDisconnectedContext
DetachContext
Detach
Detach
(this likely grossly underestimates how common this is used in the wild, as we only search functions calledDetach
)Implementations are trivial and would add a single public function to
context
.The text was updated successfully, but these errors were encountered: