Repository navigation
docs(skill): API naming contract + dependency refresh, built-in cron scheduler, CI fixes - #145
Merged
owjs3901 merged 36 commits intoOct 31, 2026
Conversation
owjs3901
marked this pull request as ready for review
October 30, 2026 11:58
The pre-baked metadata fast path in write_response_header gated on ptr::eq between two const &str uses. A const has no stable address, so whether the fast path fired depended on codegen-unit partitioning: any test-only edit flipped it and the tarpaulin build lost 3 lines (99.96% < 100%). Compare by content (len + 5-byte memcmp): same bytes on the wire, always hits for the engine version.
Changepacksvespera@0.4.0 - Cargo.tomlMaybe you forgot to write the following files to the latest version vespera@0.4.0 → 0.5.0 - crates/vespera/Cargo.tomlMinor
vespera_core@0.4.0 → 0.5.0 - crates/vespera_core/Cargo.tomlMinor
vespera_inprocess@0.4.0 → 0.4.1 - crates/vespera_inprocess/Cargo.tomlPatch
vespera_jni@0.4.0 → 0.4.1 - crates/vespera_jni/Cargo.tomlPatch
vespera_macro@0.4.0 → 0.5.0 - crates/vespera_macro/Cargo.tomlMinor
vespera-bridge@0.4.0 → 0.5.0 - libs/vespera-bridge/build.gradle.ktsMinor
vespera-bridge-gradle-plugin@0.4.0 → 0.4.1 - libs/vespera-bridge-gradle-plugin/build.gradle.ktsPatch
|
The metadata fast path in write_response_header compares the version by content, so cover an owned copy of the engine version alongside the borrowed one and drop the stale pointer-eq wording. Audit of ptr::eq/as_ptr/Arc::ptr_eq in crates/ found no other branch selection that depends on const/static addresses: slice_from_owner is a bounds-checked range test on real allocations that falls back to the copying path, the JNI overlap check compares live direct-buffer ranges, and the remaining Arc::ptr_eq/ptr::eq uses are test assertions on real allocations.
clippy::assert_is_empty rejects assert!(x.is_empty()) and its suggested fix was assert_eq!(x, [] as [T; 0]). Compare against Vec::<T>::new() instead: same failure output (the value is printed), no cast-to-zero-length-array noise, no lint suppression. The inline attrs assertion becomes an exact length check.
scripts/bisect-dep.sh and .ps1 binary-search the published releases of one crate with cargo update -p X --precise V, using a git-bisect-run style exit-code probe, and always restore Cargo.lock. Verified in both shells against a synthetic probe (either 1.13.0..1.19.0 finds first bad 1.17.0).
The Criterion gate measured the merge base in one block and the PR head in a later block, so runner drift showed up as fake regressions (PR #144's two byte-identical sides swung -12%..+15% and tripped a different bench each run). scripts/bench-interleaved.sh now builds both sides (separate fresh target dirs: a shared one makes cargo judge the second build "fresh" by mtime and silently benchmark one binary against itself), then runs 5 alternating base/head rounds with per-side CRITERION_HOME. A bench fails iff the median paired change (head/base - 1 inside a round) exceeds +30% AND the min of the paired rounds (the sign-test lower confidence limit of the median) exceeds +20%: same thresholds, but one noisy round cannot trip the gate any more. Local evidence (Git Bash, 16 gated benches, 5 rounds): - A/A: all medians within +-4%, worst single round -13%; no failures. - Injected ~2us spin in write_status_code (uncommitted, reverted): exit 1, 8 benches flagged with every paired round > +58% (e.g. resolve_path +157% median / +149% min); unaffected wire_path/1024 stayed at -0.3%.
The #[cron] macro validated with croner 4 (sloppy ranges) while the runtime tokio-cron-scheduler parsed with croner 3. A differential run against Job::new_async found 28 divergent expressions where the macro accepted what the scheduler rejected at startup (a step without a start such as "/10", and the "LW" day-of-month token). Rather than patch the validator, both sides now share one parser (vespera_core::cron, croner 4) and a small scheduler (vespera::cron) replaces tokio-cron-scheduler. The second croner, chrono-tz, uuid, tracing and num-* crates leave the cron feature's graph (320 -> 313 resolved name+version pairs workspace-wide; the croner duplicate is gone). Unused example dependencies and dead [build-dependencies] are removed as well. BREAKING: vespera::tokio_cron_scheduler re-export is removed.
Micro benches hide per-op costs that compound in large applications, so add opt-in scale tooling (not part of the PR bench gate, not a workspace member): - scripts/gen-stress-app.ps1 + scripts/measure-stress.ps1: emit a standalone N-route vespera app (schemas, enums, wide/circular/deep types, schema_type!) and report clean/incremental build time, macro_expand_crate, peak rustc working set and the log-log scaling slope. -Control/-Keep attribute cost to serde, vespera attributes, vespera! or a hand-written axum router. - crates/vespera_inprocess/benches/extreme.rs: size ladders for route tables (1k-50k), headers (10-10k), header values (ASCII/Unicode/escape-heavy), 422 envelopes (10-100k, crossing the 64 KiB hoist cap), long paths/queries, buffered bodies to 100 MiB, streaming to 1000 MiB with a counting allocator (peak heap), 1k-100k concurrent dispatches, 100-10k registered apps, thread scaling. Every case asserts the status it claims to measure. Findings (see PR notes): dispatch cost is flat from 1k to 50k routes and from 1 to 10k registered apps; per-header and per-error costs are flat; streaming peak heap is constant from 16 MiB to 1000 MiB. No superlinear hot path in vespera_macro (macro_expand_crate slope ~1.0 up to 2000 routes). Cargo.lock and bun.lock are regenerated from scratch so every transitive dependency resolves to its latest compatible release.
Bumps (via dependency-check-updates): Spring Boot 3.2.5 -> 4.1.2 (bridge, demo-app), spring-test 6.1.6 -> 7.0.10, JUnit 5.10.2 -> 6.1.3, AssertJ 3.27.7, Jackson 2.22.4, vanniktech maven-publish 0.37.0, plugin-publish 2.2.1, Gradle wrapper 9.8.2 everywhere (the demo was still on 8.11.1). Code changes needed for the major migration: - HttpStatus.PAYLOAD_TOO_LARGE -> CONTENT_TOO_LARGE (Spring 7 deprecation). - Tests: HttpHeaders is no longer a Map (containsKey -> containsHeader); the renamed 413/422 constants must match what getStatusCode() returns. - demo-app tests: TestRestTemplate moved to spring-boot-resttestclient (+ @AutoConfigureTestRestTemplate, spring-boot-restclient); Jackson 3 is now Boot's default so the test builds its own Jackson 2 ObjectMapper. Verified on JDK 17: vespera-bridge 297 tests / 0 failures, demo-app E2E 43 tests / 0 failures (6 bench-gated skips, unchanged). JNI latency on the real cdylib (median of 3 interleaved runs, ns/op): sync 1611 -> 1556, direct 1569 -> 1527, response-streaming 2052 -> 1982, bidirectional 2232 -> 2165, async 14123 -> 14278; allocated bytes/op within +-3%.
vespera-bridge moves to Spring Boot 4 (breaking for Boot 3 consumers, so a minor bump while 0.x); the Gradle plugin only changes build tooling (patch).
vespera_inprocess only needs axum's Router, yet pulled axum's default feature set (hyper, hyper-util, serde_urlencoded, tracing, ...). Turning default features off takes its normal dependency graph from 67 to 40 crates; vespera goes from 151 to 147 (it still enables form/json/query/ multipart/http1/tokio explicitly). Behaviour change: axum's rejection logging through tracing is no longer forced on; applications that want it keep it through their own axum dependency (Cargo unifies features). Dropping the implicit features exposed that vespera_inprocess used tokio::sync::mpsc without declaring tokio's "sync" feature (it only built because axum enabled it); declare it, and give the tests/benches the axum and tokio features they use as dev-dependencies.
The 422 hoist deserialized path/code/message into owned Strings (two to three allocations per error). They now borrow from the body as Cow<str> through a small BorrowedStr deserializer (serde's own Cow<str> impl always allocates) and are owned only when the JSON string held an escape. ValidationErrorItem gains a lifetime; the wire bytes are unchanged (wire_contract and the hand-vs-serde round-trip tests pass). Same-run criterion A/B vs the saved baseline (hoist_422_ab, typed_new): errors_1 103.1 ns -> 95.0 ns (-5.2%, p < 0.05) errors_5 370.7 ns -> 321.6 ns (-14.7%, p < 0.05) End to end through dispatch_from_bytes (extreme bench, errors_422): 100 errors 9.46 us -> 8.58 us (-10.0%, p < 0.05) 900 errors 77.0 us -> 68.5 us (-11.1%, p < 0.05) 10 errors: -2.3%, not significant. alloc_budget case F: 26 -> 19-21 allocations per 422 dispatch; the budget is tightened from 26 to 22 (down only).
The generic PhantomData visitor left one line (the generic Visitor impl header) reported uncovered by tarpaulin's llvm engine under the CI rustfmt layout, failing the 100% line-coverage gate. The visitor now yields a plain Cow<'de, str> and the wrapper is applied with .map(BorrowedStr): no lifetime-parameterised helper type, same behaviour, fewer lines. cargo llvm-cov shows every line of hoist.rs executed.
tarpaulin (llvm engine, CI rustfmt layout) still reported the BorrowedStr Deserialize impl header uncovered, failing the 100% line-coverage gate. Remove the custom deserializer: the strict typed pass now deserializes path/code/message as Option<&str> borrowed from the body. A string holding an escape cannot be borrowed, so that pass fails and the existing lenient serde_json::Value walk hoists the same errors with owned values (it already handles wrong-typed fields). Less code, one fewer type, and the common escape-free envelope allocates nothing per error. Same-run criterion A/B vs the saved baseline (p < 0.05 for all): hoist_422_ab typed_new errors_1 103.1 ns -> 99.4 ns (-3.7%) hoist_422_ab typed_new errors_5 370.7 ns -> 326.4 ns (-13.3%) dispatch errors_422/10 -7.7% dispatch errors_422/100 9.46 us -> 8.40 us (-11.8%) dispatch errors_422/900 77.0 us -> 64.0 us (-16.6%) alloc_budget case F: 26 -> 20 allocations per 422 dispatch (budget 22). Test: plain strings are Cow::Borrowed, escaped strings still hoist unescaped.
vespera_macro: namespace_export_schemas round-trips the spec through JSON text instead of serde_json::to_value/from_value, so the Value (de)serializer copy of every OpenAPI type's serde impl is no longer compiled into the proc-macro (LLVM IR lines 553,831 -> 483,742; own-crate rebuild 3.35s -> 2.98s, peak rustc 538 -> ~505 MB in a same-session A/B). schema! borrows the known-schema maps from schema storage as &str instead of cloning into String maps, which also drops the duplicate String monomorphization of the schema parser (-4,967 IR lines). axum-example: sea-orm runtime-tokio instead of runtime-tokio-rustls. The example only uses sqlite, so rustls/ring/webpki/untrusted and the second getrandom and windows-sys versions leave the lockfile (workspace unique crates 340 -> 321).
…graph vespera: chrono without default features (serde + now). The crate only needs the DateTime types, serde support and Utc::now() for the cron scheduler, so iana-time-zone and the windows-core/-implement/-interface proc-macro stack no longer compile for every user. `vespera::chrono::Local` (chrono's `clock` feature) is no longer enabled by vespera itself. criterion (vespera, vespera_inprocess dev-deps): default-features = false with cargo_bench_support only. The regression gate reads estimates.json, so rayon, plotters and the html_reports/gnuplot plumbing were unused. examples/third: drop the axum-test and insta dev-dependencies it never used (cargo machete clean). Lockfiles: log 0.4.35 (cargo update); bun.lock transitive refresh. Every direct Cargo, npm, Gradle and workflow dependency is at its latest release. Unique external crates: `vespera` default features 178 -> 154, workspace (all targets and features) 340 -> 313.
The examples' integration tests used axum-test for TestServer + multipart forms. It brought 30 crates (hyper, reserve-port, expect-json, typetag, rand 0.10 -> chacha20 -> a second cpufeatures, ...) into the dev graph for a handful of calls. examples/testkit drives the router with tower's oneshot and implements exactly the surface the tests use (get/post/put/patch, json, multipart, bytes/content_type/add_header, status/text/json assertions); the 82 integration tests are unchanged apart from the import line. Workspace unique external crates (all targets/features, incl. dev) 313 -> 283; duplicate crate names 6 -> 5 (cpufeatures gone). Changepack note updated for the chrono and proc-macro changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Started as a docs-only change (the API naming contract section of
SKILL.md, upstreamed from devup-mcp 880787e) and grew into a CI-fix and dependency-maintenance pass. Rust 1.99 Clippy broke the existing tests and the coverage gate, so those are fixed here too. The published-crate impact is recorded in.changepacks/changepack_log_dependency-refresh.json(vespera, vespera_core, vespera_macro, vespera-bridge: Minor; vespera_inprocess, vespera-bridge-gradle-plugin: Patch).Changes
SKILL.mdAPI naming contract (camelCase wire names, snake_case ERD columns,#[serde(rename_all = "camelCase")]includingschema_type!and multipartform_datanames).clippy::assert_is_emptyfixed by comparing againstVec::<T>::new(); no#[allow].write_response_headercomparedconst &strbyptr::eq, whose result depended on codegen-unit partitioning (tarpaulin dropped to 99.96%). It now compares by content; wire bytes are the same.scripts/bench-interleaved.shbuilds the merge base and the PR head separately and runs 5 alternating rounds in one job, so runner drift hits both sides. Same thresholds (median paired change > +30% and min paired round > +20%).tokio-cron-schedulerreplaced by a small built-in tokio scheduler (vespera::cron) driven by croner 4.#[cron]validation and the runtime now share one parser (vespera_core::cron). A differential run found 28 expressions the macro accepted and the old scheduler rejected at startup (for example/10andLW).vesperaandvespera_inprocess(vespera_inprocessnormal graph 67 -> 40 crates).vespera_inprocessnow declares the tokiosyncfeature it already used.vespera(keepsserde+now); iana-time-zone and the windows-core proc-macro stack no longer compile.&strfrom the response body:alloc_budgetcase F 26 -> 20 allocs/op (budget lowered 26 -> 22). End to end throughdispatch_from_bytes: 100 errors 9.46 us -> 8.40 us (-11.8%), 900 errors 77.0 us -> 64.0 us (-16.6%). Escaped strings fall back to the existing lenient owned walk; wire bytes unchanged.export_app!namespacing round-trips through JSON text (vespera_macroLLVM IR 553,831 -> 483,742 lines, -70k);schema!borrows its&strmaps (a further -4,967). Own-crate rebuild 3.35 s -> 2.98 s, peak rustc 538 -> ~505 MB (same-session A/B).axum-test->examples/testkit: a tower-oneshotclient with the surface the tests use (~200 lines). 82 integration tests unchanged apart from the import line. Alsosea-ormruntime-tokioinstead ofruntime-tokio-rustlsin the sqlite example, and unused dev-deps removed.criteriontrimmed todefault-features = false+cargo_bench_support(the gate only readsestimates.json).scripts/gen-stress-app.ps1,scripts/measure-stress.ps1,crates/vespera_inprocess/benches/extreme.rs, plusscripts/bisect-dep.{sh,ps1}. Finding: no superlinear hot path.macro_expand_crateslope ~1.0 up to 2000 routes, dispatch cost flat from 1k to 50k routes and 1 to 10k registered apps, streaming peak heap constant from 16 MiB to 1000 MiB.Remaining duplicate crates (5, all upstream-forced)
synvespera_macro, serde_derive, thiserror-impl on syn 3hashbrownsea-orm -> sqlx-core -> hashlink 0.11 -> hashbrown 0.16vs-> indexmap 2.14 -> hashbrown 0.17hecksea-orm -> ouroboros_macro,sea-orm-macros -> sea-bae,sea-query -> sea-query-derive; 0.5 fromsea-orm-macrosandcroner -> strum_macros 0.27strumvespera_core -> croner 4.0.2 -> strum 0.27vssea-orm -> strum 0.28itertoolscriterion 0.8.2 (dev) -> itertools 0.13vssea-orm -> itertools 0.14Shipped crates are unaffected:
vespera/vespera_core/vespera_macro/vespera_jnihave only syn 2/3,vespera_inprocessnone. The others exist only inexamples/axum-example(sea-orm) and dev-dependencies.BREAKING / behaviour changes
vespera::tokio_cron_schedulerre-export removed; usevespera::cron.vespera-bridgerequires Spring Boot 4.1 / Spring Framework 7 / JUnit 6 / Gradle 9.8; Spring Boot 3 is no longer supported.vespera::chrono::Localneeds chrono'sclockfeature, which vespera no longer enables; add it in your own manifest.tracing/tower-logfeatures are no longer forced on byvespera/vespera_inprocess; enable them in your own axum dependency if you relied on that.wire_contract.rs);examples/axum-example/openapi.jsonis not committed.Test plan
Local:
cargo clippy --all-targets --all-features -- -D warnings,cargo clippy --workspace --no-default-features -- -D warnings,cargo fmt --check,cargo test --workspace,cargo test --workspace --doc,bun run lint,bun test.CI (head
04aa5c1, all green):Test(includes the tarpaulin 100% line-coverage gate),Criterion regression gate(interleaved),JNI E2Eon ubuntu / windows / macos (vespera-bridge unit suite + demo-app E2E),Rust branch coverage (observational),changepacks,build;deployis skipped on PRs.