fix(protocol): a notice from an older generation or a left epoch is ignored (improvise-z2vr)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
committed by
Edward Langley
parent
ec5112cfe8
commit
f47efef5ae
@@ -1337,6 +1337,66 @@ mod random {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The shrunk history of the second livelock: two swaps, a transpose and a limit change, with
|
||||
/// the first swap's notice delivered three steps late and an old reply replayed at the end.
|
||||
#[test]
|
||||
fn a_late_notice_from_an_older_generation_cannot_rewind_the_client() {
|
||||
let window = Window {
|
||||
top: 0,
|
||||
left: 0,
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
};
|
||||
let mut host = host_over(workbook(), 1);
|
||||
let id = connect(&mut host);
|
||||
let mut client = EdgeClient::new(session());
|
||||
client.track(shape(), window);
|
||||
client.sync(&mut Flaky::new(&mut host), &shape(), 6);
|
||||
host.take_notices();
|
||||
let mut held_back: Vec<(usize, Notice)> = Vec::new();
|
||||
let mut old: Vec<Reply> = Vec::new();
|
||||
let plan = [
|
||||
(Some(0), Arrival::Late(3), vec![Some(0)], None),
|
||||
(Some(0), Arrival::Dropped, vec![], None),
|
||||
(Some(1), Arrival::Now, vec![], None),
|
||||
(None, Arrival::Dropped, vec![], Some(6)),
|
||||
];
|
||||
for (step, (op, arrival, sync, replay)) in plan.into_iter().enumerate() {
|
||||
match op {
|
||||
Some(0) => {
|
||||
send(
|
||||
&mut host,
|
||||
id,
|
||||
vec![ModelCommand::LoadModel("other.improv".into())],
|
||||
);
|
||||
}
|
||||
Some(_) => {
|
||||
send(&mut host, id, vec![transpose()]);
|
||||
}
|
||||
None => host.limit_delivery(2, 1),
|
||||
}
|
||||
let w = Weather {
|
||||
arrival,
|
||||
lose_connection: true,
|
||||
sync: sync.clone(),
|
||||
replay,
|
||||
};
|
||||
let fresh = host.take_notices();
|
||||
arrive(&mut client, &mut held_back, step, &w, fresh);
|
||||
client.connection_lost();
|
||||
if let Some(Reply::Correction(c)) = replay.and_then(|i| old.get(i % old.len().max(1))) {
|
||||
client.apply(c);
|
||||
}
|
||||
let script = sync.iter().map(|f| f.map(fault)).collect();
|
||||
let mut edge = Flaky::scripted(&mut host, script);
|
||||
client.sync(&mut edge, &shape(), 6);
|
||||
old.append(&mut edge.replies);
|
||||
}
|
||||
let (state, _) = client.poll(&mut Flaky::new(&mut host), &shape());
|
||||
assert_eq!(state, Synced::Settled, "an old notice must not queue work");
|
||||
assert_eq!(client.held(&shape()), Some(&drawn(&host, &window)));
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn a_corrected_state_is_the_target_and_correcting_it_again_is_the_zero_zset(
|
||||
|
||||
@@ -162,16 +162,26 @@ impl EdgeClient {
|
||||
|
||||
/// A pushed notice. Duplicates are ignored, a gap or a new epoch makes every shape ask.
|
||||
pub fn on_notice(&mut self, notice: &Notice) -> Vec<Event> {
|
||||
self.accept(notice, None)
|
||||
self.accept(notice, None, false)
|
||||
}
|
||||
|
||||
/// `only` limits the notice to one shape: a pulled notice carries no other shape's keys.
|
||||
fn accept(&mut self, notice: &Notice, only: Option<&ShapeKey>) -> Vec<Event> {
|
||||
fn accept(&mut self, notice: &Notice, only: Option<&ShapeKey>, answer: bool) -> Vec<Event> {
|
||||
let end = notice.stamp();
|
||||
let (mut read, mut ignored) = (0, 0);
|
||||
for (shape, t) in &mut self.shapes {
|
||||
if only.is_some_and(|o| o != shape) {
|
||||
continue;
|
||||
}
|
||||
read += 1;
|
||||
let older = |s: Option<Stamp>| {
|
||||
s.is_some_and(|s| s.host == end.host && end.generation < s.generation)
|
||||
};
|
||||
let left = !answer && t.left.contains(&(end.host, end.generation));
|
||||
if older(t.watermark) || older(t.seen) || left {
|
||||
ignored += 1;
|
||||
continue;
|
||||
}
|
||||
let contiguous = match t.seen {
|
||||
Some(seen) if seen.same_epoch(&end) => match notice.range.to <= seen.publication {
|
||||
true => continue,
|
||||
@@ -197,7 +207,10 @@ impl EdgeClient {
|
||||
t.watermark = Some(end);
|
||||
}
|
||||
}
|
||||
self.events_of(notice)
|
||||
match read > 0 && ignored == read {
|
||||
true => Vec::new(),
|
||||
false => self.events_of(notice),
|
||||
}
|
||||
}
|
||||
|
||||
/// The connection dropped: every shape asks for a correction from its watermark.
|
||||
@@ -365,7 +378,7 @@ impl EdgeClient {
|
||||
Ok(Reply::Correction(c)) => events.extend(self.apply_answer(&c).1),
|
||||
Ok(Reply::Notices(list)) => {
|
||||
list.iter()
|
||||
.for_each(|n| events.extend(self.accept(n, Some(shape))));
|
||||
.for_each(|n| events.extend(self.accept(n, Some(shape), true)));
|
||||
}
|
||||
Err(EdgeError::Failed) => {
|
||||
self.request_failed(shape);
|
||||
@@ -401,7 +414,7 @@ impl EdgeClient {
|
||||
match edge.call(request) {
|
||||
Ok(Reply::Notices(list)) => list
|
||||
.iter()
|
||||
.for_each(|n| events.extend(self.accept(n, Some(shape)))),
|
||||
.for_each(|n| events.extend(self.accept(n, Some(shape), true))),
|
||||
Ok(Reply::Correction(c)) => events.extend(self.apply_answer(&c).1),
|
||||
Err(EdgeError::Failed) => {
|
||||
self.request_failed(shape);
|
||||
|
||||
Reference in New Issue
Block a user