Skip to content
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

proposal: context: new package for standard library #14660

Closed
bradfitz opened this issue Mar 5, 2016 · 67 comments
Closed

proposal: context: new package for standard library #14660

bradfitz opened this issue Mar 5, 2016 · 67 comments

Comments

@bradfitz
Copy link
Contributor

bradfitz commented Mar 5, 2016

@Sajmani and I are interested in adding golang.org/x/net/context to the standard library, probably as the top-level package context.

(Why not net/context? It has little to do with networking, except that people often use it with networking; it doesn't even cross network boundaries on its own without help.)

Some places in the standard library which could use it:

  • https://golang.org/pkg/net/http/#Request -- add a new Context field, alongside like the existing (Context.Done-compatible) Request.Cancel field. It will take precedence over Cancel, and we can make it work for both outbound and inbound requests. For server requests it would use CloseNotifier's mechanism for c* ancelation. (which for http2 includes the http2-level explicit stream cancelation frame too, also used by gRPC)
  • https://golang.org/pkg/net/#Dialer -- same as http.Request. net.Dialer also has a Done-compatible Cancel field, which could be a Context.
  • https://golang.org/pkg/os/exec/#Cmd could have a new Context field to automatically kill+wait the process when a context expires, if the context expires before the child?
  • https://golang.org/pkg/database/sql/#DB.Query already takes ...interface{} which can be special-cased to say that if the first parameter is a context.Context, it's used for deadlines. Trashy or clever? Maybe both. We could also add parallel DB.QueryContext which is a bit of a mouthful.
  • .... (where else?) ....

Open questions:

  • do we do something with the io package in the same release, or later? i.e. do we add *os.File.IO(context.Context) io.ReadWriteCloser sort of methods, to curry a context into io interfaces? Or wait on that. If it'd be nice to push down cancelation into even OS-level file I/O, but we don't even really do that inside Google with IO-like methods and pervasive Context usage, since we don't really use *os.File. So I'm inclined to wait on that. Too many operating systems to deal with.

Concerns:

  • we can't add it to as many places in the standard library we'd like to since APIs are frozen. Basically we can only add it to structs. While we've told people not to add contexts to structs, I think that guidance is over-aggressive. The real advice is not to store contexts. They should be passed along like parameters. But if the struct is essentially just a parameter, it's okay. I think this concern can be addressed with package-level documentation and examples.
  • what field name in structs to use? We use "ctx" for variables, but struct names are typically spelled out. But does that cause too much stutter? e.g.
package http
type Request struct {
    Context context.Context
}
@elithrar
Copy link

elithrar commented Mar 5, 2016

For others following, there was a substantial discussion on golang-dev about this as it related to net/http: https://groups.google.com/forum/#!topic/golang-dev/cQs1z9LrJDU

As for naming: the only stutter is in the field name in the source. Package users are going to call r.Context.WithValue or r.Context.Done, which has the benefit of being explicit and (say you point out, Brad) retains the ctx naming convention for variables.

Looking forward to this happening!

@atdiar
Copy link

atdiar commented Mar 5, 2016

Thinking about http.Handler, people can alternatively rewrite a different kind of handler that has a different signature for ServeHttp. The advantage is that one can easily add a Context parameter.
But also, add return values. For instance a boolean sentinel value to control whether the handler should be the last one to be executed. Or a different http.ResponseWriter (useful if you wrap the ResponseWriter in one of your handlers, for instance when you gzip things, but not only)

The top level mux object that encapsulates the whole request dispatching logic can still remain a regular http.Handler (and the context.Background would be instantiated in its ServeHttp(w,r) and passed to the handler ServeWithCtx(ctx,w,r)). Probably better that I upload an example of what I mean.

There are also a few things that I am not very fond of. context.TODO is one of them. But I don't know if it is too late to change things or not.
Is this the right place to discuss about API design ?

@jimmyfrasche
Copy link
Member

I think naming it Context in structs is good. There's a stutter, but only for the author of the API. Couldn't be clearer for the users of the API.

Would it be possible to write a go vet check for obvious misuse of a Context in a struct? If so, it would be great to roll that out at the same time. Posting a sign next to the pool is good, but a lifeguard is better.

As much as I'd love Context to be deeply baked into the standard library, I'd be happy with it simply being added to the standard library to say "this is blessed, find good ways to use it" and later adding the best ideas to the stdlib where they can be and kept in third party packages where they cannot. However, I'm not opposed to any of the suggested integrations.

Having the first interface{} parameter to Query be a Context is clever, though, so I'd much rather have the explicit QueryContext.

@riannucci
Copy link

This is maybe a bit out of place for this particular issue, but I think it would be worth considering a different Context implementation (strawman benchmark) before it gets included in the stdlib.

In that benchmark, I compare a bogus map+lock "caching" context implementation to the default implementation. Even with as few as 10 items, there's a noticeable improvement (%-wise, not in absolute terms. I realize we're talking nanoseconds here) on lookups into the cached version. For contexts deeper than 10 items, there's a noticeable improvement. The construction overhead is similar, but I think with a non-bogus implementation the map version could be even faster for overhead than the linked list one.

