-
Notifications
You must be signed in to change notification settings - Fork 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
Don't print "context canceled" if user terminated #5778
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
❌ Your patch status has failed because the patch coverage (8.33%) is below the target coverage (50.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## master #5778 +/- ##
==========================================
- Coverage 59.42% 59.41% -0.02%
==========================================
Files 347 347
Lines 29402 29410 +8
==========================================
+ Hits 17472 17473 +1
- Misses 10958 10965 +7
Partials 972 972 |
Without breaking API compatibility, this patch allows us to know whether a returned `cli/StatusError` was caused by a context cancellation or not, which we can use to provide a nicer UX and not print the Go "context canceled" error message if this is the cause. Signed-off-by: Laura Brehm <[email protected]>
e98f488
to
ccaa2e0
Compare
@@ -30,7 +30,7 @@ import ( | |||
|
|||
func main() { | |||
err := dockerMain(context.Background()) | |||
if err != nil && !errdefs.IsCancelled(err) { | |||
if err != nil && !errdefs.IsCancelled(err) && !errors.Is(err, context.Canceled) { |
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.
Curious; errdefs.IsCancelled doesn't detect these? (that sounds like a possible bug if it doesn't)
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.
It doesn't, no. It checks that it implements the errdefs
cancelled interface, not for context.Canceled
.
// ErrCancelled signals that the action was cancelled.
type ErrCancelled interface {
Cancelled()
}
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.
But don't we want to be specific to only ignoring the user cancelled the process vs something/someone cancelled the process? AFAIK context.Canceled
can be a multitude of things and not just user cancelled.
See #5760
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.
What else could cancel it though?
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.
Anything using context.WithCancel()
can call the cancel
func inside its own logic and return ctx.Err()
. Since the error is ambiguous and can be used by anything down the call stack I'd prefer to impose a check on an error we know of. To me it seems it would be better to have something print out context canceled
in a case where we don't properly handle it than every occurrence of context.Canceled
silenced
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'm okay with cherry-picking some of #5760 and using that to filter. Does that sound good @vvoland @thaJeztah?
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.
To be honest, I'd be inclined to consider function accepting ctx
and returning context.Canceled
when the input ctx.Err() == nil
as buggy.
A logic using context.WithCancel
should be able to handle that in its own boundary.
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.
It is fine for a function to handle its own login with context.WithCancel
. I'm just saying when it returns an error and we check it all the way at the entry point of the program, then it shouldn't be silenced automatically.
@@ -30,7 +30,7 @@ import ( | |||
|
|||
func main() { | |||
err := dockerMain(context.Background()) | |||
if err != nil && !errdefs.IsCancelled(err) { | |||
if err != nil && !errdefs.IsCancelled(err) && !errors.Is(err, context.Canceled) { | |||
_, _ = fmt.Fprintln(os.Stderr, err) | |||
os.Exit(getExitCode(err)) |
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 existing code didn't do so, but was wondering if the exit code (of explicitly set) should still be regarded (not sure)
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 don't think so – in the past I think we've actually reviewed things to not exit with an erroneous exit code in some instances. We could take a look and see what other tools do though, and what makes sense. @tianon thoughts?
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 think I'm missing some nuance to the question 😅 ❤️
In general, I think the case described (Ctrl+C to cancel the pull during docker run --pull
) should have a non-zero exit code for sure (when cancelled, the command didn't complete the request successfully).
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.
For example:
$ sleep 100
^C
$ echo $?
130
(but this feels too obvious, hence my thought that I must be missing some question nuance 😂)
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.
Nooo, you're definitely right. I was just wondering if there were some weird edge cases to this (if a user cancels mid confirmation prompt for example), or if you had some other thoughts. You're my goto for "I wonder if there's any weird expectations/precedent for [insert Unixy thing]".
I know at least for the CLI we used to have that behavior:
$ docker login -u bork registry-1.docker.io
Password:
[^C]
$ echo $?
0
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.
IMO, in general a canceled operation should have a non-zero exit code, BUT with some exceptions, for example:
docker stats
by design never ends by itself, so terminating it with ctrl+c should produce 0 exit code.
However, ctrl+c docker stats --no-stream
before it finished by itself should produce a non-zero exit code.
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.
$ docker login -u bork registry-1.docker.io
Password:
[^C]
$ echo $?
0
Feels like it should fall under the general rule of non-zero exit code.
My reasoning is, that if you execute (with terminal attached) a script like:
set -x
docker login -u bork registry-1.docker.io
# do something after user logged in
docker pull ...
...
Then docker login
which was ctrl+c'd should exit with a non-zero exit code so the script can exit instead of continuing execution.
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 definitely agree. Even for the docker stats
case, one could argue that the expectation is that it indeed stays attached, and any exit (even user caused) is something that merits an exit code. Looking at tail
,
$ tail -F bork.txt
hi!
[...]
^C
$ echo $?
130
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, that's also a fair point!
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 general, I think the case described (Ctrl+C to cancel the pull during docker run --pull) should have a non-zero exit code for sure (when cancelled, the command didn't complete the request successfully).
Ah, yes, that also makes sense; I recall I was in doubt what would be correct when I wrote the ticket #5659;
I also noticed that the exit-code is non-zero in these situations, but I'm not sure what the correct thing to do is there. For example, tail also exits with a non-zero exit-code, although it uses a specific code for it (130);
So I wasn't sure what the correct way was to look at it;
- User cancelled the pull, and we cancelled, so cancelling "succeeded successfully"
docker pull
was cancelled which happened to be "by user" (in my example), but the pull didn't complete (so "unclean" shutdown of the pull)
But, obviously, that was from my perspective "I cancelled the pull", but it may not be limited to the user taking that action, and (for example) if the cancel happened as part of a script, then having a non-zero exit code would make a lot of sense (something happened, and the pull is not done!).
And now I recall we made a similar mistake on docker ps --filter XXX
and docker image ls --filter XXX
, where we considered "filtering succeeded, but didn't return anything" to be "success", which made sense when we did, but later bit us in the behind, because when scripting, it WILL be beneficial to know "no results, so we may want to take alternative action"
@@ -30,7 +30,7 @@ import ( | |||
|
|||
func main() { | |||
err := dockerMain(context.Background()) | |||
if err != nil && !errdefs.IsCancelled(err) { | |||
if err != nil && !errdefs.IsCancelled(err) && !errors.Is(err, context.Canceled) { |
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 improves clarity by reducing unnecessary negations.
- What I did
Alternative to/supercedes #5666.
Without breaking API compatibility, this patch allows us to know whether a returned
cli/StatusError
was caused by a context cancellation or not, which we can use to provide a nicer UX and not print the Go "context canceled" error message if this is the cause.This change is safe and does not affect any caller that does not set
Cause
with the root error.- How I did it
(This is basically what I suggested in #5666 – see #5666 (comment), and #5666 (comment) – and also what @ndeloof suggested – I figured it would be easier to just implement it since I think most of the maintainers are in agreement about this, and don't want to unexport
cli.StatusError
)By adding a
Cause
field to theStatusError
type, and implementingUnwrap
so thatStatusError
can be checked witherrors.Is
.We can deprecate
Status
later, or we can keep it if we want to let callers set a nicer error message (and always use that if it is set).- How to verify it
docker run --pull=always ubuntu echo "hi"
and cancel during the pull.Before:
With the change:
- Description for the changelog
- A picture of a cute animal (not mandatory but encouraged)