Skip to content

feat: SLS-497 Add explicit prestart hooks to the runpod-python SDK - #570

Open
jasonwang-runpod wants to merge 5 commits into
mainfrom
jasonwang/sls-497-explicit-prestart-hooks
Open

feat: SLS-497 Add explicit prestart hooks to the runpod-python SDK#570
jasonwang-runpod wants to merge 5 commits into
mainfrom
jasonwang/sls-497-explicit-prestart-hooks

Conversation

@jasonwang-runpod

@jasonwang-runpod jasonwang-runpod commented Aug 19, 2026

Copy link
Copy Markdown

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 in IN_QUEUE status while useful info remains in worker logs.

Solution

Add explicit prestart hooks that the SDK supervises before handler execution.

  • Workers can register sync or async hooks with @runpod.serverless.register_prestart_hook and provide an optional timeout for the prestart phase.
  • Queue-based workers take jobs concurrently with prestart, but don't run the handler function until prestart succeeds.
  • A failure or timeout is attached to a request as prestart_failed. Then the worker drains and exits.
  • Local input and single-worker hosted API mode run prestart before invoking or serving the handler. Realtime and multi-process hosted API modes are unsupported.
  • Failure payloads can carry the last 16 KB of the worker's stdout and stderr. RUNPOD_LOG_CAPTURE gates this: auto (default) captures only when hooks are registered, all always captures, off never does. Note: log capture doesn't see child processes.
  • A worker that doesn't register any hooks is unaffected.

Testing

  • uv run pytest -q: 710 passed, 94.7% coverage.
  • An A40 sglang endpoint (runpod/worker-sglang:2.0.2) across three runs:
    • Unmodified template, no hooks: COMPLETED, and the payload carried output only, with no log field.
    • Same template with a prestart hook: COMPLETED, with the handler held 73 sec while engine started up.
    • Hook pointed at a missing model (intentional failure): FAILED with a prestart_failed payload naming the failing hook, rather than the request sitting in IN_QUEUE.

Supersedes #567.

Comment thread runpod/serverless/modules/rp_prestart.py
Comment thread runpod/serverless/modules/rp_prestart.py
Comment thread runpod/serverless/modules/rp_prestart.py
Comment thread runpod/serverless/modules/rp_scale.py
Comment thread tests/test_serverless/test_prestart.py
Comment thread runpod/serverless/modules/rp_prestart.py
Comment thread runpod/serverless/modules/rp_prestart.py
Comment thread runpod/serverless/modules/rp_scale.py
Comment thread tests/test_serverless/test_prestart.py
Comment thread runpod/serverless/modules/rp_prestart.py

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
jasonwang-runpod marked this pull request as ready for review August 19, 2026 22:16
Comment thread runpod/serverless/modules/rp_capture.py Fixed
Comment thread runpod/serverless/modules/rp_prestart.py Fixed
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
jasonwang-runpod force-pushed the jasonwang/sls-497-explicit-prestart-hooks branch from 86f06ef to 27ffbe4 Compare August 20, 2026 15:16
Comment thread runpod/serverless/modules/rp_prestart.py Fixed
@jasonwang-runpod
jasonwang-runpod requested a balanced review from Copilot August 20, 2026 15:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 normal threading.Thread starts 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_CHARS limits 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 inner json.dumps can 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 first and intermittently failing the test. Make the first hook only yield once and have the second block indefinitely so the timeout deterministically occurs in second.
    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.
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.

4 participants