Skip to main content

rig_ecs/systems/
backoff.rs

1//! Provider-retry delays driven by the world's Bevy clock. Pausing virtual time
2//! holds retries until the host advances it.
3//!
4//! ```
5//! use std::time::Duration;
6//! use rig_ecs::{agent::Backoff, systems::backoff::delay};
7//! let backoff = Backoff { base: Duration::from_secs(1), max: Duration::from_secs(8) };
8//! assert_eq!(delay(&backoff, 2), Duration::from_secs(2));
9//! ```
10
11use std::time::Duration;
12
13use bevy_ecs::prelude::*;
14use bevy_reflect::Reflect;
15use bevy_time::DelayedCommandsExt;
16use rig_core::observe::Emitter;
17use serde::{Deserialize, Serialize};
18
19use crate::{
20    agent::{Backoff, RunOf},
21    bus::{PendingEffect, acquire_hold, release_hold},
22};
23
24/// The owner name of a backoff's hold.
25pub const BACKOFF_OWNER: &str = "rig-ecs/backoff";
26
27/// The turn re-issues a completion a provider retry lost: which attempt
28/// (1 for the first retry). Stamped by `advance` on the retry's turn.
29#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
30#[reflect(Component)]
31pub struct RetryAttempt(pub usize);
32
33/// The delay before attempt `attempt` (1-based): `base × 2^(attempt-1)`,
34/// capped at `max`.
35pub fn delay(backoff: &Backoff, attempt: usize) -> Duration {
36    let factor = 2u32.saturating_pow(attempt.saturating_sub(1).min(31) as u32);
37    backoff.base.saturating_mul(factor).min(backoff.max)
38}
39
40/// After `RigSet::Patch`, before `RigSet::Release`: the completion effect of a retry turn under a
41/// [`Backoff`] (the run's, else the agent's) is held as `rig-ecs/backoff`
42/// the pass it is folded, and released by a delayed command when the delay
43/// has elapsed on the world's clock.
44pub fn hold_retries(
45    mut commands: Commands,
46    effects: Query<(Entity, &ChildOf), Added<PendingEffect>>,
47    turns: Query<(&ChildOf, &RetryAttempt)>,
48    runs: Query<&RunOf>,
49    backoffs: Query<&Backoff>,
50) {
51    for (effect, turn_of) in &effects {
52        let Ok((run_of, RetryAttempt(attempt))) = turns.get(turn_of.parent()) else {
53            continue;
54        };
55        let run = run_of.parent();
56        let Ok(RunOf(agent)) = runs.get(run) else {
57            continue;
58        };
59        let Some(backoff) = backoffs.get(run).ok().or_else(|| backoffs.get(*agent).ok()) else {
60            continue;
61        };
62        let wait = delay(backoff, *attempt);
63        commands.queue(move |world: &mut World| {
64            // A removed or issued effect no longer needs this retry barrier.
65            let _ = acquire_hold(world, effect, Emitter::named(BACKOFF_OWNER));
66        });
67        commands
68            .delayed()
69            .duration(wait)
70            .queue(move |world: &mut World| {
71                let _ = release_hold(world, effect, BACKOFF_OWNER);
72            });
73    }
74}