I think there's an opportunity to have Context be a *struct-with-hidden-fields instead of an interface, which would afford the opportunity to build a faster implementation of it (e.g. by having .WithValue be a struct method instead of a package method that can be smart about chaining, or a .WithValues to avoid the linked-list-building overhead when adding multiple items to the context at the same time) and by possibly adding a .Compact method to compactify an existing Context to make subsequent lookups fast. It's not possible to build these things in a clean fashion with the current interface-based implementation (since the only way for the context to see up the chain is to actually do lookups, and the only way to know the lookup keys is to wait for some user code to provide them).

I'm mostly thinking about the pattern of:

func MyHTTPHandler(rw ResponseWriter, r *Request) {
  c := context.Background()
  // WithValue 10-30 services/contextful things here... this number will likely grow
  // as context becomes even more prevalently used.

  // 100's of lookups into c here... if it were compacted here then all the lookups
  // would be O(1).
  myActualHandler(c, rw, r) 
}

Which, I think, is a pretty common pattern (I don't have data on hand for this assumption, however).

In general, for most stdlib-type libraries, there would simply be competition for the best implementation. But I think that the community is rallying around a single Context implementation (because it's THE way to build composable/stackable libraries which are loosely coupled). There can't really be an alternate implementation that doesn't stem from the stdlib (or x/) that has any hope for survival (since everything needs to adopt the stdlib one to actually be used by anything). To be clear: I think it's GOOD for the community to standardize here, I just think it might be worth considering a slightly speedier implementation before cooking it into the 1.x API compatibility guarantee.

Note that the WithTimeout and WithDeadline package functions are well-served by the interface implementation right now: it allows e.g. mocking of the clock. Some similarly mockable interface would need to be built for that functionality, but AFAIK that mocking requirement doesn't really apply to WithValue.

edit: typo

@kr
Copy link
Contributor

kr commented Mar 5, 2016

How do you expect putting context in http.Request (or any other parameter-ish struct) will interact with adding values (or timeouts or whatever) to the context?

I can think of two basic approaches, caller-save and callee-save:

func f(w http.ResponseWriter, req *http.Request) {
    ctx := req.Context
    g(w, req)
    // ... something with ctx ...
}
func g(w http.ResponseWriter, req *http.Request) {
    req.Context = context.WithValue(req.Context, foo, bar)
    h(w, req)
}

or

func f(w http.ResponseWriter, req *http.Request) {
    g(w, req)
    // ... something with req.Context ...
}
func g(w http.ResponseWriter, req *http.Request) {
    req2 := new(http.Request)
    *req2 = *req
    req2.Context = context.WithValue(req.Context, foo, bar)
    h(w, req2)
}

Of course, it's fine if both caller and callee save the old value, but it's bad if neither of them does it, each expecting the other to do so. So, should we recommend one and document and promote it up front?

@rakyll
Copy link
Contributor

rakyll commented Mar 5, 2016

Traditionally, context objects should carry configuration. The x/net/context package extends context objects' responsibilities by providing constructs for timeout and cancellation signals. In the scope of stamp coupling of request handlers, it is a valuable addition. But I am not sure how it will scale to the rest of the standard library and furthermore whether the context may end up being abused for signaling purposes. The two cases you mention above (exec.Cmd and sql.DB.Query) does this by solely using a context object for handling signals even though there is no necessity for storage. Are you trying to solve the issue of how data is passed among functions or also trying to have primitives around how the entire execution could be signaled from outside?

One of the concerns I would add to the list is that the context should be scoped to a function call.

"Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx" (link)

If the standard library won't be agreeing with this due to compatibility reasons, don't you think it will create confusion?

@bradfitz
Copy link
Contributor Author

bradfitz commented Mar 6, 2016

@rakyll, I addressed that final concern in my first post. ("While we've told people...")

@zippoxer
Copy link

zippoxer commented Mar 7, 2016

If there's any way to add context to the standard library without making http or net aware of context (can still adjust them to play well with it though), this would be the way to not overwhelm newcomers reading the docs.

@okdave
Copy link
Contributor

okdave commented Mar 7, 2016

The plan @bradfitz is proposing makes http and net aware of context, but not in a way that requires their use for basic cases. The http package is already very large, but you only need to use a very small fraction of it to achieve "hello world"-esque examples. Adding context to these packages won't harm newcomers, and should make life easier in more complicated/large codebases.

@bradfitz
Copy link
Contributor Author

bradfitz commented Mar 8, 2016

I mailed out https://golang.org/cl/20346 &https://golang.org/cl/20347

The first copies x/net/context to the standard library. The second reworks x/net/context to just use the standard library's version.

@gopherbot
Copy link
Contributor

CL https://golang.org/cl/20346 mentions this issue.

@gopherbot
Copy link
Contributor

CL https://golang.org/cl/20347 mentions this issue.

@bradfitz
Copy link
Contributor Author

bradfitz commented Mar 8, 2016

@kr, making a new request (req2 := new(Request) ...) is probably the safer option. You don't know whether one of your callees is still using the original Request.

@bradfitz
Copy link
Contributor Author

bradfitz commented Mar 8, 2016

@riannucci, I think redesigning Context is too big and out of scope. We have tons of code already using the existing Context and we want to make migration easy.

@riannucci
Copy link

@bradfitz yeah, I kinda figured as much, but I was hoping that since it's only in x now and not in the stdlib, we still had some API flexibility (e.g. introduce new one to stdlib, deprecate x version, remove x version in 1.8-era or something like that)

@chowey
Copy link

chowey commented Mar 12, 2016

I would love to see support for Context in text/template, such that:

ctx := context.WithValue(context.TODO, "Title", "Through the Looking Glass")
template.Must(template.Parse(`Title: {{.Title}}`)).Execute(os.Stdout, ctx)

would output something like:

Title: Through the Looking Glass

If we are worried about accidentally leaking stuff from the Context into a template (because you used a string as a key elsewhere), then I propose adding a special type to distinguish values you want exposed:

// in the text/template package
type Expose string
// in your program
ctx := context.WithValue(context.TODO, template.Expose("Title"), "Through the Looking Glass")
template.Must(template.Parse(`Title: {{.Title}}`)).Execute(os.Stdout, ctx)

This way, you are deliberate about exposing values to the template.

@nodirt
Copy link
Contributor

nodirt commented Mar 12, 2016

@chowey: what problem you are trying to solve? If keys have to be dynamic, why not to use a map?

@atdiar
Copy link

atdiar commented Mar 14, 2016

@bradfitz @riannucci yes, that's my sentiment too. I understand though that x/context is widely used but since it's about to be frozen into the stdlib, I'd rather have the simplest interface possible. Not only for the current users but the ones that have yet to come. I will still understand if it's difficult from the POV of logistics but...

@elithrar
Copy link

What do you find 'not simple' about the current net/context API? The most
basic use-case revolves around WithValue / Value, which are (I think!)
fairly straightforward.

As Brad says though, it's probably a little too late to change anything:
although it's an "x" package, it's been in the wild for a long while.

On Mon, Mar 14, 2016 at 6:36 AM atdiar notifications@github.com wrote:

@bradfitz https://github.com/bradfitz @riannucci
https://github.com/riannucci yes, that's my sentiment too. I understand
though that x/context is widely used but since it's about to be frozen into
the stdlib, I'd rather have the simplest interface possible. Not only for
the current users but the ones that have yet to come. I will still
understand if it's difficult from the POV of logistics but...


Reply to this email directly or view it on GitHub
#14660 (comment).

@atdiar
Copy link

atdiar commented Mar 14, 2016

@elithrar well, the most interesting use is the propagation/monitoring of cancellation signals, not really WithValue (even though it helps for handler chaining).
I don't find WithValue very ergonomic compared to passing a mutex protected map.
If we have to have a canonical Context, we don't really need it to be an interface either. A struct with hidden fields suffices.
Also, WithTimeout and WithDeadline are somewhat doing the same. You could remove one of those. That would make things already a bit simpler.

I understand what Brad said. Still I'd rather the decision be clearly examined for the long term. It was in x/context and not the stdlib for a reason.

context.TODO was probably supposed to be for static analysis but I think it is not needed.

@bobziuchkovski
Copy link

I share @atdiar 's concerns. While I understand how golang.org/x/net/context works, it took me multiple reads through the godocs and blog post for it to click. I find the current API kind of confusing.

I threw together an alternative implementation (albeit in a rush) with what I believe is a simpler API and easier learning curve: https://github.com/bobziuchkovski/context, godocs at https://godoc.org/github.com/bobziuchkovski/context

I realize it's a bit late in the game, but I feel it's at least worth discussing the current x/net/context API before it hits stdlib.

@elithrar
Copy link

@bobziuchkovski For posterity, can you update your comment (or add a new one) to clarify the pros/cons of your lib vs. the existing x/net/context? I can see that Terminated() and TimeRemaining() are new interface methods (with clear uses), but what are the downsides here?

@bobziuchkovski
Copy link

Sure. Terminated() and TimeRemaining() are mostly synonymous with the existing Done() and Deadline(). In terms of the changes there, I think Terminated() better conveys that the context was actually canceled. I believe a x/net/context could finish it's work and be "done" but the Done() channel wouldn't close unless an expiration or CancelFunc were fired.

For TimeRemaining() vs Deadline(), there's more value (for me anyway) in knowing how much time is left vs an expiration date. I can't personally think of a case where I'd want the Deadline() date vs simply knowing how much time (if any) is left. This isn't a major concern, but just something to discuss.

Adding the WithValue and WithTimeout methods to the interface, like I've done, definitely makes it "fatter", so that could be a downside. I believe it makes the parent/child relationships clearer and makes the context cleaner to construct/work with than all of the different x/net/context.With* functions, but that might just be me.

I think my biggest concern with the current x/net/context is the CancelFunc stuff and the Background() and TODO() funcs. I "get it" now, but those were the biggest barrier to my understanding of the package. It's far easier for me to comprehend Context.Cancel() and implicit cancelation via timeout with the implementation I provided than all of the CancelFunc return values from x/net/context. Similarly, it's easier for me to comprehend creating new contexts via New(), Context.WithTimeout(), and Context.WithValue() than using TODO(), Background() and the different WithValue(), WithDeadline(), WithTimeout(), and WithCancel() constructor funcs from x/net/context. On the one hand, I get that using those keeps the interface slimmer, but on the other hand, it makes x/net/context far more confusing (to me).

I'm not necessarily putting forth this alternative implementation as the alternative, but rather an alternative to hopefully spark some discussion around the current x/net/context API.

@elithrar
Copy link

Thanks Bob.

I think that—given the recurring theme of 'net/context not being clear
about usage'—some of this could be addressed through documentation, if API
changes aren't palatable. It's not the first time I've seen confusion about
how it works/how to use it across the mailing list/SO/forums/etc.

(to address another comment: net/context exists to replace mutex-protected
maps)

On Mon, Mar 14, 2016 at 9:48 AM Bob Ziuchkovski notifications@github.com
wrote:

Sure. Terminated() and TimeRemaining() are mostly synonymous with the
existing Done() and Deadline(). In terms of the changes there, I think
Terminated() better conveys that the context was actually canceled. I
believe a x/net/context could finish it's work and be "done" but the
Done() channel wouldn't close unless an expiration or CancelFunc were
fired.

For TimeRemaining() vs Deadline(), there's more value (for me anyway) in
knowing how much time is left vs an expiration date. I can't personally
think of a case where I'd want the Deadline() date vs simply knowing how
much time (if any) is left. This isn't a major concern, but just something
to discuss.

Adding the WithValue and WithTimeout methods to the interface, like I've
done, definitely makes it "fatter", so that could be a downside. I believe
it makes the parent/child relationships clearer and makes the context
cleaner to construct/work with than all of the different
x/net/context.With* functions, but that might just be me.

I think my biggest concern with the current x/net/context is the
CancelFunc stuff and the Background() and TODO() funcs. I "get it" now, but
those were the biggest barrier to my understanding of the package. It's far
easier for me to comprehend Context.Cancel() and implicit cancelation via
timeout with the implementation I provided than all of the CancelFunc
return values from x/net/context. Similarly, it's easier for me to
comprehend creating new contexts via New and Context.WithTimeout() and
Context.WithValue than using TODO(), Background() and the different
WithValue(), WithDeadline(), WithTimeout(), and WithCancel() constructor
funcs from x/net/context. On the one hand, I get that using those keeps the
interface slimmer, but on the other hand, it makes x/net/context far more
confusing (to me).

I'm not necessarily putting forth this alternative implementation as the
alternative, but rather an alternative to hopefully spark some
discussion around the current x/net/context API.


Reply to this email directly or view it on GitHub
#14660 (comment).

@bobziuchkovski
Copy link

@elithrar I definitely agree there. Even without API changes, a lot of my concerns could be addressed via documentation.

@pkieltyka
Copy link

@bradfitz btw, I noticed your note about potentially changing the baseCtx from the server for each request in cabb140

Perhaps the same or related, what do you think about having a root context for the server that is used as the parent for each request context? this way, as the server is shutting down, it can signal to every context it should finish or cancel?

@bradfitz
Copy link
Contributor Author

@pkieltyka, "it should finish" sounds like graceful shutdown (#4674) but semantically, canceling a context is much more abrupt than that.

@pkieltyka
Copy link

@bradfitz yea its true that canceling every in-flight context would be pretty abrupt and what I'm referring to is for purposes of a graceful shutdown.

The context tree is the one thing that could actually make it graceful though. Depends how a listener on the ctx.Done() channel considers the signal - should it cancel abruptly or just stop on the next tick? I'm pretty sure in the wild we'll see both cases. hmm.. should there be a ctx.Canceled() channel?

@riannucci
Copy link

FWIW, our team has used the cancelation of contexts for graceful shutdown (e.g. wiring SIGTERM -> cancel()). It would be useful to have degrees of cancellation (e.g. STOP EVERYTHING NOW v. Stop when you can). That said, we tend to write crashsafe software, so when we want stop-everything-now, we usually don't want to rely on the application logic to implement that (and we just kill -9 it).

@bradfitz
Copy link
Contributor Author

@pkieltyka, @riannucci, changing the context API and/or semantics aren't really in scope for this bug. They've been frozen for some time. I suggest we move any discussion of graceful shutdown to #4674. The answer may involve contexts in some way, but not by canceling some root context.

@gopherbot
Copy link
Contributor

CL https://golang.org/cl/22529 mentions this issue.

gopherbot pushed a commit that referenced this issue Apr 28, 2016
Updates #14660

Change-Id: Ifa5c97ba327ad7ceea0a9a252e3dbd9d079dae54
Reviewed-on: https://go-review.googlesource.com/22529
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
@OneOfOne
Copy link
Contributor

There's a typo @ 2cc27a7#diff-2f929a7660c606fd0c5b268dad1a9bd4R415

waitDone is redefined inside the if block.

@bradfitz

@bradfitz
Copy link
Contributor Author

@OneOfOne, https://go-review.googlesource.com/22581. In the future, you can reply to code in Gerrit which we use for our code reviews.

@bradfitz
Copy link
Contributor Author

bradfitz commented May 5, 2016

This is done, so closing. Context lives in package context for now (maybe subject to change yet, but unlikely), and is used by net, net/http, net/http/httptrace, and os/exec for now. The database/sql changes to use Context didn't happen for 1.7.

@bradfitz bradfitz closed this as completed May 5, 2016
@dmitshur
Copy link
Contributor

dmitshur commented May 5, 2016

Context lives in package context for now (maybe subject to change yet, but unlikely)

Are there some contending alternative names that have been suggested? I am just curious, because I could not think of any. If there was a discussion about this elsewhere, can you point to that; I'd rather not start bikeshedding here.

@minux
Copy link
Member

minux commented May 5, 2016 via email

@bradfitz
Copy link
Contributor Author

bradfitz commented May 5, 2016

@minux, no rush. Eventually. Maybe at Go 1.8 time.

@phuslu
Copy link

phuslu commented May 8, 2016

@riannucci

I share you concern about the WithValue linked-list performance.

Now I use below pattern in my codes,

func MyHTTPHandler(rw ResponseWriter, r *Request) {
  c := context.Background()
  c = c.WithValue(c, "placeholder", &struct{Member1 string, Member2 string, Member3 int}{"foo", "bar", 42})
  myActualHandler(c, rw, r) 
}

Do you think it could help the looking up performance or not?

@Thomasdezeeuw
Copy link
Contributor

@phuslu instead of guessing, why not write a benchmark?

@phuslu
Copy link

phuslu commented May 9, 2016

@Thomasdezeeuw Thank you.

But the more important thing(I think) is, Does this pattern is the RIGHT practice of context.Context ?

As you see, the original context.Context is used as a nested structure and seems that each node is immutable(Please correct me if I misunderstand). the original common pattern is,

ctx = context.WithValue(ctx, "placeholder", ...)

And the pattern which I used is treat context.Context as a mutable object -- although it may has potential performance improvement.
It will encourage people use below code against context.Context

ctx.Value("placeholder").(*struct {}).Member1 = ...

It smells bad. Hopefully could get comments from you and others.

@bradfitz
Copy link
Contributor Author

bradfitz commented May 9, 2016

@phuslu @riannucci @Thomasdezeeuw, please move discussion of Context.WithValue performance to a new bug.

@sonatard
Copy link

sonatard commented Jul 8, 2016

Hello!!
I have a question about sharing values between middlewares.

I think we'll be able to share values between middlewares in the following code by go 1.7.

func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        user := GetUserFromRequestCookie(r)
        ctx := r.Context()
        ctx = context.WithValue(ctx, "user", user)

        r = r.WithContext(ctx)

        a.next.ServeHTTP(w, r) // next handler is ArticleHandle
}

func (a *ArticleHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() 
    user := ctx.Value("user").(*User)
}

But r.WithContext(ctx) exec shallow copy for goroutine. I need not shallow copy for only sharing values. It is slow. right? I want to set context to Request. But now it cannot.

func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        user := UserFromSession(r)
        ctx := r.Context()
        ctx = context.WithValue(ctx, "user", user)

        // r = r.WithContext(ctx)
        r.SetContext(ctx)

        a.next.ServeHTTP(w, r)
}

func (a *ArticleHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() 
    user := ctx.Value("user").(*User)
}

Please tell me best way sharing values between middlewares using only net/http and context. I think we should not implement context wrapper like 3rd party framework impl for only sharing values.

@davecheney
Copy link
Contributor

@sona-tar We don't the issue tracker to ask questions. Please see https://golang.org/wiki/Questions for good places to ask. Thanks.

@sonatard
Copy link

@golang golang locked and limited conversation to collaborators Jul 14, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests