Pitfalls — Rust + engine traps that have bitten us
Body is Send-not-Sync → don't hold &Message across .await
Message carries a Body whose payload stream is Send but not Sync.
Any future that holds a &Message (or a closure capturing one) across an
.await becomes non-Send, and the async service trait requires Send
futures. The compiler error is future cannot be sent between threads safely,
usually pointing at the async fn handle signature, not the real line.
Fix pattern: extract owned values before the await. Pull out what you need
as owned data — header values as String, the Range, a bodyless response
template via msg.response(StatusCode::OK, None) — then await. Concretely:
- Don't
let t = || msg.response(...)(a closure capturing&msg) and call it after an await. Inlinemsg.response(...)at each call site so the borrow ends before the await. (This exact bug appeared in the static-site change.) - In the pipeline executor, clone branch inputs eagerly and move owned bodies into retry closures.
- In
discovery::handle, takeMessageby value.
When you see the non-Send error after a refactor, look for a value still
borrowing msg that lives across an await — not the function signature.
Hot-path allocation
Code at the Runtime::dispatch/handle choke point runs per request. Gate
optional work behind a cheap predicate: e.g. boundary logging calls
LogStore::enabled() first and builds no LogRecord when the sink is the
no-op NullLogStore. Skipping this regressed the G1 p99 from ~20µs to ~250µs
(still under the 1ms target, but wasteful). Don't format! or allocate for
telemetry that may be discarded.
wasmtime 37 (feature wasm)
- bindgen uses
imports: { default: async }— notasync: true. wasmtime_wasi::WasiViewreturns aWasiCtxView.- Host
add_to_linkerneedsHasSelf<T>.
v8 150 (feature js)
- Pinned scopes: the
v8::scope!/v8::tc_scope!macros; helpers take&mut v8::PinScope<'s,'_>. TryCatchis interrogated only through the concretePinnedRef<TryCatch<HandleScope>>type.Value::to_rust_string_lossy(&self, &PinScope).- Sandbox
console.*and the WITlog()both route toHostApi::log; the JS prelude (engines/js_prelude.js) mapsconsole.log/info→info,warn→warn,error→error,debug→debug. - Editing the bootstrap or
js_prelude.jsmeans regenerating the prelude snapshot.build_runtimeboots each per-request isolate from a committed V8 startup snapshot (engines/js_prelude.snapshot.bin) so it skips recompiling ~500 lines of prelude (~4× faster per invocation). The blob is a pure optimization — an empty blob falls back to running the prelude from source — but a stale blob bakes the old prelude and silently ignores your edit.tests/prelude_snapshot.rsfails on drift; fix withcargo run -p rs2-core --example gen-js-snapshot --features js, then commit the regenerated.bin+.hash. The snapshot can't be built in the serving process (V8 inits in snapshot or normal mode per process, not both), which is why generation is a separate example, not automatic.
Smaller traps
jsonschemais pulled withdefault-features = false— don't assume default features are on.ureqhas nojsonfeature here: useinto_string()+serde_json::from_str, notinto_json().- The
rs2CLImainis not aResult-returning fn — no bare?; chain withand_then/explicit match. serde_json::ValuenumericFrom:status as i64/n as i64coerce cleanly intoValue;u128(e.g.timeUnixNano) is serialized as a string to survive JSON number precision.