feat: SLS-497 Add explicit prestart hooks to the runpod-python SDK - #570
Conversation
There was a problem hiding this comment.
Pull request overview
Adds supervised prestart hooks across supported Serverless worker modes, including startup-failure reporting and bounded stdout/stderr capture.
Changes:
- Adds ordered sync/async prestart hooks with optional phase timeout.
- Gates handlers during initialization and drains failed queue workers.
- Captures bounded logs for startup and handler failures.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
runpod/serverless/__init__.py |
Exposes hooks and validates runtime modes. |
runpod/serverless/worker.py |
Installs output capture for workers. |
runpod/serverless/modules/rp_prestart.py |
Implements hook registration and execution. |
runpod/serverless/modules/rp_capture.py |
Implements bounded contextual output capture. |
runpod/serverless/modules/rp_scale.py |
Integrates queue gating, failure delivery, and shutdown. |
runpod/serverless/modules/rp_local.py |
Runs hooks before local handlers. |
runpod/serverless/modules/rp_fastapi.py |
Runs hooks through FastAPI lifespan. |
runpod/serverless/modules/rp_job.py |
Attaches captured logs to handler failures. |
docs/serverless/worker.md |
Documents prestart configuration and modes. |
tests/test_serverless/test_prestart.py |
Tests the public hook contract and mode guards. |
tests/test_serverless/test_prestart_lifecycle.py |
Tests queue lifecycle and process termination. |
tests/test_serverless/test_capture.py |
Tests capture and output bounds. |
tests/test_serverless/test_modules/test_local.py |
Tests local prestart behavior. |
tests/test_serverless/test_init.py |
Verifies the new public export. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
86f06ef to
27ffbe4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (3)
docs/serverless/worker.md:77
- This overstates what the implementation captures. The active buffer is a
ContextVar, and a normalthreading.Threadstarts without that context, so direct stdout/stderr writes from engine/background threads created by a hook or handler are omitted even while that hook or handler is running. Document the child-thread limitation (or propagate the context) so users do not rely on missing diagnostics.
Capture replaces `sys.stdout`/`sys.stderr` at worker startup, so it sees `print`
and direct stream writes made while a hook or handler runs. It does not see
child processes and log handlers created before startup.
runpod/serverless/modules/rp_capture.py:26
MAX_CAPTURED_CHARSlimits Unicode code points, not the encoded size sent to/job-done. A 16,384-character non-ASCII tail can occupy up to 64 KB in UTF-8 (and the innerjson.dumpscan expand characters further), so the advertised 16 KB bound and the payload-size protection are not enforced. Bound the encoded/serialized byte length while preserving valid character boundaries; the ring buffer,clip, and prestart payload slicing need to use the same byte-based contract.
MAX_CAPTURED_CHARS = 16 * 1024
tests/test_serverless/test_prestart.py:91
- This assertion has only 10 ms of scheduling slack. On a loaded CI runner the first 30 ms sleep can resume after the 40 ms phase deadline, making the reported hook
firstand intermittently failing the test. Make the first hook only yield once and have the second block indefinitely so the timeout deterministically occurs insecond.
def test_timeout_bounds_the_whole_phase_and_names_current_hook(self):
async def first():
await asyncio.sleep(0.03)
async def second():
await asyncio.sleep(0.03)
with self.assertRaises(PrestartTimeout) as ctx:
_run(run_prestart_hooks_async((first, second), timeout=0.04))
deanq
left a comment
There was a problem hiding this comment.
Reviewed the prestart-hooks + capture changes. The concurrency design (concurrent intake with a gated handler, failing the claimed request on startup failure) is sound and well-tested. Flagging a few correctness/security gaps on the failure and shutdown paths, plus one nit.
One cross-cutting note: the prestart-failure path routes termination through rp_fitness._terminate_unhealthy (the fitness force-kill helper) but defers its os._exit behind the graceful loop-drain -- see the inline note on run(). That partially undoes the hard-exit guarantee fitness relies on. Details inline.
309295c to
be3b312
Compare
Hooks run once per worker before the handler. Capture tees stdout/stderr into a bounded buffer so a failure can report what the worker printed, and handler errors gain a logs field. Capture stays off unless hooks are registered or RUNPOD_LOG_CAPTURE says otherwise. Only logs is bounded; error_message and error_traceback are unchanged.
Queue workers take requests while hooks run but hold the handler behind a gate, and a failure is reported against held requests before the worker exits. Local and hosted API modes finish hooks before running a handler or serving. Realtime rejects registered hooks.
rp_capture no longer imports the prestart registry to answer its own auto-mode question; the caller passes it in. Removes the import cycle CodeQL flagged and leaves the capture module standalone. No behavior change.
The second hook now blocks indefinitely, so the phase deadline lands in it regardless of scheduler load. Also notes in the docs that threads started directly do not inherit the capture context.
- decouple handler failure log capture from prestart hook registration - fail claimed requests explicitly on prestart shutdown as prestart_cancelled - gate handlers on prestart success instead of the ready event - cancel idle long polls and bound failure reporting on prestart failure - bound the hook cancellation drain and name the claim timeout constant
be3b312 to
e36ffce
Compare
deanq
left a comment
There was a problem hiding this comment.
All six review threads addressed and resolved; CI green across 3.10–3.14, e2e, and CodeQL. The prestart failure/cancellation paths now fail held requests explicitly (prestart_cancelled), bound the hook-cancel drain, and cap failure reporting below the HTTP timeout. LGTM.
Promptless documentation updates
|
Problem
Workers often load models or start inference engines before calling
runpod.serverless.start(). The SDK cannot observe that prestart work, so failures leave requests inIN_QUEUEstatus while useful info remains in worker logs.Solution
Add explicit prestart hooks that the SDK supervises before handler execution.
@runpod.serverless.register_prestart_hookand provide an optional timeout for the prestart phase.prestart_failed. Then the worker drains and exits.RUNPOD_LOG_CAPTUREgates this:auto(default) captures only when hooks are registered,allalways captures,offnever does. Note: log capture doesn't see child processes.Testing
uv run pytest -q: 710 passed, 94.7% coverage.An sglang endpoint was tested w/ different scenarios:
prestart_failedprestart_timeout=5with slow hook: FAILED (as expected) in 4.9s withPrestartTimeout; logs had the captured stdoutcapture=auto: FAILED (as expected), with no logs fieldSupersedes #567.