checkpoint

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-05-21 16:39:38 -04:00
parent b5a7627b43
commit a664892fe6
4 changed files with 514 additions and 41 deletions

397
run.json

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,147 @@
diff --git a/lib/crates/fabro-core/src/executor.rs b/lib/crates/fabro-core/src/executor.rs
index 454636e92..19cf142a1 100644
--- a/lib/crates/fabro-core/src/executor.rs
+++ b/lib/crates/fabro-core/src/executor.rs
@@ -297,9 +297,9 @@ impl<G: Graph + 'static> Executor<G> {
graph: &G,
) -> Result<NodeResult<G::Meta>> {
let policy = self.handler.retry_policy(node, graph);
- let start = Instant::now();
for attempt in 1..=policy.max_attempts {
+ let attempt_start = Instant::now();
let attempt_ctx = AttemptContext {
node,
attempt,
@@ -318,7 +318,7 @@ impl<G: Graph + 'static> Executor<G> {
let delay = policy.backoff.delay_for_attempt(attempt);
let result = node_result_from_outcome(
outcome,
- start.elapsed(),
+ attempt_start.elapsed(),
attempt,
policy.max_attempts,
);
@@ -336,7 +336,7 @@ impl<G: Graph + 'static> Executor<G> {
let final_outcome = self.handler.on_retries_exhausted(node, outcome);
let result = node_result_from_outcome(
final_outcome,
- start.elapsed(),
+ attempt_start.elapsed(),
attempt,
policy.max_attempts,
);
@@ -353,7 +353,7 @@ impl<G: Graph + 'static> Executor<G> {
Ok(outcome) => {
let result = node_result_from_outcome(
outcome,
- start.elapsed(),
+ attempt_start.elapsed(),
attempt,
policy.max_attempts,
);
@@ -369,8 +369,12 @@ impl<G: Graph + 'static> Executor<G> {
}
Err(e) if can_retry && e.is_retryable() => {
let delay = policy.backoff.delay_for_attempt(attempt);
- let fail_result =
- NodeResult::from_error(&e, start.elapsed(), attempt, policy.max_attempts);
+ let fail_result = NodeResult::from_error(
+ &e,
+ attempt_start.elapsed(),
+ attempt,
+ policy.max_attempts,
+ );
let ctx = AttemptResultContext {
node,
result: &fail_result,
@@ -386,7 +390,7 @@ impl<G: Graph + 'static> Executor<G> {
let outcome = e.to_fail_outcome();
let result = node_result_from_outcome(
outcome,
- start.elapsed(),
+ attempt_start.elapsed(),
attempt,
policy.max_attempts,
);
@@ -1329,6 +1333,80 @@ mod tests {
assert_eq!(log, vec![(1, true), (2, false)]);
}
+ #[tokio::test]
+ async fn executor_retry_attempt_wall_time_excludes_prior_attempts_and_backoff() {
+ let wall_times = Arc::new(Mutex::new(Vec::<Duration>::new()));
+
+ struct WallTimeTracker(Arc<Mutex<Vec<Duration>>>);
+ #[async_trait]
+ impl RunLifecycle<TestGraph> for WallTimeTracker {
+ async fn after_attempt(
+ &self,
+ ctx: &AttemptResultContext<'_, TestGraph>,
+ _s: &ExecutionState,
+ ) -> Result<()> {
+ self.0.lock().unwrap().push(ctx.result.wall_time);
+ Ok(())
+ }
+ }
+
+ struct SlowRetryThenSuccess(AtomicU32);
+ #[async_trait]
+ impl NodeHandler<TestGraph> for SlowRetryThenSuccess {
+ async fn execute(
+ &self,
+ _n: &TestNode,
+ _c: &Context,
+ _g: &TestGraph,
+ ) -> Result<Outcome> {
+ sleep(Duration::from_millis(5)).await;
+ let call = self.0.fetch_add(1, Ordering::Relaxed);
+ if call == 0 {
+ Ok(Outcome {
+ status: StageOutcome::Failed {
+ retry_requested: true,
+ },
+ ..Outcome::default()
+ })
+ } else {
+ Ok(Outcome::success())
+ }
+ }
+
+ fn retry_policy(&self, _n: &TestNode, _g: &TestGraph) -> RetryPolicy {
+ RetryPolicy {
+ max_attempts: 2,
+ backoff: BackoffPolicy {
+ initial_delay: Duration::from_millis(500),
+ factor: 1.0,
+ max_delay: Duration::from_millis(500),
+ jitter: false,
+ },
+ }
+ }
+ }
+
+ let g = linear_graph(&["start", "end"]);
+ let state = ExecutionState::new(&g).unwrap();
+ let executor =
+ ExecutorBuilder::new(Arc::new(SlowRetryThenSuccess(AtomicU32::new(0)))
+ as Arc<dyn NodeHandler<TestGraph>>)
+ .lifecycle(Box::new(WallTimeTracker(Arc::clone(&wall_times))))
+ .build();
+
+ executor.run(&g, state).await.unwrap();
+
+ let wall_times = wall_times.lock().unwrap().clone();
+ assert_eq!(wall_times.len(), 2);
+ for wall_time in wall_times {
+ assert!(wall_time >= Duration::from_millis(5));
+ assert!(
+ wall_time < Duration::from_millis(300),
+ "attempt wall time should not include retry backoff or prior attempts: {wall_time:?}"
+ );
+ }
+ }
+
#[tokio::test]
async fn executor_retry_lifecycle_before_attempt_skip_stops_retry() {
let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));

View file

@ -0,0 +1,6 @@
{
"outcome": "succeeded",
"notes": "Stage completed: simplify_gpt",
"failure_reason": null,
"timestamp": "2026-05-21T20:36:21.174781Z"
}

View file

@ -0,0 +1,5 @@
{
"script": "cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1",
"command": "exec 2>&1\ncargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1",
"language": "shell"
}