@@ -439,6 +439,106 @@ void testSnapshotUseLatestLeaderEpoch(@TempDir File snapshotKvTabletDir) throws
439439 .isEqualTo (latestLeaderEpoch );
440440 }
441441
442+ @ Test
443+ void testBrokenSnapshotRecovery (@ TempDir File snapshotKvTabletDir ) throws Exception {
444+ TableBucket tableBucket = new TableBucket (DATA1_TABLE_ID_PK , 1 );
445+
446+ // create test context with custom snapshot store
447+ TestSnapshotContext testKvSnapshotContext =
448+ new TestSnapshotContext (snapshotKvTabletDir .getPath ());
449+ ManuallyTriggeredScheduledExecutorService scheduledExecutorService =
450+ testKvSnapshotContext .scheduledExecutorService ;
451+ TestingCompletedKvSnapshotCommitter kvSnapshotStore =
452+ testKvSnapshotContext .testKvSnapshotStore ;
453+
454+ // create a replica and make it leader
455+ Replica kvReplica =
456+ makeKvReplica (DATA1_PHYSICAL_TABLE_PATH_PK , tableBucket , testKvSnapshotContext );
457+ makeKvReplicaAsLeader (kvReplica );
458+
459+ // put initial data and create first snapshot
460+ KvRecordBatch kvRecords =
461+ genKvRecordBatch (
462+ Tuple2 .of ("k1" , new Object [] {1 , "a" }),
463+ Tuple2 .of ("k2" , new Object [] {2 , "b" }));
464+ putRecordsToLeader (kvReplica , kvRecords );
465+
466+ // trigger first snapshot
467+ scheduledExecutorService .triggerNonPeriodicScheduledTask ();
468+ kvSnapshotStore .waitUntilSnapshotComplete (tableBucket , 0 );
469+
470+ // put more data and create second snapshot
471+ kvRecords =
472+ genKvRecordBatch (
473+ Tuple2 .of ("k1" , new Object [] {3 , "c" }),
474+ Tuple2 .of ("k3" , new Object [] {4 , "d" }));
475+ putRecordsToLeader (kvReplica , kvRecords );
476+
477+ // trigger second snapshot
478+ scheduledExecutorService .triggerNonPeriodicScheduledTask ();
479+ kvSnapshotStore .waitUntilSnapshotComplete (tableBucket , 1 );
480+
481+ // put more data and create third snapshot (this will be the broken one)
482+ kvRecords =
483+ genKvRecordBatch (
484+ Tuple2 .of ("k4" , new Object [] {5 , "e" }),
485+ Tuple2 .of ("k5" , new Object [] {6 , "f" }));
486+ putRecordsToLeader (kvReplica , kvRecords );
487+
488+ // trigger third snapshot
489+ scheduledExecutorService .triggerNonPeriodicScheduledTask ();
490+ CompletedSnapshot snapshot2 = kvSnapshotStore .waitUntilSnapshotComplete (tableBucket , 2 );
491+
492+ // verify that snapshot2 is the latest one before we break it
493+ assertThat (kvSnapshotStore .getLatestCompletedSnapshot (tableBucket ).getSnapshotID ())
494+ .isEqualTo (2 );
495+
496+ // now simulate the latest snapshot (snapshot2) being broken by
497+ // deleting its metadata files and unshared SST files
498+ // This simulates file corruption while ZK metadata remains intact
499+ snapshot2 .getKvSnapshotHandle ().discard ();
500+
501+ // ZK metadata should still show snapshot2 as latest (file corruption hasn't been detected
502+ // yet)
503+ assertThat (kvSnapshotStore .getLatestCompletedSnapshot (tableBucket ).getSnapshotID ())
504+ .isEqualTo (2 );
505+
506+ // make the replica follower to destroy the current kv tablet
507+ makeKvReplicaAsFollower (kvReplica , 1 );
508+
509+ // create a new replica with the same snapshot context
510+ // During initialization, it will try to use snapshot2 but find it broken,
511+ // then handle the broken snapshot and fall back to snapshot1
512+ testKvSnapshotContext =
513+ new TestSnapshotContext (snapshotKvTabletDir .getPath (), kvSnapshotStore );
514+ kvReplica = makeKvReplica (DATA1_PHYSICAL_TABLE_PATH_PK , tableBucket , testKvSnapshotContext );
515+
516+ // make it leader again - this should trigger the broken snapshot recovery logic
517+ // The system should detect that snapshot2 files are missing, clean up its metadata,
518+ // and successfully recover using snapshot1
519+ makeKvReplicaAsLeader (kvReplica , 2 );
520+
521+ // verify that KvTablet is successfully initialized despite the broken snapshot
522+ assertThat (kvReplica .getKvTablet ()).isNotNull ();
523+ KvTablet kvTablet = kvReplica .getKvTablet ();
524+
525+ // verify that the data from snapshot1 is restored (snapshot2 was broken and cleaned up)
526+ // snapshot1 should contain: k1->3,c and k3->4,d
527+ List <Tuple2 <byte [], byte []>> expectedKeyValues =
528+ getKeyValuePairs (
529+ genKvRecords (
530+ Tuple2 .of ("k1" , new Object [] {3 , "c" }),
531+ Tuple2 .of ("k3" , new Object [] {4 , "d" })));
532+ verifyGetKeyValues (kvTablet , expectedKeyValues );
533+
534+ // Verify the core functionality: KvTablet successfully initialized despite broken snapshot
535+ // The key test is that the system can handle broken snapshots and recover correctly
536+
537+ // Verify that we successfully simulated the broken snapshot condition
538+ File metadataFile = new File (snapshot2 .getMetadataFilePath ().getPath ());
539+ assertThat (metadataFile .exists ()).isFalse ();
540+ }
541+
442542 @ Test
443543 void testRestore (@ TempDir Path snapshotKvTabletDirPath ) throws Exception {
444544 TableBucket tableBucket = new TableBucket (DATA1_TABLE_ID_PK , 1 );
0 commit comments