feat: SLS-497 Add explicit prestart hooks to the runpod-python SDK - #570
Open
jasonwang-runpod wants to merge 5 commits into
Open
feat: SLS-497 Add explicit prestart hooks to the runpod-python SDK#570jasonwang-runpod wants to merge 5 commits into
jasonwang-runpod wants to merge 5 commits into
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.
jasonwang-runpod
marked this pull request as ready for review
August 19, 2026 22:16
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.
jasonwang-runpod
force-pushed
the
jasonwang/sls-497-explicit-prestart-hooks
branch
from
August 20, 2026 15:16
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))
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.
jhcipar
approved these changes
Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.runpod/worker-sglang:2.0.2) across three runs:COMPLETED, and the payload carriedoutputonly, with no log field.COMPLETED, with the handler held 73 sec while engine started up.FAILEDwith aprestart_failedpayload naming the failing hook, rather than the request sitting inIN_QUEUE.Supersedes #567.