Process worker: drain in-flight tasks before exiting - #1
wolever-gl wants to merge 6 commits into
Conversation
Patch: prefect-3.6.27-drain-on-sigterm.patch Changes: - Add PREFECT_WORKER_DRAIN_ON_SIGTERM env var support - Gracefully drain active flow runs on SIGTERM instead of hard-killing - Skip process termination/aclose during drain - Version: 3.6.27+growthloop
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3103838. Configure here.
| await self._wait_for_in_flight_runs() | ||
| except asyncio.CancelledError: | ||
| if not self._draining: | ||
| raise |
There was a problem hiding this comment.
Drain escalation cancels nothing
High Severity
A second SIGTERM is supposed to escalate out of drain, but ProcessWorker.start swallows asyncio.CancelledError whenever _draining is set. That turns escalation into a normal context exit, so teardown waits on _runs_task_group instead of cancelling it, and in-flight runs keep draining. The SIGTERM handler also only ever sends SIGINT, never SIGKILL, so a stuck drain may never force-stop.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3103838. Configure here.
| async def _submit_run_and_capture_errors( | ||
| self, | ||
| flow_run: "FlowRun", | ||
| task_status: anyio.abc.TaskStatus[int | Exception] | None = None, | ||
| ) -> BaseWorkerResult | Exception: | ||
| self._in_flight_flow_run_ids.add(flow_run.id) | ||
| try: | ||
| return await super()._submit_run_and_capture_errors( | ||
| flow_run, task_status=task_status | ||
| ) | ||
| finally: | ||
| self._in_flight_flow_run_ids.discard(flow_run.id) |
There was a problem hiding this comment.
This fixes a bug in BaseWorker.start (link) which assumes that self._limiter can be used to track the number of in-flight run executions, and it's safe to exit when bool(self._limiter) == False. When a subprocess limit is set, this is safe. But when self._limiter = None - the default - the main process will terminate even if there are in-flight runs.


Summary
This PR creates a GrowthLoop vendor build of Prefect
3.6.27+growthloopand adds an opt-in graceful-drain mode for process workers during SIGTERM shutdown.The production use case is Kubernetes rollout/termination behavior. When a process-worker pod receives SIGTERM, the worker should stop accepting new flow runs while allowing already-started flow subprocesses to finish, instead of immediately forwarding an interrupt that can kill in-flight work.
Behavior
When
PREFECT_WORKER_DRAIN_ON_SIGTERM=trueis set for a process worker:When the env var is unset or not
true, worker SIGTERM behavior should remain upstream behavior. Non-process workers are not wired into this vendor drain path.Deploy / Install Instructions
The current wheel has been deployed to the public GrowthLoop bucket:
gs://growthloop-wolever-public/prefect/prefect-3.6.27+growthloop-py3-none-any.whlgs://growthloop-wolever-public/prefect/3.6.27+growthloop/prefect-3.6.27+growthloop-py3-none-any.whlhttps://storage.googleapis.com/growthloop-wolever-public/prefect/prefect-3.6.27%2Bgrowthloop-py3-none-any.whlec6635650c11294f85f5aa2a10d898d560c6266570b3cebc67b422226b5e6fc11820051bytesInstall or upgrade an environment directly from the wheel:
python -m pip install --force-reinstall \ 'https://storage.googleapis.com/growthloop-wolever-public/prefect/prefect-3.6.27%2Bgrowthloop-py3-none-any.whl'With
uv:uv pip install --reinstall-package prefect \ 'https://storage.googleapis.com/growthloop-wolever-public/prefect/prefect-3.6.27%2Bgrowthloop-py3-none-any.whl'Verify the installed version:
Enable drain behavior for process workers by setting the env var on the worker container/process:
For Kubernetes deployments, add
PREFECT_WORKER_DRAIN_ON_SIGTERM=trueto the worker pod env and make sureterminationGracePeriodSecondsis long enough for the longest in-flight flow run you intend to let finish.Implementation Details
Core changes in this PR:
pyproject.tomlpins the vendor package version to3.6.27+growthloop.README.mddocuments that this repository is a GrowthLoop-maintained vendor fork and includes wheel build instructions.src/prefect/cli/worker.pydefers worker signal handler setup until after worker construction and scopes drain wiring toProcessWorkerinstances.src/prefect/utilities/processutils/__init__.pyadds drain-state helpers and SIGTERM drain handling support.open_process()should skip terminating a child only on a clean drain exit, but should still close process resources and should terminate on cancellation/escalation.src/prefect/workers/base.pyadds worker drain state and keeps cleanup of submitting flow-run ids infinallypaths.src/prefect/workers/process.pyhandles process-worker drain shutdown and tracks in-flight infrastructure lifetime past subprocess PID handoff.tools/write_build_info.pyis used as a Hatch custom build hook so clean static-version wheel builds still includeprefect._build_info.PR Comments / Review Follow-up
Cursor Bugbot reported two issues:
_active_flow_run_idswas cleared after subprocess start, not subprocess completion.open_process()skippedprocess.aclose()during drain.The follow-up fixes for both issues, the clean-wheel
_build_infofailure, non-process worker drain scoping, and the in-flight submit-loop race have been committed and pushed to this PR branch. The relevant Bugbot threads are resolved.Current Status
Resolved locally and included in the deployed wheel:
prefect.__version__ == "3.6.27+growthloop".open_process()closes resources during drain and terminates on cancellation/escalation.Still open:
main. A direct in-place merge was conflict-free after resolution, but it staged a large upstream delta, so it was intentionally unwound. This needs a deliberate rebase/merge strategy before final merge.Automated Test Plan
Implemented or prepared locally:
src/prefect/_build_info.py, install into a fresh venv, andimport prefect.limit=Noneandlimit=1.Recommended additional test before production rollout:
terminationGracePeriodSeconds, launch a sleep flow, delete the pod, and assert the flow completes before the pod exits; repeat with a short grace period or second signal to verify escalation.Validation Run Locally
Result: the focused follow-up suite passed
26 passed, 3 skipped; the real process-worker drain lifecycle module passed19 passed; and the base-worker suite passed104 passed, 1 skipped.Result: format check and ruff check passed.
Note
Medium Risk
Changes worker shutdown, subprocess lifecycle, and packaging for a forked runtime—important for K8s rollouts but scoped behind an env flag and process workers only.
Overview
This PR vendors Prefect as
3.6.27+growthloop(README fork notes, pinnedpyproject.tomlversion, Hatchwrite_build_infohook for wheels without git) and adds opt-in graceful shutdown for process workers viaPREFECT_WORKER_DRAIN_ON_SIGTERM=true.When enabled, the worker CLI registers SIGTERM after the worker exists and passes
ProcessWorker.request_draininto signal setup. On Unix, the first SIGTERM enters drain mode (stops polling, keeps heartbeats/healthchecks healthy) instead of immediately forwarding SIGINT; a second SIGTERM escalates to SIGINT.BaseWorkerstops scheduling new runs while draining;ProcessWorkertracks in-flight runs until subprocess infrastructure exits.open_processskipsterminate()only on a clean drain exit but still **aclose()**s; cancellation still terminates.Default SIGTERM behavior is unchanged when the env var is unset; non-process workers are not wired to drain.
Reviewed by Cursor Bugbot for commit 3103838. Bugbot is set up for automated code reviews on this repo. Configure here.