@@ -151,6 +151,35 @@ def release(self) -> None:
151151 self ._release .set ()
152152
153153
154+ class BlockingBufferedExporter :
155+ """Blocks export and publishes buffered records only when flush runs."""
156+
157+ max_record_size_bytes = None
158+
159+ def __init__ (self ) -> None :
160+ self .started = threading .Event ()
161+ self ._release = threading .Event ()
162+ self .buffered : list [Any ] = []
163+ self .published : list [Any ] = []
164+ self .flushed = 0
165+
166+ def render (self , record : dict [str , Any ]) -> Any :
167+ return record
168+
169+ def export (self , record : dict [str , Any ]) -> None :
170+ self .started .set ()
171+ self ._release .wait (5.0 )
172+ self .buffered .append (record .get ("v" ))
173+
174+ def flush (self ) -> None :
175+ self .flushed += 1
176+ self .published .extend (self .buffered )
177+ self .buffered .clear ()
178+
179+ def release (self ) -> None :
180+ self ._release .set ()
181+
182+
154183class FailingExporter :
155184 """Raises in both export and flush."""
156185
@@ -200,6 +229,16 @@ def __sizeof__(self) -> int:
200229 raise RuntimeError ("size unavailable" )
201230
202231
232+ class _TrackedLargeList (list [Any ]):
233+ def __init__ (self ) -> None :
234+ super ().__init__ ([None ] * 10_000 )
235+ self .iterated = False
236+
237+ def __iter__ (self ):
238+ self .iterated = True
239+ return super ().__iter__ ()
240+
241+
203242# -- lazy worker creation / one worker per exporter --------------------------
204243
205244
@@ -731,6 +770,42 @@ def test_retained_size_traverses_slots_after_shallow_size_failure():
731770 assert exporter .exported_values () == ["inflight" ]
732771
733772
773+ def test_retained_size_saturates_before_traversing_large_shallow_container ():
774+ exporter = BlockingExporter ()
775+ scheduler = _ExportScheduler ([exporter ], max_pending_bytes = 2_500 )
776+ lane = scheduler ._lanes [0 ]
777+ scheduler .schedule (ARN_A , _rec (ARN_A , "inflight" ))
778+ assert _wait_until (exporter .started .is_set )
779+
780+ payload = _TrackedLargeList ()
781+ record = _rec (ARN_B , "large-shallow" )
782+ record ["payload" ] = payload
783+ scheduler .schedule (ARN_B , record )
784+
785+ assert payload .iterated is False
786+ assert lane ._pending_count () == 0
787+ assert lane ._pending_bytes_count () == 0
788+ exporter .release ()
789+ scheduler .end_invocation (5.0 )
790+
791+
792+ def test_retained_size_counts_memoryview_backing_buffer ():
793+ exporter = BlockingExporter ()
794+ scheduler = _ExportScheduler ([exporter ], max_pending_bytes = 2_500 )
795+ lane = scheduler ._lanes [0 ]
796+ scheduler .schedule (ARN_A , _rec (ARN_A , "inflight" ))
797+ assert _wait_until (exporter .started .is_set )
798+
799+ record = _rec (ARN_B , "memoryview" )
800+ record ["payload" ] = memoryview (bytearray (4_000 ))
801+ scheduler .schedule (ARN_B , record )
802+
803+ assert lane ._pending_count () == 0
804+ assert lane ._pending_bytes_count () == 0
805+ exporter .release ()
806+ scheduler .end_invocation (5.0 )
807+
808+
734809def test_record_sizing_exception_does_not_escape_schedule ():
735810 exporter = BlockingExporter ()
736811 scheduler = _ExportScheduler ([exporter ])
@@ -809,97 +884,93 @@ def test_cancelled_barrier_preserves_generation_order_and_terminal():
809884 assert lane ._queue_len () == 0
810885
811886
812- def test_cancelled_barrier_is_cleaned_up_and_worker_exits ():
887+ def test_timed_out_barrier_flushes_eventually_and_worker_exits ():
813888 exporter = BlockingExporter ()
814889 scheduler = _ExportScheduler ([exporter ])
815890 lane = scheduler ._lanes [0 ]
816891 scheduler .schedule (ARN_A , _rec (ARN_A , "a1" ))
817892 assert _wait_until (exporter .started .is_set )
818- ok = scheduler .end_invocation (0.1 ) # times out -> barrier cancelled
893+ ok = scheduler .end_invocation (0.1 )
819894 assert ok is False
820- # Once the exporter unblocks, the worker drains the cancelled barrier
821- # (skipping the pointless flush) and exits idle -- no permanent leak.
895+ # The caller returns on time, but one detached flush remains queued so a
896+ # buffered exporter can publish before the worker exits idle.
897+ assert lane ._queued_flush_count () == 1
822898 exporter .release ()
823899 assert _wait_until (lambda : not lane ._worker_alive ())
824- assert exporter .flushed == 0 # cancelled barrier did not flush
900+ assert exporter .flushed == 1
901+
902+
903+ def test_timed_out_buffered_export_is_published_eventually ():
904+ exporter = BlockingBufferedExporter ()
905+ scheduler = _ExportScheduler ([exporter ])
906+ lane = scheduler ._lanes [0 ]
907+ scheduler .schedule (ARN_A , _rec (ARN_A , "terminal" , status = "SUCCEEDED" ))
908+ assert _wait_until (exporter .started .is_set )
909+
910+ assert scheduler .end_invocation (0.1 ) is False
911+ assert exporter .published == []
912+ exporter .release ()
913+
914+ assert _wait_until (lambda : not lane ._worker_alive ())
915+ assert exporter .published == ["terminal" ]
916+ assert exporter .flushed == 1
825917
826918
827919def test_repeated_timeouts_behind_blocked_exporter_stay_bounded ():
828- """A blocked exporter across many warm invocations must not accumulate
829- barriers or grow queue state, must keep the SAME worker (no replacement),
830- must not execute any cancelled flush, and must drain + exit after release.
831- """
920+ """Warm timeouts coalesce to one eventual flush on the same worker."""
832921 exporter = BlockingExporter ()
833922 scheduler = _ExportScheduler ([exporter ])
834923 lane = scheduler ._lanes [0 ]
835924
836- # First record puts the single worker into a blocked export.
837925 scheduler .schedule (ARN_A , _rec (ARN_A , "a1" ))
838926 assert _wait_until (exporter .started .is_set )
839927 worker = lane ._worker
840928 assert worker is not None and worker .is_alive ()
841929
842- # Many warm invocations. Each schedules a record for the same ARN then ends
843- # with a short timeout; the barrier always times out because the worker is
844- # still stuck in the first export.
845930 for i in range (50 ):
846931 scheduler .schedule (ARN_A , _rec (ARN_A , f"a{ i + 2 } " ))
847- ok = scheduler .end_invocation (0.02 )
848- assert ok is False # degraded every time -- worker is blocked
849- # The cancelled barrier is pulled from the queue immediately, so no
850- # _FLUSH marker lingers behind the blocked worker.
851- assert lane ._queued_flush_count () == 0
852- # Each cancelled barrier can leave one generation token, but both
853- # records and tokens remain bounded by the per-execution FIFO depth.
854- assert lane ._queue_len () <= 16
932+ assert scheduler .end_invocation (0.02 ) is False
933+ # At most 16 generation tokens plus one detached eventual flush.
934+ assert lane ._queued_flush_count () == 1
935+ assert lane ._queue_len () <= 17
855936 assert lane ._pending_record_count () <= 16
856937
857- # Bounded state: one in-flight ARN with a bounded pending FIFO and bounded
858- # generation tokens, with no growing pile of barriers.
859- assert lane ._queue_len () <= 16
938+ assert lane ._queue_len () <= 17
860939 assert lane ._pending_count () <= 1
861940 assert lane ._pending_record_count () <= 16
862- assert lane ._queued_flush_count () == 0
863- # The blocked worker was never replaced.
941+ assert lane ._queued_flush_count () == 1
864942 assert lane ._worker is worker
865943 assert worker .is_alive ()
866944 assert _lane_worker_count (lane ) == 1
867- # No cancelled flush ran while the worker was blocked.
868945 assert exporter .flushed == 0
869946
870- # Release: the worker drains the newest bounded window, then exits idle.
871947 exporter .release ()
872948 assert _wait_until (lambda : not lane ._worker_alive ())
873949 exported = exporter .exported_values ()
874- assert exported [0 ] == "a1" # the in-flight record delivered first
875- assert len (exported ) <= 17 # a1 plus at most 16 pending FIFO records
876- assert exported [- 1 ] == "a51" # newest snapshot was retained
877- # Cancelled barriers never triggered a flush, and the idle-stop path does
878- # not flush either.
879- assert exporter .flushed == 0
950+ assert exported [0 ] == "a1"
951+ assert len (exported ) <= 17
952+ assert exported [- 1 ] == "a51"
953+ assert exporter .flushed == 1
880954
881955
882- def test_cancel_flush_removes_queued_barrier_immediately ():
883- """Queued-barrier race: while the worker is blocked the barrier is still in
884- the queue, so cancel_flush pulls it out and completes it synchronously --
885- without waiting for the worker and without ever flushing."""
956+ def test_cancel_flush_replaces_queued_barrier_with_detached_flush ():
886957 exporter = BlockingExporter ()
887958 scheduler = _ExportScheduler ([exporter ])
888959 lane = scheduler ._lanes [0 ]
889960 scheduler .schedule (ARN_A , _rec (ARN_A , "a1" ))
890- assert _wait_until (exporter .started .is_set ) # worker blocked in export
961+ assert _wait_until (exporter .started .is_set )
891962 barrier = lane .enqueue_flush ()
892963 assert lane ._queued_flush_count () == 1
964+
893965 lane .cancel_flush (barrier )
894- # Removed from the queue and completed here, without the worker.
895- assert lane ._queued_flush_count () == 0
966+
967+ assert lane ._queued_flush_count () == 1
896968 assert barrier .canceled is True
897969 assert barrier .is_done ()
898- # Finish the in-flight export and go idle; the pulled barrier never flushed.
899970 exporter .release ()
900971 lane .request_stop_when_idle ()
901972 assert _wait_until (lambda : not lane ._worker_alive ())
902- assert exporter .flushed == 0
973+ assert exporter .flushed == 1
903974 assert exporter .exported_values () == ["a1" ]
904975
905976
0 commit comments