1111
1212from __future__ import annotations
1313
14- import json
1514import logging
1615import threading
1716import time
@@ -183,6 +182,13 @@ def __deepcopy__(self, memo: dict[int, Any]) -> Any:
183182 raise RuntimeError ("uncopyable payload" )
184183
185184
185+ class _Unsized :
186+ """A payload whose custom ``__sizeof__`` raises."""
187+
188+ def __sizeof__ (self ) -> int :
189+ raise RuntimeError ("size unavailable" )
190+
191+
186192# -- lazy worker creation / one worker per exporter --------------------------
187193
188194
@@ -220,6 +226,25 @@ def test_one_worker_per_exporter():
220226 )
221227
222228
229+ def test_worker_start_failure_disables_lane_and_fails_barrier (monkeypatch ):
230+ exporter = RecordingExporter ()
231+ scheduler = _ExportScheduler ([exporter ])
232+ lane = scheduler ._lanes [0 ]
233+
234+ def fail_start (self ):
235+ raise RuntimeError ("cannot start new thread" )
236+
237+ monkeypatch .setattr (threading .Thread , "start" , fail_start )
238+ scheduler .schedule (ARN_A , _rec (ARN_A , "a1" ))
239+
240+ assert lane ._disabled is True
241+ assert lane ._pending_count () == 0
242+ assert lane ._pending_bytes_count () == 0
243+ assert lane ._queue_len () == 0
244+ assert scheduler .end_invocation (0.1 ) is False
245+ assert exporter .exported_values () == []
246+
247+
223248def test_repeated_scheduling_does_not_grow_threads ():
224249 base = _insight_thread_count ()
225250 exporter = RecordingExporter ()
@@ -629,7 +654,7 @@ def test_pending_record_cap_preserves_original_lane_memory_bound():
629654 assert exporter .exported_values () == ["a1" , "c1" , "d1" , "a2" ]
630655
631656
632- def test_unmeasurable_record_does_not_evict_existing_backlog ():
657+ def test_non_json_record_reaches_exporter_without_evicting_backlog ():
633658 exporter = BlockingExporter ()
634659 scheduler = _ExportScheduler ([exporter ])
635660 lane = scheduler ._lanes [0 ]
@@ -638,19 +663,19 @@ def test_unmeasurable_record_does_not_evict_existing_backlog():
638663 scheduler .schedule (ARN_B , _rec (ARN_B , "b1" ))
639664 scheduler .schedule (ARN_C , _rec (ARN_C , "c1" ))
640665
641- unmeasurable = _rec (ARN_D , "bad " )
642- unmeasurable ["payload" ] = {"not-json" }
643- scheduler .schedule (ARN_D , unmeasurable )
666+ non_json = _rec (ARN_D , "custom " )
667+ non_json ["payload" ] = {"not-json" }
668+ scheduler .schedule (ARN_D , non_json )
644669
645- assert lane ._pending_count () == 2
670+ assert lane ._pending_count () == 3
646671 exporter .release ()
647672 scheduler .end_invocation (5.0 )
648- assert exporter .exported_values () == ["inflight" , "b1" , "c1" ]
673+ assert exporter .exported_values () == ["inflight" , "b1" , "c1" , "custom" ]
649674
650675
651676def test_individually_over_budget_record_does_not_evict_existing_backlog ():
652677 exporter = BlockingExporter ()
653- scheduler = _ExportScheduler ([exporter ], max_pending_bytes = 2_000 )
678+ scheduler = _ExportScheduler ([exporter ], max_pending_bytes = 3_500 )
654679 lane = scheduler ._lanes [0 ]
655680 scheduler .schedule (ARN_A , _rec (ARN_A , "inflight" ))
656681 assert _wait_until (exporter .started .is_set )
@@ -659,29 +684,28 @@ def test_individually_over_budget_record_does_not_evict_existing_backlog():
659684 scheduler .schedule (ARN_D , _rec (ARN_D , "d" * 3_000 ))
660685
661686 assert lane ._pending_count () == 2
662- assert lane ._pending_bytes_count () <= 2_000
687+ assert lane ._pending_bytes_count () <= 3_500
663688 exporter .release ()
664689 scheduler .end_invocation (5.0 )
665690 exported = exporter .exported_values ()
666691 assert exported [0 ] == "inflight"
667692 assert exported [1 :] == ["b" * 700 , "c" * 700 ]
668693
669694
670- def test_record_sizing_exception_does_not_escape_schedule (monkeypatch ):
695+ def test_record_sizing_exception_does_not_escape_schedule ():
671696 exporter = BlockingExporter ()
672697 scheduler = _ExportScheduler ([exporter ])
673698 scheduler .schedule (ARN_A , _rec (ARN_A , "inflight" ))
674699 assert _wait_until (exporter .started .is_set )
675700
676- def fail_sizing (* args , ** kwargs ):
677- raise RecursionError ("record nesting is too deep" )
678-
679- monkeypatch .setattr (json , "dumps" , fail_sizing )
680- scheduler .schedule (ARN_B , _rec (ARN_B , "too-deep" ))
701+ record = _rec (ARN_B , "custom-sized" )
702+ record ["payload" ] = _Unsized ()
703+ scheduler .schedule (ARN_B , record )
681704
682- assert scheduler ._lanes [0 ]._pending_count () == 0
705+ assert scheduler ._lanes [0 ]._pending_count () == 1
683706 exporter .release ()
684707 scheduler .end_invocation (5.0 )
708+ assert exporter .exported_values () == ["inflight" , "custom-sized" ]
685709
686710
687711def test_pending_record_cap_evicts_true_oldest_across_arns ():
@@ -703,7 +727,7 @@ def test_pending_record_cap_evicts_true_oldest_across_arns():
703727
704728def test_pending_byte_budget_evicts_oldest_large_record ():
705729 exporter = BlockingExporter ()
706- scheduler = _ExportScheduler ([exporter ], max_pending_bytes = 2_000 )
730+ scheduler = _ExportScheduler ([exporter ], max_pending_bytes = 3_000 )
707731 lane = scheduler ._lanes [0 ]
708732 scheduler .schedule (ARN_A , _rec (ARN_A , "inflight" ))
709733 assert _wait_until (exporter .started .is_set )
@@ -712,7 +736,7 @@ def test_pending_byte_budget_evicts_oldest_large_record():
712736 scheduler .schedule (ARN_C , _rec (ARN_C , "c" * 1_500 ))
713737
714738 assert lane ._pending_record_count () == 1
715- assert lane ._pending_bytes_count () <= 2_000
739+ assert lane ._pending_bytes_count () <= 3_000
716740 exporter .release ()
717741 scheduler .end_invocation (5.0 )
718742 exported = exporter .exported_values ()
0 commit comments