Skip to content

fix(runtime): derive shard and procedure ids from the declaration site - #253

Open
JuanMarchetto wants to merge 1 commit into
tokio-rs:mainfrom
JuanMarchetto:fix/deterministic-endpoint-ids
Open

fix(runtime): derive shard and procedure ids from the declaration site#253
JuanMarchetto wants to merge 1 commit into
tokio-rs:mainfrom
JuanMarchetto:fix/deterministic-endpoint-ids

Conversation

@JuanMarchetto

Copy link
Copy Markdown
Contributor

Summary

Fixes #252. #[shard] and #[procedure] minted their endpoint id with Uuid::new_v4() at macro expansion time, so every compilation produced new URLs under /_topcoat/shards/<id> and /_topcoat/procedures/<id>. A browser tab left open across a deploy keeps polling the previous binary's id, the new binary does not serve it, and the failure is silent: the runtime swaps the empty body in and the widget content disappears with nothing in the console. topcoat asset bundle compiles the crate, so the bundling step can mint fresh ids partway through a deploy sequence.

The macros now emit module_path!() into the generated code and hash it in the declaring crate, the same shape as AssetId::new doing its work at the declaration site. The key is CARGO_CRATE_NAME, the module path, and the function name, folded with the in-tree fnv1a const hash and rendered as 16 ASCII hex characters. Two items cannot share a name inside one module, so that key is collision-free for declarations at module scope, where hashing the source file would not be: two modules in one file can each hold a #[shard] fn rows. The derivation, the argument for it, and the check that it compiles and survives a rebuild are AmeinEskinder's, from his comment on #252.

ShardId and ProcedureId keep their &'static str newtype and const fn new, and the route path still comes from as_str. Nothing in the tree parses or validates the id shape: the two route builders interpolate it with format!, ReactiveScope writes it into the scope comment, and the browser runtime passes it through encodeURIComponent. The id is 16 hex characters rather than a 36-character uuid, which no consumer notices.

The tradeoff, stated plainly because it is a maintainer decision: a derived id makes a shard or procedure URL permanent across deploys, where today it changes on every build, so someone who records a URL keeps a working one instead of losing it at the next deploy. Shard endpoints need their own authorization either way, as the guards note from #251 says. If you want a different derivation, such as a per-deploy salt or an opt-in #[shard(id = "...")] escape hatch, redirect me and I will redo it.

Testing

Before, on main, expanding examples/shard twice with only a comment changed between the two runs:

$ cargo +nightly rustc -p shard --profile=check -- -Zunpretty=expanded | grep -o 'ShardId::new("[^"]*")'
ShardId::new("4d223daa-a583-4c0a-9976-fb7ce4c1b9c7")

$ printf '\n// touch to force a rebuild\n' >> examples/shard/src/main.rs
$ cargo +nightly rustc -p shard --profile=check -- -Zunpretty=expanded | grep -o 'ShardId::new("[^"]*")'
ShardId::new("5404c1da-f715-45ef-927c-fa05f6a46b92")

After, the id is computed in the declaring crate, so it appears in the binary rather than in the expansion. 6aaf9450fdc57264 is endpoint_id_hash("shard", "shard", "combobox_content"), and the same comment edit leaves it in place:

$ cargo build -p shard && grep -ao 6aaf9450fdc57264 target/debug/shard
6aaf9450fdc57264
$ md5sum target/debug/shard
eadc9c7ad4d30bc55034f7fd3da6427e  target/debug/shard

$ printf '\n// touch to force a rebuild\n' >> examples/shard/src/main.rs
$ cargo build -p shard && grep -ao 6aaf9450fdc57264 target/debug/shard
6aaf9450fdc57264
$ md5sum target/debug/shard
eadc9c7ad4d30bc55034f7fd3da6427e  target/debug/shard

examples/procedure behaves the same way, serving 82edc9c5c31a7706 for print_on_server.

New tests: crates/topcoat-runtime/src/id.rs pins the id of a known crate, module, and name, so a change in the derivation fails loudly, and covers the module and crate separation plus the hex width. The shard and procedure grammars each gain a test that two expansions of one source are identical and that the id comes from crate, module, and name.

Check gauntlet from .agents/skills/check/SKILL.md:

$ cargo +nightly fmt --all
$ cargo +nightly fmt --all --check
(clean)

$ cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 33s
(exit 0, no warnings)

$ cargo test --workspace --all-features
(exit 0, 1656 passed, 0 failed)

$ RUSTDOCFLAGS="--cfg docsrs -Dwarnings" cargo +nightly doc --workspace --all-features --no-deps --locked
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.29s
   Generated target/doc/alpine_ajax/index.html and 59 other files
(exit 0)

crates/topcoat-runtime/browser is untouched, so dist/index.js is unchanged and the yarn steps do not apply.

Disclaimer

Written with Claude Opus 5 in Claude Code. The agent located the defect, wrote the fix, and ran the checks; I reviewed the diff and the test output before opening this PR.

`#[shard]` and `#[procedure]` minted their endpoint id with
`Uuid::new_v4()` at expansion time, so every compile produced new URLs
under /_topcoat/shards and /_topcoat/procedures. A tab left open across
a deploy kept polling an id the new binary no longer served, and the
runtime swapped in an empty body without an error.

The macros now emit `module_path!()` into the generated code and hash it
in the declaring crate, keyed by crate name, module path, and function
name.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shard and procedure endpoint ids change on every build

1 participant