Skip to content

fix(proxy): kill mid-drain retired prisma engines at shutdown instead of abandoning them - #34999

Open
ryan-crabbe-berri wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_flush_prisma_engine_retirement_on_shutdown
Open

fix(proxy): kill mid-drain retired prisma engines at shutdown instead of abandoning them#34999
ryan-crabbe-berri wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_flush_prisma_engine_retirement_on_shutdown

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Shutdown abandoned engine retirement tasks still waiting on their drain deadline
  • The replaced query engine survived as an orphan holding DB connections

How it solves it:

  • Proxy shutdown now flushes pending retirements: cancel, await, SIGKILL the engine
  • Retirements track (pid, task) so even never-started tasks get their kill

Relevant issues

Follow-up to #34749

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

The bug only manifests when the proxy shuts down while a warm IAM rotation's drain window (up to 90s) is open, which needs an IAM-enabled RDS instance to reach; there is no curl-visible surface because the leak is a child process outliving the proxy. Manual verification recipe for an IAM environment: run the proxy against IAM RDS, wait for a rotation log line ("RDS IAM token refreshed successfully"), immediately SIGTERM the proxy inside the drain window, then ps aux | grep query-engine. At 7cd009caf7 (before) the old engine pid survives the proxy exit until Postgres idle timeouts reap its connections; at 1219fde8a1 (after) no query-engine process remains

Deterministic reproduction is the added regression test: at 7cd009caf7 it fails because stop_token_refresh_task returns without touching the pending retirement (no kill signal is ever sent), and at 1219fde8a1 it passes. The full database suites (615 tests across tests/test_litellm/proxy/db/, tests/test_litellm/proxy/utils/prisma_and_spend/, and the engine watchdog suite) pass at 1219fde8a1

Type

🐛 Bug Fix

Changes

PR #34749 retires a replaced engine via a background task that waits for in-flight work to drain (bounded at 90s) before killing the old engine. Those retirement tasks had no shutdown owner: a task still mid-drain when the proxy shut down was cancelled at event-loop teardown before its kill ever ran, orphaning the replaced query-engine subprocess and the DB connections it holds. Containers reap the orphan with the pod; bare-metal and dev deployments leaked it until Postgres idle timeouts fired

stop_token_refresh_task, which the proxy shutdown hook already calls on every wrapper (the routing wrapper forwards it to writer and reader), now flushes pending retirements after stopping the refresh loop so a mid-rotation cancellation can still schedule its cleanup before the flush runs. The flush cancels each retirement task, awaits it, and then SIGKILLs the engine pid directly. The direct kill matters because a task cancelled before its first run never executes its body, so relying on the task's own cancellation handler would silently skip the kill; retirements are therefore tracked as (pid, task) pairs. Independently, a retirement task cancelled from anywhere now SIGKILLs its engine before propagating the cancellation, covering the window between the drain deadline's SIGTERM and its SIGKILL backstop

A rotation completing after the flush has taken its snapshot (an in-flight refresh spawned by the getattr stale-token fallback) cannot escape either: once the flush has run, _schedule_engine_retirement kills the old engine inline instead of creating a task. The event loop is single threaded and both the flag-set-plus-snapshot and the schedule are synchronous, so every schedule either lands in the flush snapshot or sees the flag

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

… of abandoning them

Follow-up to #34749. Engine retirement tasks had no shutdown owner: a
task still waiting for its drain deadline when the proxy shut down was
cancelled at event-loop teardown before its kill ran, orphaning the
replaced query-engine subprocess and the DB connections it holds. In
containers the pod teardown reaps the orphan, but bare-metal and dev
deployments leaked it until Postgres idle timeouts fired.

stop_token_refresh_task, which the proxy shutdown hook already calls
on every wrapper (the routing wrapper forwards it to writer and
reader), now flushes pending retirements after stopping the refresh
loop: cancel each retirement task, await it, then SIGKILL the engine
pid directly. The direct kill also covers a retirement task cancelled
before it ever ran, which would otherwise skip its own kill entirely,
so retirements are tracked as (pid, task) pairs. A retirement task
cancelled anywhere else likewise SIGKILLs its engine before
propagating the cancellation
Comment thread litellm/proxy/db/prisma_client.py
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes the shutdown race that could leave retired Prisma query engines alive.

  • Tracks each pending retirement with both its engine PID and task.
  • Cancels and awaits pending retirement tasks before directly killing their engines during shutdown.
  • Kills engines inline when retirement is scheduled after shutdown flushing has begun.
  • Adds regression coverage for pending and post-flush retirement scheduling.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the flush flag is set before the synchronous retirement snapshot, and every later retirement schedule observes the flag and kills its engine inline.

Important Files Changed

Filename Overview
litellm/proxy/db/prisma_client.py Adds shutdown ownership for pending engine retirements and synchronously handles retirements scheduled after flushing begins.
tests/test_litellm/proxy/db/test_prisma_planned_engine_restart.py Updates retirement-task helpers for PID/task records and verifies both pending-retirement flushing and post-flush synchronous termination.

Reviews (3): Last reviewed commit: "fix(proxy): kill retirements scheduled a..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/db/prisma_client.py 93.75% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

…ronously

An in-flight refresh triggered by the __getattr__ stale-token fallback
can complete its rotation after flush_engine_retirements has taken its
snapshot, scheduling a retirement task that no longer has an owner and
gets abandoned at event-loop teardown. Once the flush has run,
_schedule_engine_retirement now SIGKILLs the old engine inline instead
of creating a task. The event loop is single threaded and both the
flag-set-plus-snapshot and the schedule are synchronous, so every
schedule either lands in the flush snapshot or sees the flag; no
retirement can escape both
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

Pushed a637b32 addressing the P1: once the shutdown flush has run, _schedule_engine_retirement kills the old engine inline instead of creating a task, so a rotation completing after the flush snapshot (an in-flight getattr refresh) cannot leave an unowned retirement. Single threaded event loop plus no awaits between flag set, snapshot, and schedule means every retirement either lands in the snapshot or sees the flag. Regression test added that fails without the flag

@greptileai re review

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@codspeed-hq

codspeed-hq Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_flush_prisma_engine_retirement_on_shutdown (a637b32) with litellm_internal_staging (7cd009c)

Open in CodSpeed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant