From e026d77a021f546b0dc51dd63524e7335f12dc74 Mon Sep 17 00:00:00 2001 From: "Claude (zset-rollback)" Date: Sat, 10 Oct 2026 18:14:59 -0700 Subject: [PATCH] fix(zset): count the rollbacks that undid a step so a Rebuild cannot outlive its chunks (improvise-bw4p.5) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze --- crates/zset/src/circuit.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/zset/src/circuit.rs b/crates/zset/src/circuit.rs index 12f6e2f3..05f50b6c 100644 --- a/crates/zset/src/circuit.rs +++ b/crates/zset/src/circuit.rs @@ -407,6 +407,11 @@ pub struct Circuit { token: Rc<()>, /// `Some(ticks)` as of `begin`, while a transaction is open. open: Option, + /// A step or piece ran since `begin`. + stepped: bool, + /// Rollbacks that undid a step or piece. It never goes down, unlike `ticks`, so a + /// `Rebuild` can tell that a rollback took some of its chunks. + undone: u64, } type Z = ZSet; @@ -441,6 +446,7 @@ impl Circuit { "zset: a transaction is already open on this circuit" ); self.open = Some(self.ticks); + self.stepped = false; for n in &mut self.nodes { n.begin(); } @@ -468,9 +474,12 @@ impl Circuit { n.rollback(); } self.ticks = ticks; + self.undone += u64::from(self.stepped); + self.stepped = false; } fn run(&mut self, cont: bool) { + self.stepped |= self.open.is_some(); for n in &mut self.nodes { n.eval(cont); } @@ -735,6 +744,8 @@ pub struct Rebuild { chunk: usize, /// The circuit's tick count right after this job's first chunk, once it has run one. began_at: Option, + /// The circuit's `undone` count when this job first ran, even if that chunk panicked. + undone_at: Option, } impl Rebuild { @@ -745,6 +756,7 @@ impl Rebuild { rest: snapshot.into_iter(), chunk: chunk.max(1), began_at: None, + undone_at: None, } } @@ -753,10 +765,11 @@ impl Rebuild { Rc::ptr_eq(&self.input.1, &circuit.token) } - /// Whether a `Circuit::step` split this job's tick. A finished job is never interrupted; - /// an interrupted one never finishes: start another from the snapshot. + /// Whether a `Circuit::step` split this job's tick, or a rollback undid a step after it + /// first ran (finished or not). An interrupted job never finishes: start another. pub fn interrupted(&self, circuit: &Circuit) -> bool { - self.rest.len() > 0 && self.began_at.is_some_and(|t| t != circuit.ticks()) + let split = self.rest.len() > 0 && self.began_at.is_some_and(|t| t != circuit.ticks()); + split || self.undone_at.is_some_and(|u| u != circuit.undone) } /// Runs chunks until done or `expired()`, at least one per call. A predicate, not an @@ -766,9 +779,10 @@ impl Rebuild { self.belongs_to(circuit), "zset: the rebuild's input belongs to another circuit" ); + self.undone_at.get_or_insert(circuit.undone); assert!( !self.interrupted(circuit), - "zset: the rebuild was interrupted by a Circuit::step before it finished" + "zset: the rebuild was interrupted by a Circuit::step or a rollback before it finished" ); loop { let chunk: ZSet = self.rest.by_ref().take(self.chunk).collect();