Skip to content

Commit 4b84be9

Browse files
committed
Improve compile time
1 parent b6cab63 commit 4b84be9

12 files changed

Lines changed: 335 additions & 222 deletions

File tree

crates/vespera/src/multipart.rs

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,23 @@ impl TypedMultipartError {
168168
}
169169
}
170170

171+
/// Canonical JSON error envelope, byte-identical to `Validated<T>`'s 422
172+
/// envelope — `{"errors":[{"message":...,"path":...}]}` (message before path)
173+
/// — so multipart failures are consumed uniformly and, under JNI, the 422
174+
/// body hoists into the wire header exactly like a `Validated` rejection.
175+
/// Serialized through a borrowing `Serialize` (no `serde_json::Value`
176+
/// map/array/object intermediate).
177+
#[derive(serde::Serialize)]
178+
struct MultipartOneError<'a> {
179+
message: &'a str,
180+
path: &'a str,
181+
}
182+
183+
#[derive(serde::Serialize)]
184+
struct MultipartErrorEnvelope<'a> {
185+
errors: [MultipartOneError<'a>; 1],
186+
}
187+
171188
impl IntoResponse for TypedMultipartError {
172189
fn into_response(self) -> Response {
173190
let status = match &self {
@@ -189,31 +206,22 @@ impl IntoResponse for TypedMultipartError {
189206
Self::FieldTooLarge { .. } => StatusCode::PAYLOAD_TOO_LARGE,
190207
Self::Other { .. } => StatusCode::INTERNAL_SERVER_ERROR,
191208
};
192-
// Canonical JSON error envelope, byte-identical to `Validated<T>`'s
193-
// 422 envelope — `{"errors":[{"message":...,"path":...}]}` (message
194-
// before path) — so multipart failures are consumed uniformly and,
195-
// under JNI, the 422 body hoists into the wire header exactly like a
196-
// `Validated` rejection. Serialized through a borrowing `Serialize`
197-
// (no `serde_json::Value` map/array/object intermediate). `path` is
198-
// the offending field name when known, else empty.
199-
#[derive(serde::Serialize)]
200-
struct OneError<'a> {
201-
message: &'a str,
202-
path: &'a str,
203-
}
204-
#[derive(serde::Serialize)]
205-
struct Envelope<'a> {
206-
errors: [OneError<'a>; 1],
207-
}
209+
// Serialize the canonical 422 envelope (see module-scope
210+
// `MultipartErrorEnvelope` / `MultipartOneError`); `path` is the
211+
// offending field name when known, else empty.
208212
let path = self.field_name().unwrap_or("");
209213
let message = self.response_message();
210-
let body = serde_json::to_vec(&Envelope {
211-
errors: [OneError {
214+
let body = serde_json::to_vec(&MultipartErrorEnvelope {
215+
errors: [MultipartOneError {
212216
message: &message,
213217
path,
214218
}],
215219
})
216-
.expect("multipart error envelope is infallible to serialize");
220+
// Serializing a struct of two `&str` is infallible in practice; the
221+
// fallback keeps this request-time error path panic-free (matching
222+
// `Validated<T>`'s 422 envelope) by emitting a minimal valid envelope
223+
// instead of unwinding inside a handler.
224+
.unwrap_or_else(|_| br#"{"errors":[{"message":"serialization error","path":""}]}"#.to_vec());
217225
(
218226
status,
219227
[(

crates/vespera_inprocess/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ description = "In-process HTTP dispatch for axum — drive a Router via oneshot
66
license.workspace = true
77
repository.workspace = true
88

9+
[features]
10+
# Compiles the criterion A/B "before" twins (serde-based wire header
11+
# parse/serialize, the `http::request::Builder` request build, the
12+
# `serde_json::Value` 422 hoist) and the `bench_support` surface that
13+
# `benches/dispatch.rs` calls. OFF by default so production builds (and
14+
# the shipped JNI cdylib) never compile the serde wire-header scaffolding
15+
# that exists only to benchmark the hand-rolled production path against.
16+
# Enable with `cargo bench -p vespera_inprocess --features bench-support`.
17+
bench-support = []
18+
919
[dependencies]
1020
# Lock-free read snapshot for the multi-app router registry: named-app
1121
# dispatch (every request in a multi-app JNI deployment) resolves with a

crates/vespera_inprocess/benches/dispatch.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,7 @@ fn bench_async_spawn_pattern(c: &mut Criterion) {
803803
/// - `response_serialize_*`: slice-serialize of a many-header response
804804
/// (10 single-value + 3-value `set-cookie` + content-type/length) —
805805
/// `write_wire_header_into_slice` (hand) vs the `serde_json` twin.
806+
#[cfg(feature = "bench-support")]
806807
fn bench_wire_header_serde(c: &mut Criterion) {
807808
use vespera_inprocess::ResponseMetadata;
808809
use vespera_inprocess::bench_support::{
@@ -893,6 +894,7 @@ fn bench_wire_header_serde(c: &mut Criterion) {
893894
/// Fixtures span the dispatch hot path's real request shapes: a bodyless `GET`
894895
/// (the DIRECT sweet spot), a `GET` with 3 headers, a small `POST` with
895896
/// `content-type`, and a `POST` with 8 realistic headers.
897+
#[cfg(feature = "bench-support")]
896898
fn bench_request_build_path(c: &mut Criterion) {
897899
use vespera_inprocess::bench_support::{bench_build_request_new, bench_build_request_old};
898900

@@ -967,6 +969,7 @@ fn bench_request_build_path(c: &mut Criterion) {
967969
/// Fixtures: a 1-error envelope (typical single-field failure) and a 5-error
968970
/// envelope (form-heavy request) — where the eliminated `Value` map/array/key
969971
/// allocations scale with error count.
972+
#[cfg(feature = "bench-support")]
970973
fn bench_hoist_422_path(c: &mut Criterion) {
971974
use vespera_inprocess::bench_support::{bench_hoist_new, bench_hoist_old};
972975

@@ -1056,9 +1059,23 @@ criterion_group!(
10561059
bench_registry_ab,
10571060
bench_headers_path,
10581061
bench_streaming_path,
1059-
bench_async_spawn_pattern,
1062+
bench_async_spawn_pattern
1063+
);
1064+
1065+
// The within-run A/B groups compare the production hand-rolled paths against
1066+
// the retained `serde_json` / `http::request::Builder` / `serde_json::Value`
1067+
// "before" twins. Those twins live behind the `bench-support` feature so a
1068+
// production build never compiles them — run these groups with
1069+
// `cargo bench -p vespera_inprocess --bench dispatch --features bench-support`.
1070+
#[cfg(feature = "bench-support")]
1071+
criterion_group!(
1072+
ab_benches,
10601073
bench_wire_header_serde,
10611074
bench_request_build_path,
10621075
bench_hoist_422_path
10631076
);
1077+
1078+
#[cfg(feature = "bench-support")]
1079+
criterion_main!(benches, ab_benches);
1080+
#[cfg(not(feature = "bench-support"))]
10641081
criterion_main!(benches);

crates/vespera_inprocess/src/dispatch.rs

Lines changed: 74 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,65 @@ use crate::envelope::{RequestEnvelope, ResponseEnvelope, ResponseMetadata};
1313
use crate::internal::{dispatch_and_split, dispatch_parts, to_response_envelope_text};
1414
use crate::registry::resolve_app_router;
1515
use crate::wire::{
16-
WIRE_HEADER_RESERVE, WIRE_VERSION, error_wire, header_capacity_estimate, parse_wire_header,
17-
split_wire_borrowed, split_wire_request, to_wire_bytes, write_wire_header_into_slice,
18-
write_wire_header_into_vec,
16+
WIRE_HEADER_RESERVE, WIRE_VERSION, WireRequestHeader, error_wire, header_capacity_estimate,
17+
parse_wire_header, split_wire_borrowed, split_wire_request, to_wire_bytes,
18+
write_wire_header_into_slice, write_wire_header_into_vec,
1919
};
2020

21+
// ── Shared wire prelude (used by every wire entry point) ─────────────
22+
23+
/// Ingress-cap guard shared by the **buffered** wire entry points
24+
/// (`dispatch_from_bytes_async`, `dispatch_into_async`,
25+
/// `dispatch_into_async_borrowed`, and the response-streaming pair).
26+
/// Returns the `413` wire bytes when the request exceeds the configured
27+
/// maximum, else `None`. Centralizing the message keeps the cap identical
28+
/// across entry points; **bidirectional** streaming is intentionally exempt
29+
/// (it is `O(chunk)` RAM) and so does not call this.
30+
#[inline]
31+
pub fn check_ingress_cap(len: usize) -> Option<Vec<u8>> {
32+
if crate::config::request_exceeds_limit(len) {
33+
Some(error_wire(
34+
413,
35+
&format!(
36+
"request size {len} bytes exceeds configured maximum of {} bytes",
37+
crate::config::max_request_bytes()
38+
),
39+
))
40+
} else {
41+
None
42+
}
43+
}
44+
45+
/// Wire-prelude shared by **every** wire entry point (buffered,
46+
/// direct-write, and streaming): parse the header, enforce the protocol
47+
/// [`WIRE_VERSION`], and resolve the target app [`Router`]. Centralizing
48+
/// this keeps the security-sensitive version check + app resolution
49+
/// byte-identical across all dispatchers — the previous per-entry-point
50+
/// copies were a drift hazard.
51+
///
52+
/// `header_bytes` is the wire header-JSON region; the returned
53+
/// [`WireRequestHeader`] borrows from it, so the caller MUST keep it alive
54+
/// for as long as the header is used. On failure the `Err` carries the
55+
/// exact wire error bytes to deliver in the caller's shape (`400` for a
56+
/// parse error or version mismatch, `400`/`404` from app resolution).
57+
#[inline]
58+
pub fn parse_validate_resolve(
59+
header_bytes: &[u8],
60+
) -> Result<(WireRequestHeader<'_>, Router), Vec<u8>> {
61+
let header = parse_wire_header(header_bytes).map_err(|msg| error_wire(400, &msg))?;
62+
if header.v != WIRE_VERSION {
63+
return Err(error_wire(
64+
400,
65+
&format!(
66+
"unsupported wire version: got {}, expected {WIRE_VERSION}",
67+
header.v
68+
),
69+
));
70+
}
71+
let router = resolve_app_router(&header)?;
72+
Ok((header, router))
73+
}
74+
2175
// ── Dispatch (direct API — backward compatible) ──────────────────────
2276

2377
/// Dispatch a [`RequestEnvelope`] through an axum [`Router`] and
@@ -118,40 +172,20 @@ pub fn dispatch_from_bytes(input: Vec<u8>, runtime: &tokio::runtime::Runtime) ->
118172
/// guarantees as [`dispatch_from_bytes`]), including `404` when no app
119173
/// is registered under the requested name.
120174
pub async fn dispatch_from_bytes_async(input: Vec<u8>) -> Vec<u8> {
121-
// Ingress cap (defense-in-depth): reject an oversized buffered
122-
// request with 413 before doing any further work. Unlimited by
123-
// default (see `max_request_bytes`); streaming paths are exempt.
124-
if crate::config::request_exceeds_limit(input.len()) {
125-
return error_wire(
126-
413,
127-
&format!(
128-
"request size {} bytes exceeds configured maximum of {} bytes",
129-
input.len(),
130-
crate::config::max_request_bytes()
131-
),
132-
);
175+
// Ingress cap (defense-in-depth): reject an oversized buffered request
176+
// with 413 before any further work. Unlimited by default; bidirectional
177+
// streaming is exempt. See [`check_ingress_cap`].
178+
if let Some(err) = check_ingress_cap(input.len()) {
179+
return err;
133180
}
134-
// Wire-level checks next: malformed input must report parse
135-
// errors regardless of whether an app is registered.
181+
// Malformed input must report parse errors regardless of whether an app
182+
// is registered, so split first, then the shared parse/version/resolve.
136183
let (header_bytes, body_bytes) = match split_wire_request(input) {
137184
Ok(parts) => parts,
138185
Err(msg) => return error_wire(400, &msg),
139186
};
140-
let header = match parse_wire_header(&header_bytes) {
141-
Ok(h) => h,
142-
Err(msg) => return error_wire(400, &msg),
143-
};
144-
if header.v != WIRE_VERSION {
145-
return error_wire(
146-
400,
147-
&format!(
148-
"unsupported wire version: got {}, expected {WIRE_VERSION}",
149-
header.v
150-
),
151-
);
152-
}
153-
let router = match resolve_app_router(&header) {
154-
Ok(r) => r,
187+
let (header, router) = match parse_validate_resolve(&header_bytes) {
188+
Ok(parts) => parts,
155189
Err(wire) => return wire,
156190
};
157191

@@ -296,41 +330,15 @@ pub fn dispatch_into(
296330
pub async fn dispatch_into_async(input: Vec<u8>, out: &mut [u8]) -> DirectWriteResult {
297331
// Ingress cap (defense-in-depth) — same policy as
298332
// `dispatch_from_bytes_async`; 413 written into the caller buffer.
299-
if crate::config::request_exceeds_limit(input.len()) {
300-
return write_wire_into(
301-
out,
302-
&error_wire(
303-
413,
304-
&format!(
305-
"request size {} bytes exceeds configured maximum of {} bytes",
306-
input.len(),
307-
crate::config::max_request_bytes()
308-
),
309-
),
310-
);
333+
if let Some(err) = check_ingress_cap(input.len()) {
334+
return write_wire_into(out, &err);
311335
}
312336
let (header_bytes, body_bytes) = match split_wire_request(input) {
313337
Ok(parts) => parts,
314338
Err(msg) => return write_wire_into(out, &error_wire(400, &msg)),
315339
};
316-
let header = match parse_wire_header(&header_bytes) {
317-
Ok(h) => h,
318-
Err(msg) => return write_wire_into(out, &error_wire(400, &msg)),
319-
};
320-
if header.v != WIRE_VERSION {
321-
return write_wire_into(
322-
out,
323-
&error_wire(
324-
400,
325-
&format!(
326-
"unsupported wire version: got {}, expected {WIRE_VERSION}",
327-
header.v
328-
),
329-
),
330-
);
331-
}
332-
let router = match resolve_app_router(&header) {
333-
Ok(r) => r,
340+
let (header, router) = match parse_validate_resolve(&header_bytes) {
341+
Ok(parts) => parts,
334342
Err(wire) => return write_wire_into(out, &wire),
335343
};
336344

@@ -381,41 +389,15 @@ pub async fn dispatch_into_async(input: Vec<u8>, out: &mut [u8]) -> DirectWriteR
381389
/// the same error / `422` / overflow semantics apply.
382390
pub async fn dispatch_into_async_borrowed(input: &[u8], out: &mut [u8]) -> DirectWriteResult {
383391
// Ingress cap (defense-in-depth) — same policy as `dispatch_into_async`.
384-
if crate::config::request_exceeds_limit(input.len()) {
385-
return write_wire_into(
386-
out,
387-
&error_wire(
388-
413,
389-
&format!(
390-
"request size {} bytes exceeds configured maximum of {} bytes",
391-
input.len(),
392-
crate::config::max_request_bytes()
393-
),
394-
),
395-
);
392+
if let Some(err) = check_ingress_cap(input.len()) {
393+
return write_wire_into(out, &err);
396394
}
397395
let (header_bytes, body_bytes) = match split_wire_borrowed(input) {
398396
Ok(parts) => parts,
399397
Err(msg) => return write_wire_into(out, &error_wire(400, &msg)),
400398
};
401-
let header = match parse_wire_header(header_bytes) {
402-
Ok(h) => h,
403-
Err(msg) => return write_wire_into(out, &error_wire(400, &msg)),
404-
};
405-
if header.v != WIRE_VERSION {
406-
return write_wire_into(
407-
out,
408-
&error_wire(
409-
400,
410-
&format!(
411-
"unsupported wire version: got {}, expected {WIRE_VERSION}",
412-
header.v
413-
),
414-
),
415-
);
416-
}
417-
let router = match resolve_app_router(&header) {
418-
Ok(r) => r,
399+
let (header, router) = match parse_validate_resolve(header_bytes) {
400+
Ok(parts) => parts,
419401
Err(wire) => return write_wire_into(out, &wire),
420402
};
421403

crates/vespera_inprocess/src/internal.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub async fn dispatch_parts<'h>(
5454
/// Start a request builder with method + URI. When `query` is empty
5555
/// the borrowed `path` feeds `Uri` parsing directly — no intermediate
5656
/// `String`; otherwise a single exact-capacity join is allocated.
57+
#[cfg(any(test, feature = "bench-support"))]
5758
fn request_builder(method: Method, path: &str, query: &str) -> http::request::Builder {
5859
let builder = Request::builder().method(method);
5960
if query.is_empty() {
@@ -174,6 +175,7 @@ fn build_request_from_bytes<'h>(
174175
/// `wire_header_serde` group's hand-vs-`serde_json` twin). Routes the request
175176
/// through the builder state machine the production path replaced; produces a
176177
/// byte-identical request. Not used on any production path.
178+
#[cfg(any(test, feature = "bench-support"))]
177179
fn build_request_from_bytes_builder_old<'h>(
178180
method_str: &str,
179181
path: &str,
@@ -204,6 +206,7 @@ fn build_request_from_bytes_builder_old<'h>(
204206
/// Sum a built request's method / path / query / header byte lengths so the
205207
/// `request_build_ab` A/B cannot be optimised down to a partial build.
206208
/// Bench-only.
209+
#[cfg(any(test, feature = "bench-support"))]
207210
fn request_field_len_sum(req: &Request<Body>) -> usize {
208211
let mut acc = req.method().as_str().len() + req.uri().path().len();
209212
if let Some(query) = req.uri().query() {
@@ -217,6 +220,7 @@ fn request_field_len_sum(req: &Request<Body>) -> usize {
217220

218221
/// Bench A/B: production direct-construction request build cost. Returns a
219222
/// summed length so the optimiser cannot elide the build. Bench-only.
223+
#[cfg(any(test, feature = "bench-support"))]
220224
#[doc(hidden)]
221225
#[must_use]
222226
pub fn bench_build_request_new(
@@ -232,6 +236,7 @@ pub fn bench_build_request_new(
232236

233237
/// Bench A/B: previous `http::request::Builder` request build cost.
234238
/// Bench-only.
239+
#[cfg(any(test, feature = "bench-support"))]
235240
#[doc(hidden)]
236241
#[must_use]
237242
pub fn bench_build_request_old(

crates/vespera_inprocess/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ pub use wire::error_wire;
107107
/// `pub` items) can call both the hand-rolled and the retained
108108
/// `serde_json` wire-header paths in the same measurement run. Hidden
109109
/// from docs; do not depend on it.
110+
#[cfg(any(test, feature = "bench-support"))]
110111
#[doc(hidden)]
111112
pub mod bench_support {
112113
pub use crate::internal::{bench_build_request_new, bench_build_request_old};

0 commit comments

Comments
 (0)