@@ -46,6 +46,17 @@ def _insight_thread_count() -> int:
4646 )
4747
4848
49+ def _lane_worker_count (lane ) -> int :
50+ """Count live worker threads that belong to *this* lane by identity.
51+
52+ Each lane names its worker ``workflow-insight-export-{id(lane)}``, so this is
53+ scoped to the given lane and is unaffected by daemon workers other tests may
54+ still be winding down -- unlike a process-global thread-count delta.
55+ """
56+ name = f"workflow-insight-export-{ id (lane )} "
57+ return sum (1 for t in threading .enumerate () if t .name == name and t .is_alive ())
58+
59+
4960class RecordingExporter :
5061 """Records every export/flush in call order (fast, non-blocking)."""
5162
@@ -350,6 +361,7 @@ def test_export_and_flush_exceptions_are_isolated():
350361def test_shared_timeout_bounds_invocation_end_delay ():
351362 exporter = BlockingExporter ()
352363 scheduler = _ExportScheduler ([exporter ])
364+ lane = scheduler ._lanes [0 ]
353365 scheduler .schedule (ARN_A , _rec (ARN_A , "a1" ))
354366 assert _wait_until (exporter .started .is_set )
355367 start = time .monotonic ()
@@ -358,6 +370,9 @@ def test_shared_timeout_bounds_invocation_end_delay():
358370 assert ok is False # degraded to best-effort
359371 assert elapsed < 2.0 # bounded by the shared deadline, not the blocked export
360372 exporter .release () # let the daemon drain and exit
373+ # Wait for the released worker to actually stop so it cannot leak into a
374+ # later test's baseline thread count.
375+ assert _wait_until (lambda : not lane ._worker_alive ())
361376
362377
363378def test_shared_timeout_across_multiple_lanes_is_not_additive ():
@@ -373,26 +388,38 @@ def test_shared_timeout_across_multiple_lanes_is_not_additive():
373388 assert elapsed < 0.9
374389 e1 .release ()
375390 e2 .release ()
391+ # Wait for both released workers to actually stop so neither leaks into a
392+ # later test's baseline thread count.
393+ assert _wait_until (
394+ lambda : not any (lane ._worker_alive () for lane in scheduler ._lanes )
395+ )
376396
377397
378398# -- worker lifecycle ---------------------------------------------------------
379399
380400
381401def test_blocked_worker_is_not_replaced ():
382- base = _insight_thread_count ()
383402 exporter = BlockingExporter ()
384403 scheduler = _ExportScheduler ([exporter ])
385404 lane = scheduler ._lanes [0 ]
386405 scheduler .schedule (ARN_A , _rec (ARN_A , "a1" ))
387406 assert _wait_until (exporter .started .is_set )
388407 worker = lane ._worker
408+ assert worker is not None and worker .is_alive ()
409+ # The blocked lane already has exactly one live worker of its own.
410+ assert _lane_worker_count (lane ) == 1
389411 # More scheduling and an invocation-end (which enqueues a flush + requests
390412 # stop) must not spawn a replacement while the worker is blocked.
391413 scheduler .schedule (ARN_A , _rec (ARN_A , "a2" ))
392414 scheduler .schedule (ARN_B , _rec (ARN_B , "b1" ))
393415 scheduler .end_invocation (0.1 )
416+ # Identity: the lane still holds the SAME blocked worker -- no replacement
417+ # thread was swapped in -- and it is still the only live worker for this
418+ # lane. Both checks are scoped to this lane, so they cannot flake on daemon
419+ # workers other tests are winding down.
394420 assert lane ._worker is worker
395- assert _insight_thread_count () - base == 1
421+ assert worker .is_alive ()
422+ assert _lane_worker_count (lane ) == 1
396423 exporter .release ()
397424
398425
0 commit comments