Skip to content
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

Defer work until polled #165

Closed
wants to merge 20 commits into from
Closed

Conversation

ollie-etl
Copy link
Collaborator

@ollie-etl ollie-etl commented Nov 4, 2022

This PR closes #120. It changes the semantics of all oeprations to do no work unless polled, which is generally the expected behavior of Futures.

As an aside, the API change gets rid of quite a few unwraps around Op creation, which are now mostly infallible, unless they have fallible steps during their creation

@ollie-etl ollie-etl changed the title Defer work Defer work until polled Nov 4, 2022
@ollie-etl
Copy link
Collaborator Author

ollie-etl commented Nov 4, 2022

This isn't ready, but I thought I'd solicit feedback early

@ollie-etl
Copy link
Collaborator Author

ollie-etl commented Nov 5, 2022

@Noah-Kennedy I have a question: What are the semantics for Ops polled outside of the runtime? Failure or panic?
In this test

drop(file);
we are to using io_uring resources out of context, and there appears to be significant special casing for this Op.

This PR keeps that special casing (Close is the only caller of try_submit_with). I actually think try_submit_with with should be try_immediate_submit_with in this case, to force the submission into a submitted state. This would be the only user of Submitted then also.

However, I'm unsure why we're going to such lengths to allow Ops to execute outside of a runtime?

src/driver/op.rs Outdated
@@ -40,14 +42,18 @@ pub(crate) struct Op<T: 'static, CqeType = SingleCQE> {
/// A Marker for Ops which expect only a single completion event
pub(crate) struct SingleCQE;

/// A Marker for Ops which may be submitted when the ring is not present
/// The only user of this is `Close` currently
pub(crate) struct Fallible;
Copy link
Collaborator Author

@ollie-etl ollie-etl Nov 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unconvinced this is needed. changing try_submit_with to try_submit_with_immediate or similar, which would submit the operation immediately would be sufficient

@ollie-etl
Copy link
Collaborator Author

Summary:

Ops no longer immediately submit. Instead, when a new op is created, the Sqe is calculated as before, and placed in a new Lifecycle variant, Lifecycle::Pending. If the op is then dropped, the Lifecycle is removed, but no transaction occurs on the ring. If instead the Op is Polled, the Lifecycle transitions to Lifecycle::Waiting, and the sqe placed on the submission ring. From this point onward, things proceed as before.

This has some nice properties.

  • Op construction is infallible, unless other IO is required in the creation.
  • Errors that occur when pushing the op to the queue can now be handled. They cause the op to complete with io::Error
  • Futures behave as advertised / expected.

The question re Close remains, although now I've dug into it, i have that question anyway. Its mostly orthogonal to this PR
I also need to do something with the unit tests on Op. Because Polling now does the submit, not creation, its much harder (impossible) to create a fake Op which doesn't submit as a test. Or rather I could, but it'd need its own future impl, which seems a bit pointless.

@Noah-Kennedy
Copy link
Contributor

@Noah-Kennedy I have a question: What are the semantics for Ops polled outside of the runtime? Failure or panic? In this test

drop(file);

we are to using io_uring resources out of context, and there appears to be significant special casing for this Op.

This PR keeps that special casing (Close is the only caller of try_submit_with). I actually think try_submit_with with should be try_immediate_submit_with in this case, to force the submission into a submitted state. This would be the only user of Submitted then also.

However, I'm unsure why we're going to such lengths to allow Ops to execute outside of a runtime?

I actually think that this special casing of close needs to go. We should really be doing a synchronous close anyways.

That said, this is an issue that needs some discussion. Right now "panic" is fine, but if we add a Handle type, this would change, but we can cross that bridge if we get there.

@ollie-etl ollie-etl marked this pull request as ready for review November 7, 2022 08:50
@ollie-etl
Copy link
Collaborator Author

ollie-etl commented Nov 7, 2022

