@@ -609,4 +609,196 @@ public TestPreservedJournalEntry(string formatKey, ReadOnlyMemory<byte> payload)
609609
610610 public string FormatKey { get ; }
611611 }
612+
613+ [ Fact ]
614+ public void BinaryFormat_Replay_ParsesConcatenatedLegacyRecordsInCommandOrder ( )
615+ {
616+ var firstPayload = System . Text . Encoding . UTF8 . GetBytes ( "first" ) ;
617+ var secondPayload = System . Text . Encoding . UTF8 . GetBytes ( "second" ) ;
618+ using var firstRecord = CreateLegacyWriter ( bodyLength : null , streamId : 1 , commandVersion : 0 , firstPayload ) ;
619+ using var secondRecord = CreateLegacyWriter ( bodyLength : null , streamId : 2 , commandVersion : 0 , secondPayload ) ;
620+ using var firstRecordBytes = firstRecord . PeekSlice ( firstRecord . Length ) ;
621+ using var secondRecordBytes = secondRecord . PeekSlice ( secondRecord . Length ) ;
622+ using var data = CreateWriter ( [ .. firstRecordBytes . ToArray ( ) , .. secondRecordBytes . ToArray ( ) ] ) ;
623+ var reader = new JournalBufferReader ( data . Reader , isCompleted : true ) ;
624+ var consumer = new CollectingConsumer ( ) ;
625+ var context = JournalTestReplayContext . Create ( OrleansBinaryJournalFormat . JournalFormatKey , consumer . Bind ( 1 , 2 ) ) ;
626+
627+ ( ( IJournalFormat ) new OrleansBinaryJournalFormat ( SessionPool ) ) . Replay ( reader , context ) ;
628+
629+ Assert . Collection (
630+ consumer . Entries ,
631+ entry =>
632+ {
633+ Assert . Equal ( 1U , entry . StreamId ) ;
634+ Assert . Equal ( firstPayload , entry . Payload ) ;
635+ Assert . Equal ( "first" , System . Text . Encoding . UTF8 . GetString ( entry . Payload ) ) ;
636+ } ,
637+ entry =>
638+ {
639+ Assert . Equal ( 2U , entry . StreamId ) ;
640+ Assert . Equal ( secondPayload , entry . Payload ) ;
641+ Assert . Equal ( "second" , System . Text . Encoding . UTF8 . GetString ( entry . Payload ) ) ;
642+ } ) ;
643+ Assert . Equal ( 0 , reader . Length ) ;
644+ }
645+
646+ [ Fact ]
647+ public void BinaryFormat_Replay_RejectsZeroLengthLegacyBodyWithoutConsumingInput ( )
648+ {
649+ using var data = CreateLegacyWriter ( bodyLength : 0 , streamId : null , commandVersion : null , [ ] ) ;
650+ var reader = new JournalBufferReader ( data . Reader , isCompleted : true ) ;
651+ var originalLength = reader . Length ;
652+ var consumer = new CollectingConsumer ( ) ;
653+ var context = JournalTestReplayContext . Create ( OrleansBinaryJournalFormat . JournalFormatKey , consumer . Bind ( 1 ) ) ;
654+
655+ // A legacy varuint zero is encoded as 0x01, which the public format dispatches as V1 framing.
656+ var exception = Assert . Throws < InvalidOperationException > (
657+ ( ) => ( ( IJournalFormat ) new OrleansBinaryJournalFormat ( SessionPool ) ) . Replay ( reader , context ) ) ;
658+
659+ Assert . Equal ( 1 , originalLength ) ;
660+ Assert . Equal ( "Malformed binary journal entry stream at byte offset 0: truncated fixed-width entry header." , exception . Message ) ;
661+ Assert . Empty ( consumer . Entries ) ;
662+ Assert . Equal ( originalLength , reader . Length ) ;
663+ }
664+
665+ [ Fact ]
666+ public void BinaryFormat_Replay_RejectsOversizedLegacyStreamIdWithoutConsumingInput ( )
667+ {
668+ const ulong oversizedStreamId = ( ulong ) uint . MaxValue + 1 ;
669+ using var data = CreateLegacyWriter ( bodyLength : null , oversizedStreamId , commandVersion : 0 , [ ] ) ;
670+ var reader = new JournalBufferReader ( data . Reader , isCompleted : true ) ;
671+ var originalLength = reader . Length ;
672+ var consumer = new CollectingConsumer ( ) ;
673+ var context = JournalTestReplayContext . Create ( OrleansBinaryJournalFormat . JournalFormatKey , consumer . Bind ( 1 ) ) ;
674+
675+ var exception = Assert . Throws < NotSupportedException > (
676+ ( ) => ( ( IJournalFormat ) new OrleansBinaryJournalFormat ( SessionPool ) ) . Replay ( reader , context ) ) ;
677+
678+ Assert . Equal ( "Unsupported legacy binary journal stream id at byte offset 0: 4294967296." , exception . Message ) ;
679+ Assert . Empty ( consumer . Entries ) ;
680+ Assert . Equal ( originalLength , reader . Length ) ;
681+ }
682+
683+ [ Fact ]
684+ public void BinaryFormat_Replay_RejectsLegacyRecordMissingCommandVersionWithoutConsumingInput ( )
685+ {
686+ using var data = CreateLegacyWriter ( bodyLength : null , streamId : 7 , commandVersion : null , [ ] ) ;
687+ var reader = new JournalBufferReader ( data . Reader , isCompleted : true ) ;
688+ var originalLength = reader . Length ;
689+ var consumer = new CollectingConsumer ( ) ;
690+ var context = JournalTestReplayContext . Create ( OrleansBinaryJournalFormat . JournalFormatKey , consumer . Bind ( 7 ) ) ;
691+
692+ var exception = Assert . Throws < InvalidOperationException > (
693+ ( ) => ( ( IJournalFormat ) new OrleansBinaryJournalFormat ( SessionPool ) ) . Replay ( reader , context ) ) ;
694+
695+ Assert . Equal ( "Malformed binary journal entry stream at byte offset 0: missing legacy command format version." , exception . Message ) ;
696+ Assert . Empty ( consumer . Entries ) ;
697+ Assert . Equal ( originalLength , reader . Length ) ;
698+ }
699+
700+ [ Fact ]
701+ public void BinaryFormat_Replay_RejectsUnsupportedLegacyCommandVersionWithoutConsumingInput ( )
702+ {
703+ using var data = CreateLegacyWriter ( bodyLength : null , streamId : 7 , commandVersion : 1 , [ 0xAA ] ) ;
704+ var reader = new JournalBufferReader ( data . Reader , isCompleted : true ) ;
705+ var originalLength = reader . Length ;
706+ var consumer = new CollectingConsumer ( ) ;
707+ var context = JournalTestReplayContext . Create ( OrleansBinaryJournalFormat . JournalFormatKey , consumer . Bind ( 7 ) ) ;
708+
709+ var exception = Assert . Throws < NotSupportedException > (
710+ ( ) => ( ( IJournalFormat ) new OrleansBinaryJournalFormat ( SessionPool ) ) . Replay ( reader , context ) ) ;
711+
712+ Assert . Equal ( "Unsupported legacy binary journal command format version at byte offset 0: 1." , exception . Message ) ;
713+ Assert . Empty ( consumer . Entries ) ;
714+ Assert . Equal ( originalLength , reader . Length ) ;
715+ }
716+
717+ private static ArcBufferWriter CreateLegacyWriter ( uint ? bodyLength , ulong ? streamId , byte ? commandVersion , ReadOnlySpan < byte > payload )
718+ {
719+ var writer = new ArcBufferWriter ( ) ;
720+ var serializerWriter = Writer . Create ( writer , session : null ! ) ;
721+ serializerWriter . WriteVarUInt32 ( bodyLength ?? checked ( ( uint ) (
722+ ( streamId . HasValue ? GetVarUInt64ByteCount ( streamId . Value ) : 0 )
723+ + ( commandVersion . HasValue ? 1 : 0 )
724+ + payload . Length ) ) ) ;
725+
726+ if ( streamId . HasValue )
727+ {
728+ serializerWriter . WriteVarUInt64 ( streamId . Value ) ;
729+ }
730+
731+ if ( commandVersion . HasValue )
732+ {
733+ serializerWriter . WriteByte ( commandVersion . Value ) ;
734+ }
735+
736+ serializerWriter . Commit ( ) ;
737+ writer . Write ( payload ) ;
738+ return writer ;
739+ }
740+
741+ private static int GetVarUInt64ByteCount ( ulong value )
742+ {
743+ var result = 1 ;
744+ while ( value >= 128 )
745+ {
746+ value >>= 7 ;
747+ result ++ ;
748+ }
749+
750+ return result ;
751+ }
752+
753+ [ Fact ]
754+ public void BinaryFormat_Replay_DispatchesConcatenatedV0AndV1RecordsInPhysicalOrder ( )
755+ {
756+ var legacyPayload = System . Text . Encoding . UTF8 . GetBytes ( "legacy" ) ;
757+ var currentPayload = System . Text . Encoding . UTF8 . GetBytes ( "current" ) ;
758+ using var legacyRecord = CreateLegacyWriter ( bodyLength : null , streamId : 1 , commandVersion : 0 , legacyPayload ) ;
759+ using var currentRecord = new OrleansBinaryJournalBufferWriter ( ) ;
760+ AppendEntry ( currentRecord . CreateJournalStreamWriter ( new JournalStreamId ( 1 ) ) , currentPayload ) ;
761+ using var legacyRecordBytes = legacyRecord . PeekSlice ( legacyRecord . Length ) ;
762+ using var data = CreateWriter ( [ .. legacyRecordBytes . ToArray ( ) , .. ToArray ( currentRecord ) ] ) ;
763+ var reader = new JournalBufferReader ( data . Reader , isCompleted : true ) ;
764+ var consumer = new CollectingConsumer ( ) ;
765+ var context = JournalTestReplayContext . Create ( OrleansBinaryJournalFormat . JournalFormatKey , consumer . Bind ( 1 ) ) ;
766+ IJournalFormat format = new OrleansBinaryJournalFormat ( SessionPool ) ;
767+
768+ format . Replay ( reader , context ) ;
769+
770+ Assert . Collection (
771+ consumer . Entries ,
772+ entry =>
773+ {
774+ Assert . Equal ( 1U , entry . StreamId ) ;
775+ Assert . Equal ( legacyPayload , entry . Payload ) ;
776+ Assert . Equal ( "legacy" , System . Text . Encoding . UTF8 . GetString ( entry . Payload ) ) ;
777+ } ,
778+ entry =>
779+ {
780+ Assert . Equal ( 1U , entry . StreamId ) ;
781+ Assert . Equal ( currentPayload , entry . Payload ) ;
782+ Assert . Equal ( "current" , System . Text . Encoding . UTF8 . GetString ( entry . Payload ) ) ;
783+ } ) ;
784+ Assert . Equal ( 0 , reader . Length ) ;
785+ }
786+
787+ [ Fact ]
788+ public void BinaryFormat_Replay_UnsupportedLegacyCommandVersionReportsContextWithoutPartialApplication ( )
789+ {
790+ using var data = CreateLegacyWriter ( bodyLength : null , streamId : 1 , commandVersion : 1 , [ 0xAA ] ) ;
791+ var reader = new JournalBufferReader ( data . Reader , isCompleted : true ) ;
792+ var originalLength = reader . Length ;
793+ var consumer = new CollectingConsumer ( ) ;
794+ var context = JournalTestReplayContext . Create ( OrleansBinaryJournalFormat . JournalFormatKey , consumer . Bind ( 1 ) ) ;
795+ IJournalFormat format = new OrleansBinaryJournalFormat ( SessionPool ) ;
796+
797+ var exception = Assert . Throws < NotSupportedException > ( ( ) => format . Replay ( reader , context ) ) ;
798+
799+ Assert . Equal ( "Unsupported legacy binary journal command format version at byte offset 0: 1." , exception . Message ) ;
800+ Assert . Null ( exception . InnerException ) ;
801+ Assert . Empty ( consumer . Entries ) ;
802+ Assert . Equal ( originalLength , reader . Length ) ;
803+ }
612804}
0 commit comments