-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
io: fix 'Seek::poll' when the underlying 'AsyncRead::start_seek' call immediately returns Ready. #1986
Conversation
I'm not familiar with what should happen in the mocked file tests - any pointers? |
After some debugging, I think I figured out what's going on. #[test]
fn write_seek_write_err() {
let (mock, file) = sys::File::mock();
mock.write_err().seek_start_ok(0);
let mut file = File::from_std(file);
let mut t = task::spawn(file.write(HELLO));
assert_ready_ok!(t.poll());
pool::run_one();
// file is now "Busy" with the write completed (with error)
{
// NB: the underlying `AsyncSeek::start_seek` implementation for `File` retrieves (but does not return) that write error and then return `Ready(Ok(()))`
let mut t = task::spawn(file.seek(SeekFrom::Start(0)));
// BEFORE this PR: The `Seek` future gets `Ready(Ok(()))` and returns Pending, even though nothing will ever wake the task. That's the bug I'm trying to fix.
// AFTER this PR: The `Seek` future gets `Ready(Ok(()))`, then tries `poll_complete` immediately which returns the write error. So this assert fails.
assert_pending!(t.poll());
}
pool::run_one();
let mut t = task::spawn(file.write(FOO));
// BEFORE: t.poll() now (finally) returns the error from the first attempted write
assert_ready_err!(t.poll());
} I am not sure what the right way to fix this should be. I can change what the test tests, but I am uncertain on when write errors should be returned immediately and when they should be stashed for a later |
After looking over the other code, I've changed
If we want |
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 saving write errors to return from the AsyncWrite
implementation rather than from AsyncSeek
is the right thing to do, especially given that the AsyncRead
implementation for File
already does this:
Lines 69 to 71 in 64dba7a
/// Errors from writes/flushes are returned in write/flush calls. If a write | |
/// error is observed while performing a read, it is saved until the next | |
/// write / flush call. |
Lines 477 to 481 in 64dba7a
Operation::Write(Err(e)) => { | |
assert!(self.last_write_err.is_none()); | |
self.last_write_err = Some(e.kind()); | |
self.state = Idle(Some(buf)); | |
} |
I notice that AsyncRead
panics if another write error occurs while self.last_write_error
is Some
, but AsyncSeek
's methods do not:
Lines 529 to 531 in 64dba7a
Operation::Write(Err(e)) => { | |
self.last_write_err = Some(e.kind()); | |
} |
and
Lines 550 to 552 in 64dba7a
Operation::Write(Err(e)) => { | |
self.last_write_err = Some(e.kind()); | |
} |
Do you think we should change that in this PR as well?
tokio/src/io/seek.rs
Outdated
Poll::Ready(Err(e)) => Poll::Ready(Err(e)), | ||
Poll::Pending => Poll::Pending, |
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.
nit, take it or leave it: these lines could be collapsed into just
poll => poll,
or similar. Not a blocker... perhaps the current code is clearer.
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 one actually can't be, because the left poll
is Result<(), _>
and the right side poll
must be Result<u64, _>
. Something like poll => poll.map(|r| Err(r.unwrap_err())),
works but is more confusing IMO
IIUC, that panic should actually be unreachable because |
Yeah, your version probably makes more sense than mine did. Thanks for fixing my broken code! |
f2a49ce
to
4a15d8a
Compare
Rebased so the two changes are clear without the noise commits. I think all this is missing is additional tests if desired. I also don't know who should press which kind of merge button once this is ready. |
I have a suggestion. Let's add a test case. use std::io::Cursor;
use tokio::prelude::*;
#[tokio::main]
async fn main() -> io::Result<()> {
let mut cursor = Cursor::new(b"abcdefg".iter().copied().collect::<Vec<_>>());
// the `seek` method is defined by this trait
cursor.seek(SeekFrom::Start(3)).await?;
let mut buffer = [0; 1];
let n = f.read(&mut buffer[..]).await?;
assert_eq!(buf, [b'd']);
Ok(())
} |
Write errors should only be returned on subsequent writes or on flush. Also copy the last_write_err assert from 'poll_read' to both 'start_seek' and 'poll_complete' for consistency.
If the first 'AsyncRead::start_seek' call returns Ready, 'AsyncRead::poll_complete' should be called. Before this commit, a start_seek that immediately returned 'Ready' would cause the Seek adapter to return 'Pending' without registering a Waker.
4a15d8a
to
867d727
Compare
Added a test/example - I think I am finally happy with this PR. |
This is the case with
Cursor
, for example.EDIT: This is also the case with
File
if it is not already busy.