@Noah-Kennedy @FrankReh Ready for review

  • All ops are now deferred until Polled. The special case handling for Close shutdown is preserved (that is outside the scope of this PR), although submit_try_with has been renamed enqueue, and the signature changed to separate Op creation from submission.

  • Several Ops new signatures have changed from io::result<Op<T>> to Op<T>, as the Op creation is now infallible. If an op is encountered during ring submission, the error is propagated in the Future. See https://github.com/etlsystems/tokio-uring/blob/a04e4bcce198e883022af5e95b164f79299e6f90/src/driver/op.rs#L192

  • As a summary of performance, this causes either no change or maybe a slight performance improvement vs master

Benchmarks

Benchmarking no_op/1: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 22.1s, or reduce sample count to 20.
no_op/1                 time:   [215.09 ms 215.73 ms 216.45 ms]
                        thrpt:  [462.00 Kelem/s 463.55 Kelem/s 464.91 Kelem/s]
                 change:
                        time:   [-2.6494% -2.0570% -1.4711%] (p = 0.00 < 0.05)
                        thrpt:  [+1.4930% +2.1002% +2.7215%]
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  4 (4.00%) high mild
  3 (3.00%) high severe
no_op/32                time:   [31.378 ms 31.428 ms 31.481 ms]
                        thrpt:  [3.1765 Melem/s 3.1818 Melem/s 3.1870 Melem/s]
                 change:
                        time:   [-2.1162% -1.9144% -1.7006%] (p = 0.00 < 0.05)
                        thrpt:  [+1.7300% +1.9517% +2.1619%]
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  6 (6.00%) high mild
  1 (1.00%) high severe
no_op/64                time:   [28.056 ms 28.098 ms 28.146 ms]
                        thrpt:  [3.5529 Melem/s 3.5590 Melem/s 3.5643 Melem/s]
                 change:
                        time:   [-1.4561% -1.2270% -0.9898%] (p = 0.00 < 0.05)
                        thrpt:  [+0.9997% +1.2423% +1.4777%]
                        Change within noise threshold.
Found 8 outliers among 100 measurements (8.00%)
  5 (5.00%) high mild
  3 (3.00%) high severe
no_op/256               time:   [25.777 ms 25.821 ms 25.868 ms]
                        thrpt:  [3.8658 Melem/s 3.8728 Melem/s 3.8795 Melem/s]
                 change:
                        time:   [-0.3218% -0.0816% +0.1423%] (p = 0.51 > 0.05)
                        thrpt:  [-0.1421% +0.0817% +0.3228%]
                        No change in performance detected.
Found 3 outliers among 100 measurements (3.00%)
  3 (3.00%) high mild

examples/cat.rs Outdated Show resolved Hide resolved
examples/tcp_listener.rs Outdated Show resolved Hide resolved
@@ -9,7 +9,7 @@ pub(crate) struct Accept {
}

