@@ -253,7 +253,7 @@ await transportConnection.Application.Output.WriteAsync(
253253 }
254254
255255 [ Fact ]
256- public async Task ClientConnectionOutgoingAbortCanEndLifeTime ( )
256+ public async Task TestClientConnectionOutgoingAbortCanEndLifeTime ( )
257257 {
258258 using ( StartVerifiableLog ( out var loggerFactory , LogLevel . Warning , expectedErrors : c => true ,
259259 logChecker : logs =>
@@ -310,7 +310,7 @@ await transportConnection.Application.Output.WriteAsync(
310310 }
311311
312312 [ Fact ]
313- public async Task ClientConnectionContextAbortCanSendOutCloseMessage ( )
313+ public async Task TestClientConnectionContextAbortCanSendOutCloseMessage ( )
314314 {
315315 using ( StartVerifiableLog ( out var loggerFactory , LogLevel . Warning , expectedErrors : c => true ,
316316 logChecker : logs =>
@@ -377,7 +377,7 @@ await transportConnection.Application.Output.WriteAsync(
377377 }
378378
379379 [ Fact ]
380- public async Task ClientConnectionWithDiagnosticClientTagTest ( )
380+ public async Task TestClientConnectionWithDiagnosticClientTagTest ( )
381381 {
382382 using ( StartVerifiableLog ( out var loggerFactory , LogLevel . Debug ) )
383383 {
@@ -434,7 +434,7 @@ await transportConnection.Application.Output.WriteAsync(
434434 }
435435
436436 [ Fact ]
437- public async Task ClientConnectionLastWillCanSendOut ( )
437+ public async Task TestClientConnectionLastWillCanSendOut ( )
438438 {
439439 using ( StartVerifiableLog ( out var loggerFactory , LogLevel . Warning , expectedErrors : c => true ,
440440 logChecker : logs =>
@@ -489,6 +489,166 @@ await transportConnection.Application.Output.WriteAsync(
489489 }
490490 }
491491
492+ [ Fact ]
493+ public async Task TestPartialMessagesShouldFlushCorrectly ( )
494+ {
495+ using ( StartVerifiableLog ( out var loggerFactory , LogLevel . Warning , expectedErrors : c => true ,
496+ logChecker : logs =>
497+ {
498+ Assert . Empty ( logs ) ;
499+ return true ;
500+ } ) )
501+ {
502+ var ccm = new TestClientConnectionManager ( ) ;
503+ var ccf = new ClientConnectionFactory ( ) ;
504+ var protocol = new ServiceProtocol ( ) ;
505+ TestConnection transportConnection = null ;
506+ var connectionFactory = new TestConnectionFactory ( conn =>
507+ {
508+ transportConnection = conn ;
509+ return Task . CompletedTask ;
510+ } ) ;
511+ var services = new ServiceCollection ( ) ;
512+
513+ var connectionHandler = new TextContentConnectionHandler ( ) ;
514+ services . AddSingleton ( connectionHandler ) ;
515+ var builder = new ConnectionBuilder ( services . BuildServiceProvider ( ) ) ;
516+ builder . UseConnectionHandler < TextContentConnectionHandler > ( ) ;
517+ ConnectionDelegate handler = builder . Build ( ) ;
518+ var connection = new ServiceConnection ( protocol , ccm , connectionFactory , loggerFactory , handler , ccf ,
519+ "serverId" , Guid . NewGuid ( ) . ToString ( "N" ) , null , null , null , new DefaultClientInvocationManager ( ) , new AckHandler ( ) , closeTimeOutMilliseconds : 500 ) ;
520+
521+ var connectionTask = connection . StartAsync ( ) ;
522+
523+ // completed handshake
524+ await connection . ConnectionInitializedTask . OrTimeout ( ) ;
525+ Assert . Equal ( ServiceConnectionStatus . Connected , connection . Status ) ;
526+ var clientConnectionId = Guid . NewGuid ( ) . ToString ( ) ;
527+
528+ var waitClientTask = ccm . WaitForClientConnectionAsync ( clientConnectionId ) ;
529+ await transportConnection . Application . Output . WriteAsync (
530+ protocol . GetMessageBytes ( new OpenConnectionMessage ( clientConnectionId , new Claim [ ] { } ) ) ) ;
531+
532+ var clientConnection = await waitClientTask . OrTimeout ( ) ;
533+
534+ var enumerator = connectionHandler . EnumerateContent ( ) . GetAsyncEnumerator ( ) ;
535+
536+ // for normal message, it should flush immediately.
537+ await transportConnection . Application . Output . WriteAsync (
538+ protocol . GetMessageBytes ( new ConnectionDataMessage ( clientConnectionId , Encoding . UTF8 . GetBytes ( "{\" protocol\" :\" json\" ,\" version\" :1}\u001e " ) ) ) ) ;
539+ await enumerator . MoveNextAsync ( ) ;
540+ Assert . Equal ( "{\" protocol\" :\" json\" ,\" version\" :1}\u001e " , enumerator . Current ) ;
541+
542+ // for partial message, it should wait for next message to complete.
543+ await transportConnection . Application . Output . WriteAsync (
544+ protocol . GetMessageBytes ( new ConnectionDataMessage ( clientConnectionId , Encoding . UTF8 . GetBytes ( "{\" type\" :1," ) ) { IsPartial = true } ) ) ;
545+ var delay = Task . Delay ( 100 ) ;
546+ var moveNextTask = enumerator . MoveNextAsync ( ) . AsTask ( ) ;
547+ Assert . Same ( delay , await Task . WhenAny ( delay , moveNextTask ) ) ;
548+
549+ // when next message comes, it should complete the previous partial message.
550+ await transportConnection . Application . Output . WriteAsync (
551+ protocol . GetMessageBytes ( new ConnectionDataMessage ( clientConnectionId , Encoding . UTF8 . GetBytes ( "\" target\" :\" method\" }\u001e " ) ) ) ) ;
552+ await moveNextTask ;
553+ if ( enumerator . Current == "{\" type\" :1," )
554+ {
555+ Assert . Equal ( "{\" type\" :1," , enumerator . Current ) ;
556+ await enumerator . MoveNextAsync ( ) ;
557+ Assert . Equal ( "\" target\" :\" method\" }\u001e " , enumerator . Current ) ;
558+ }
559+ else
560+ {
561+ // maybe merged into one message.
562+ Assert . Equal ( "{\" type\" :1,\" target\" :\" method\" }\u001e " , enumerator . Current ) ;
563+ }
564+
565+ // complete reading to end the connection
566+ transportConnection . Application . Output . Complete ( ) ;
567+
568+ await clientConnection . LifetimeTask . OrTimeout ( ) ;
569+
570+ // 1s for application task to timeout
571+ await connectionTask . OrTimeout ( 1000 ) ;
572+ Assert . Equal ( ServiceConnectionStatus . Disconnected , connection . Status ) ;
573+ Assert . Empty ( ccm . ClientConnections ) ;
574+ }
575+ }
576+
577+ [ Fact ]
578+ public async Task TestPartialMessagesShouldBeRemovedWhenReconnected ( )
579+ {
580+ using ( StartVerifiableLog ( out var loggerFactory , LogLevel . Warning , expectedErrors : c => true ,
581+ logChecker : logs =>
582+ {
583+ Assert . Empty ( logs ) ;
584+ return true ;
585+ } ) )
586+ {
587+ var ccm = new TestClientConnectionManager ( ) ;
588+ var ccf = new ClientConnectionFactory ( ) ;
589+ var protocol = new ServiceProtocol ( ) ;
590+ TestConnection transportConnection = null ;
591+ var connectionFactory = new TestConnectionFactory ( conn =>
592+ {
593+ transportConnection = conn ;
594+ return Task . CompletedTask ;
595+ } ) ;
596+ var services = new ServiceCollection ( ) ;
597+
598+ var connectionHandler = new TextContentConnectionHandler ( ) ;
599+ services . AddSingleton ( connectionHandler ) ;
600+ var builder = new ConnectionBuilder ( services . BuildServiceProvider ( ) ) ;
601+ builder . UseConnectionHandler < TextContentConnectionHandler > ( ) ;
602+ ConnectionDelegate handler = builder . Build ( ) ;
603+ var connection = new ServiceConnection ( protocol , ccm , connectionFactory , loggerFactory , handler , ccf ,
604+ "serverId" , Guid . NewGuid ( ) . ToString ( "N" ) , null , null , null , new DefaultClientInvocationManager ( ) , new AckHandler ( ) , closeTimeOutMilliseconds : 500 ) ;
605+
606+ var connectionTask = connection . StartAsync ( ) ;
607+
608+ // completed handshake
609+ await connection . ConnectionInitializedTask . OrTimeout ( ) ;
610+ Assert . Equal ( ServiceConnectionStatus . Connected , connection . Status ) ;
611+ var clientConnectionId = Guid . NewGuid ( ) . ToString ( ) ;
612+
613+ var waitClientTask = ccm . WaitForClientConnectionAsync ( clientConnectionId ) ;
614+ await transportConnection . Application . Output . WriteAsync (
615+ protocol . GetMessageBytes ( new OpenConnectionMessage ( clientConnectionId , new Claim [ ] { } ) ) ) ;
616+
617+ var clientConnection = await waitClientTask . OrTimeout ( ) ;
618+
619+ var enumerator = connectionHandler . EnumerateContent ( ) . GetAsyncEnumerator ( ) ;
620+
621+ // for partial message, it should wait for next message to complete.
622+ await transportConnection . Application . Output . WriteAsync (
623+ protocol . GetMessageBytes ( new ConnectionDataMessage ( clientConnectionId , Encoding . UTF8 . GetBytes ( "{\" type\" :1," ) ) { IsPartial = true } ) ) ;
624+ var delay = Task . Delay ( 100 ) ;
625+ var moveNextTask = enumerator . MoveNextAsync ( ) . AsTask ( ) ;
626+ Assert . Same ( delay , await Task . WhenAny ( delay , moveNextTask ) ) ;
627+
628+ // for reconnect message, it should remove all partial messages for the connection.
629+ await transportConnection . Application . Output . WriteAsync (
630+ protocol . GetMessageBytes ( new ConnectionReconnectMessage ( clientConnectionId ) ) ) ;
631+ delay = Task . Delay ( 100 ) ;
632+ Assert . Same ( delay , await Task . WhenAny ( delay , moveNextTask ) ) ;
633+
634+ // when next message comes, there is no partial message.
635+ await transportConnection . Application . Output . WriteAsync (
636+ protocol . GetMessageBytes ( new ConnectionDataMessage ( clientConnectionId , Encoding . UTF8 . GetBytes ( "{\" type\" :1,\" target\" :\" method\" }\u001e " ) ) ) ) ;
637+ await moveNextTask ;
638+ Assert . Equal ( "{\" type\" :1,\" target\" :\" method\" }\u001e " , enumerator . Current ) ;
639+
640+ // complete reading to end the connection
641+ transportConnection . Application . Output . Complete ( ) ;
642+
643+ await clientConnection . LifetimeTask . OrTimeout ( ) ;
644+
645+ // 1s for application task to timeout
646+ await connectionTask . OrTimeout ( 1000 ) ;
647+ Assert . Equal ( ServiceConnectionStatus . Disconnected , connection . Status ) ;
648+ Assert . Empty ( ccm . ClientConnections ) ;
649+ }
650+ }
651+
492652 private sealed class TestConnectionHandler : ConnectionHandler
493653 {
494654 private TaskCompletionSource < object > _startedTcs = new TaskCompletionSource < object > ( ) ;
@@ -587,6 +747,64 @@ public override Task OnConnectedAsync(ConnectionContext connection)
587747 }
588748 }
589749
750+ private sealed class TextContentConnectionHandler : ConnectionHandler
751+ {
752+ private TaskCompletionSource < object > _startedTcs = new TaskCompletionSource < object > ( ) ;
753+ private LinkedList < TaskCompletionSource < string > > _content = new LinkedList < TaskCompletionSource < string > > ( ) ;
754+
755+ public TextContentConnectionHandler ( )
756+ {
757+ _content . AddFirst ( new TaskCompletionSource < string > ( ) ) ;
758+ }
759+
760+ public Task Started => _startedTcs . Task ;
761+
762+ public override async Task OnConnectedAsync ( ConnectionContext connection )
763+ {
764+ _startedTcs . TrySetResult ( null ) ;
765+
766+ while ( true )
767+ {
768+ var result = await connection . Transport . Input . ReadAsync ( ) ;
769+
770+ try
771+ {
772+ if ( ! result . Buffer . IsEmpty )
773+ {
774+ var last = _content . Last . Value ;
775+ _content . AddLast ( new TaskCompletionSource < string > ( ) ) ;
776+ var text = Encoding . UTF8 . GetString ( result . Buffer ) ;
777+ last . TrySetResult ( text ) ;
778+ }
779+
780+ if ( result . IsCompleted )
781+ {
782+ _content . Last . Value . TrySetResult ( null ) ;
783+ break ;
784+ }
785+ }
786+ finally
787+ {
788+ connection . Transport . Input . AdvanceTo ( result . Buffer . End ) ;
789+ }
790+ }
791+ }
792+
793+ public async IAsyncEnumerable < string > EnumerateContent ( )
794+ {
795+ while ( true )
796+ {
797+ var result = await _content . First . Value . Task ;
798+ _content . RemoveFirst ( ) ;
799+ if ( result == null )
800+ {
801+ yield break ;
802+ }
803+ yield return result ;
804+ }
805+ }
806+ }
807+
590808 internal sealed class TestClientConnectionManager : IClientConnectionManager
591809 {
592810 private readonly ClientConnectionManager _ccm = new ClientConnectionManager ( ) ;
0 commit comments