You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Phase B of the response-cap rework, building on the operator knobs and
conformance scaffolding from 086e3ef.
Worker-visible budgets on OutputCollector:
* ``out.remaining_response_bytes: int | None`` — wire body bytes the
framework will accept this iteration before triggering a continuation
token (producer) or strict-fail (unary/exchange).
* ``out.remaining_externalized_response_bytes: int | None`` — external
channel bytes left this iteration.
* ``out.externalization_enabled: bool`` — whether the server has a
storage backend wired up.
All snapshot semantics — fixed at collector construction, not live.
Wire bytes include IPC framing (slightly conservative for a worker
computing payload size). Defaults to None/False so non-HTTP transports
and existing call sites are unchanged.
Framework strict-fail across all HTTP methods, with method-type-aware
hardness:
* Unary: both caps are *hard*. Overshoot raises
``_RpcHttpError(500)``; ``_set_http_status`` rewrites to
200 + ``X-VGI-RPC-Error`` carrying the EXCEPTION batch.
* Stream-exchange: both caps are *hard*. Overshoot replaces the
response body with a fresh IPC stream containing the EXCEPTION batch.
* Stream-producer: wire cap is *soft* — continuation tokens cover
overshoots. External cap stays *hard*: externalised uploads have
no escape valve, so a producer pushing more than
``max_externalized_response_bytes`` writes an EXCEPTION batch into
the in-progress IPC stream and breaks the loop.
A new ``_enforce_response_budgets`` helper in ``_responses.py``
centralises the cap check and the error message format. Cross-language
ports must produce error messages containing the tokens
``max_response_bytes`` and ``max_externalized_response_bytes`` —
the conformance tests assert on those substrings.
Conformance tests (HTTP-only via ``transports=("http",)``):
* ``http_response_cap.unary_strict_fail``
* ``http_response_cap.exchange_strict_fail``
* ``http_response_cap.producer_external_strict_fail``
* ``http_response_cap.externalized_strict_fail``
Tests probe the active server's caps via ``http_capabilities()``
(URL stashed on ``LogCollector.http_base_url`` by the CLI runner) and
self-skip when caps aren't configured or when the wrong combination of
caps + externalisation is in effect. ``LogCollector`` gained the
``http_base_url`` field; ``run_conformance`` gained a ``transports=``
parameter that gates incompatible tests with a clear skip reason.
A ``TestHttpResponseCap`` / ``TestHttpResponseCapSoftWire`` pytest
class pair in ``_pytest_suite.py`` exercises the same contracts via
the strict-cap fixture (``conformance_http_strict_cap_port`` from
Phase A).
Strict-cap conformance worker bumped from 64 KiB to 1 MiB so the
existing conformance suite runs cleanly against it (smaller streams
like ``cancellable_producer`` no longer trip the wire cap).
Documentation in ``docs/porting-guide.md`` and ``CLAUDE.md`` covers
the two-cap design, capability discovery via response headers, the
``transports=`` field, and the strict-fail contract for ports.
Verification:
* ruff format / ruff check / mypy / ty: clean.
* HTTP conformance against regular worker (no caps): 4 strict-fail
tests skip cleanly; 97 existing tests pass.
* HTTP conformance against strict-cap worker (no externalisation):
unary + exchange strict-fail PASS; externalised pair SKIP.
* HTTP conformance against strict-cap + externalisation: producer
external + externalized PASS; unary + exchange SKIP (wire rescued).
* Pipe conformance: 4 strict-fail tests skip via transports filter
with clear reason; existing 97 tests pass.
* Full pytest: 2986 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+10Lines changed: 10 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,6 +82,16 @@ The full process before committing code is
82
82
83
83
-**`http/`***(optional — `pip install vgi-rpc[http]`)* — HTTP transport package using Falcon (server) and httpx (client). Exposes `make_wsgi_app()` to serve an `RpcServer` as a Falcon WSGI app, `serve_http()` as a convenience wrapper that combines `make_wsgi_app` + automatic free-port selection + `waitress.serve` (prints `PORT:<port>` to stdout for machine-readable discovery), and `http_connect()` for the client side. Streaming is stateless: each exchange carries serialized `StreamState` in a signed token in Arrow custom metadata. Supports pluggable authentication via an `authenticate` callback and `_AuthMiddleware`. Includes `_testing.py` with `make_sync_client()` for in-process testing without a real HTTP server.
84
84
85
+
**HTTP response caps**. Two independent operator knobs gate response size:
86
+
-`max_response_bytes` caps the HTTP body (what literally lands on the wire). Default `None` = unbounded. For producer streams this is *soft* — continuation tokens cover overshoot; for unary and stream-exchange it is *hard* and surfaces as `RpcError` (200 + `X-VGI-RPC-Error: true` + EXCEPTION batch).
87
+
-`max_externalized_response_bytes` caps total bytes uploaded to external storage during one HTTP response. Always *hard* — externalised uploads have no escape valve. Strict-fail surfaces the same way.
88
+
89
+
Externalised payloads are NOT charged against `max_response_bytes` (they leave only tiny pointer batches on the wire). The two knobs answer different operator questions: HTTP body size (proxy/gateway limit) vs per-call data volume.
90
+
91
+
Both are surfaced via response headers (`VGI-Max-Response-Bytes`, `VGI-Max-Externalized-Response-Bytes`, `VGI-Externalization-Enabled`) so `http_capabilities()` and conformance tests can probe them. Workers can read `out.remaining_response_bytes` / `out.remaining_externalized_response_bytes` / `out.externalization_enabled` on `OutputCollector` to size emits within budget.
92
+
93
+
The deprecated alias `max_stream_response_bytes` (constructor kwarg, `--max-stream-response-bytes` CLI flag, `VGI_RPC_MAX_STREAM_RESPONSE_BYTES` env) is retained for one release cycle; emits a DeprecationWarning when set. The cap is no longer stream-only — it now governs all HTTP method responses.
94
+
85
95
### Wire protocol
86
96
87
97
Multiple IPC streams are written sequentially on the same pipe. Every request batch carries `vgi_rpc.request_version` in custom metadata; the server validates this before dispatch and rejects mismatches with `VersionError`. Each method call writes one request stream and reads one response stream:
Copy file name to clipboardExpand all lines: docs/porting-guide.md
+99Lines changed: 99 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -82,6 +82,105 @@ In order, smallest-blast-radius first:
82
82
-**Java**: aligned for the stdio transport (120 access-log entries validate). HTTP transport does not yet wire the dispatch hook; stream_id stability across HTTP continuations is a follow-up — see `vgi-rpc-go` for the reference state-token plumbing.
83
83
-**Rust**: aligned for the stdio transport (120 access-log entries validate). HTTP transport does not yet wire the new DispatchInfo fields (remote_addr, request_data, stable stream_id via state-token round-trip) — see `vgi-rpc-go` for reference plumbing.
84
84
85
+
## HTTP response-cap conformance
86
+
87
+
The conformance suite includes HTTP-only tests under category
88
+
`http_response_cap.*` that verify the framework refuses to emit
89
+
oversize responses when the operator has configured caps and there is
90
+
no externalisation escape valve.
91
+
92
+
### Two operator knobs
93
+
94
+
-**`max_response_bytes`** — caps the HTTP body size (the bytes that
95
+
literally land on the wire). Externalised payloads do not count
96
+
against this; their pointer batches are tiny.
97
+
-**`max_externalized_response_bytes`** — caps the total bytes
98
+
uploaded to external storage during one HTTP response. Bounds how
99
+
much data the client will end up fetching for one RPC, regardless
100
+
of how the framework chose to deliver it.
101
+
102
+
Both default to `None` (unbounded) and are configurable via
103
+
constructor kwargs, CLI flags, or env vars (see `make_wsgi_app`,
104
+
`serve_http`, and `run_server` for the full set; the deprecated
105
+
`max_stream_response_bytes` alias remains for one release cycle).
106
+
107
+
### Capability discovery (HTTP response headers)
108
+
109
+
Servers advertise the configured caps so capability-aware clients and
110
+
conformance tests can probe without a separate handshake:
111
+
112
+
| Header | Value when set |
113
+
|---|---|
114
+
|`VGI-Max-Response-Bytes`| integer body cap |
115
+
|`VGI-Max-Externalized-Response-Bytes`| integer external cap |
116
+
|`VGI-Externalization-Enabled`|`true` or `false` (always present) |
117
+
118
+
Cross-language ports must emit these headers on every response when
119
+
the corresponding knob is configured.
120
+
121
+
### Strict-fail behaviour by method type
122
+
123
+
| Method type | Wire cap (`max_response_bytes`) | External cap |
124
+
|---|---|---|
125
+
| Unary | hard — strict-fail | hard — strict-fail |
126
+
| Stream-exchange | hard — strict-fail | hard — strict-fail |
also requires externalisation enabled and an external cap)
156
+
-`http_response_cap.externalized_strict_fail` (HTTP only; same
157
+
preconditions)
158
+
159
+
The tests self-skip when caps aren't configured, so a port can run
160
+
the full suite against any worker without these failing. To
161
+
exercise them, boot a strict-cap worker — see
162
+
`tests/serve_conformance_http_strict.py` for the Python reference
163
+
(defaults to 1 MiB body + 1 MiB external).
164
+
165
+
### Worker visibility (optional)
166
+
167
+
`OutputCollector` exposes three new properties so worker code can
168
+
size its emit to the available budget:
169
+
170
+
-`out.remaining_response_bytes: int | None` — wire body bytes left
171
+
this iteration.
172
+
-`out.remaining_externalized_response_bytes: int | None` — external
173
+
channel bytes left.
174
+
-`out.externalization_enabled: bool` — whether the server has a
175
+
storage backend wired up.
176
+
177
+
Snapshot semantic: each value is fixed at collector construction;
178
+
within one `state.process()` call it does not update as the worker
179
+
emits. Wire bytes include IPC framing (slightly conservative for a
180
+
worker computing payload size). Optional surface; ports that don't
181
+
expose it are still conformant — strict-fail catches workers that
182
+
ignore the budget.
183
+
85
184
## Gotchas
86
185
87
186
-**Arrow dictionary encoding.** Across language Arrow libraries, the placement of dictionary messages in IPC streams differs. The schema's `request_data` round-trip rule was chosen specifically to absorb this — don't try to byte-match Python.
0 commit comments