impl Op<Accept> {
pub(crate) fn accept(fd: &SharedFd) -> io::Result<Op<Accept>> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Op creation is infallible


pub(crate) struct Close {
fd: RawFd,
}

impl Op<Close> {
/// Close a file descriptor
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments unrelated to the PR, but helped me understand the whole Close shutdown saga whilst implementing

pub(crate) fn close(fd: RawFd) -> io::Result<Op<Close>> {
use io_uring::{opcode, types};

Op::try_submit_with(Close { fd }, |close| {
let op = Op::submit_with(Close { fd }, |close| {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a Close Op: infallible

})
});

op.enqueue()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

immediately enqueue the Op: fallible

@@ -13,7 +13,7 @@ pub(crate) struct Connect {

impl Op<Connect> {
/// Submit a request to connect.
pub(crate) fn connect(fd: &SharedFd, socket_addr: SockAddr) -> io::Result<Op<Connect>> {
pub(crate) fn connect(fd: &SharedFd, socket_addr: SockAddr) -> Op<Connect> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Op creation is infallible

Comment on lines +261 to +266
for (_, cycle) in self.lifecycle.iter() {
match cycle {
Lifecycle::Completed(_) | Lifecycle::Pending(_) => {}
c => unreachable!("Lifecycle {:?} found during driver drop", c),
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stricvly unrelated, but gives better panic messages when something goes wrong during debug. can revert or PR elsewhere if preferred.

@@ -46,7 +48,10 @@ pub(crate) trait Completable {
}

pub(crate) enum Lifecycle {
/// The operation has been submitted to uring and is currently in-flight
/// The operation has been created, and will be submitted when polled
Pending(Entry),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The meat of the PR

Comment on lines +72 to +83
impl std::fmt::Debug for Lifecycle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pending(_) => f.debug_tuple("Pending").finish(),
Self::Submitted => f.debug_tuple("Submitted").finish(),
Self::Waiting(_) => f.debug_tuple("Waiting").finish(),
Self::Ignored(_) => f.debug_tuple("Ignored").finish(),
Self::Completed(_) => f.debug_tuple("Completed").finish(),
Self::CompletionList(_) => f.debug_tuple("CompletionList").finish(),
}
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug only. Again, can revert or PR elsewhere if preferred.

/// the kernel.
pub(super) fn submit_with<F>(data: T, f: F) -> io::Result<Self>
pub(super) fn submit_with<F>(mut data: T, f: F) -> Self
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submission to the driver is infallible. This now places the seq for submission into the Pending state

///
/// This is useful when any failure in io_uring submission needs
/// to be handled at the point of submission.
pub(super) fn enqueue(self) -> io::Result<Self> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does nothing if called on an already submitted operation. TODO: improve docs

Lifecycle::Submitted | Lifecycle::Waiting(_) => {
*lifecycle = Lifecycle::Ignored(Box::new(self.data.take()));
}
Lifecycle::Completed(..) => {
Lifecycle::Completed(..) | Lifecycle::Pending(_) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the op is dropped before it is submitted to the ring, no work on the ring is scheduled.

//
// TODO: Should we warn?
*state = match CONTEXT.try_with(|cx| cx.is_set()) {
Ok(true) => match Op::close(self.fd) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Close is special cased (preserving the current behavior to immediately submitting to the ring) , we can fallback to synchronous Close as required.

*state = State::Closed;
Poll::Ready(())
Poll::Ready(r)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Propagates any error in Close from io_uring

} else {
Err(io::ErrorKind::Other.into())
}
/// Enqueue an operation on the submission ring, without `await`ing the future
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could end the sentence with a period, to be consistent with other changes to this file.

Copy link
Collaborator

@FrankReh FrankReh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not expressing an opinion on this one. @Noah-Kennedy 's is the only one that matters here as he's working on this issue too. I'm happy to read and think through this more if either of you think it's warranted however.

@Noah-Kennedy
Copy link
Contributor

@ollie-etl see my comments in #120. I'm not sure that this is the right approach.

@ollie-etl
Copy link
Collaborator Author

@mzabaluev As you're pretty active in PR's currently, do you have a view on deferred operations?

// Try to push the new operation
while unsafe { driver.uring.submission().push(&sqe).is_err() } {
// Fail with an IoError if encountered
driver.submit()?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lifecycle entry will stay in the Submitted state even if this fails?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yes. Bug. It should be completed

@@ -40,12 +40,12 @@ impl Socket {
}

pub(crate) async fn write<T: IoBuf>(&self, buf: T) -> crate::BufResult<usize, T> {
let op = Op::write_at(&self.fd, buf, 0).unwrap();
let op = Op::write_at(&self.fd, buf, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the async transformation works so that this is not run until the future returned by write is polled for the first time. If Op creation only happens in such places, we have nothing to worry about?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow, sorry

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 43 is not executed synchronously, that happens on the first poll of the future returned by write. So this method does not actually do any work until polled.
Is there any instance where submit_with is called inside a synchronous fn?

@Noah-Kennedy
Copy link
Contributor

@oliverbunting as a result of the convo we had in the issue thread as well as discord, I think it might be better to move this to draft

@Noah-Kennedy
Copy link
Contributor

Or potentially even close this since its actually part of a larger discussion we are having.

@oliverbunting
Copy link

In my experience, PRs marked draft get ignored, and Id like to invite feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

possibly deferring work in the creation of an operation
5 participants