@@ -723,155 +723,56 @@ public IReadOnlyList<Activity> StoppedActivities
723723
724724 /// <summary>
725725 /// Issue #479: when the first attempt succeeds, no `retry.attempt`
726- /// events should be emitted (there were no retries) .
726+ /// events fire .
727727 /// </summary>
728728 [ Fact ]
729- public async Task RetryTelemetry_NoRetryNeeded_EmitsNoAttemptEvents_Issue479 ( )
729+ public async Task RetryTelemetry_NoRetry_EmitsNoEvents_Issue479 ( )
730730 {
731731 using var capture = new ActivityCapture ( "TestSource" ) ;
732+ var mockHandler = new MockHttpMessageHandler ( new HttpResponseMessage ( HttpStatusCode . OK ) ) ;
733+ var retryHandler = new RetryHttpHandler ( mockHandler , new MockActivityTracer ( ) , 5 , 5 , true , true ) ;
732734
733- var mockHandler = new MockHttpMessageHandler (
734- new HttpResponseMessage ( HttpStatusCode . OK )
735- {
736- Content = new StringContent ( "Success" )
737- } ) ;
738-
739- var mockTracer = new MockActivityTracer ( ) ;
740- var retryHandler = new RetryHttpHandler ( mockHandler , mockTracer , 5 , 5 , true , true ) ;
741-
742- var httpClient = new HttpClient ( retryHandler ) ;
743- var response = await httpClient . GetAsync ( "http://test.com" ) ;
744-
745- Assert . Equal ( HttpStatusCode . OK , response . StatusCode ) ;
746- Assert . Equal ( 1 , mockHandler . RequestCount ) ;
747-
748- Activity ? sendAsyncActivity = capture . StoppedActivities
749- . FirstOrDefault ( a => a . OperationName == "SendAsync" ) ;
750- Assert . NotNull ( sendAsyncActivity ) ;
735+ await new HttpClient ( retryHandler ) . GetAsync ( "http://test.com" ) ;
751736
752- var retryEvents = sendAsyncActivity ! . Events . Where ( e => e . Name == "retry.attempt" ) . ToList ( ) ;
753- Assert . Empty ( retryEvents ) ;
737+ Activity sendAsync = capture . StoppedActivities . Single ( a => a . OperationName == "SendAsync" ) ;
738+ Assert . DoesNotContain ( sendAsync . Events , e => e . Name == "retry.attempt" ) ;
754739 }
755740
756741 /// <summary>
757- /// Issue #479: each retry triggered by a Retry-After 503 response must
758- /// emit a `retry.attempt` event carrying `attempt_number`, `delay_ms`,
759- /// and a `reason` describing why the retry was scheduled.
742+ /// Issue #479: each retry triggered by a Retry-After response emits a
743+ /// `retry.attempt` event with attempt_number / delay_ms / reason. The
744+ /// reason string must reference the status code so 429 throttles are
745+ /// distinguishable from 503s.
760746 /// </summary>
761- [ Fact ]
762- public async Task RetryTelemetry_RetryAfter503_EmitsAttemptEvents_Issue479 ( )
747+ [ Theory ]
748+ [ InlineData ( HttpStatusCode . ServiceUnavailable , "503" ) ]
749+ [ InlineData ( ( HttpStatusCode ) 429 , "429" ) ]
750+ public async Task RetryTelemetry_RetryAfter_EmitsAttemptEvents_Issue479 ( HttpStatusCode status , string expectedReasonSubstring )
763751 {
764752 using var capture = new ActivityCapture ( "TestSource" ) ;
765753
766- var mockHandler = new MockHttpMessageHandler (
767- new HttpResponseMessage ( HttpStatusCode . ServiceUnavailable )
768- {
769- Headers = { { "Retry-After" , "1" } } ,
770- Content = new StringContent ( "Service Unavailable" )
771- } ) ;
772-
773- // Succeed after 2 503 responses (i.e. 2 retries).
774- mockHandler . SetResponseAfterRetryCount ( 2 , new HttpResponseMessage ( HttpStatusCode . OK )
754+ var mockHandler = new MockHttpMessageHandler ( new HttpResponseMessage ( status )
775755 {
776- Content = new StringContent ( "Success" )
756+ Headers = { { "Retry-After" , "1" } }
777757 } ) ;
758+ mockHandler . SetResponseAfterRetryCount ( 2 , new HttpResponseMessage ( HttpStatusCode . OK ) ) ;
778759
779- var mockTracer = new MockActivityTracer ( ) ;
780- var retryHandler = new RetryHttpHandler ( mockHandler , mockTracer , 10 , 10 , true , true ) ;
760+ var retryHandler = new RetryHttpHandler ( mockHandler , new MockActivityTracer ( ) , 10 , 10 , true , true ) ;
761+ await new HttpClient ( retryHandler ) . GetAsync ( "http://test.com" ) ;
781762
782- var httpClient = new HttpClient ( retryHandler ) ;
783- var response = await httpClient . GetAsync ( "http://test.com" ) ;
763+ Activity sendAsync = capture . StoppedActivities . Single ( a => a . OperationName == "SendAsync" ) ;
764+ var events = sendAsync . Events . Where ( e => e . Name == "retry.attempt" ) . ToList ( ) ;
765+ Assert . Equal ( 2 , events . Count ) ;
784766
785- Assert . Equal ( HttpStatusCode . OK , response . StatusCode ) ;
786- Assert . Equal ( 3 , mockHandler . RequestCount ) ; // initial + 2 retries
787-
788- Activity ? sendAsyncActivity = capture . StoppedActivities
789- . FirstOrDefault ( a => a . OperationName == "SendAsync" ) ;
790- Assert . NotNull ( sendAsyncActivity ) ;
791-
792- // We retried twice → two retry.attempt events.
793- var retryEvents = sendAsyncActivity ! . Events
794- . Where ( e => e . Name == "retry.attempt" )
795- . ToList ( ) ;
796- Assert . Equal ( 2 , retryEvents . Count ) ;
797-
798- // Every retry.attempt event must carry attempt_number, delay_ms,
799- // and reason. attempt_number must be monotonically increasing
800- // starting at 1 (the first retry is the 1st *retry*, not the 0th).
801- for ( int i = 0 ; i < retryEvents . Count ; i ++ )
767+ for ( int i = 0 ; i < events . Count ; i ++ )
802768 {
803- var evt = retryEvents [ i ] ;
804- var tags = evt . Tags . ToList ( ) ;
805-
806- var attemptTag = tags . FirstOrDefault ( t => t . Key == "attempt_number" ) ;
807- Assert . True ( attemptTag . Key == "attempt_number" ,
808- $ "retry.attempt event #{ i } missing attempt_number. Tags: [" +
809- string . Join ( ", " , tags . Select ( t => $ "{ t . Key } ={ t . Value } ") ) + "]" ) ;
810- Assert . Equal ( i + 1 , Convert . ToInt32 ( attemptTag . Value ) ) ;
811-
812- var delayTag = tags . FirstOrDefault ( t => t . Key == "delay_ms" ) ;
813- Assert . True ( delayTag . Key == "delay_ms" ,
814- $ "retry.attempt event #{ i } missing delay_ms. Tags: [" +
815- string . Join ( ", " , tags . Select ( t => $ "{ t . Key } ={ t . Value } ") ) + "]" ) ;
816- // Retry-After of "1" → 1000ms.
817- Assert . True ( Convert . ToInt64 ( delayTag . Value ) >= 1000 ,
818- $ "retry.attempt event #{ i } delay_ms should be >= 1000 (Retry-After: 1). " +
819- $ "Got: { delayTag . Value } ") ;
820-
821- var reasonTag = tags . FirstOrDefault ( t => t . Key == "reason" ) ;
822- Assert . True ( reasonTag . Key == "reason" ,
823- $ "retry.attempt event #{ i } missing reason. Tags: [" +
824- string . Join ( ", " , tags . Select ( t => $ "{ t . Key } ={ t . Value } ") ) + "]" ) ;
825- var reason = reasonTag . Value as string ;
826- Assert . NotNull ( reason ) ;
827- // We don't pin the exact spelling, but it must reference 503.
828- Assert . Contains ( "503" , reason ! ) ;
769+ var tags = events [ i ] . Tags . ToDictionary ( t => t . Key , t => t . Value ) ;
770+ Assert . Equal ( i + 1 , Convert . ToInt32 ( tags [ "attempt_number" ] ) ) ;
771+ Assert . True ( Convert . ToInt64 ( tags [ "delay_ms" ] ) >= 1000 ) ; // Retry-After: 1 → 1000ms
772+ Assert . Contains ( expectedReasonSubstring , ( string ) tags [ "reason" ] ! ) ;
829773 }
830774 }
831775
832- /// <summary>
833- /// Issue #479: rate-limit 429 retries must also emit `retry.attempt`
834- /// events with a 429-shaped `reason` so the trace makes the throttle
835- /// case distinguishable from generic 503.
836- /// </summary>
837- [ Fact ]
838- public async Task RetryTelemetry_RetryAfter429_EmitsAttemptEventWithRateLimitReason_Issue479 ( )
839- {
840- using var capture = new ActivityCapture ( "TestSource" ) ;
841-
842- var mockHandler = new MockHttpMessageHandler (
843- new HttpResponseMessage ( ( HttpStatusCode ) 429 )
844- {
845- Headers = { { "Retry-After" , "1" } } ,
846- Content = new StringContent ( "Too Many Requests" )
847- } ) ;
848- mockHandler . SetResponseAfterRetryCount ( 1 , new HttpResponseMessage ( HttpStatusCode . OK )
849- {
850- Content = new StringContent ( "Success" )
851- } ) ;
852-
853- var mockTracer = new MockActivityTracer ( ) ;
854- var retryHandler = new RetryHttpHandler ( mockHandler , mockTracer , 10 , 10 , true , true ) ;
855-
856- var httpClient = new HttpClient ( retryHandler ) ;
857- var response = await httpClient . GetAsync ( "http://test.com" ) ;
858-
859- Assert . Equal ( HttpStatusCode . OK , response . StatusCode ) ;
860-
861- Activity ? sendAsyncActivity = capture . StoppedActivities
862- . FirstOrDefault ( a => a . OperationName == "SendAsync" ) ;
863- Assert . NotNull ( sendAsyncActivity ) ;
864-
865- var retryEvents = sendAsyncActivity ! . Events . Where ( e => e . Name == "retry.attempt" ) . ToList ( ) ;
866- Assert . Single ( retryEvents ) ;
867- var reasonTag = retryEvents [ 0 ] . Tags . FirstOrDefault ( t => t . Key == "reason" ) ;
868- Assert . True ( reasonTag . Key == "reason" ) ;
869- var reason = reasonTag . Value as string ;
870- Assert . NotNull ( reason ) ;
871- // Must reference 429 so 429 retries are distinguishable from 503.
872- Assert . Contains ( "429" , reason ! ) ;
873- }
874-
875776 /// <summary>
876777 /// Mock HttpMessageHandler for testing the RetryHttpHandler.
877778 /// </summary>
0 commit comments