feat(thread_aware_core): add the stable core crate for thread-aware state - #643
feat(thread_aware_core): add the stable core crate for thread-aware state#643martintmk wants to merge 58 commits into
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a761577-6422-4fd1-a0f7-88807d7f6134
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a761577-6422-4fd1-a0f7-88807d7f6134
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a761577-6422-4fd1-a0f7-88807d7f6134
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #643 +/- ##
=======================================
Coverage 100.0% 100.0%
=======================================
Files 560 562 +2
Lines 60859 60945 +86
=======================================
+ Hits 60859 60945 +86
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
✅ Version increments look sufficient
|
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Make the stable core crate dependency- and feature-free, and remove external-type implementations and their benchmarks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Keep the core crate dependency-free while providing Path, PathBuf, and HashMap implementations behind an opt-in std feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 392f99fd-c9b0-40c7-aad7-3e87a132b16e
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f9c8e6e-73c6-47c0-830e-2f674656eceb
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
crates/thread_aware_core/src/lib.rs:18
- The PR description talks about
Place/Originandu16newtypes, but the actual API added here isThread+Owner/NumaNodewithu32storage. Please align the PR description (or, if the intent really isPlace/Origin, align the code/docs) so reviewers and future readers don’t have two conflicting vocabularies for the same change.
//! This crate contains the small API shared by thread-aware libraries:
//!
//! - [`ThreadAware`] notifies a value that it has moved.
//! - [`Thread`] records where it now runs: which runtime, which OS thread, and which memory is
//! closest to it.
crates/thread_aware_core/src/thread_aware.rs:164
- Unit-test modules in this repo are typically annotated with
#[cfg_attr(coverage_nightly, coverage(off))]so the test-only lines don’t count against the coverage gate. This file adds a#[cfg(test)] mod testswithout that attribute, which is inconsistent with the established pattern (e.g.,crates/tick/src/error.rs:103-105).
#[cfg(test)]
mod tests {
Drop the `impl Encoder` block. The surrounding prose already carries the boundary point, and the example is about deriving the trait. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e215e3bc-7b6e-4743-afb4-55779bebd24b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
crates/thread_aware_core/src/lib.rs:62
- In this doc example, “build dependency” is easy to misread as Cargo
[build-dependencies]. Sincethread_awarewould be a normal/proc-macro dependency used at compile time, consider wording that more precisely communicates “implementation-only dependency” / “not part of the public API”.
//! // A build dependency, not part of what this library promises.
crates/thread_aware_core/src/thread_aware.rs:164
- The test module here is missing the
#[cfg_attr(coverage_nightly, coverage(off))]attribute that other modules in this crate use (e.g.src/thread.rsandsrc/impls.rs). Without it, these tests can count against the coverage gate in coverage runs.
#[cfg(test)]
mod tests {
crates/thread_aware_core/src/thread.rs:33
- The PR description says the runtime/node ids are
u16newtypes (and refers toOrigin/Place), but the stable API implemented here usesOwner(u32)(andThread). Please align the PR description (or the code/docs) so reviewers/users aren’t left with conflicting API details.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Owner(u32);
…features References to the companion crate were bare code spans. This crate depends on nothing, so intra-doc links cannot reach it; use explicit docs.rs URLs instead. Three targets, each verified to resolve: the crate root, the `#[derive(ThreadAware)]` macro page, and the per-core `Arc`. Restructure the features section to match `tick`: a `# Features` heading, a lead-in sentence, and `- **`name`** *(default)* - ...` bullets. The `no_std` story now says which items survive with default features off, rather than only that the build works. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e215e3bc-7b6e-4743-afb4-55779bebd24b
"The two roles" was lopsided: the implementor role carried four
paragraphs and an example while the runtime role got one sentence,
placed after the implementor's example. Split it into two peer sections
named for their audience, each self-contained with its own example:
# Library authors: implementing the trait
# Runtime authors: driving relocation
Merge the duplicated forwarding explanation, which said the same thing
either side of the derive example, and repoint the trait's cross
reference from the removed `#the-two-roles` anchor.
Cut the features section to its single bullet. `tick`'s lead-in and
trailing paragraph earn their place across five features; across one
they restated what the crate intro and `Thread`'s own "Without `std`"
section already say.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e215e3bc-7b6e-4743-afb4-55779bebd24b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
crates/thread_aware_core/src/thread_aware.rs:165
mod testshere is missing#[cfg_attr(coverage_nightly, coverage(off))]. Other test modules in this crate already apply it (e.g.src/thread.rsandsrc/impls.rs) to avoid test-only lines counting against the 100% coverage gate; this module should do the same for consistency.
#[cfg(test)]
mod tests {
martintmk
left a comment
There was a problem hiding this comment.
[AI AGENT]: I reviewed this as a crate publication, so I treated the entire reachable surface as new: ThreadAware (supertrait, required method, dyn-compatibility, downstream implementability), Thread / Owner / NumaNode (visibility, constructors, derives, accessors, auto traits), the std feature boundary and what it removes, the ~90 ThreadAware impls in impls.rs read as a coherence lattice, the manifest and packaged contents, and DESIGN.md / STABILIZATION.md as the stabilization argument.
I relied on this PR's own CI for lint, format and the full suite (green at 604e252, with fuzz-testing still pending and mutation-testing skipped) rather than re-running it, and used narrow local probes only to prove specific claims: an E0119 probe for the reference impls, two failing Cow tests, an E0277 probe for BTreeMap<NumaNode, _> under --no-default-features, cargo package --list for the packaged set, and cargo build / cargo test -p thread_aware_core --lib in both feature configurations to check each proposed fix.
Two findings I would act on before this ships. The concrete &str and &Path impls permanently foreclose the general impl<T: Sync + ?Sized> ThreadAware for &T — verified as a hard E0119 conflict — and that is the one thing in this PR the additive escape hatches in DESIGN.md cannot recover. And Cow::relocate clones borrowed data into a fresh allocation on the first relocation, including when source == destination, which contradicts three of the trait's own documented requirements and is precisely the allocation a Cow<'static, str> field exists to avoid.
Three non-blocking notes, inline: Owner and NumaNode are not Ord, so a no_std consumer cannot key the BTreeMap the crate docs tell them to use; DESIGN.md's "every coordinate has a default variant" rule is not applied to the two coordinates shipping now, leaving no way to express "unknown node"; and my answer to the naming question you raised is on thread.rs.
The rest of the public-contract pass came out clean, and that is worth stating rather than omitting. The Send supertrait plus the assert_obj_safe! / assert_impl_all! pins are the right guards for the two silent-regression classes you identified; private fields with accessors do make Thread extensible without #[non_exhaustive]; the inherent new over From<integer> analysis holds and I could not find a fourth evolution path it misses; and nothing reaches a consumer's graph — the only manifest entry is a dev-dependency, and cargo package --list confirms the LFS-tracked logo.png and favicon.ico stay out of the packaged set (12 files, all src/, docs/**/*.md, examples/, README.md, Cargo.toml), referenced by absolute URL as the packaging guidelines require. I skipped the 0.1.0-versus-1.0 wording in STABILIZATION.md and the missing coverage(off) on thread_aware.rs's test module, since copilot-pull-request-reviewer already has open threads on both.
…eads` `Owner` no longer takes a caller-supplied number. Every new owner is unique, which removes the collision hazard the docs previously warned about at length and made the embedding application's problem. It also reports `min_threads`: the smallest number of threads its runtime runs, so a value can pre-size per-thread state before it has seen any of them. `0` means the runtime promises none and spawns on demand, which falls through `HashMap::with_capacity` as a no-op, so both cases share one expression at the call site. Named `min_threads` rather than `threads_hint` on review. In std, `_hint` already means a bound -- `size_hint` returns a lower bound plus an optional upper, and overshooting is a bug -- so "hint" would have contradicted the suffix while leaving `0` a magic value. A floor is also what pre-sizing wants: `Vec::from_iter` sizes from `size_hint().0`, and `with_capacity` means "at least". Equality and hashing use identity alone; the count takes no part. Also drop three implementations. References (`&str`, `&Path`) would adapt something the value only borrows, whose owner is relocated on its own account. `Cow` needed to clone a borrowed value into owned storage before relocating it, which is too much work to hide behind an advisory call; it can return later if a caller wants it. Review of the reshaped type found four defects, all fixed here: * An `AtomicU32` counter wraps after 2^32 constructions, silently handing out a live identity twice -- exactly the collision the change claims to remove. Now `AtomicUsize`, which also moves the documented requirement from 32-bit to pointer-width atomics. * Two sentences still told callers to keep owner ids distinct. * `DESIGN.md` never mentioned the atomics requirement at all. * The new tests passed for the wrong reason: the `Eq`/`Hash` consistency test compared a value with its own copy, so a `Hash` including the count would still have passed. It now builds two owners sharing an identity, and fails under exactly that mutation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e215e3bc-7b6e-4743-afb4-55779bebd24b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
crates/thread_aware_core/src/thread_aware.rs:165
- This crate follows the pattern of excluding
#[cfg(test)] mod testsfrom the coverage gate via#[cfg_attr(coverage_nightly, coverage(off))](as inthread.rsandimpls.rs), but this tests module is missing that attribute. Add it here for consistency and to avoid test-only lines counting toward coverage.
#[cfg(test)]
mod tests {
`cargo mutants` replaced `<impl Hash for Owner>::hash` with `()` and no test noticed: the only hash assertion checked that two owners sharing an identity hash *equally*, which a no-op hash satisfies trivially. Assert the converse as well -- two distinct owners with the same thread count must hash apart -- which fails under exactly that mutation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e215e3bc-7b6e-4743-afb4-55779bebd24b
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
crates/thread_aware_core/src/thread.rs:89
Owner::newusesAtomicUsize::fetch_add, which wraps on overflow. That would violate the documented guarantee that every newOwneris unique by potentially reissuing an identity once the counter wraps (especially on 32-bit targets). Consider usingfetch_updatewithchecked_addto fail fast on exhaustion without mutating the counter into a wrapped state.
pub fn new(min_threads: usize) -> Self {
Self {
id: NEXT_OWNER.fetch_add(1, Ordering::Relaxed),
min_threads,
}
crates/thread_aware_core/src/thread_aware.rs:165
- This crate consistently marks
#[cfg(test)] mod testswith#[cfg_attr(coverage_nightly, coverage(off))]to keep test-only lines from counting against the 100% coverage gate (e.g.crates/tick/src/error.rs). This test module is missing that attribute.
#[cfg(test)]
mod tests {
Resolved a conflict in the workspace Cargo.toml: main cascaded thread_aware, thread_aware_macros and thread_aware_macros_impl to 0.11.0, while this branch adds thread_aware_core. Kept main's version bumps and retained the new thread_aware_core = 0.1.0 entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 698f459b-8a21-45d8-a46f-1b39e71df42a
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (1)
crates/thread_aware_core/src/thread_aware.rs:165
- Tests modules in this repo are consistently annotated with
#[cfg_attr(coverage_nightly, coverage(off))]so unit-test-only lines don’t count against the coverage gate. Thismod testsis missing that attribute (unlikethread.rs/impls.rsin the same crate).
#[cfg(test)]
mod tests {
Review flagged the single-element case of `impl_transfer_tuple!` as broken. It is not: the arm emits a literal comma after the head, so the 1-tuple expands to `let (A,) = self` and `impl<A,> ThreadAware for (A,)`, both of which are correct (a trailing comma is legal in a generic parameter list). The existing coverage could not show this, though. `test_tuples` uses `(42,)`, and `i32::relocate` is a no-op, so the assertion holds whether or not the element was ever visited. Add a test built on `Tracker`, whose `relocate` flips an observable flag, covering the 1-tuple, the 2-tuple and the 12-element maximum. Verified the test is load-bearing: reproducing the reported defect, by dropping that literal comma, makes the 1-tuple bind the whole tuple and call its own impl again, overflowing the stack. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 698f459b-8a21-45d8-a46f-1b39e71df42a
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (1)
crates/thread_aware_core/src/thread_aware.rs:165
- The unit-test module here is missing
#[cfg_attr(coverage_nightly, coverage(off))]. Other test modules in this new crate already apply it (e.g., thread.rs and impls.rs), and omitting it can cause test-only lines to count against the coverage gate.
#[cfg(test)]
mod tests {
Summary
Adds
thread_aware_core, the stable vocabulary that thread-aware libraries share. Nothingelse in the workspace changes.
ThreadAwarenotifies a value that it has moved, viarelocate(source: Option<&Thread>, destination: &Thread). The call is advisory: a valuemust remain correct if it is never called, called twice, or called with a
Threadit hasnever seen. That single property is what lets the method be infallible and lets a runtime
call it opportunistically.
Threadrecords where a value runs —Owner(which runtime owns it), astd::thread::ThreadId, andNumaNode(the memory closest to that thread). Fields areprivate and read through accessors.
Owneridentity is assigned, not supplied: every new owner is unique, so two runtimesalive at once cannot collide. It also reports
min_threads, the smallest number of threadsits runtime runs, so a value can pre-size per-thread state before it has seen any of them.
NumaNodeis an opaqueu32newtype built with an inherentconst fn new(u32).dev-dependency. The
stdfeature is on by default and adds implementations forHashMap,PathandPathBuf. Withdefault-features = falsethe crate needs onlyallocandpointer-width atomics;
Threadthen loses its thread id and cannot be constructed, leavingOwnerandNumaNodereadable so ano_stdlibrary can still implement the trait.docs/DESIGN.mdrecords why the crate is split out, and the rules that letThreadgaincoordinates later without breaking callers or changing behaviour.
docs/STABILIZATION.mdrecords the stable boundary.Why
Ownerassigns its own identityCallers used to choose the number, and the documentation warned at length that a collision
was "worse than a slowdown" and that keeping owners distinct was the embedding application's
problem. Taking the identity from a process-wide counter removes the hazard outright rather
than documenting it. Equality and hashing use identity alone.
min_threadsis a floor, not an estimate.0means the runtime promises none and spawns ondemand, which falls through
HashMap::with_capacityas a no-op, so both cases share oneexpression at the call site. It was named
threads_hintfirst; in std,_hintalready meansa bound —
size_hintreturns a lower bound plus an optional upper, and overshooting is a bug— so "hint" would have contradicted the suffix while leaving
0a magic value.Why the ids take
newrather thanFrom<u16>A
From<integer>impl on a permanently stable type is a one-shot commitment. Every way ofevolving one is breaking, verified against rustc:
From<u16>withFrom<u32>u16(E0277)From<u32>besideFrom<u16>from(1)falls back toi32(E0277)From<i32>from(1)to the new implcargo semver-checks0.48 reports "no semver update required" for all three, so none of themwould be caught in review.
NumaNodeis thereforeu32-backed with an inherentnew. Theaccepted width already exceeds any node count real hardware reaches — Windows' own topology
APIs report NUMA nodes as
USHORT— and a wider or fallible constructor can still be addedlater under a different name without touching existing callers.
Provided implementations
Containers forward to what they hold; primitives and other types with nothing tied to a
thread get an empty implementation. Three cases are deliberately absent:
&str,&Path) — relocating through one would adapt something the valueonly borrows, and whoever owns it is relocated on its own account.
Cow— relocating a borrowed one has to clone it into owned storage first, which istoo much work to hide behind an advisory call. It can return later if a caller wants it.
Arcand sets — whether a shared allocation should split per thread depends on what itholds, and mutating a set element could change its hash and corrupt the container.
Naming
thread_aware_core::Threadshares a name withstd::thread::Threadbut is a different kindof thing: an immutable coordinate that owns no OS resource. Importing both unqualified is
E0252, and both expose an identicalid(&self) -> ThreadId. This is called out in the cratedocs and in a
# Relation to std::thread::Threadsection on the type. Raised deliberatelyhere in case reviewers would prefer a different name.
Scope
thread_awareis unchanged and still ships its ownAffinity-based API. Nothing dependson
thread_aware_coreyet; adoption is left for a later change. The diff againstmainisthe new crate plus its workspace wiring (
Cargo.toml,Cargo.lock,CHANGELOG.md,README.md) and nothing more.Validation
--all-features; 25 unit tests and 12 doctests with--no-default-features. One doctest isignored: it demonstrates#[derive(ThreadAware)], which lives inthread_aware, and compiling it here wouldrequire the dependency the example exists to argue against.
-D warnings --cfg docsrs, formatting,spelling, and the generated README all pass.
Send,Sync,Unpin,UnwindSafe,RefUnwindSafe) on all three public types, andassert_obj_safe!pins dyn-compatibility ofThreadAware. Both guard silent regressions: a coordinate added later that is notSyncwould strip that property with no signature change, and a defaulted-but-generic trait method
would keep every
implcompiling while breaking everyBox<dyn ThreadAware>.found real defects rather than wording nits: a counter width that would wrap and reissue a
live identity, a claim that
ThreadIdis the operating-system thread id (it explicitly isnot), an equality claim that was false under
no_std, and anEq/Hashtest that passedvacuously because it compared a value with its own copy.