@@ -491,6 +491,212 @@ public void testRecoveryDoesNotFlagFreshUnusedSegment() throws Exception {
491491 });
492492 }
493493
494+ @ Test
495+ public void testRecoveryZeroesTornTailResidueOnDisk () throws Exception {
496+ TestUtils .assertMemoryLeak (() -> {
497+ // Recovery must not only REPORT the torn tail, it must sanitize
498+ // it: the first open still reports the observed residue (operator
499+ // signal), but a second open of the untouched file must find a
500+ // clean zero suffix. Pre-fix the residue survived forever.
501+ String path = tmpDir + "/seg-zeroed.sfa" ;
502+ long buf = Unsafe .malloc (16 , MemoryTag .NATIVE_DEFAULT );
503+ long lastGood ;
504+ try {
505+ try (MmapSegment seg = MmapSegment .create (path , 0L , 4096 )) {
506+ for (int i = 0 ; i < 3 ; i ++) {
507+ fillPattern (buf , 16 , i );
508+ seg .tryAppend (buf , 16 );
509+ }
510+ lastGood = seg .publishedOffset ();
511+ long addr = seg .address ();
512+ for (long off = lastGood ; off + 4 <= 4096 ; off += 4 ) {
513+ Unsafe .getUnsafe ().putInt (addr + off , 0xCAFEBABE );
514+ }
515+ seg .msync ();
516+ }
517+ try (MmapSegment seg = MmapSegment .openExisting (path )) {
518+ assertEquals ("first recovery must report the observed residue" ,
519+ 4096L - lastGood , seg .tornTailBytes ());
520+ }
521+ try (MmapSegment seg = MmapSegment .openExisting (path )) {
522+ assertEquals ("first recovery must have zeroed the residue on disk" ,
523+ 0L , seg .tornTailBytes ());
524+ assertEquals (lastGood , seg .publishedOffset ());
525+ assertEquals (3L , seg .frameCount ());
526+ }
527+ } finally {
528+ Unsafe .free (buf , 16 , MemoryTag .NATIVE_DEFAULT );
529+ }
530+ });
531+ }
532+
533+ @ Test
534+ public void testResealGapResidueCannotSurviveRecoveryAppends () throws Exception {
535+ TestUtils .assertMemoryLeak (() -> {
536+ // Two-crash reseal regression at segment level. Crash #1 leaves
537+ // residue up to the file end; recovery resumes at lastGood; the
538+ // resumed writer fills the segment but its last frame stops short
539+ // of the file end (the remaining gap cannot fit another frame).
540+ // Pre-fix the stale residue survived in that gap, so the segment
541+ // -- sealed as-is by rotation -- failed the sealed-suffix-must-
542+ // be-zero check on the NEXT recovery, permanently failing startup.
543+ long segSize = MmapSegment .HEADER_SIZE
544+ + 4 * (MmapSegment .FRAME_HEADER_SIZE + 16 )
545+ + 12 ; // reseal gap: a 5th 24-byte frame can never fit
546+ String path = tmpDir + "/seg-reseal.sfa" ;
547+ long buf = Unsafe .malloc (16 , MemoryTag .NATIVE_DEFAULT );
548+ try {
549+ // Session 1: two frames, then a crash mid-write near the end
550+ // -- garbage from the torn frame all the way to the file end.
551+ try (MmapSegment seg = MmapSegment .create (path , 0L , segSize )) {
552+ fillPattern (buf , 16 , 0 );
553+ seg .tryAppend (buf , 16 );
554+ fillPattern (buf , 16 , 1 );
555+ seg .tryAppend (buf , 16 );
556+ long addr = seg .address ();
557+ for (long off = seg .publishedOffset (); off + 4 <= segSize ; off += 4 ) {
558+ Unsafe .getUnsafe ().putInt (addr + off , 0xCAFEBABE );
559+ }
560+ seg .msync ();
561+ }
562+ // Recovery #1 + session 2: fill the segment to its rotation
563+ // point. The 4th frame ends 12 bytes short of the file end --
564+ // a region session 2 never overwrites.
565+ try (MmapSegment seg = MmapSegment .openExisting (path )) {
566+ assertEquals (2L , seg .frameCount ());
567+ fillPattern (buf , 16 , 2 );
568+ assertTrue (seg .tryAppend (buf , 16 ) >= 0 );
569+ fillPattern (buf , 16 , 3 );
570+ assertTrue (seg .tryAppend (buf , 16 ) >= 0 );
571+ assertEquals ("next append must not fit (rotation would seal here)" ,
572+ -1L , seg .tryAppend (buf , 16 ));
573+ seg .msync ();
574+ }
575+ // Recovery #2: the state a sealed segment presents at the next
576+ // startup. Its suffix must be clean or ring recovery bricks.
577+ try (MmapSegment seg = MmapSegment .openExisting (path )) {
578+ assertEquals ("all four frames must recover" , 4L , seg .frameCount ());
579+ assertEquals ("no residue may survive in the reseal gap: a sealed "
580+ + "segment with a non-zero suffix permanently fails "
581+ + "ring recovery" ,
582+ 0L , seg .tornTailBytes ());
583+ }
584+ } finally {
585+ Unsafe .free (buf , 16 , MemoryTag .NATIVE_DEFAULT );
586+ }
587+ });
588+ }
589+
590+ @ Test
591+ public void testDiscardedStaleFrameNotResurrectedByLaterRecovery () throws Exception {
592+ TestUtils .assertMemoryLeak (() -> {
593+ // The frame envelope [crc(len|payload)][len][payload] binds
594+ // neither position nor FSN, so a stale frame with a valid CRC
595+ // sitting past the tear -- byte-aligned with the resumed writer's
596+ // frames, natural with fixed-size records -- would be silently
597+ // re-adopted by the next recovery scan at a recycled FSN.
598+ // Recovery #1 discarded it; recovery #2 must not bring it back.
599+ String path = tmpDir + "/seg-stale.sfa" ;
600+ long buf = Unsafe .malloc (16 , MemoryTag .NATIVE_DEFAULT );
601+ long frameB ;
602+ try {
603+ try (MmapSegment seg = MmapSegment .create (path , 0L , 4096 )) {
604+ fillPattern (buf , 16 , 0 );
605+ seg .tryAppend (buf , 16 ); // frame A, FSN 0
606+ fillPattern (buf , 16 , 1 );
607+ frameB = seg .tryAppend (buf , 16 ); // frame B, FSN 1
608+ fillPattern (buf , 16 , 2 );
609+ seg .tryAppend (buf , 16 ); // frame C, FSN 2
610+ // The crash tears frame B only: flip its CRC. C keeps a
611+ // valid CRC at a frame-aligned offset past the tear.
612+ long addr = seg .address ();
613+ int crc = Unsafe .getUnsafe ().getInt (addr + frameB );
614+ Unsafe .getUnsafe ().putInt (addr + frameB , crc ^ 0x5A5A5A5A );
615+ seg .msync ();
616+ }
617+ // Recovery #1: B fails CRC, so the scan stops at B; B and C
618+ // are discarded (and, with sanitization, zeroed).
619+ try (MmapSegment seg = MmapSegment .openExisting (path )) {
620+ assertEquals ("scan must stop at the torn frame" , 1L , seg .frameCount ());
621+ // The resumed writer re-issues FSN 1 with a fresh payload
622+ // of the old B's exact size -- the byte-aligned case.
623+ fillPattern (buf , 16 , 7 );
624+ assertEquals (frameB , seg .tryAppend (buf , 16 ));
625+ seg .msync ();
626+ }
627+ // Recovery #2: pre-fix the scan walked A, B' and then adopted
628+ // the STALE C (valid CRC) as a live frame -- data recovery #1
629+ // had already discarded, resurrected behind the engine's back.
630+ try (MmapSegment seg = MmapSegment .openExisting (path )) {
631+ assertEquals ("stale frame C must stay discarded, not be "
632+ + "resurrected at a recycled FSN" , 2L , seg .frameCount ());
633+ assertEquals (0L , seg .tornTailBytes ());
634+ }
635+ } finally {
636+ Unsafe .free (buf , 16 , MemoryTag .NATIVE_DEFAULT );
637+ }
638+ });
639+ }
640+
641+ @ Test
642+ public void testTornTailZeroingSyncFailureAbortsRecovery () throws Exception {
643+ TestUtils .assertMemoryLeak (() -> {
644+ // Sanitization is load-bearing: if the zeroes cannot be made
645+ // durable, recovery must fail closed rather than hand back a
646+ // segment whose reseal could permanently fail the next startup.
647+ // A failed attempt may still leave zeroes in the page cache;
648+ // that is safe (a retry either sees a clean tail or re-zeroes),
649+ // so the follow-up open with a healthy facade must succeed.
650+ long buf = Unsafe .malloc (16 , MemoryTag .NATIVE_DEFAULT );
651+ try {
652+ String msyncPath = tmpDir + "/seg-zero-msync-fail.sfa" ;
653+ String fsyncPath = tmpDir + "/seg-zero-fsync-fail.sfa" ;
654+ for (String path : new String []{msyncPath , fsyncPath }) {
655+ try (MmapSegment seg = MmapSegment .create (path , 0L , 4096 )) {
656+ fillPattern (buf , 16 , 0 );
657+ seg .tryAppend (buf , 16 );
658+ long addr = seg .address ();
659+ Unsafe .getUnsafe ().putInt (addr + seg .publishedOffset (), 0xCAFEBABE );
660+ Unsafe .getUnsafe ().putInt (addr + seg .publishedOffset () + 4 , 16 );
661+ seg .msync ();
662+ }
663+ }
664+
665+ FaultyFilesFacade msyncFailure = new FaultyFilesFacade ();
666+ msyncFailure .failOnMsync = true ;
667+ try {
668+ MmapSegment .openExisting (msyncFailure , msyncPath ).close ();
669+ fail ("expected recovery to abort when the zeroed tail cannot be msync'd" );
670+ } catch (MmapSegmentException expected ) {
671+ assertTrue (expected .getMessage (),
672+ expected .getMessage ().contains ("zeroed torn tail" ));
673+ }
674+ assertEquals (1 , msyncFailure .msyncCalls );
675+ assertEquals (0 , msyncFailure .fsyncCalls );
676+ try (MmapSegment seg = MmapSegment .openExisting (msyncPath )) {
677+ assertEquals (1L , seg .frameCount ());
678+ }
679+
680+ FaultyFilesFacade fsyncFailure = new FaultyFilesFacade ();
681+ fsyncFailure .failOnFsync = true ;
682+ try {
683+ MmapSegment .openExisting (fsyncFailure , fsyncPath ).close ();
684+ fail ("expected recovery to abort when the zeroed tail cannot be fsync'd" );
685+ } catch (MmapSegmentException expected ) {
686+ assertTrue (expected .getMessage (),
687+ expected .getMessage ().contains ("zeroed torn tail" ));
688+ }
689+ assertEquals (1 , fsyncFailure .msyncCalls );
690+ assertEquals (1 , fsyncFailure .fsyncCalls );
691+ try (MmapSegment seg = MmapSegment .openExisting (fsyncPath )) {
692+ assertEquals (1L , seg .frameCount ());
693+ }
694+ } finally {
695+ Unsafe .free (buf , 16 , MemoryTag .NATIVE_DEFAULT );
696+ }
697+ });
698+ }
699+
494700 @ Test
495701 public void testRecoverySignalsTornTailWithByteCount () throws Exception {
496702 TestUtils .assertMemoryLeak (() -> {
0 commit comments