-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
replace context.WithCancel with WithCancelCause #4457
Conversation
@@ -41,6 +41,8 @@ linters-settings: | |||
forbid: | |||
- '^fmt\.Errorf(# use errors\.Errorf instead)?$' | |||
- '^logrus\.(Trace|Debug|Info|Warn|Warning|Error|Fatal)(f|ln)?(# use bklog\.G or bklog\.L instead of logrus directly)?$' | |||
- '^context\.WithCancel(# use context\.WithCancelCause instead)?$' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! Should we require context.WithTimeoutCause
and context.WithDeadlineCause
in this PR also? (I don't think we use WithDeadline anywhere, but should probably forbid it for the future)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, marked in TODO comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I'm not sure if anything can be done for this. Looks like WithTimeoutCause/WithDeadlineCause
have a completely different signature and cause is added as a parameter instead of returning func(error)
. This isn't really useful as it is basically just doing ctx.WithValue("cause", cause)
and doesn't detect when the cancellation actually happened.
Note that if context actually reached timeout then this is somewhat expected as it will happen somewhere in stdlib but if CancelFunc
gets called then with this stdlib function it does not allow detecting the location.
Maybe for the defer()
case still something can be done about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess maybe something like this could work?
cause := &timeoutCause{}
ctx, cancel := context.WithTimeoutCause(ctx, ..., cause.Init())
cancelErr = cause.WithCancel(cancel)
cancelErr(errors.WithStack(context.DeadlineExceeded))
type timeoutCause {
error
}
func (t *timeoutCause) Init() {
// todo: atomic
// store the context creation stack. this will be shown when timeout is reached and runtime cancels
t.error = errors.WithStack(context.DeadlineExceeded)
}
func (t *timeoutCause) WithCancel(c func()) func(error) {
return func(e error) {
// todo: atomic
// store the context cancellation stack. this will be shown when cancel is called manually
t.error = errors.WithStack(o)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the go issue golang/go#56661 there is also another workaround mentioned.
ctx, cancel := context.WithCancelCause(context.Background())
ctx, _ = context.WithTimeoutCause(ctx, 1time.Second, tooSlow)
what is simpler but I'm afraid this will cause a linter error for leaking context that needs to be disabled all the time. Really don't understand why WithTimeoutCause
just does not return CancelCauseFunc
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm for WithTimeoutCause
I use something like:
context.WithTimeoutCause(ctx, time.Minute*5, errors.WithStack(context.DeadlineExceeded))
So we can get the stack for which specific timeout what triggered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will give you the stacktrace to place that created the context. But for example if you have:
{
ctx, cancel := context.WithTimeoutCause(ctx, time.Minute*5, errors.WithStack(context.DeadlineExceeded))
defer cancel()
go func() {
// leaky goroutine
err := foo(ctx)
// cancellation error because context was discarded but no stacktrace to where it happened
}()
return nil
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, yeah, maybe add a helper function to wrap timeouts, and add that to the forbidigo rule?
func withTimeout(ctx context.Context, d time.Duration) (context.Context, func(error)) {
ctx, cancel := context.WithCancelCause(ctx)
ctx, _ = context.WithTimeoutCause(ctx, d, errors.WithStack(context.DeadlineExceeded))
return ctx, cancel
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I turns out the linter only has this leak detection for context.WithTimeout
but nobody has yet added a rule for context.WithTimeoutCause
yet. So this is a problem for someone in the future trying to update the linter 😉
bdf21ff
to
2b3a949
Compare
err = ctx.Err() | ||
// Cause can't be used here because this error is returned for Err() in custom context | ||
// implementation and unfortunately stdlib does not allow defining Cause() for custom contexts | ||
err = ctx.Err() //nolint: forbidigo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there is no way to define Cause()
atm for custom implementations of context interface so this needs to return Err()
directly. Maybe this can be improved in the future if we could avoid defining Err()
and instead would use embedded stdlib context. But I think we can look at this case separately.
2b3a949
to
86ad76e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm!
Keep stack traces for cancellation errors where possible. Signed-off-by: Tonis Tiigi <[email protected]>
Signed-off-by: Tonis Tiigi <[email protected]>
86ad76e
to
09648f4
Compare
Start to bring some sanity to the "context canceled" errors without a stacktrace.
TODO:
Same for Deadline/Timeoutsee replace context.WithCancel with WithCancelCause #4457 (comment)For cases that dolet's do this in follow-up as not directly relateddefer cancel()
we could have a helper that adds fallback handling for code paths (in vendor) that returnctx.Err()
directly and add the stacktrace from theCause()
still to the error.