-
-
Notifications
You must be signed in to change notification settings - Fork 3k
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
[PSA] closing output on context cancel #4592
Comments
Looking at the docker source, it looks like a good way to do this is to:
That means callers can just wait for all the error channels to close to know that the requests have completed. |
I fully agree that the current API is error-prone. In one codebase that had a goroutine leak, I ended up finding something like:
This will build fine, and most likely print something unhelpful like So, knowing that the API actually returns a channel, my fix was like:
The leak is gone, but this code is wrong once again. I naively thought that just looking at the I personally think that using channels for iterator APIs is a clever hack that can work well when prototyping APIs, but it's got a number of sharp edges that are difficult to fix. You could add more documentation with warnings, but that's not the best solution. Adding a second channel to the function signature adds more ways to misuse the API, unfortunately. I think that, for an API with such high exposure as go-ipfs, it's worth it to declare iterator types for each of these types to iterate on. The API could roughly be like:
Here, the error is returned alongside the pin, so forgetting to check it would most likely result in a "declared but not used" compiler error. We also don't have any channel, so there are no sharp edges like goroutine leaks. This would require declaring a new named type for each type we want to iterate on, but I reckon it would just be 2-3 methods (as shown above) and at most 30 or so lines of boilerplate. I think that's definitely worth it to end up with a nicer API for the many end users. If multiple types need iterators we'll need a bit of copy-pasting, but I still think that's worth it. And it will get easier once generics are in place. cc @aschmahmann since we discussed this briefly on Slack |
@Stebalien Regarding the comment:
I am not sure that 3 is best, since it prevents receiving results in a With no error channel, the results channel must not be closed. Otherwise, when the context is canceled, then the caller cannot be certain if the goroutine finished before or after the cancellation. Downside is that results cannot be ranged over, and both results and ctx.Done() must be examined in select. With an error channel, the results channel should be closed. That will allow ranging over results and examining the error channel afterwards. Downside of this is that behavior must be well documented so the caller can trust that results channel is closed in the event of an error/cancel. |
The pin objects sent over a channel also embed an error value within them. If we see an error, we must handle it and stop. The upstream issue ipfs/kubo#4592 covers some of the rough edges of this channel-based API.
This isn't a cleaver hack in this case. The API returns a stream, not just an iterator. Using a channel allows one to select/loop over the channel normally. There would need to be a very strong motivation for making such an API the default choice in situations like this, given the added complexity, loss of select, etc. It may make sense for these external APIs given their infrequency, but I'd take a thorough survey of existing projects to get a sense for some consensus/common design pattern.
I agree. Take a look at ipfs/interface-go-ipfs-core#62. Basically, the resulting pattern is: res, err := MakeRequest(ctx)
var results []stuff
for r := res {
results = append(results, r)
// do stuff
}
return results, <-err
etc... |
Now that go has iterator functions, one can be defined to provide a simple interface to using channels for reading results and error from a cancelable goroutine. Here is an example that demonstrates getting a series of results and an error from a goroutine, with the ability to cancel the goroutine: https://go.dev/play/p/d4di-62Gunp The first part of the example shows using channels to read the results and The second part shows how this can be done using an iterator to return While both approaches do the same work, the iterator provides a cleaner Using Raw Channels and no Iterator ctx, cancel := context.WithCancel(context.Background())
defer cancel()
results, asyncErr := asyncTask1(ctx)
for result := range results {
fmt.Println("Day:", result)
if result == cancelAt {
cancel() // cancel asyncTask1
asyncErr = nil // canceled, so do not get error
fmt.Println(result, "is may last day")
break
}
}
if asyncErr != nil {
if err := <-asyncErr; err != nil {
return fmt.Errorf("Error1: %w", err)
}
} Using Iterator for result, err := range asyncIter() {
if err != nil {
return fmt.Errorf("Error2: %w", err)
}
fmt.Println("Day:", result)
if result == cancelAt {
fmt.Println(result, "is may last day")
break
}
} |
Yeah, although that doesn't allow one to select on multiple streams of results. |
But I'm closing this as it isn't actionable. It would be kind of nice to open the discussions on this repo for code discussions, but it would probably get inundated with help requests that should really go to the forums. |
@ipfs/go-team
Hello all,
I've noticed (and have fallen prey to) a bad pattern with contexts and channels that we all need to be aware of (it's non-obvious and rarely causes visible bugs but can cause nasty, hard to track down bugs). For example, the following function has a bug:
What's the bug? Well, let's say we don't trust the function to exit when it should (and want to bail as fast as we can) so we do the following:
We'd expect this program to either print:
Or:
Unfortunately, this program can actually print, e.g.:
How? Well, if we something cancels the context (e.g., a timeout expires),
Count
could notice this, return, and close the output channel. Unfortunately, main could notice this before it notices that the context has been canceled and think we're done.One solution is to write an error to the
output
channel. However, that's kind of a pain and still expects the caller to not wait on the context itself.The correct solution, IMO, is to simply not close the output channel when the context is canceled. The Caller should deal with this case itself.
Correct solution:
Note: I've also noticed the following issue but, at least, that simply leaks a goroutine and doesn't cause any nasty silent bugs. However, please be careful of the following:
The text was updated successfully, but these errors were encountered: