Skip to content

Add restate.serve: serve an app over HTTP/2 - #266

Merged
igalshilman merged 3 commits into
mainfrom
restate-serve
Nov 4, 2026
Merged

igalshilman merged 3 commits into
mainfrom
restate-serve

Conversation

@igalshilman

Copy link
Copy Markdown
Contributor

restate.serve

A one-line way to serve a Restate app, as the counterpart of restate.connect:

app = restate.app([greeter])

if __name__ == "__main__":
    asyncio.run(restate.serve(app))                      # serve on $PORT or 9080
    # asyncio.run(restate.connect(app, name="greeter"))  # or connect out to Restate Cloud
  • What it serves: async def serve(app, *, host="0.0.0.0", port=None) listens with the cleartext HTTP/2 that Restate connects with, on uvicorn 0.53+ with zttp. It stops within a few seconds of SIGINT or SIGTERM.
  • A new serve extra: pip install "restate-sdk[serve]" brings uvicorn and zttp on Python 3.11+. Without the extra, serve raises an ImportError naming it, and ordinary installs get no new dependencies.
  • Signals: the app installs its own SIGTERM hook on its first request, through install_shutdown_hook. That keeps one asyncio handler per signal and would replace uvicorn's handlers, so serve registers the server's exit through the same hook first and turns off uvicorn's own signal capture. The server and the app's hook then both run.
  • README: the example is served with restate.serve; hypercorn stays as the alternative, including for Python 3.10.

Tests

tests/serve.py:

  • Without the extra, serve raises an ImportError naming it (checked in a subprocess with uvicorn hidden).
  • End to end, a program serving a greeter with restate.serve:
    • registers with a real Restate, which reports HTTP/2.0 for the deployment;
    • answers a call through Restate;
    • stops promptly on SIGTERM after serving those requests.
    • (Skipped where the extra isn't installed, which includes Python 3.10.)

#262 (session objects and agents) will use this in place of its own sessions.serve.

🤖 Generated with Claude Code

…te.connect

    asyncio.run(restate.serve(app))

serves on $PORT, or 9080, with the cleartext HTTP/2 Restate connects
with (uvicorn 0.53+ with zttp), and stops within a few seconds of SIGINT
or SIGTERM. It needs the new serve extra (Python 3.11+); without it,
it raises an ImportError naming the extra.

The app installs its SIGTERM hook on its first request through
install_shutdown_hook, which keeps one asyncio handler per signal and
would replace uvicorn's; so serve registers the server's exit through
the same hook first, and uvicorn captures no signals itself.

The README serves its example with restate.serve, and keeps hypercorn
as the alternative (which also works on Python 3.10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — three small rough edges inline.

Reviewed changes

Reviewed the full PR: a new restate.serve entrypoint on uvicorn+zttp HTTP/2, a serve extra, README updates, and tests. I also checked it locally with uvicorn 0.54 and zttp 0.0.35 on Python 3.12. /discover is served over HTTP/2, and both SIGTERM and SIGINT stop the process in about 0.25s while an h2 connection is still open. The shared-hook ordering (server exit first, then the app's _on_sigterm) works as the comment describes.

  • restate.serve — lazily imports uvicorn/zttp, checks for uvicorn ≥ 0.53, and serves with http="zttp", http2=True, lifespan="off" and a 3s graceful shutdown.
  • Signal handling — sends uvicorn's handle_exit through install_shutdown_hook, so it shares one asyncio dispatcher with the app's SIGTERM hook, and turns off uvicorn's own capture_signals.
  • serve extra — uvicorn>=0.53 and zttp>=0.0.34, both limited to Python 3.11+. uv.lock now uses uvicorn 0.38 below 3.11 and 0.54 from 3.11 on.
  • README — the example now uses restate.serve, with hypercorn still listed as the alternative (including for 3.10).
  • Tests — a subprocess check of the missing-extra ImportError, plus a Docker end-to-end test: register, check HTTP/2.0, invoke, then SIGTERM.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread python/restate/serving.py Resolved
Comment thread python/restate/serving.py Resolved
Comment thread tests/serve.py Resolved
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Test Results

  8 files  ±0    8 suites  ±0   3m 36s ⏱️ -19s
 71 tests ±0   71 ✅ ±0  0 💤 ±0  0 ❌ ±0 
322 runs  ±0  322 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit ba95e1b. ± Comparison against base commit 39bb159.

@igalshilman igalshilman mentioned this pull request Nov 4, 2026
…ort=0; stop under load

- serve only turns off uvicorn's own signal capture once its shutdown
  hooks are installed: on Windows (no add_signal_handler) uvicorn keeps
  handling Ctrl+C itself.
- An explicit port=0 (one the OS picks) is no longer taken for unset.
- The end-to-end test sends SIGTERM while Restate still holds its HTTP/2
  connections open, and stops Restate only after the program exited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. All three earlier findings are fixed.

Reviewed changes

Reviewed bcbdb31, the follow-up commit since the last Pullfrog review.

  • Signal fallback — server.capture_signals is now replaced only after both install_shutdown_hook calls succeed. On Windows or in a non-main thread, the first add_signal_handler call raises, so uvicorn keeps its own signal handling and no hook is left half-installed.
  • port=0 — an explicit 0 now reaches uvicorn as an OS-assigned port instead of falling back to $PORT/9080. The docstring now says this.
  • Shutdown test — SIGTERM is now sent while Restate still holds its HTTP/2 connections. The test waits up to 10s for the program to exit and only then stops the container in finally, so docker stop time doesn't count against the budget.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

… wait until it listens

The program's port was chosen before the Restate container started, and
Docker could map one of Restate's ports onto it before the program bound
it. Now the container starts first, the test waits until the program
accepts connections before registering it, and a failure shows what the
program printed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
igalshilman added a commit that referenced this pull request Nov 4, 2026
… port, and wait until it listens

As in tests/serve.py (#266): Docker could map one of the Restate
container's ports onto the port chosen for the example before the
example bound it. The container now starts first, the test waits until
the example accepts connections, and SIGTERM comes while Restate still
holds its connections open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. The new commit makes the end-to-end test steadier and easier to debug when it fails.

Reviewed changes

Reviewed 97fea5e, a test-only commit to tests/serve.py since the last Pullfrog review.

  • Container-first ordering — RestateContainer now starts before find_free_port(). Docker's host-port mappings are then already taken and can't collide with the program's port.
  • Readiness wait — a new listening() helper polls a TCP connect on 127.0.0.1:{port} (up to about 10s) and fails early with the program's output if the program exits. This replaces the 100-try registration retry loop, so registration now happens once against a server that is already listening.
  • Program output capture — stdout and stderr go to tmp_path/program.log and are attached to the readiness and shutdown assertion messages. Before, they were sent to DEVNULL.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@igalshilman
igalshilman merged commit 6fecac0 into main Nov 4, 2026
10 checks passed
@igalshilman
igalshilman deleted the restate-serve branch November 4, 2026 19:29
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant