fix(llm): subprocess hard-timeout deadlock + caller message-dict mutation - #280
Conversation
…eout deadlock _completion_with_hard_timeout joined the child process before draining the multiprocessing result queue. A large completion payload overflows the OS pipe buffer, so the child's queue-feeder thread blocks on put() until a reader drains it -- and the child cannot exit while that thread is blocked. Joining first therefore deadlocked the parent against a finished-but-wedged child, tripping a false LLMHardTimeoutError on a large-but-successful result (reproduced: a 2 MB payload burned the full hard timeout and raised). Drain the queue before join, bounded by the same hard_timeout budget, so the feeder unblocks and the child exits. Hard-timeout kill semantics (terminate, then kill) and the ok/error snapshot marshalling are preserved; a true hang still surfaces LLMHardTimeoutError and an exit-without-result still raises LiteLLMClientError.
generate_chat_response did list(messages) (shallow copy sharing dict objects) then merged into final_messages[0] in place, corrupting the caller's list and re-prepending on reuse. Replace the slot with a new dict.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughModified generate_chat_response to build a new merged system-message dict instead of mutating caller-provided messages, and reworked _completion_with_hard_timeout to drain the result queue before joining the subprocess, preventing deadlocks with large payloads. Added corresponding regression tests and reformatted two unrelated patch calls. ChangesText generation fixes
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
PoemA rabbit hopped through queues at night, 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
Two latent robustness fixes in the litellm client (both confirmed with a repro test that fails pre-fix / passes post-fix). A third CodeRabbit-flagged item (
_build_completion_params/_apply_prompt_cachingmutation) was investigated and found to be a false positive — those already copy-on-write — so it's not touched.1. Subprocess hard-timeout deadlock → false timeout (availability)
_completion_with_hard_timeoutjoined the child process before draining themp.Queueresult. A large completion payload (>~64KB) fills the OS pipe buffer, so the child blocks onqueue.put(...)while the parent blocks onjoin(...)— the parent then hits its timeout and raisesLLMHardTimeoutErrorfor a call that actually succeeded. Fix: drain the queue (bounded by the samehard_timeoutbudget) before join; kill/terminate + ok/error snapshot semantics preserved. Regression test uses a 2 MB payload.2. Caller message-dict mutation on
system_messagemerge (correctness)generate_chat_responsedidfinal_messages = list(messages)(a shallow copy sharing the caller's dict objects) then merged the system message intofinal_messages[0]in place, corrupting the caller's list and re-prepending on reuse/retry. Fix: replace the list slot with a new dict instead of mutating the shared one.Test plan
git stashof the fix) / pass-post.tests/server/llm/490 passed / 60 skipped, no mock snapshot churn; ruff clean; pyright 0 errors on changed files.Note
Summary by CodeRabbit