I don't understand this lifetime + async quirk

I have a type that was not Send because:

    = help: the trait `Sync` is not implemented for `(dyn IPeerConnections<EpochMessage> + std::marker::Send + 'static)`
note: future is not `Send` as this value is used across an await
   --> /home/dpc/lab/fedimint.git/main/fedimint-server/src/lib.rs:318:25
    |
295 |           for epoch_num in self.next_epoch_to_process()..=last_outcome.epoch {
    |           -                ---- has type `&FedimintServer` which is not `Send`
    |  _________|
    | |
296 | |             let (items, epoch, prev_epoch_hash, rejected_txs, at_know_trusted_checkpoint) =
297 | |                 if epoch_num == last_outcome.epoch {
298 | |                     (
...   |
318 | |                         .await
    | |                         ^^^^^^ await occurs here, with `self` maybe used later
...   |
351 | |             }
352 | |         }
    | |_________- `self` is later dropped her

where this await is an a function:

    pub async fn process_outcome(
        &mut self,
        last_outcome: HbbftConsensusOutcome,
    ) -> Result<(), EpochVerifyError> {

which is holding a &mut, so should be exclusive access and ... Send.

That self.next_epoch_to_process() is taking &self, but returning u64, so should not be holding reference to &self after that call is done.

I finally figured out that the following fixes it:

-        for epoch_num in self.next_epoch_to_process()..=last_outcome.epoch {
+        let next_epoch_to_process = self.next_epoch_to_process();
+        for epoch_num in next_epoch_to_process..=last_outcome.epoch {

but I don't understand why. What borrowck feature/quirk am I missing?

In case the you'd like to see the whole code: permalink

This is probably about the scope of temporary variables. The temporary variables in the expression evaluating the collection for a for loop will only be dropped at the end of the surrounding statement (i. e. in this case, after the loop) and there's also no smart detection in the compiler (yet?) that could figure out that is can actually be made to live for shorter, to avoid holding the reference over an await.

Edit on second though, I'm not 100% sure why there would be a temporary holding the reference in the first place. I don't feel like maybe there ought not to be a temporary at all.

Edit2 well, there technically always exist temporaries for all function arguments of a function, so that they get dropped if a later argument panics. If that's the origin of the problem here that seems like something that should definitely be improved upon (in the analysis of what could possible be held over an await point) since those temporaries are always moved out of before the call happens, which is way before the await could ever be reached. That being said, AFAIR, the same detection has a hard time in general reasoning about the fact that moved-out-of variables aren't "really" ever being held over an await point.

1 Like

I have no idea about it. Just to raise a simplified version I can see. And it unfortunately compiles. playground

Your &FedimintServer is not Send and hence it cannot be used across await points (think about awaiting in one iteration) and then coming again to for ...

This might help as you don't have to do self.next_epoch_to_process() in the for loop.

let next_epoch = self.next_epoch_to_proccess();

// We are done using `self` here and is no longer used in the for loop.

for epoch_num in next_epoch..=last_outcome { 
...
}

Isn't S simply Send + Sync in this "simplified" code?

This is slightly incorrect. I'll try to make it like the real code. Thanks!

OK, so I was confused in one important way. It's not that this sturct is not Send. It's some future created by some other code that touches it that is not Send, and that error is pointing to the reason.

But I still can't see how moving this expression out is changing it. I thought it's something obviously, but now I'll try to make a repro case.

Modified to make it work: Rust Playground

2 Likes

I think:

2 Likes

Yes, OP's error msg looks very close to what the comment raised in that issue.
An example from that issue: Rust Playground

Sorry, I misread part of your question - It seems the solution above, you have already implemented.

The problem you mention, may be one of that of a design issue highlighted by the borrow checker (too much of shared state). See if it's possible to break the structure into two parts - a) Part that is Send and will be used across await points and part b) that is not Send and can be used only on one side of the await point.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.