-
Notifications
You must be signed in to change notification settings - Fork 111
[consensus::marshal
] Mailbox functions return error.
#1745
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
base: main
Are you sure you want to change the base?
Conversation
consensus::marshal
] Require genesis block in config. Getters return error.
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.
Yeah, that's nice.
I wonder if withthese new APIs the special cases in the Automaton implementors could go away (for example alto) that always check if a digest if genesis.
Deploying monorepo with
|
Latest commit: |
430a161
|
Status: | ✅ Deploy successful! |
Preview URL: | https://60334dcf.monorepo-eu0.pages.dev |
Branch Preview URL: | https://bc-marshal-getters-3.monorepo-eu0.pages.dev |
07b3713
to
2f9a469
Compare
consensus::marshal
] Require genesis block in config. Getters return error.consensus::marshal
] Mailbox functions return error.
I removed any of the genesis stuff from this PR to keep it scoped Probably will go into #1707 or its own PR |
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.
Suspect this will be a pattern we eventually follow in #1110
) -> oneshot::Receiver<B> { | ||
let (tx, rx) = oneshot::channel(); | ||
if self | ||
// Ignore the result. |
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.
To move subscribe
closer to the other types methods of this PR, instead of a oneshot::Receiver<B>
one could return a Subscription<B>
that exposes some of the same events, i.e. (typed this from memory and haven't run it; so this might not compile):
use std::pin::Pin;
use std::task::Poll;
use futures::{ready, FutureExt as _};
/// A subscription to a block returned by [`Mailbox::subscribe`].
pin_project_lite::pin_project! {
struct Subscription<B> {
inner: oneshot::Receiver<B>,
}
}
impl<B> Subscription<B> {
// recv_blocking or whatever; probably not even necessary
}
impl<B> Future for Subscription<B> {
type Output = Result<B, Closed>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match ready!(this.inner.poll_unpin(cx)) }
Ok(value) -> Poll::Ready(Ok(value)),
Err(_err) -> Poll::Ready(Err(Closed)),
}
}
}
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.
My feeling is that this would add type noise without much benefit. Would prefer the client uses map_err(|_| Closed)
at the call site if they REALLY need the error type to be the same between two different calls to marshal
, though I don't expect this to be needed most of the time.
To me, the other alternative I would consider is Result<oneshot::Receiver<B>, Closed>
in order to propagate the Closed
error on send()
directly, but marshal
should never drop the subscription unless the actor itself shuts down, so it doesn't seem necessary to me to differentiate between Closed
and oneshot::Canceled
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.
Yeah, I think I agree.
I also think that returning the explicit subscription feels unnecessary (just like it was for the other cases). Say somebody really wanted to send the subscription to another thread to be awaited there instead of at the call site, they could just move the mailbox to an async fn:
# Current situation
let subscription = mailbox.subscribe().await;
# do something else
let block = subscription.await;
# await the oneshot inside the `fn subscribe`
let subscription = { let mailbox = mailbox.clone(); async move { mailbox().subscribe().await } };
# do something else
let block = subscription.await;
Now if you wanted to dot get rid of the boilerplate (because moving the subscription is a reaistic use case), a common pattern is:
async fn subscribe_owned(self) -> Block {}
# usage
let subscription = mailbox.clone().subscribe_owned();
# do smth else
let block = subscription.await;
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.
Returning the explicit oneshot::Receiver<B>
seems like a feature to me in this case, avoids holding &mut self
across awaiting for the block to become available. We would expect the caller move the await
elsewhere, for example if they want to put it in a select!
opposite a timeout.
As you mentioned, cloning the mailbox first would work, but it could be easy to overlook for clients (easy to forget). I think it's more ergonomic as-is. If we were to add a more ergonomic version for awaiting the block, I suggest cloning the sender inside of that function, which removes the need for the client to clone the mailbox. For example:
pub async fn wait_for_block(
&self, // avoids borrowing `&mut self`
round: Option<Round>,
commitment: B::Commitment,
) -> Result<B, Closed> {
let (tx, rx) = oneshot::channel();
let mut sender = self.sender.clone();
sender
.send(Message::Subscribe { round, commitment, response: tx })
.await
.map_err(|_| Closed)?;
rx.await.map_err(|_| Closed)
}
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.
That's not quite the same because async fn f(&self) -> T
desugars to fn f(&self) -> impl Future<Output = T> + 'a
, and so you don't get 'static
future, irrespective of the function body. So client.clone().f_owned()
is the go-to for functions for futures that need mut self
and have cheaply clonable clients.
I guess you could do fn f(&self) -> impl Future<Output = T> + 'static
.
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.
Looks like you're suggesting the following?
use core::future::Future;
...
pub fn subscribe_owned(
&self,
round: Option<Round>,
commitment: B::Commitment,
) -> impl Future<Output = Result<B, Closed>> + 'static {
let mut sender = self.sender.clone();
async move {
let (tx, rx) = oneshot::channel();
sender
.send(Message::Subscribe {
round,
commitment,
response: tx,
})
.await
.map_err(|_| Closed)?;
rx.await.map_err(|_| Closed)
}
}
And use it by:
let subscriber = marshal.clone();
let subscription = subscriber.subscribe_owned(...);
...
let block = subscription.await?;
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.
Yeah, that's what I was thinking of. Although it's less nice to look at when looking at docs (I know that I usually shy away from scary looking signatures).
For what it's worth, there are examples in the ecosystem for either introducing async fn f_owned
methods [1] [2], or simply telling people to clone their clients clients to work around concurrent usage of &mut self
methods [3] (just a few that I remember at the top of my head).
1: https://docs.rs/tokio-util/latest/tokio_util/sync/struct.CancellationToken.html#method.cancelled_owned
2: https://docs.rs/tokio/latest/tokio/sync/mpsc/struct.Sender.html#method.try_reserve_owned
3: https://docs.rs/tonic/latest/tonic/client/index.html#concurrent-usage
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #1745 +/- ##
==========================================
- Coverage 92.28% 92.20% -0.09%
==========================================
Files 304 305 +1
Lines 79179 79528 +349
==========================================
+ Hits 73072 73327 +255
- Misses 6107 6201 +94
... and 59 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Ergonomic changes.
consensus::marshal
methods now returnResult
to optionally indicate a processing error (rather than flattening into aNone
)