Skip to content

Commit 57fc6b7

Browse files
committed
test(engine): cover repeated graceful restart for timeseries
Add a regression test driving two consecutive graceful (SIGTERM → final-checkpoint-flush) restarts of a timeseries collection: the first exercises the final-checkpoint flush and partition stamp, the second replays against the stamp written by the first, so a tail-loss or an under-covering stamp is only observable across both restarts and an exact row count. Covers #172.
1 parent c2dbbd3 commit 57fc6b7

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

nodedb/tests/engine_surface_timeseries.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,88 @@ async fn wal_restart_durability() {
177177
assert!((v - expected).abs() < 0.01);
178178
}
179179

180+
/// Regression for #172: a timeseries collection must survive a GRACEFUL
181+
/// (SIGTERM → final-checkpoint flush) restart with EXACTLY its rows — and stay
182+
/// stable across a SECOND graceful restart. The two restarts are the crux the
183+
/// single-restart durability test above cannot see:
184+
/// * restart 1 exercises the final-checkpoint flush + partition stamp; a
185+
/// tail-loss drops the most-recent point here.
186+
/// * restart 2 replays against the partition stamped on restart 1; a stamp
187+
/// that under-covers the flushed set makes replay re-append the resident
188+
/// rows, so the survivors come back DOUBLED. Both faces are silent on an
189+
/// append-only engine, so only the exact count separates them.
190+
#[tokio::test]
191+
async fn timeseries_survives_repeated_graceful_restart_172() {
192+
let srv = TestServer::start().await;
193+
srv.exec(
194+
"CREATE COLLECTION ts_172 \
195+
COLUMNS (id TEXT, ts BIGINT TIME_KEY, val FLOAT) \
196+
WITH (engine='timeseries')",
197+
)
198+
.await
199+
.unwrap();
200+
201+
// Five distinct WAL records (separate INSERTs) so the last is a real tail
202+
// record and any per-record flush/stamp error is observable.
203+
const N: usize = 5;
204+
for i in 0..N {
205+
srv.exec(&format!(
206+
"INSERT INTO ts_172 (id, ts, val) VALUES ('p{i}', {ts}, {i}.0)",
207+
ts = 1000 + i as u64 * 1000
208+
))
209+
.await
210+
.unwrap();
211+
}
212+
213+
// Live sanity: all N present before any restart, so any later discrepancy
214+
// is recovery, not ingest.
215+
let live = srv.query_rows("SELECT id FROM ts_172").await.unwrap();
216+
assert_eq!(
217+
live.len(),
218+
N,
219+
"all {N} points must read back pre-restart: {live:?}"
220+
);
221+
222+
// ── Graceful restart 1 ── (runs the final checkpoint flush + stamp)
223+
// `open_on_path` returns the live data dir separately (the reopened server
224+
// only references the path), so we keep `dir` for the second restart rather
225+
// than calling `take_dir()` on the reopened server, which would hand back a
226+
// different, empty dir.
227+
let (srv, dir) = srv.take_dir();
228+
srv.graceful_shutdown().await;
229+
let (srv2, dir) = TestServer::open_on_path(dir).await;
230+
231+
let after_1 = srv2
232+
.query_rows("SELECT id FROM ts_172 ORDER BY ts")
233+
.await
234+
.unwrap();
235+
assert_eq!(
236+
after_1.len(),
237+
N,
238+
"after ONE graceful restart, exactly {N} points must remain (fewer = the \
239+
tail record was lost; more = replay re-applied an already-flushed \
240+
record): {after_1:?}"
241+
);
242+
assert_eq!(
243+
after_1.last().map(|r| r[0].as_str()),
244+
Some("p4"),
245+
"the most-recent point must survive the first restart, not just the count"
246+
);
247+
248+
// ── Graceful restart 2 ── (the mode that duplicated in #172)
249+
srv2.graceful_shutdown().await;
250+
let (srv3, _dir) = TestServer::open_on_path(dir).await;
251+
252+
let after_2 = srv3.query_rows("SELECT id FROM ts_172").await.unwrap();
253+
assert_eq!(
254+
after_2.len(),
255+
N,
256+
"after a SECOND graceful restart the count must still be exactly {N}; \
257+
more means replay re-appended the partition-resident rows on top of \
258+
themselves (#172 duplication): {after_2:?}"
259+
);
260+
}
261+
180262
// ── Continuous-aggregate registration must survive past the in-memory Data ──
181263
//
182264
// `CREATE CONTINUOUS AGGREGATE` currently dispatches `RegisterContinuousAggregate`

0 commit comments

Comments
 (0)