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

x/net/context: Documentation for Context.Err doesn't explicitly state what values to expect before Done() is closed #14292

Closed
riannucci opened this issue Feb 10, 2016 · 9 comments

Comments

@riannucci
Copy link

In https://godoc.org/golang.org/x/net/context#Context, there are two possible readings for the documentation of Err:

  1. Err() must return nil before Done() isn't closed, and atomically switch to returning some non-nil error when Done() IS closed
  2. Err() is undefined before Done() is closed, and the only valid use for Err() is after blocking on Done()

Looking at the implementation, I suspect that 1 is the correct interpretation. I would propose that the documentation on this function be amended to read:

    // Err returns a non-nil error value after Done is closed.  Err returns
    // Canceled if the context was canceled or DeadlineExceeded if the
    // context's deadline passed.  If Done is not yet closed, Err must return nil.
    // No other values for Err are defined. After Done is closed, successive
    // calls to Err return the same value.
    Err() error

Note the added "If Done is not yet closed, Err must return nil." sentence.

@bradfitz bradfitz added this to the Unreleased milestone Feb 10, 2016
@bradfitz
Copy link
Contributor

/cc @adg @Sajmani

@bcmills
Copy link
Contributor

bcmills commented Jan 13, 2017

I would argue that (2) is correct. There is no reason to call Err without Done: if nothing else, it encourages the writing of racy code.

(To poll whether a Context is done without blocking, you can use a select with a default case.)

@riannucci
Copy link
Author

It's been a while, but if I recall, the reason to call Err might be something like:

for ctx.Err() != nil {
  // do short operation that can't easily select on Done()
}

Otherwise this would need to look like

outer: for {
  select {
    case <-ctx.Done():
      break outer
    default:
      // do short operation 
  }
}

I don't recall the exact specifics of why this came up, though. I don't see why expecting to be able to call Err() without Done() is inherently racy though? I suppose if you were checking Err() and then doing something with the context, that could be racy (sort-of... the context itself would have to be racy), but I don't see why that would be NOT the case in the event that you do something in the default clause of the select with the context object either:

outer: for {
  select {
    case <-ctx.Done():
      break outer
    default:
      // context could actually be Done here anyway
  }
}

@bcmills
Copy link
Contributor

bcmills commented Jan 18, 2017

A more concrete use-case would be helpful. In my experience, more often than not the loops you're describing actually look like:

for {
  select {
  case <-ctx.Done():
    return nil, ctx.Err()
  default:
  }
  // Do short operation that can't easily select on Done().
}
return someValue, nil

Calling Err without waiting on Done is racy if the particular implementation of the Context interface implements the Err method without adding synchronization beyond closing the Done channel. There is currently no prohibition on such an implementation. (The cancelCtx in the standard library uses a sync.Mutex, but that's more to guard the fast-path cancelers than c.err per se.)

The fundamental problem is this: if the implementation of Err doesn't add a hard sequence point to the program, then the compiler (or the CPU, for that matter!) can assume that its value never changes and optimize your loop to:

for {
  // do short operation that can't easily select on Done()
}

But if Err does add a hard sequence point, it becomes much more expensive: for example, cancelCtx.Err acquires a Mutex lock, which means that if you have N CPUs all executing those loops simultaneously, your code will appear to execute many copies of the short operation in parallel but actually spend most of its time moving the mutex around from cache to cache. (It's a similar problem to the one underlying #17973.)

@riannucci
Copy link
Author

I guess that's why I was hoping the documentation could be clarified 😄. It sounds like the documentation should say something along the lines of "It is undefined behavior to call Err() before blocking on Done()."

@riannucci
Copy link
Author

(I was hoping that it would be reasonable to give Err() a meaning before Done() in order to make it safe, but it sounds like documenting this behavior is insufficient to make it reliable since other implementations of Context presumably don't couple these two.)

@riannucci
Copy link
Author

(sorry for all the updates)

Would it make sense to have govet check for this misuse of Err?

@nya3jp
Copy link
Contributor

nya3jp commented Mar 22, 2021

I believe this issue can be closed now that #19856 was accepted and implemented.

@bcmills
Copy link
Contributor

bcmills commented Mar 22, 2021

Duplicate of #19856

@bcmills bcmills marked this as a duplicate of #19856 Mar 22, 2021
@bcmills bcmills closed this as completed Mar 22, 2021
@golang golang locked and limited conversation to collaborators Mar 22, 2022
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

5 participants