Repository navigation
Drop third-party dependencies from the default build (v1.1.0) - #17
Conversation
With default features error-forge pulled eight third-party crates into every dependent's build (pastey, parking_lot, lock_api, parking_lot_core, scopeguard, smallvec, cfg-if, and windows-link or libc). The 14 local dependents use only the ForgeError trait. Both always-on dependencies are gone; the default graph is now error-forge plus its own error-forge-derive, with no third-party crate on any target. pastey: define_errors! needs it only to lowercase variant names into constructor names, which macro_rules cannot do. error-forge-derive now carries a small proc macro for that, written against proc_macro alone (error_forge::__private::paste!, not public API). Its syn, quote and proc-macro2 dependencies move behind a new default-on `derive` feature, and error-forge depends on it with default features off, so the derive still costs nothing unless enabled. The error-forge-derive feature that Cargo used to create implicitly is declared explicitly with the same meaning; cargo semver-checks against 1.0.2 passes all 196 checks. Constructor names, spans and keyword errors match pastey. parking_lot: CircuitBreaker now uses std::sync::Mutex and takes it through PoisonError::into_inner, so no method can panic on a poisoned lock, as before. A successful closed-state call no longer takes the lock a second time in on_success, which only acts on the half-open probe. Against 1.0.2, execute is 17% (Windows) / 20% (Linux) faster uncontended and 47% / 10% faster with eight threads on one breaker. With identical single-lock code parking_lot is within 1 ns uncontended, about 20 ns/call faster contended on Linux and 80 ns slower on Windows. A clean debug build of a default-features dependent drops from 1.09 s to 0.78 s wall (32 jobs) and 2.67 s to 0.99 s on one job. CI gains a gate that fails if the default dependency graph of error-forge contains a third-party crate on any target. STABILITY.md, architecture.md, COMPARISON.md, migration.md, SECURITY.md and the README describe the new dependency set; STABILITY.md narrows its dependency rules to what dependents can observe.
Bump error-forge and error-forge-derive to 1.1.0, require error-forge-derive 1.1.0, and point the README install snippet at 1.1.0. The CHANGELOG entry stays under [Unreleased] until the release is approved and dated.
|
Decisions on the three open questions:
Before merging I re-ran the gates on the branch, which already contains current
The default dependency graph is One addition: a tokio advisory published 2026-10-13 (RUSTSEC-2026-0386, unsound |
Summary
With default features, error-forge pulled 8 third-party crates into every dependent's normal build, while the dependents (throttle-net, rate-net, better-bucket and the rest) use only the
ForgeErrortrait. Both always-on dependencies are now gone. The default graph iserror-forgeplus its ownerror-forge-derive, which has no dependencies of its own. No public item was added to, removed from or changed inerror-forge, anddefault = []is unchanged.define_errors!constructor names (fn [<$variant:lower>])pasteyerror-forge-deriveonproc_macroalone (error_forge::__private::paste!, internal).syn/quote/proc-macro2move behind a new default-onderivefeature of the derive crate;error-forgedepends on it with default features offrecovery::CircuitBreakerinternal stateparking_lot,lock_api,parking_lot_core,scopeguard,smallvec,cfg-if,windows-link/libcstd::sync::MutexviaPoisonError::into_inner, so it still never panics. The success path now takes the lock once instead of twiceAll other third-party deps (
serde,log,tracing,async-trait,rand, andsynforderive) were already behind opt-in features and are unchanged.Evidence
cargo tree -p error-forge -e normal, default features, excluding selferror-forge-derive, 0 deps)Clean debug build of a dependent (Windows,
CARGO_INCREMENTAL=0, median of 5):-j321.09 s → 0.78 s;-j12.67 s → 0.99 s.CircuitBreaker::execute(release, no-op closure, ns per call):Uncontended, std and parking_lot are equal. Under heavy synthetic contention parking_lot is about 20 ns/call better on Linux and about 80 ns/call worse on Windows. Next to the I/O a breaker guards, neither difference matters, so it doesn't meet the "necessary and measurably better" bar. Compared with 1.0.2, the branch is faster everywhere.
Gates
cargo fmt --all -- --check-D warningson 1.95 and 1.99: all-features, default, no-default, derive crate aloneerror-forge-derive,async log tracing jitter, and all-features (140, incl. trybuild UI) on Windows 1.95. Linux 1.99 all-features: 140RUSTDOCFLAGS=-D warnings cargo doccargo audit --deny warningsclean;cargo deny checkok (same warnings as main)cargo semver-checks -p error-forge --baseline-version 1.0.2: 196 pass, no semver update required. The tool can't check proc-macro crates, soerror-forge-derivewas reviewed by hand: it only adds a default-onderivefeature and a hidden macrocargo publish --workspace --dry-run: both crates package and verify--target all)New tests:
tests/paste_test.rscovers constructor naming (acronyms,r#variants, non-ASCII, digits) and the macro's modifiers and nesting.tests/ui/fail/paste_bad_modifier.rscovers each error path. ATypevariant gives the sameexpected identifier, found keyworderror as 1.0.2 (checked against the published crate).Decisions (made 2026-11-09: MINOR; ship as 1.1.0, runway to 1.2.0; Dependabot after)
error-forge-derivegoes from optional to always-on. VERSIONING.md, which is authoritative across the portfolio, classes neither change as breaking: the "would user code still compile?" test passes, and semver-checks agrees. The 1.0.2 removal ofthiserrorset the same precedent. This PR narrows those clauses to third-party dependencies and public-API exposure, with a paragraph explaining why. Please confirm this is MINOR rather than MAJOR.Cargo.lock, so whichever merges second gets a trivial lockfile conflict.After approval, the release steps are: date the CHANGELOG section, tag
v1.1.0, publisherror-forge-deriveand thenerror-forge. Local notes are in.dev/audits/2026-10-14.mdand.dev/release/v1.1.0.md(gitignored).