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

sync: oneshot::Receiver::{is_empty, is_closed} #7137

Closed

Conversation

cratelyn
Copy link
Contributor

@cratelyn cratelyn commented Feb 3, 2025

this commit proposes two new methods for tokio's
sync::oneshot::Receiver<T> type: is_empty() and is_closed().

these follow the same general pattern as existing is_empty() and is_closed() methods on other channels such as
sync::mpsc::Sender::is_closed(), sync::mpsc::Receiver::is_closed(), sync::oneshot::Receiver::is_closed(), sync::broadcast::Receiver::is_empty(), sync::mpsc::Receiver::is_empty(), ...and so forth.

broadly speaking, users of the oneshot channel are encouraged to .await the Receiver<T> directly, as it will only yield a single value.

users that are writing low-level code, implementing Futures directly, may instead poll the receiver via
<Receiver<T> as Future<Output = Result<T, RecvError>::poll(..).

because (a) poll() has an &mut self receiver that does not take ownership in the same way as .awaiting the receiver will, and (b) the contract of std::future::Future states that clients should not poll a future after it has yielded Poll::Ready(value), users implementing Futures in terms of a oneshot channel do not have a way to inspect the state of a oneshot channel.

this commit aims to provide such users means to passively inspect the state of a oneshot channel, to avoid violating the contact of Future::poll(..), or requiring that a oneshot channel users track this state themselves externally.

this commit proposes two new methods for tokio's
`sync::oneshot::Receiver<T>` type: `is_empty()` and `is_closed()`.

