-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Write::write_all erroring when encountering Ok(0) interacts poorly with the contract of Write::write #56889
Comments
They'll be retried on subsequent writes, but this plays poorly with write_all. See rust-lang/rust#56889. Hat tip to #90.
@rust-lang/libs, any thoughts? |
This requirement seems at best annoying and at worst unenforceable. For example, I have no idea if OpenSSL's |
AFAICT, the at-most-one requirement is necessary for this other part to be true in practice, though:
And that's a pretty useful property. |
That is also not a thing that you can guarantee when e.g. wrapping third party libraries. |
I agree; I just think it's a good thing to strive for. But either way, I don't think we have to cross that bridge if we change the (undocumented) behavior that write_all bails on |
That will break the use of write_all with all writers which return Ok(0). |
IIRC handling Ok(0) was intended to handle some weird cases of pipes on some OS somewhere where a write to a closed pipe/socket didn't return an error but rather returned Ok(0). The "at most one write" language I think should be toned down to a general guideline, as it's basically just saying "please don't make |
I'm not sure I follow -- what would the middle ground be between "one write" and "like write_all", and how would that obviate the need to return |
To me the middle ground is "if you can, make sure |
What would my implementation choice be, then, that doesn't return Or, to look at it from another angle: how widespread is the usage of weird OS/pipe interactions vs the usage of types that end up in unintuitive, contrary-to-docs behavior because of |
I believe base64 is the same case as flate2. Some internal buffering is required and writes into the adapter don't match up with writes to the other end. Writes of just a few bytes in gzip can generate megabytes of output, so often the input needs to be blocked while that happens. |
But doesn't flate2's way imply that it can return an error and have written some data? Any time we retry anything that could be the case...? |
Here's my interpretation of this so far:
If I've interpreted the requirements correctly, "represents at most one attempt to write to any wrapped object" is already ignored by several |
@jebrosen everything you mentioned sounds accurate to me, and I think supports the idea that we should just relax the language here as it isn't really intended to be so strict anyway. @marshallpierce I don't believe that's the case for |
So, it seems that one way to go here would be to remove the "just write once" restriction, which would have the effect of causing What if instead we tackle the other end of the problem, which is that |
I think that's in the vein of where we want to go, yes, but I don't think that we need to remove such a clause entirely. I think we just need to say something in the spirit of "you should strive to make I don't think we're in a position to change the meaning of |
So, is the upshot then that this is the suggested path for base64...?
That means, for instance, that some data can be written successfully followed by an error (data from the internal buffer, not from the input buffer passed to |
I don't think multiple calls to |
I think it's okay if the user-facing contract of
The key here is that the This allows us to relax the "at most one This also allows us to make the contract more strict in the other direction: because This is distinguished from |
IMHO a sane writer should return an error when it is already at the end. This would be in line with eg. a block device (a typical size-limited object to write to...). |
Related to @Blub's point: another crucial property of this design is that it means that looped writes (a common pattern) may not loop forever. If |
We discussed this issue in the libs api meeting just now, and agreed that the current behaviour of
Nope. That could result in an infinite loop if |
The documentation of
write
(emphasis added) specifies that returningOk(0)
is a valid thing to do, albeit with a possible implementation-specific "this writer is dead" meaning:Despite the above,
Write::write_all
's current implementation returns an error when an underlyingwrite
returnsOk(0)
, effectively making this a forbidden return value for implementations ofWrite
. However, in cases where aWrite
is transforming or buffering data, it can be useful to returnOk(0)
to avoid violating this other, more firmly worded, requirement ofwrite
:I came across this issue while working on a streaming base64 encoder
Write
for thebase64
crate, and subsequently found other cases where these features have clashed (hat tip tojebrosen
on IRC).In the case of base64, suppose a user constructs a base64 writer with an internal buffer that delegates to, say, stdout. The user calls
write
with a 6-byte input, which is base64 encoded into 8 bytes in the internal buffer, and subsequently written to the delegate (stdout). The delegate writes 5 bytes, and the remaining 3 bytes are kept in the buffer. On the next call towrite
from the user, I want to consume no input (and therefore returnOk(0)
), but rather simply try again to write the remaining 3 bytes to the delegate. (Even with a different implementation that does consume as much input as can be encoded and still fit within the buffer, eventually that buffer will fill, and we're back to returningOk(0)
.)The same issue affects flate2:
That violates the "at most one write" part of
write
, but it could be avoided if returningOk(0)
wasn't going to breakwrite_all
.BufWriter
in the std lib has a similar problem:If
BufWriter::write
is called when its buffer is nonempty with an input that won't fit in the buffer, the secondif
leads to two calls to the delegate writer for oneBufWriter::write
call, which violates the contract ofwrite
. Similarly, returningOk(0)
there would solve the issue: the caller could then retry with the same buffer, which would be passed to the delegate writer untouched.No doubt there are complexities I don't see yet, but given that
write_all
's documentation doesn't say anything about erroring onOk(0)
, is it feasible to simply continue looping instead of erroring onOk(0)
? If the concept of "writer exhaustion" the "typically..." clause inwrite
's docs is referring to needs to be represented, could that not be anErrorKind
?The text was updated successfully, but these errors were encountered: