4303af5a1f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
188 lines
4.9 KiB
Rust
188 lines
4.9 KiB
Rust
//! A panicking `Clone` anywhere in a transactional step is rolled back, and `MemTrace::apply`
|
|
//! survives one. Evidence: `context/test-evidence/zset.md`.
|
|
|
|
use std::cell::Cell;
|
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
use zset::{Circuit, Input, MemTrace, Output, Stream, Trace, ZSet};
|
|
|
|
type Z<K> = ZSet<K>;
|
|
|
|
fn quiet() {
|
|
static O: std::sync::Once = std::sync::Once::new();
|
|
O.call_once(|| {
|
|
let prev = std::panic::take_hook();
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
let s = format!("{info}");
|
|
if !(s.contains("tripped") || s.contains("overflow")) {
|
|
prev(info);
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
|
|
thread_local! { static CLONE_TRIP: Cell<Option<u32>> = const { Cell::new(None) }; }
|
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
struct Boom(u8);
|
|
|
|
impl Clone for Boom {
|
|
fn clone(&self) -> Self {
|
|
CLONE_TRIP.with(|t| {
|
|
if let Some(n) = t.get() {
|
|
if n == 0 {
|
|
t.set(None);
|
|
panic!("clone tripped");
|
|
}
|
|
t.set(Some(n - 1));
|
|
}
|
|
});
|
|
Boom(self.0)
|
|
}
|
|
}
|
|
|
|
type B = (Boom, Boom);
|
|
|
|
struct Rig {
|
|
c: Circuit,
|
|
a: Input<B>,
|
|
b: Input<B>,
|
|
streams: Vec<Stream<Z<B>>>,
|
|
agg: Stream<Z<(Boom, u8)>>,
|
|
sinks: Vec<Output<B>>,
|
|
}
|
|
|
|
fn rig(which: u8) -> Rig {
|
|
let mut c = Circuit::new();
|
|
let (a, sa) = c.input::<B>();
|
|
let (b, sb) = c.input::<B>();
|
|
let mut streams = vec![sa.clone(), sb.clone()];
|
|
if which & 1 != 0 {
|
|
streams.push(c.integrate(&sa));
|
|
}
|
|
if which & 2 != 0 {
|
|
streams.push(c.distinct(&sa));
|
|
}
|
|
if which & 4 != 0 {
|
|
streams.push(c.join_indexed(&sa, &sb, |k, x, y| (Boom(k.0), Boom(x.0 ^ y.0))));
|
|
}
|
|
if which & 8 != 0 {
|
|
streams.push(c.delay(&sa));
|
|
}
|
|
let agg = c.aggregate(&sa, |_, vals: &[(Boom, i64)]| Some(vals.len() as u8));
|
|
let sinks = streams.iter().map(|s| c.output(s)).collect();
|
|
Rig {
|
|
c,
|
|
a,
|
|
b,
|
|
streams,
|
|
agg,
|
|
sinks,
|
|
}
|
|
}
|
|
|
|
impl Rig {
|
|
fn view(&self) -> String {
|
|
format!(
|
|
"{:?} {:?}",
|
|
self.streams.iter().map(|s| s.current()).collect::<Vec<_>>(),
|
|
self.agg.current()
|
|
)
|
|
}
|
|
fn drain(&self) -> String {
|
|
format!(
|
|
"{:?}",
|
|
self.sinks.iter().map(|s| s.take()).collect::<Vec<_>>()
|
|
)
|
|
}
|
|
}
|
|
|
|
fn bz(v: &[(u8, u8, i64)]) -> Z<B> {
|
|
v.iter()
|
|
.map(|(k, x, w)| ((Boom(*k), Boom(*x)), *w))
|
|
.collect()
|
|
}
|
|
|
|
fn clone_panic_sweep(which: u8) -> Vec<String> {
|
|
quiet();
|
|
let mut failures = vec![];
|
|
for n in 0..400u32 {
|
|
let (mut x, mut y) = (rig(which), rig(which));
|
|
for r in [&x, &y] {
|
|
r.a.push(bz(&[(1, 1, 1), (2, 2, 1)]));
|
|
r.b.push(bz(&[(1, 3, 1), (3, 3, 1)]));
|
|
}
|
|
x.c.step();
|
|
y.c.step();
|
|
x.drain();
|
|
let (v0, d0) = (x.view(), x.drain());
|
|
y.drain();
|
|
x.c.begin();
|
|
x.a.push(bz(&[(1, 5, 1), (4, 4, 1), (2, 2, -1)]));
|
|
x.b.push(bz(&[(4, 6, 1), (1, 3, -1)]));
|
|
CLONE_TRIP.with(|t| t.set(Some(n)));
|
|
let r = catch_unwind(AssertUnwindSafe(|| x.c.step()));
|
|
let fired = CLONE_TRIP.with(|t| t.get()).is_none();
|
|
CLONE_TRIP.with(|t| t.set(None));
|
|
if !fired {
|
|
assert!(r.is_ok());
|
|
break;
|
|
}
|
|
x.c.rollback();
|
|
let (v1, d1) = (x.view(), x.drain());
|
|
if v1 != v0 || d1 != d0 {
|
|
failures.push(format!(
|
|
"n={n}: rollback state differs\n view {v1}\n want {v0}\n drain {d1}"
|
|
));
|
|
continue;
|
|
}
|
|
for r in [&x, &y] {
|
|
// The keys the failed step touched come back, so state leaked by one shows.
|
|
r.a.push(bz(&[
|
|
(1, 7, 1),
|
|
(4, 1, 1),
|
|
(1, 5, 1),
|
|
(4, 4, 1),
|
|
(2, 2, -1),
|
|
]));
|
|
r.b.push(bz(&[(4, 2, 1), (1, 9, 1)]));
|
|
}
|
|
x.c.step();
|
|
y.c.step();
|
|
if x.view() != y.view() || x.drain() != y.drain() {
|
|
failures.push(format!("n={n}: post tick differs"));
|
|
}
|
|
}
|
|
failures
|
|
}
|
|
|
|
#[test]
|
|
fn c1_a_clone_panic_anywhere_in_a_step_is_rolled_back() {
|
|
let mut all = vec![];
|
|
for which in [1u8, 2, 4, 8, 15] {
|
|
for f in clone_panic_sweep(which) {
|
|
all.push(format!("which={which} {f}"));
|
|
}
|
|
}
|
|
assert!(
|
|
all.is_empty(),
|
|
"{} failures:\n{}",
|
|
all.len(),
|
|
all.join("\n")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn c2_memtrace_value_clone_panic_leaves_an_empty_group() {
|
|
quiet();
|
|
let mut t: MemTrace<u8, Boom> = MemTrace::new();
|
|
CLONE_TRIP.with(|c| c.set(Some(0)));
|
|
let r = catch_unwind(AssertUnwindSafe(|| t.apply(&1, &Boom(1), 1)));
|
|
CLONE_TRIP.with(|c| c.set(None));
|
|
assert!(r.is_err());
|
|
assert_eq!(
|
|
t.keys(),
|
|
0,
|
|
"[c2] MemTrace::apply panicked but left a group"
|
|
);
|
|
}
|