@@ -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