Background
The orchestrator's run loop currently polls get_payload_and_round() in a tight loop (with a 2s sleep) after executing a round, waiting for on-chain state to advance before starting the next round. This is a consequence of Creator only exposing a snapshot API — the orchestrator has no way to block until the round actually changes.
Problem
The polling approach has a few downsides:
- It requires the orchestrator to maintain a
HashSet<u64> of executed rounds to avoid re-broadcasting Start for the same round
- The 2s poll interval is an arbitrary constant — too short wastes calls to the provider, too long adds latency to the start of the next round
- The orchestrator is doing work that semantically belongs to the
Creator
Proposed Change
Add a wait_for_new_round(current: u64) -> Result<(Vec<u8>, u64)> method to the Creator trait that blocks until the round advances past current:
pub trait Creator {
type TaskData;
async fn get_payload_and_round(&self) -> Result<(Vec<u8>, u64)>;
async fn wait_for_new_round(&self, current: u64) -> Result<(Vec<u8>, u64)>;
fn get_task_metadata(&self) -> Self::TaskData;
}
The orchestrator's post-execution path then becomes:
// After successful execution:
executed_round = current_round;
// inner loop drains until continue_time as normal, then:
let (payload, current_round) = self.task_creator.wait_for_new_round(executed_round).await?;
// guaranteed fresh round, no HashSet needed
CounterCreator (and other implementations) can implement this by polling the provider internally with appropriate backoff, keeping that logic out of the orchestrator.
Benefits
- Eliminates
executed_rounds: HashSet<u64> from the orchestrator
- Eliminates the 2s polling constant
- Correct by construction — the orchestrator cannot re-execute the same round because it blocks until a new one is available
- Provider polling strategy (interval, backoff) lives in the
Creator implementation where it has context about the underlying data source
Related
Introduced as a follow-up to the fix in #[PR for bagelface/fix-retrigger-after-threshold-bug].
Background
The orchestrator's run loop currently polls
get_payload_and_round()in a tight loop (with a 2s sleep) after executing a round, waiting for on-chain state to advance before starting the next round. This is a consequence ofCreatoronly exposing a snapshot API — the orchestrator has no way to block until the round actually changes.Problem
The polling approach has a few downsides:
HashSet<u64>of executed rounds to avoid re-broadcastingStartfor the same roundCreatorProposed Change
Add a
wait_for_new_round(current: u64) -> Result<(Vec<u8>, u64)>method to theCreatortrait that blocks until the round advances pastcurrent:The orchestrator's post-execution path then becomes:
CounterCreator(and other implementations) can implement this by polling the provider internally with appropriate backoff, keeping that logic out of the orchestrator.Benefits
executed_rounds: HashSet<u64>from the orchestratorCreatorimplementation where it has context about the underlying data sourceRelated
Introduced as a follow-up to the fix in #[PR for bagelface/fix-retrigger-after-threshold-bug].