@@ -255,16 +255,22 @@ def _drain(self, deadline: Optional[float]) -> Tuple[int, bool]:
255255 and not self ._retry_after .is_open ()
256256 )
257257
258- spans = self ._encode (batch )
259- if not spans :
258+ spans , failed = self ._encode (batch )
259+ if failed :
260+ # Out of the queue before the send is settled, so a failed
261+ # send does not count them a second time.
260262 with self ._lock :
261263 if self ._is_closed ():
262264 return removed , True
263- del self ._queue [:size ]
264- self ._reset_head_batch_budget_locked ()
265- remaining -= size
266- removed += size
267- continue
265+ for index in reversed (failed ):
266+ del self ._queue [index ]
267+ if not spans :
268+ self ._reset_head_batch_budget_locked ()
269+ size -= len (failed )
270+ remaining -= len (failed )
271+ removed += len (failed )
272+ if not spans :
273+ continue
268274
269275 # The first request is exempt, so a flush called with no budget left
270276 # (a serverless handler, say) still ships a batch.
@@ -361,15 +367,18 @@ def _is_closed(self) -> bool:
361367 # A method, so the check after each send is not narrowed away.
362368 return self ._closed
363369
364- def _encode (self , batch : List [SpanRecord ]) -> List [dict ]:
370+ def _encode (self , batch : List [SpanRecord ]) -> Tuple [List [dict ], List [int ]]:
371+ """The batch as OTLP spans, and the indexes of records that could not be encoded."""
365372 encoded : List [dict ] = []
366- for record in batch :
373+ failed : List [int ] = []
374+ for index , record in enumerate (batch ):
367375 try :
368376 encoded .append (build_otlp_span (record ))
369377 except Exception :
370378 log .debug ("Failed to encode a span; dropping it" , exc_info = True )
371379 self ._drops .record (1 , "its attributes could not be encoded" )
372- return encoded
380+ failed .append (index )
381+ return encoded , failed
373382
374383 def _discard_if_disabled_locked (self ) -> bool :
375384 if not getattr (self ._client , "disabled" , False ):
@@ -431,16 +440,15 @@ def _rearm_after_pass_locked(self) -> None:
431440 self ._arm_timer_locked (delay , replace = True )
432441
433442 def _arm_timer_locked (self , delay : float , replace : bool = False ) -> None :
434- if self ._flush_timer is not None :
435- if not replace :
436- return
437- self ._flush_timer .cancel ()
438- self ._flush_timer = None
443+ if self ._flush_timer is not None and not replace :
444+ return
439445 timer = threading .Timer (delay , lambda : self ._timer_flush (timer ))
440446 timer .daemon = True
441- # Registered only once started , so a failed start leaves nothing
442- # behind and the next span end arms again .
447+ # Started before the old timer is cancelled , so a failed start keeps
448+ # whatever flush was already scheduled .
443449 timer .start ()
450+ if self ._flush_timer is not None :
451+ self ._flush_timer .cancel ()
444452 self ._flush_timer = timer
445453 self ._flush_timer_fires_at = time .monotonic () + delay
446454
0 commit comments