[Fix] Tests - drain logging worker in test_router_caching_ttl to fix flakiness - #26355
Conversation
[Infra] Promote interal staging to main
The mocked async_increment_cache_pipeline is invoked from Router's deployment_callback_on_success, registered as an async success callback. Those callbacks are enqueued to GLOBAL_LOGGING_WORKER and run on a background task, so the mock may not have been called yet when the test asserts on it. Flush the worker before asserting.
Low: Test flakiness fix with no security impactThis PR fixes a flaky test by improving the Status: 0 open Posted by Veria AI · 2026-04-23T22:07:15.960Z |
Greptile SummaryThis PR fixes intermittent Confidence Score: 5/5Safe to merge — the Both changes are correct: No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/logging_worker.py | Fixes flush() race window by replacing the empty()-guarded while loop with a direct await self._queue.join(), correctly handling the dequeued-but-not-done state. |
| tests/local_testing/test_tpm_rpm_routing_v2.py | Adds await GLOBAL_LOGGING_WORKER.flush() after router.acompletion() to drain the async callback queue before asserting on the mock, eliminating the flakiness race. |
Sequence Diagram
sequenceDiagram
participant Test
participant Router
participant GLOBAL_LOGGING_WORKER
participant asyncio.Queue
participant Callback as deployment_callback_on_success
Test->>Router: await acompletion(...)
Router->>GLOBAL_LOGGING_WORKER: enqueue(callback coroutine)
GLOBAL_LOGGING_WORKER->>asyncio.Queue: put(task) [_unfinished_tasks++]
Router-->>Test: response returned
Note over Test,asyncio.Queue: Before fix: test asserts here — callback may not have run yet
Test->>GLOBAL_LOGGING_WORKER: await flush()
GLOBAL_LOGGING_WORKER->>asyncio.Queue: await join() [blocks until _unfinished_tasks == 0]
asyncio.Queue->>Callback: worker dequeues and runs callback
Callback->>asyncio.Queue: task_done() [_unfinished_tasks--]
asyncio.Queue-->>GLOBAL_LOGGING_WORKER: join() returns
GLOBAL_LOGGING_WORKER-->>Test: flush() returns
Test->>Test: assert mock_client.call_args ✓
Reviews (2): Last reviewed commit: "fix: make LoggingWorker.flush() wait for..." | Re-trigger Greptile
|
|
||
| # Async success callbacks are dispatched to GLOBAL_LOGGING_WORKER's | ||
| # background queue; drain it before asserting the mock was invoked. | ||
| await GLOBAL_LOGGING_WORKER.flush() |
There was a problem hiding this comment.
flush() has a race window that can still allow early return
The flush() implementation checks self._queue.empty() before calling join(). If the worker loop has already dequeued the item (queue becomes empty) but hasn't yet called task_done(), flush() will skip join() entirely and return before the callback finishes. In that scenario the mock assertion immediately below would still fail.
asyncio.Queue.join() already handles an empty queue correctly (returns immediately when _unfinished_tasks == 0), so the guard is both redundant in the normal path and wrong in the dequeued-but-not-done path. A safer flush() would just be:
async def flush(self) -> None:
if self._queue is None:
return
await self._queue.join() # waits for all in-flight task_done() calls tooSince flush() is in a shared module and other callers could hit the same edge case, fixing it there is lower risk than relying on scheduling luck in every call site.
The previous `while not self._queue.empty(): await self._queue.join()` pattern skipped the join entirely when the worker had already dequeued a task but not yet called task_done(). asyncio.Queue.join() tracks _unfinished_tasks (incremented by put, decremented by task_done), not queue depth, so it already handles that case on its own.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…ingTest [Fix] Tests - drain logging worker in test_router_caching_ttl to fix flakiness
Relevant issues
Summary
Failure Path (Before Fix)
test_router_caching_ttlintermittently fails withAttributeError: 'NoneType' object has no attribute 'kwargs'when readingmock_client.call_args. The mockedasync_increment_cache_pipelineis invoked fromRouter.deployment_callback_on_success, registered as an async success callback. Those callbacks are enqueued toGLOBAL_LOGGING_WORKERand processed on a background task, so they may not have executed when the test asserts on the mock.LoggingWorker.flush()also had a latent race:while not self._queue.empty(): await self._queue.join()skipped the wait entirely when the worker had already dequeued a task but not yet calledtask_done().Fix
GLOBAL_LOGGING_WORKERin the test afterrouter.acompletion(...)before asserting on the mock.LoggingWorker.flush()to unconditionallyawait self._queue.join().asyncio.Queue.join()tracks_unfinished_tasks(incremented byput, decremented bytask_done), so it already handles the in-flight case — theempty()guard was wrong.Testing
test_router_caching_ttl5x locally — passes consistently.Type
🐛 Bug Fix
✅ Test