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: io: register close finalizers on Pipe #53362
Comments
This seems like a pretty major behavior change, like when people suggest garbage collecting stuck goroutines. This garbage collects a reason that a goroutine is stuck, unsticking it. The nice thing about deadlocks and stuck goroutines is all you have to do is look at the stacks and they're right there. Why is this important to change the semantics of? |
That presumes that someone is actually looking for them. The reality is that even experienced Go programmers write buggy code that results in goroutine leaks which results in deadlocks or high memory consumption that can go unnoticed for a some time. It is better if such failures can be detected earlier on. Semantic 2 proposed above would not change the behavior, but print out in |
This proposal has been added to the active column of the proposals project |
The difficulty here is managing the lifetimes of the reader and writer, and ensuring that eventually one of them gets closed. In my experience, the code that calls io.Pipe in a good position to gauge that lifetime. This suggests an alternative solution: // PipeContext calls io.Pipe. When ctx is done, both the reader and writer are closed with the error from ctx.Err.
func PipeContext(ctx context.Context) (*io.PipeReader, *io.PipeWriter) This can't live in package It can be implemented straightforwardly outside of the standard library, at the cost of an additional goroutine (wrap io.Pipe) or code duplication (copy+modify io.Pipe). Having something like it in the standard library would help with discoverability and documentation. If it existed, I'd be tempted to deprecate io.Pipe, on the grounds that it is hard to use safely. |
I like the idea of It's not clear to me that func PipeContext(ctx interface{ Done() <-chan struct{} }) (*PipeReader, *PipeWriter) or more simply as just: func PipeCancelable(done <-chan struct{}) (*PipeReader, *PipeWriter) |
@dsnet I think it'd have to be: func PipeContext(ctx interface{ Done() <-chan struct{}; Err() error }) (*PipeReader, *PipeWriter) so that the error from the context closure can be propagated to |
Change https://go.dev/cl/412955 mentions this issue: |
For #53362 Fixes #53414 Change-Id: I352164e70c136eed210c7ee4ceba5dc631f81f94 Reviewed-on: https://go-review.googlesource.com/c/go/+/412955 Auto-Submit: Ian Lance Taylor <iant@google.com> Reviewed-by: Joseph Tsai <joetsai@digital-static.net> Reviewed-by: Ian Lance Taylor <iant@google.com> Run-TryBot: Ian Lance Taylor <iant@google.com> Reviewed-by: Alex Rakoczy <alex@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org>
It's unclear to me that io really needed to have Pipe at all. |
I am sympathetic to this. But this whole conversation started (over in Gopher Slack) with the need for io.Pipe to deal with lacunae in the standard library, like #51092, or hooking up json.Encoder (writer) to http.Request.Body (reader) for the common task of making an HTTP request with a JSON body. As a result, the standard library itself needs a way to document and/or explain the correct way to write the code. See as a data point the continuing conversation at https://go-review.googlesource.com/c/go/+/412955. Also, for core functionality, people will empirically reach for error-prone std functions over safer third party libraries. Putting io.Pipe variants in an x repo would help with the second challenge, as x has the requisite trust factor. Another way to help that sociological problem would be to "bless" a third party library by referring to it in the io.Pipe docs. I assume Google is unwilling to do that for anything but x repos.
FWIW, since contexts are cancellable, PipeContext covers PipeCancellable. :) But yes, a buffered version would be nice to have sometimes. (That said, if this doesn’t go into std or x in any form, I will create a third-party library that does this right, so that I can use it.) |
It seems clear we need a good answer for the gzip/json/http problem, but I'm not sure io.PipeContext improves that situation dramatically, given that the 1-line change in CL 412955 makes it not needed for that. It seems like we haven't found the obviously right answer yet. |
Based on the discussion above, this proposal seems like a likely decline. |
No change in consensus, so declined. |
For golang#53362 Fixes golang#53414 Change-Id: I352164e70c136eed210c7ee4ceba5dc631f81f94 Reviewed-on: https://go-review.googlesource.com/c/go/+/412955 Auto-Submit: Ian Lance Taylor <iant@google.com> Reviewed-by: Joseph Tsai <joetsai@digital-static.net> Reviewed-by: Ian Lance Taylor <iant@google.com> Run-TryBot: Ian Lance Taylor <iant@google.com> Reviewed-by: Alex Rakoczy <alex@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org>
Problem Statement
A semi-common problem in Go is the
io.Reader
<->io.Writer
inversion problem:io.Writer
on hand, but an API is expecting anio.Reader
, ORio.Reader
on hand, but an API is expecting anio.Writer
.One such example of this problem is #51092.
The typical way to solve this problem is to use an
io.Pipe
and a goroutine to shuffle data between the two. For example:In this pattern, it is utterly paramount that either
bodyReader.Close
orhttpWriter.Close
eventually get called, otherwise we have a goroutine leak.However, practice shows that even experience Go programmers often fail to close the pipe in all circumstances, resulting a deadlocked resource leak.
For example, the
gzio.CompressReader
example present a possible resource leak depending on how it is adapted for user code. The example relies onbodyReader.Close
being called by a subsequent call tohttp.Client.Do
, but this will not happen ifhttp.NewRequest
returns an error. In the example, this is fine, since the arguments to constructor are all valid. However, in a real use-case, you can imagine thatts.URL
be dynamically constructed and happens to be an invalid URL. This would causehttp.NewRequest
to return an error and circumventhttp.Client.Do
having an opportunity to callbodyReader.Close
.In almost all cases of resource leaks, one of the endpoints (either the
io.PipeReader
orio.PipeWriter
) have disappeared and been GC'd and the other endpoint is left hanging, not realizing that it will never make progress.Proposal
The current logic of
io.Pipe
is as follows:I propose, we augment it to be like:
Essentially, we close either endpoint once it has been GC'd, informing the other end that there is no possibility of progress being made.
I see two reasonable semantics for the finalizer:
Read
orWrite
call ever sees the sentinelerrClosedPipeGCd
error, which suggests that a resource leak is occurring.\cc @josharian
The text was updated successfully, but these errors were encountered: