[runtime] Create a trait for briding async and parallel code#3636
[runtime] Create a trait for briding async and parallel code#3636cronokirby wants to merge 2 commits into
Conversation
Deploying monorepo with
|
| Latest commit: |
1a04122
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://8fcdea30.monorepo-eu0.pages.dev |
| Branch Preview URL: | https://ck-strategist.monorepo-eu0.pages.dev |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
commonware-mcp | 1a04122 | Apr 22 2026, 09:16 PM |
|
We could also get rid of the |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dba50ae. Configure here.
| .await; | ||
| } | ||
|
|
||
| work.insert(updated_view, round); |
There was a problem hiding this comment.
Round removed from work but leader_nullified still needs it
Medium Severity
When updated_view equals current.view, the round is removed from work at line 666. The forward_proposal check at line 648 runs before the remove and works fine. However, if a subsequent iteration's vote arm (line 632) fires for the same view while a different updated_view is being processed in on_end, Self::leader_nullified(¤t, &work) will not find the current view's round (since it was removed but not yet reinserted). In the old code, rounds were borrowed in-place via get_mut and never removed from work, so leader_nullified always saw the current round. The remove-process-reinsert pattern can cause a missed leader-nullify detection if the timing aligns.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit dba50ae. Configure here.
| timer.cancel(); | ||
| } | ||
| result | ||
| }; |
There was a problem hiding this comment.
Redundant with_strategy calls for likely-no-op certificate construction
Low Severity
Three sequential with_strategy calls (construct_notarization, construct_nullification, construct_finalization) are made unconditionally for every updated view. Each with_strategy call may involve channel communication or thread pool dispatch depending on the runtime. In practice, at most one of these will return Some (and they each have fast-path early returns checking has_*() and quorum counts), but the overhead of three round-trips through the strategy bridge is incurred regardless. Combining these three into a single with_strategy call that attempts all three constructions at once would reduce the overhead.
Reviewed by Cursor Bugbot for commit dba50ae. Configure here.
| if filtered.len() >= quorum as usize { | ||
| if let Some(certificate) = Certificate::from_acks(&*scheme, filtered, &self.strategy) { | ||
| let certificate = self | ||
| .context |
There was a problem hiding this comment.
I think we should keep strategy less tightly bound to context? We could accept strategy with it?
dba50ae to
c68f587
Compare
When calling into code doing heavy computation using a Strategy, you want to avoid blocking the tokio thread on waiting for the computation. Instead, you want the waiting to be async, so that other tasks can use the thread while you wait for the computation to finish in the rayon thread pool. To facilitate this pattern, a new capability, Strategist, is added to to the runtime Context. This takes in a synchronous closure, which needs a strategy to do parallel computation. Runtimes implement this correctly, by using, e.g. a oneshot channel to asynchronously wait on the rayon result, or using the tokio blocking pool for a sequential strategy. To use this change throughout the code base, places where a Strategy was stored have been replaced with using the context instead.
c68f587 to
1a04122
Compare
| RuntimeStrategy::Sequential(strategy) => { | ||
| let strategy = RuntimeStrategy::Sequential(strategy); | ||
| let handle = self | ||
| .clone() | ||
| .shared(true) | ||
| .spawn(move |_| async move { f(&strategy) }); | ||
| Either::Left(async move { handle.await.expect("strategy task failed") }) | ||
| } | ||
| RuntimeStrategy::Rayon(strategy) => { | ||
| let (sender, receiver) = oneshot::channel(); | ||
| let pool = strategy.thread_pool().clone(); | ||
| let strategy = RuntimeStrategy::Rayon(strategy); | ||
| pool.spawn(move || { | ||
| let _ = sender.send(f(&strategy)); | ||
| }); | ||
| Either::Right(async move { receiver.await.expect("strategy task failed") }) | ||
| } |
There was a problem hiding this comment.
| RuntimeStrategy::Sequential(strategy) => { | |
| let strategy = RuntimeStrategy::Sequential(strategy); | |
| let handle = self | |
| .clone() | |
| .shared(true) | |
| .spawn(move |_| async move { f(&strategy) }); | |
| Either::Left(async move { handle.await.expect("strategy task failed") }) | |
| } | |
| RuntimeStrategy::Rayon(strategy) => { | |
| let (sender, receiver) = oneshot::channel(); | |
| let pool = strategy.thread_pool().clone(); | |
| let strategy = RuntimeStrategy::Rayon(strategy); | |
| pool.spawn(move || { | |
| let _ = sender.send(f(&strategy)); | |
| }); | |
| Either::Right(async move { receiver.await.expect("strategy task failed") }) | |
| } | |
| RuntimeStrategy::Sequential(_) => { | |
| let handle = self | |
| .clone() | |
| .shared(true) | |
| .spawn(move |_| async move { f(&strategy) }); | |
| Either::Left(async move { handle.await.expect("strategy task failed") }) | |
| } | |
| RuntimeStrategy::Rayon(ref rayon) => { | |
| let (sender, receiver) = oneshot::channel(); | |
| let pool = rayon.thread_pool().clone(); | |
| pool.spawn(move || { | |
| let _ = sender.send(f(&strategy)); | |
| }); | |
| Either::Right(async move { receiver.await.expect("strategy task failed") }) | |
| } |
I was looking at this block for a while trying to internalize exactly what's happening here, and in playing around with it I thought this made it easier to read. Feel free to ignore if this is not as idiomatic though.
(I did not know the ref part would be needed / had never seen the "use of partially moved value" compiler error that occurs with out.)
| let filtered = acks | ||
| .values() | ||
| .filter(|a| a.item.digest == ack.item.digest) | ||
| .cloned() |
There was a problem hiding this comment.
wonder if there is a way to avoid this clone with some more careful lifetime management?


When calling into code doing heavy computation using a Strategy, you want to avoid blocking the tokio thread on waiting for the computation. Instead, you want the waiting to be async, so that other tasks can use the thread while you wait for the computation to finish in the rayon thread pool.
To facilitate this pattern, a new capability, Strategist, is added to to the runtime Context. This takes in a synchronous closure, which needs a strategy to do parallel computation. Runtimes implement this correctly, by using, e.g. a oneshot channel to asynchronously wait on the rayon result, or using the tokio blocking pool for a sequential strategy.
To use this change throughout the code base, places where a Strategy was stored have been replaced with using the context instead.