fix(graph): build peer retirement guards before spawning their tasks

A task aborted before its first poll never runs its body, so a guard
constructed inside the future did not exist yet and the pre-provisioned
teammate identity stayed messageable. Built outside and moved in, the
guard drops with the unpolled future on both the frontier and map items.

Also rewords the two timeout: 0 validator warnings: the graph-level one
no longer claims max_loop_iterations always applies (it may be 0 too),
and the node-level one names the enclosing bounds that still end the
node instead of saying it runs until it returns.
This commit is contained in:
Dark-Alex-17 committed 2026-09-11 10:56:28 -06:00
1 parent 02e2227aa0
commit 24b5504e21
3 files changed
+82 -32

No files matched your search

+56 -13
View File
@@ -247,20 +247,21 @@ impl GraphExecutor {
#[cfg(test)]
let observer = frontier_observer.clone();
// Retires the teammate identity however this task ends:
// dropped explicitly right after step() on the normal path,
// by unwinding on the abort return, task abort, or panic.
// Built before the spawn so a task aborted before its first
// poll still drops it with the future.
let retire = registry_for_task
.zip(peer_id)
.map(|(registry, id)| PeerRetireGuard {
registry,
id,
#[cfg(test)]
observer,
});
let task = tokio::spawn(async move {
// Retires the teammate identity however this task ends:
// dropped explicitly right after step() on the normal
// path, by unwinding on the abort return, task abort, or
// panic.
let retire =
registry_for_task
.zip(peer_id)
.map(|(registry, id)| PeerRetireGuard {
registry,
id,
#[cfg(test)]
observer,
});
let retire = retire;
let _permit = sem_clone
.acquire()
.await
@@ -1722,6 +1723,48 @@ nodes:
assert!(!registry.is_finished(&id_b), "the sibling is untouched");
}
/// A task aborted before its first poll never runs its body, so a guard
/// built inside the future would never exist. Built outside and moved in,
/// it drops with the unpolled future and still retires the identity.
#[tokio::test]
async fn peer_retire_guard_fires_when_task_is_aborted_before_first_poll() {
let (registry, assignments) = provision_frontier_peers(vec![
("a".to_string(), "worker".to_string()),
("b".to_string(), "worker".to_string()),
]);
let registry = registry.expect("two flagged nodes should get a registry");
let id_a = assignments["a"].0.clone();
let id_b = assignments["b"].0.clone();
let outside = PeerRetireGuard {
registry: Arc::clone(&registry),
id: id_a.clone(),
observer: None,
};
let inside_registry = Arc::clone(&registry);
let inside_id = id_b.clone();
let task = tokio::spawn(async move {
let _outside = outside;
let _inside = PeerRetireGuard {
registry: inside_registry,
id: inside_id,
observer: None,
};
std::future::pending::<()>().await;
});
// No await between spawn and abort: on the current-thread test
// runtime the task has not been polled yet.
task.abort();
let err = task.await.expect_err("the task was aborted");
assert!(err.is_cancelled(), "{err}");
assert!(registry.is_finished(&id_a), "the moved-in guard retires");
assert!(
!registry.is_finished(&id_b),
"a guard built inside never existed"
);
}
/// A session already aborted when the run starts trips the synchronous
/// pre-check: no bridge task, no super-step, the start script never runs.
#[tokio::test]