these follow the same general pattern as existing `is_empty()` and
`is_closed()` methods on other channels such as
[`sync::mpsc::Sender::is_closed()`](https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Sender.html#method.is_closed),
[`sync::mpsc::Receiver::is_closed()`](https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Receiver.html#method.is_closed),
[`sync::oneshot::Receiver::is_closed()`](https://docs.rs/tokio/latest/tokio/sync/oneshot/struct.Sender.html#method.is_closed),
[`sync::broadcast::Receiver::is_empty()`](https://docs.rs/tokio/latest/tokio/sync/broadcast/struct.Receiver.html#method.is_empty),
[`sync::mpsc::Receiver::is_empty()`](https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Receiver.html#method.is_empty),
...and so forth.

broadly speaking, users of the oneshot channel are encouraged to
`.await` the `Receiver<T>` directly, as it will only yield a single
value.

users that are writing low-level code, implementing `Future`s directly,
may instead poll the receiver via
`<Receiver<T> as Future<Output = Result<T, RecvError>::poll(..)`.

because (a) `poll()` has an `&mut self` receiver that does not take
ownership in the same way as `.await`ing the receiver will, and (b) the
contract of `std::future::Future` states that clients should not poll a
future after it has yielded `Poll::Ready(value)`, users implementing
`Future`s in terms of a oneshot channel do not have a way to inspect
the state of a oneshot channel.

this commit aims to provide such users means to passively inspect the
state of a oneshot channel, to avoid violating the contact of
`Future::poll(..)`, or requiring that a oneshot channel users track this
state themselves externally.
@github-actions github-actions bot added the R-loom-sync Run loom sync tests on this PR label Feb 3, 2025
Comment on lines +987 to +988
/// This happens when the corresponding sender is either dropped or sends a
/// value, or when this receiver has closed the channel.
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, this feels somewhat different than the semantics of is_closed on other channels, which only returns true when the other side has been dropped. On the other hand, there's a valid argument that this is just because those channels don't have the property that sending a value closes the channel. I'm not sure how I feel about this: I think it might be worth being consistent that "closed" means "dropped by the other side", but I'm open to being convinced otherwise.

Copy link
Member

Choose a reason for hiding this comment

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

Also, the Sender::is_closed method currently exists and only inspects the value of the CLOSED bit:

/// Returns `true` if the associated [`Receiver`] handle has been dropped.
///
/// A [`Receiver`] is closed by either calling [`close`] explicitly or the
/// [`Receiver`] value is dropped.
///
/// If `true` is returned, a call to `send` will always result in an error.
///
/// [`Receiver`]: Receiver
/// [`close`]: Receiver::close
///
/// # Examples
///
/// ```
/// use tokio::sync::oneshot;
///
/// #[tokio::main]
/// async fn main() {
/// let (tx, rx) = oneshot::channel();
///
/// assert!(!tx.is_closed());
///
/// drop(rx);
///
/// assert!(tx.is_closed());
/// assert!(tx.send("never received").is_err());
/// }
/// ```
pub fn is_closed(&self) -> bool {
let inner = self.inner.as_ref().unwrap();
let state = State::load(&inner.state, Acquire);
state.is_closed()
}

Of course, that method can't ever be called if a value was sent, as doing so consumes the sender. so. Hm.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah ... I'm not sure what is the least surprising here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is a very interesting conundrum!

to begin by restating the problem directly: we are asking ourselves whether or
not tokio::sync::oneshot::Receiver::is_closed() should return true when
the corresponding tokio::sync::oneshot::Sender has been consumed by sending
a value.

let's consider the alternatives and how they would affect callers using them
to determine whether or not a receiver may be polled.

option a: is_closed() does not include completed channel

in this model, is_closed() only returns true when the sender is dropped, or
when the receiver closes the channel. it does not return true if a value
was sent.

in this case, we would see the following values when inquiring about closedness
and emptiness.

- is_closed() is_empty() safe to poll?
initial false true yes
receiver closes channel true true or false yes
sender was dropped true true yes
sender sent value (unreceived) false false yes
sender sent value (received) false true no

option b: is_closed() does include completed channel

in this model, is_closed() returns true when the receiver closes the channel,
when the sender is dropped, or when the sender is used to send a value.

- is_closed() is_empty() safe to poll?
initial false true yes
receiver closes channel true true or false yes
sender was dropped true true yes
sender sent value (unreceived) true false yes
sender sent value (received) true true no

⚖️ comparing a and b

now, this brings us to @hawkw's note:

Hmm, this feels somewhat different than the semantics of is_closed on other
channels, which only returns true when the other side has been dropped. On
the other hand, there's a valid argument that this is just because those
channels don't have the property that sending a value closes the channel. I'm
not sure how I feel about this: I think it might be worth being consistent
that "closed" means "dropped by the other side", but I'm open to being
convinced otherwise.

first, i would note that we actually have some minor semantic differences
between different channels' respective is_closed methods already, upon
closer inspection. i'd argue that is fine, and inherent to the fact that
different channel have different semantics, and that is bound to be reflected
in methods querying their conceptual state.

as an example, sync::watch::Sender::is_closed returns true if all receivers
are dropped, while sync::mpsc::Sender::is_closed returns true if all
receivers are dropped or when Receiver::close is called. a sync::watch
channel can have many receivers, so it makes sense that its notion of
closedness would be narrower than a channel that conversely has a single
receiver.

it seems like it would be in line with the above for a oneshot channel to
have its own unique caveat related for is_closed, returning true when the
sender has been dropped by virtue of having sent a value.

...a secret third thing?

now, zooming back out for a moment: i found myself curious about this because
i've frequently encountered a pattern when interacting with oneshot channels
wherein they are very frequently wrapped in an Option<T>. once the receiver
is polled and yields a Poll::Ready(_) value of some kind, the caller takes
the receiver out of the Option<T> and mem::drop()s it to prevent from being
polled again.

prior to reading the internals of the oneshot channel, and learning more
about how the sender and receiver interact, my initial intuition was that one
could check that a receiver was finished by checking that the channel was
closed, and empty.

looking at the tables above however, i've found that neither option a or b
provide a surefire way to check that a receiver is finished, or if it is safe
to poll(). for example: an empty, closed channel could indicate that the
sender was dropped, but that doesn't provide information about whether the
Err(RecvError) has been returned yet.

now, looking at the receiver, and its Future implementation:

// tokio/src/sync/oneshot.rs (abbrevated)

#[derive(Debug)]
pub struct Receiver<T> {
    inner: Option<Arc<Inner<T>>>,
    // spans...
}

impl<T> Future for Receiver<T> {
    type Output = Result<T, RecvError>;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // If `inner` is `None`, then `poll()` has already completed.
        let ret = if let Some(inner) = self.as_ref().get_ref().inner.as_ref() {
            ready!(inner.poll_recv(cx))?
        } else {
            panic!("called after complete");
        };

        self.inner = None;
        Ready(Ok(ret))
    }
}

i see utility for a third method tokio::sync::oneshot::Receiver::is_finished().

impl<T> Receiver<T> {
    /// Checks if this receiver is finished.
    ///
    /// This will return true after this receiver has been polled and yielded
    /// a result.
    fn is_finished(&self) -> bool {
        self.inner.is_none()
    }
}

if this was an option, i would feel personally much less attached to the
decision over whether or not is_closed reports true when a value has been
sent.

this would directly let callers check if polling the receiver would panic, and
in friendly, direct terms. this would also mean that the common pattern of
wrapping receivers in Option<Receiver<T>> would be largely outmoded.
is_finished would let us inquire about the state of the receiver's own
internal Option<T>, without needing to wrap it in another outer option we
can inspect.

what do you think, @Darksonn, and @hawkw?

Copy link
Contributor

Choose a reason for hiding this comment

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

If the intent of this PR is a way to detect whether polling the channel will panic, then having a function that does exactly that sounds good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the intent of this PR, broadly speaking, is to introduce accessors that allows sync::oneshot::Receiver<T> to inspect the channel's status in the same manner that other channels like sync::mpsc's and sync::broadcast's respective Receiver<T>s can.

detecting panics is one acute example, but i've also felt a need for these interfaces when writing Body implementations for types backed by a oneshot, to provide another example.

a Body backed by a oneshot receiver cannot report proper is_end_stream() hints because the receiver doesn't provide a way to inspect the channel. these felt like relatively agnostic methods to expose in part because they follow patterns exposed by other channels.

91682a9 introduces an is_finished() method. i'd be happy to outline that into a separate PR if we don't feel a disposition to merge is_empty() and is_closed() methods. what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

91682a9 introduces an is_finished() method. i'd be happy to outline that into a separate PR if we don't feel a disposition to merge is_empty() and is_closed() methods. what do you think?

Personally, I'd be inclined to suggest a PR adding a method like that separately, so that the change you actually need isn't blocked on deciding the right semantics for is_closed. I think that change should hopefully be fairly uncontroversial.

Regarding naming for that method, I think "is_finished" is probably fine, but I'll note that the futures crate's FusedFuture trait calls its method with similar semantics is_terminated. Although we probably won't be adding FusedFuture implementations in Tokio due to stability concerns, it might be worth choosing a name that's consistent with the one in futures? I could be convinced either way though.

Copy link
Member

Choose a reason for hiding this comment

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

And, to be clear, I'd also still like to have is_closed() and is_empty() — I just think we could add an is_finished_and_or_terminated() without having to wait for figuring out is_closed().

I think we could probably also land an is_empty() method separately, if you like. I don't think there's much ambiguity about what "empty" means in this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thank you both for the advice and for your time, @Darksonn and @hawkw! i agree, landing these each separately sounds like a more prudent path forward.

i'm going to close this, and let us consider each of these proposals individually.

see:

@Darksonn Darksonn added A-tokio Area: The main tokio crate M-sync Module: tokio/sync labels Feb 4, 2025
@Altiboynemati

This comment was marked as spam.

this avoids consuming the receiver, so we can inspect the value returned
by `is_empty()`.

Signed-off-by: katelyn martin <[email protected]>
this commit introduces a method that can be used to inquire whether a
oneshot channel's receiver has or has not yielded a value.

this is useful for callers that may be polling the receiver as a future,
to avoid inducing a panic. the receiver panics if polled after yielding
a `Poll::Ready<T>`. note that this is acceptable, per the
`Future::poll()` documentation regarding panics:

> Once a future has completed (returned Ready from poll), calling its
poll method again may panic, block forever, or cause other kinds of
problems; the Future trait places no requirements on the effects of such
a call.

NB: this commit makes one somewhat noteworthy change to the
implementation of `<Receiver<T> as Future>::poll()`. the inner state is
now taken when an error is yielded. this also follows the rules
proscribed by `std::future::Future::poll()`, to be clear!

the upside of this is that it means a broken or closed channel, e.g.
when the sender is dropped, will settle as "finished" after it yields an
error.

see: <https://doc.rust-lang.org/stable/std/future/trait.Future.html#panics>.

Signed-off-by: katelyn martin <[email protected]>
@cratelyn cratelyn force-pushed the sync-oneshot-receiver-is-complete branch from b9d4ea1 to 91682a9 Compare February 10, 2025 00:58
cratelyn added a commit to cratelyn/tokio that referenced this pull request Feb 13, 2025
this commit introduces a new method to
`tokio::sync::oneshot::Receiver<T>`.

broadly speaking, users of the oneshot channel are encouraged to
`.await` the `Receiver<T>` directly, as it will only yield a single
value.

users implementing their own `std::future::Future`s directly may instead
poll the receiver via
`<Receiver<T> as Future<Output = Result<T, RecvError>::poll(..)`.
note that the contract of `Future::poll()` states that clients should
not poll a future after it has yielded `Poll::Ready(value)`.

this commit provides a way to inspect the
state of a receiver, to avoid violating the contact of
`Future::poll(..)`, or requiring that a oneshot channel users track this
state themselves externally via mechanisms like
`futures::future::FusedFuture`, or wrapping the receiver in an
`Option<T>`.

NB: this makes a small behavioral change to the implementation of
`<Receiver<T> as Future<Output = Result<T, RecvError>::poll(..)`. this
change is acceptable, per the `Future::poll()` documentation regarding
panics:

> Once a future has completed (returned Ready from poll), calling its
poll method again may panic, block forever, or cause other kinds of
problems; the Future trait places no requirements on the effects of such
a call.

the upside of this is that it means a broken or closed channel, e.g.
when the sender is dropped, will settle as "finished" after it yields an
error.

see:
* tokio-rs#7137 (comment)
* https://doc.rust-lang.org/stable/std/future/trait.Future.html#panics

Signed-off-by: katelyn martin <[email protected]>
cratelyn added a commit to cratelyn/tokio that referenced this pull request Feb 13, 2025
this commit introduces a new method to
`tokio::sync::oneshot::Receiver<T>`.

this method returns true if the channel has no messages waiting to be
received. this is similar to the existing
`tokio::sync::mpsc::Receiver::is_empty()` and
`tokio::sync::broadcast::Receiver::is_empty()` methods.

see:
* https://docs.rs/tokio/latest/tokio/sync/broadcast/struct.Receiver.html#method.is_empty
* https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Receiver.html#method.is_empty
* tokio-rs#7137 (comment)

Signed-off-by: katelyn martin <[email protected]>
cratelyn added a commit to cratelyn/tokio that referenced this pull request Feb 13, 2025
this commit introduces a new method to
`tokio::sync::oneshot::Receiver<T>`.

this method returns true if the channel is closed.

this is similar to the existing
`tokio::sync::mpsc::UnboundedReceiver::is_closed()` and
`tokio::sync::mpsc::Receiver::is_closed()` methods.

see:
* https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Receiver.html#method.is_closed
* https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.UnboundedReceiver.html#method.is_closed
* tokio-rs#7137 (comment)

Signed-off-by: katelyn martin <[email protected]>
@cratelyn
Copy link
Contributor Author

this work is continued in #7152, #7153, and #7154.

@cratelyn cratelyn closed this Feb 13, 2025
cratelyn added a commit to cratelyn/tokio that referenced this pull request Feb 13, 2025
this commit introduces a new method to
`tokio::sync::oneshot::Receiver<T>`.

this method returns true if the channel has no messages waiting to be
received. this is similar to the existing
`tokio::sync::mpsc::Receiver::is_empty()` and
`tokio::sync::broadcast::Receiver::is_empty()` methods.

see:
* https://docs.rs/tokio/latest/tokio/sync/broadcast/struct.Receiver.html#method.is_empty
* https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Receiver.html#method.is_empty
* tokio-rs#7137 (comment)

Signed-off-by: katelyn martin <[email protected]>
cratelyn added a commit to cratelyn/tokio that referenced this pull request Feb 15, 2025
this commit introduces a new method to
`tokio::sync::oneshot::Receiver<T>`.

broadly speaking, users of the oneshot channel are encouraged to
`.await` the `Receiver<T>` directly, as it will only yield a single
value.

users implementing their own `std::future::Future`s directly may instead
poll the receiver via
`<Receiver<T> as Future<Output = Result<T, RecvError>::poll(..)`.
note that the contract of `Future::poll()` states that clients should
not poll a future after it has yielded `Poll::Ready(value)`.

this commit provides a way to inspect the
state of a receiver, to avoid violating the contact of
`Future::poll(..)`, or requiring that a oneshot channel users track this
state themselves externally via mechanisms like
`futures::future::FusedFuture`, or wrapping the receiver in an
`Option<T>`.

NB: this makes a small behavioral change to the implementation of
`<Receiver<T> as Future<Output = Result<T, RecvError>::poll(..)`. this
change is acceptable, per the `Future::poll()` documentation regarding
panics:

> Once a future has completed (returned Ready from poll), calling its
poll method again may panic, block forever, or cause other kinds of
problems; the Future trait places no requirements on the effects of such
a call.

the upside of this is that it means a broken or closed channel, e.g.
when the sender is dropped, will settle as "finished" after it yields an
error.

see:
* tokio-rs#7137 (comment)
* https://doc.rust-lang.org/stable/std/future/trait.Future.html#panics

Signed-off-by: katelyn martin <[email protected]>
cratelyn added a commit to cratelyn/tokio that referenced this pull request Feb 15, 2025
this commit introduces a new method to
`tokio::sync::oneshot::Receiver<T>`.

this method returns true if the channel has no messages waiting to be
received. this is similar to the existing
`tokio::sync::mpsc::Receiver::is_empty()` and
`tokio::sync::broadcast::Receiver::is_empty()` methods.

see:
* https://docs.rs/tokio/latest/tokio/sync/broadcast/struct.Receiver.html#method.is_empty
* https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Receiver.html#method.is_empty
* tokio-rs#7137 (comment)

Signed-off-by: katelyn martin <[email protected]>
cratelyn added a commit to cratelyn/tokio that referenced this pull request Feb 16, 2025
this commit introduces a new method to
`tokio::sync::oneshot::Receiver<T>`.

this method returns true if the channel has no messages waiting to be
received. this is similar to the existing
`tokio::sync::mpsc::Receiver::is_empty()` and
`tokio::sync::broadcast::Receiver::is_empty()` methods.

see:
* https://docs.rs/tokio/latest/tokio/sync/broadcast/struct.Receiver.html#method.is_empty
* https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Receiver.html#method.is_empty
* tokio-rs#7137 (comment)

Signed-off-by: katelyn martin <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate M-sync Module: tokio/sync R-loom-sync Run loom sync tests on this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants