@@ -643,6 +643,7 @@ struct ModelRunner {
643643 harness : ScenarioHarness ,
644644 oracle : MvccOracle ,
645645 l0_ssts : Vec < u64 > ,
646+ // When set, reads run against this exact snapshot until a mutating op invalidates it.
646647 active_snapshot_ts : Option < u64 > ,
647648 active_snapshot : Option < TxSnapshot > ,
648649 allow_reopen : bool ,
@@ -684,6 +685,8 @@ impl ModelRunner {
684685 }
685686
686687 fn clear_active_snapshot ( & mut self ) {
688+ // Model snapshots are treated as read-only checkpoints. Any write/flush operation
689+ // invalidates the pinned snapshot so follow-up reads observe a fresh view.
687690 self . active_snapshot_ts = None ;
688691 self . active_snapshot = None ;
689692 }
@@ -973,6 +976,68 @@ fn mvcc_oracle_visible_version_prefers_tombstone() {
973976 assert_eq ! ( scan, vec![ ( "c" . to_string( ) , 31 ) ] ) ;
974977}
975978
979+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
980+ async fn model_runner_mutation_ops_clear_active_snapshot ( ) -> Result < ( ) , Box < dyn std:: error:: Error > >
981+ {
982+ let mut runner = ModelRunner :: new ( 7 , "compaction-correctness-model-snapshot-clear" ) . await ?;
983+
984+ runner. apply_op ( OpKind :: Snapshot ) . await ?;
985+ assert ! ( runner. active_snapshot_ts. is_some( ) ) ;
986+ assert ! ( runner. active_snapshot. is_some( ) ) ;
987+
988+ runner. apply_op ( OpKind :: Put ) . await ?;
989+ assert ! ( runner. active_snapshot_ts. is_none( ) ) ;
990+ assert ! ( runner. active_snapshot. is_none( ) ) ;
991+
992+ runner. apply_op ( OpKind :: Snapshot ) . await ?;
993+ assert ! ( runner. active_snapshot_ts. is_some( ) ) ;
994+ assert ! ( runner. active_snapshot. is_some( ) ) ;
995+
996+ runner. apply_op ( OpKind :: Delete ) . await ?;
997+ assert ! ( runner. active_snapshot_ts. is_none( ) ) ;
998+ assert ! ( runner. active_snapshot. is_none( ) ) ;
999+
1000+ runner. apply_op ( OpKind :: Snapshot ) . await ?;
1001+ assert ! ( runner. active_snapshot_ts. is_some( ) ) ;
1002+ assert ! ( runner. active_snapshot. is_some( ) ) ;
1003+
1004+ runner. apply_op ( OpKind :: Flush ) . await ?;
1005+ assert ! ( runner. active_snapshot_ts. is_none( ) ) ;
1006+ assert ! ( runner. active_snapshot. is_none( ) ) ;
1007+
1008+ Ok ( ( ) )
1009+ }
1010+
1011+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
1012+ async fn model_runner_reads_use_pinned_snapshot_until_mutation ( )
1013+ -> Result < ( ) , Box < dyn std:: error:: Error > > {
1014+ let mut runner = ModelRunner :: new ( 11 , "compaction-correctness-model-pinned-read" ) . await ?;
1015+
1016+ runner. apply_op ( OpKind :: Snapshot ) . await ?;
1017+ let pinned_ts = runner
1018+ . active_snapshot_ts
1019+ . ok_or ( "snapshot should set active_snapshot_ts" ) ?;
1020+
1021+ runner. apply_op ( OpKind :: Get ) . await ?;
1022+ let last = runner
1023+ . trace
1024+ . entries
1025+ . back ( )
1026+ . ok_or ( "expected get op in trace" ) ?;
1027+ match & last. op {
1028+ Op :: Get { snapshot_ts, .. } => assert_eq ! ( * snapshot_ts, pinned_ts) ,
1029+ other => return Err ( format ! ( "expected Get op, got {other:?}" ) . into ( ) ) ,
1030+ }
1031+ assert_eq ! ( runner. active_snapshot_ts, Some ( pinned_ts) ) ;
1032+ assert ! ( runner. active_snapshot. is_some( ) ) ;
1033+
1034+ runner. apply_op ( OpKind :: Put ) . await ?;
1035+ assert ! ( runner. active_snapshot_ts. is_none( ) ) ;
1036+ assert ! ( runner. active_snapshot. is_none( ) ) ;
1037+
1038+ Ok ( ( ) )
1039+ }
1040+
9761041#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
9771042async fn compaction_correctness_overwrite_chain ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
9781043 let scenario = "overwrite_chain" ;
0 commit comments