2020from unittest import TestCase
2121from unittest .mock import ANY , Mock , patch
2222
23- from google .rpc .code_pb2 import ( # pylint: disable=no-name-in-module
24- ALREADY_EXISTS ,
25- OK ,
26- UNAVAILABLE ,
27- )
28- from google .rpc .status_pb2 import Status # pylint: disable=no-name-in-module
29- from grpc import Compression , server
30- from grpc_status import rpc_status
23+ from grpc import Compression , StatusCode , server
3124
3225from opentelemetry .exporter .otlp .proto .common .trace_encoder import (
3326 encode_spans ,
@@ -90,25 +83,36 @@ def shutdown(self, timeout_millis=30_000):
9083class TraceServiceServicerWithExportParams (TraceServiceServicer ):
9184 def __init__ (
9285 self ,
93- export_result : int ,
86+ export_result : StatusCode ,
9487 optional_export_sleep : Optional [float ] = None ,
88+ optional_first_time_retry_millis : Optional [int ] = None ,
9589 ):
9690 self .export_result = export_result
9791 self .optional_export_sleep = optional_export_sleep
92+ self .optional_first_time_retry_millis = (
93+ optional_first_time_retry_millis
94+ )
95+ self .first_attempt = True
96+ self .num_requests = 0
97+ self .now = time .time ()
9898
9999 # pylint: disable=invalid-name,unused-argument
100100 def Export (self , request , context ):
101- logger . warning ( "Export Request Received" )
101+ self . num_requests += 1
102102 if self .optional_export_sleep :
103103 time .sleep (self .optional_export_sleep )
104- if self .export_result != OK :
105- context .abort_with_status (
106- rpc_status .to_status (
107- Status (
108- code = self .export_result ,
104+ if self .export_result != StatusCode .OK :
105+ if self .optional_first_time_retry_millis and self .first_attempt :
106+ self .first_attempt = False
107+ context .set_trailing_metadata (
108+ (
109+ (
110+ "grpc-retry-pushback-ms" ,
111+ str (self .optional_first_time_retry_millis ),
112+ ),
109113 )
110114 )
111- )
115+ context . abort ( self . export_result , "" )
112116
113117 return ExportTraceServiceResponse ()
114118
@@ -287,7 +291,7 @@ def test_otlp_exporter_otlp_compression_envvar(
287291
288292 def test_shutdown (self ):
289293 add_TraceServiceServicer_to_server (
290- TraceServiceServicerWithExportParams (OK ),
294+ TraceServiceServicerWithExportParams (StatusCode . OK ),
291295 self .server ,
292296 )
293297 self .assertEqual (
@@ -305,7 +309,9 @@ def test_shutdown(self):
305309
306310 def test_shutdown_wait_last_export (self ):
307311 add_TraceServiceServicer_to_server (
308- TraceServiceServicerWithExportParams (OK , optional_export_sleep = 1 ),
312+ TraceServiceServicerWithExportParams (
313+ StatusCode .OK , optional_export_sleep = 1
314+ ),
309315 self .server ,
310316 )
311317
@@ -324,7 +330,9 @@ def test_shutdown_wait_last_export(self):
324330
325331 def test_shutdown_doesnot_wait_last_export (self ):
326332 add_TraceServiceServicer_to_server (
327- TraceServiceServicerWithExportParams (OK , optional_export_sleep = 3 ),
333+ TraceServiceServicerWithExportParams (
334+ StatusCode .OK , optional_export_sleep = 3
335+ ),
328336 self .server ,
329337 )
330338
@@ -346,7 +354,7 @@ def test_export_over_closed_grpc_channel(self):
346354 # pylint: disable=protected-access
347355
348356 add_TraceServiceServicer_to_server (
349- TraceServiceServicerWithExportParams (OK ),
357+ TraceServiceServicerWithExportParams (StatusCode . OK ),
350358 self .server ,
351359 )
352360 self .exporter .export ([self .span ])
@@ -358,61 +366,56 @@ def test_export_over_closed_grpc_channel(self):
358366 str (err .exception ), "Cannot invoke RPC on closed channel!"
359367 )
360368
369+ def test_retry_with_server_pushback (self ):
370+ mock_trace_service = TraceServiceServicerWithExportParams (
371+ StatusCode .UNAVAILABLE , optional_first_time_retry_millis = 200
372+ )
373+ add_TraceServiceServicer_to_server (
374+ mock_trace_service ,
375+ self .server ,
376+ )
377+ exporter = OTLPSpanExporterForTesting (insecure = True , timeout = 10 )
378+ before = time .time ()
379+ self .assertEqual (
380+ exporter .export ([self .span ]),
381+ SpanExportResult .FAILURE ,
382+ )
383+ after = time .time ()
384+ # We set the `grpc-retry-pushback-ms` header to 200 millis on the first server response.
385+ # So we expect the first request at time 0, second at time 0.2,
386+ # third at 1.2 (start of backoff policy), fourth at time 3.2, last at time 7.2.
387+ self .assertEqual (mock_trace_service .num_requests , 5 )
388+ # The backoffs have a jitter +- 20%, so we have to put a higher bound than 7.2.
389+ self .assertTrue (after - before < 8.8 )
390+
361391 def test_retry_timeout (self ):
392+ mock_trace_service = TraceServiceServicerWithExportParams (
393+ StatusCode .UNAVAILABLE
394+ )
362395 add_TraceServiceServicer_to_server (
363- TraceServiceServicerWithExportParams ( UNAVAILABLE ) ,
396+ mock_trace_service ,
364397 self .server ,
365398 )
366399 # Set timeout to 1.5 seconds.
367400 exporter = OTLPSpanExporterForTesting (insecure = True , timeout = 1.5 )
368- with self .assertLogs (level = WARNING ) as warning :
369- self .assertEqual (
370- exporter .export ([self .span ]),
371- SpanExportResult .FAILURE ,
372- )
373- # Our GRPC retry policy starts with a 1 second backoff then doubles.
374- # So we expect just two calls: one at time 0, one at time 1.
375- # The final log is from when export fails.
376- self .assertEqual (len (warning .records ), 3 )
377- for idx , log in enumerate (warning .records ):
378- if idx != 2 :
379- self .assertEqual (
380- "Export Request Received" ,
381- log .message ,
382- )
383- else :
384- self .assertEqual (
385- "Failed to export traces to localhost:4317, error code: StatusCode.DEADLINE_EXCEEDED" ,
386- log .message ,
387- )
388- with self .assertLogs (level = WARNING ) as warning :
389- exporter = OTLPSpanExporterForTesting (insecure = True , timeout = 5 )
390- # pylint: disable=protected-access
391- self .assertEqual (exporter ._timeout , 5 )
392- self .assertEqual (
393- exporter .export ([self .span ]),
394- SpanExportResult .FAILURE ,
395- )
396- # We expect 3 calls: time 0, time 1, time 3, but not time 7.
397- # The final log is from when export fails.
398- self .assertEqual (len (warning .records ), 4 )
399- for idx , log in enumerate (warning .records ):
400- if idx != 3 :
401- self .assertEqual (
402- "Export Request Received" ,
403- log .message ,
404- )
405- else :
406- self .assertEqual (
407- "Failed to export traces to localhost:4317, error code: StatusCode.DEADLINE_EXCEEDED" ,
408- log .message ,
409- )
401+ before = time .time ()
402+ self .assertEqual (
403+ exporter .export ([self .span ]),
404+ SpanExportResult .FAILURE ,
405+ )
406+ after = time .time ()
407+ # Our retry starts with a 1 second backoff then doubles.
408+ # So we expect just two calls: one at time 0, one at time 1.
409+ self .assertEqual (mock_trace_service .num_requests , 2 )
410+ # gRPC retry config waits for the timeout (1.5) before cancelling the request.
411+ self .assertTrue (after - before < 1.6 )
410412
411413 def test_timeout_set_correctly (self ):
414+ mock_trace_service = TraceServiceServicerWithExportParams (
415+ StatusCode .OK , optional_export_sleep = 0.5
416+ )
412417 add_TraceServiceServicer_to_server (
413- TraceServiceServicerWithExportParams (
414- OK , optional_export_sleep = 0.5
415- ),
418+ mock_trace_service ,
416419 self .server ,
417420 )
418421 exporter = OTLPSpanExporterForTesting (insecure = True , timeout = 0.4 )
@@ -427,6 +430,7 @@ def test_timeout_set_correctly(self):
427430 "Failed to export traces to localhost:4317, error code: StatusCode.DEADLINE_EXCEEDED" ,
428431 warning .records [- 1 ].message ,
429432 )
433+ self .assertEqual (mock_trace_service .num_requests , 1 )
430434 exporter = OTLPSpanExporterForTesting (insecure = True , timeout = 0.8 )
431435 self .assertEqual (
432436 exporter .export ([self .span ]),
@@ -444,7 +448,9 @@ def test_otlp_headers_from_env(self):
444448 def test_permanent_failure (self ):
445449 with self .assertLogs (level = WARNING ) as warning :
446450 add_TraceServiceServicer_to_server (
447- TraceServiceServicerWithExportParams (ALREADY_EXISTS ),
451+ TraceServiceServicerWithExportParams (
452+ StatusCode .ALREADY_EXISTS
453+ ),
448454 self .server ,
449455 )
450456 self .assertEqual (
0 commit comments