From 1f928f5035a4d150878b0128aa017c3e2ef82794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:14:34 -0300 Subject: [PATCH 1/9] docs(thread_aware): add a thread-aware authoring guide Adds a `_documentation` module with a task-oriented guide for authors of thread-aware types, complementing the existing (reference-style) API docs. Covers what thread-awareness is and why it exists, how to author a type (derive, `#[thread_aware(skip)]`, hand-written impls, `Unaware`, strategy `Arc`), how to choose among them, how to test that relocation reaches the right fields, how to debug and read relocation telemetry, and how to validate correctness. The anti-patterns section folds in the migration experience of moving a large production service onto an Oxidizer runtime - `Clone` copying stored affinity rather than relocating, `#[thread_aware(skip)]` on a sole field silently no-op'ing, not trusting inherited markings, and relocating the whole dependency graph once at a boundary. Follows the `recoverable::_documentation` pattern. All examples are doctested under both default and all-feature configurations. Refs AB#7552151, AB#7722787. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/thread_aware/src/_documentation/mod.rs | 254 ++++++++++++++++++ crates/thread_aware/src/lib.rs | 4 + 2 files changed, 258 insertions(+) create mode 100644 crates/thread_aware/src/_documentation/mod.rs diff --git a/crates/thread_aware/src/_documentation/mod.rs b/crates/thread_aware/src/_documentation/mod.rs new file mode 100644 index 000000000..a62c886fe --- /dev/null +++ b/crates/thread_aware/src/_documentation/mod.rs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! A guide to authoring thread-aware types. +//! +//! The crate-level docs explain *what* [`ThreadAware`](crate::ThreadAware) is and the relocation +//! contract it expresses. This guide is the companion *how-to*: how to make your own types +//! thread-aware correctly, which implementation to reach for, how to test and debug the result, +//! and the mistakes that compile cleanly yet quietly do nothing. +//! +//! It is written for authors who see `T: ThreadAware` in an API and need to satisfy it, and for +//! reviewers deciding whether a `#[derive(ThreadAware)]` or a `#[thread_aware(skip)]` is the right +//! call. The lessons in [Anti-patterns](#anti-patterns) are drawn from migrating a large +//! production service onto an Oxidizer-backed runtime. +//! +//! # Why thread-awareness exists +//! +//! Oxidizer runtimes are thread-per-core: each worker owns its slice of the machine, and shared +//! state that silently spans cores turns into cross-NUMA traffic and lock contention. A +//! thread-aware type is told, through [`relocate`](crate::ThreadAware::relocate), that it has just +//! moved from one worker to another, and is given the chance to *rebind* its affinity-bearing +//! state - reconnect to the destination's I/O scheduler, re-home an allocation in the local NUMA +//! node, or detach from memory it was sharing with the previous worker. +//! +//! Relocation is a **performance cooperation**, never a correctness guarantee. A type must remain +//! correct if `relocate` is called at the wrong moment, called with the wrong threads, or never +//! called at all - see [Performance vs. Correctness](crate#performance-vs-correctness). That +//! single fact drives most of the guidance below: because nothing enforces relocation, a type that +//! *silently* fails to relocate is the failure mode to design against. +//! +//! # Authoring a thread-aware type +//! +//! ## Prefer the derive +//! +//! In almost all cases, implement [`ThreadAware`](crate::ThreadAware) with the derive macro. It +//! generates a [`relocate`](crate::ThreadAware::relocate) that forwards the notification to every +//! field, which is exactly what a compound type owes its parts: +//! +//! ```rust +//! use thread_aware::{Thread, ThreadAware}; +//! +//! #[derive(ThreadAware)] +//! struct Connection { +//! pool: Vec, +//! scratch: String, +//! } +//! +//! // A runtime hands `relocate` the worker the value came from and the one it is moving to. +//! fn on_move(mut c: Connection, from: Option<&Thread>, to: &Thread) { +//! c.relocate(from, to); +//! } +//! ``` +//! +//! The `std` library types you are most likely to hold - `Vec`, `Box`, `Option`, `Result`, tuples, +//! arrays, maps - already implement the trait, so the derive "just works" on compounds of them. +//! +//! ## Skipping a field +//! +//! Annotate a field with `#[thread_aware(skip)]` when it carries no affinity and should be moved +//! as-is: a plain identifier, a length, a foreign handle that does no thread-local work. A skipped +//! field is never relocated, and the derive adds a `where Self: Send` bound to keep the +//! `ThreadAware: Send` supertrait satisfied. +//! +//! ```rust +//! use thread_aware::ThreadAware; +//! +//! #[derive(ThreadAware)] +//! struct Request { +//! body: Vec, +//! // A request id has no thread affinity; moving it verbatim is correct. +//! #[thread_aware(skip)] +//! id: u64, +//! } +//! ``` +//! +//! `skip` is a claim that a field genuinely has nothing to rebind. It is not an escape hatch for +//! "this field does not implement `ThreadAware` yet" - reach for [`Unaware`](crate::Unaware) or +//! [`Arc`](crate::Arc) for that, so the intent is visible in the type. +//! +//! ## What the generated bounds mean +//! +//! The derive bounds the **field type**, not the parameters inside it. For every relocated field +//! whose type mentions a generic parameter, it emits `where : ThreadAware` - the exact +//! obligation the generated body discharges when it relocates that field. So a `Vec` field +//! yields `where Vec: ThreadAware`, and a `Wrapper` field yields +//! `where Wrapper: ThreadAware`, governed by that wrapper's own impl rather than by a bound on +//! `T`. A field whose +//! type reaches no parameter, and a marker payload behind a function pointer +//! (`PhantomData`), owe no bound at all. See +//! [the derive's reference](crate::ThreadAware#generic-bounds) for the full rules. +//! +//! ## Implementing the trait by hand +//! +//! Write the impl yourself when relocation means something specific - re-homing an allocation, +//! swapping a per-core cache, reconnecting to a scheduler. The method receives the source worker +//! (`None` if unknown) and the destination: +//! +//! ```rust +//! use thread_aware::{Thread, ThreadAware}; +//! +//! struct PerCoreScratch { +//! buffer: Vec, +//! } +//! +//! impl ThreadAware for PerCoreScratch { +//! fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) { +//! // The scratch buffer belonged to the previous worker; drop it so the next use +//! // re-allocates in the destination's NUMA node instead of reaching across. +//! self.buffer = Vec::new(); +//! } +//! } +//! ``` +//! +//! # Choosing an implementation +//! +//! | You have… | Reach for | Because | +//! |---|---|---| +//! | A compound of thread-aware fields | `#[derive(ThreadAware)]` | Forwards relocation to each field. | +//! | A field with genuine per-core behavior | a hand-written impl | Only you know what "rebind" means. | +//! | A foreign type that carries no affinity | [`Unaware`](crate::Unaware) | A `MoveAsIs`: implements the trait as a no-op. | +//! | Shared state that should differ per worker | [`Arc`](crate::Arc) | Materializes a separate `T` per destination. | +//! | Shared state that is the same everywhere | [`Arc`](crate::Arc) | Behaves as a vanilla `Arc`. | +//! +//! [`Unaware`](crate::Unaware) wraps a value and satisfies `ThreadAware` without reacting to +//! relocation - use it for inert, foreign, or allocation-free values that legitimately do not care +//! which worker they are on. Wrapping a type that *does* implement the trait is discouraged: it +//! silences that type's own relocation (a performance loss, not a correctness bug). +//! +//! The strategy-partitioned [`Arc`](crate::Arc) is the usual bridge to a type that does not +//! implement the trait itself: an `Arc` gives each worker its own `Foo`, while an +//! `Arc` shares one - the same `Arc` API, differing only in what relocation does. +//! +//! # Anti-patterns +//! +//! These are the shapes that compile, satisfy `T: ThreadAware`, and still leave state stranded on +//! the wrong worker. None of them produces a compile error, and most produce no runtime warning +//! either, so they are worth recognizing by sight. +//! +//! ## `Clone` does not relocate +//! +//! This is the one to internalize first. A thread-aware type stores its affinity in a field that +//! only [`relocate`](crate::ThreadAware::relocate) mutates. **`Clone` copies that stored affinity +//! verbatim.** Cloning a value that was built on worker A and using the clone on worker B does not +//! move it to B - it is still bound to A, quietly, until something calls `relocate`. +//! +//! ```text +//! let services = build_on_startup_worker(); // affinity = startup worker +//! let per_request = services.clone(); // affinity = startup worker (copied!) +//! // `per_request` now funnels every task back onto the startup worker. +//! ``` +//! +//! In one migration this single clone routed an entire process's work onto one core while the +//! others idled. If you clone a long-lived, affinity-bearing graph, relocate the clone at the point +//! it enters its new worker. +//! +//! ## `skip` on the only field is a silent no-op +//! +//! `#[thread_aware(skip)]` on the *sole* field of a type makes `relocate` do nothing, yet the type +//! still satisfies `T: ThreadAware`. Downstream code compiles, runtimes accept it, and no affinity +//! ever moves - with no error and no warning. A type whose every field is skipped is +//! indistinguishable from one that is genuinely inert; make sure that is what you meant. +//! +//! ## Do not trust inherited markings +//! +//! An existing `#[derive(ThreadAware)]` or `#[thread_aware(skip)]` is a decision someone made under +//! their constraints, and at least one such marking per audit tends to be a compile-shortcut that +//! reduces to a silent no-op. When you take a dependency on a type being thread-aware, verify that +//! its relocation actually reaches the state you care about rather than inheriting the annotation as +//! fact. +//! +//! ## Relocate the whole graph once, at the boundary +//! +//! When work crosses into a worker from the outside - an FFI entry, a hand-off from a foreign +//! thread - relocate the entire long-lived dependency graph **once**, at that boundary, rather than +//! special-casing each affinity-bearing dependency downstream. Make the graph's root `ThreadAware` +//! (by +//! derive) so a single `relocate` at the entry rebinds every affinity-bearing resource beneath it. +//! Relocating a subtree while its parent was built from a stale clone (see above) is how affinity +//! goes stale in practice. +//! +//! # Testing +//! +//! Because relocation is silent when it is wrong, test it by observation, not by trusting that the +//! derive did the right thing. The reliable pattern is a leaf type whose `relocate` records that it +//! was called, composed into the type under test; after one relocation, assert that every +//! non-skipped field was reached and every skipped field was not. +//! +//! ```rust +//! use thread_aware::{Thread, ThreadAware}; +//! +//! /// Counts relocations so a test can prove which fields the derive reaches. +//! #[derive(Default)] +//! struct Tracker { +//! relocations: usize, +//! } +//! +//! impl ThreadAware for Tracker { +//! fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) { +//! self.relocations += 1; +//! } +//! } +//! +//! #[derive(ThreadAware)] +//! struct UnderTest { +//! tracked: Tracker, +//! #[thread_aware(skip)] +//! skipped: Tracker, +//! } +//! +//! fn assert_reaches_the_right_fields(from: Option<&Thread>, to: &Thread) { +//! let mut value = UnderTest { +//! tracked: Tracker::default(), +//! skipped: Tracker::default(), +//! }; +//! value.relocate(from, to); +//! assert_eq!(value.tracked.relocations, 1, "non-skipped fields must be relocated"); +//! assert_eq!(value.skipped.relocations, 0, "skipped fields must not be relocated"); +//! } +//! ``` +//! +//! Construct the [`Thread`](crate::Thread) values a real test needs with +//! [`ThreadBuilder`](crate::ThreadBuilder) (available with the default `std` feature). The +//! `test-utils` feature additionally offers a [`Relocator`](crate::Relocator) helper for driving +//! relocations in tests. +//! +//! # Debugging and telemetry +//! +//! When a value seems bound to the wrong worker, the question is almost always *was `relocate` +//! called, and did it reach this field?* The `Tracker` pattern above answers it in a test; in a +//! running system, a runtime that detects affinity-bearing state being touched from the wrong +//! worker is the signal to watch - for example, a debug-build warning such as a `*.thread_mismatch` +//! event with a backtrace at the offending access. Treat such a warning as a missing or too-late +//! `relocate`, most often a [stale clone](#clone-does-not-relocate). +//! +//! Remember that the *absence* of a warning does not prove correctness: relocation is best-effort, +//! so a value can be on the wrong worker with no diagnostic at all. Coverage of the relocation path +//! belongs in your tests, not in production telemetry. +//! +//! # Validating correctness +//! +//! What the toolchain checks for you, and what it cannot: +//! +//! * **The compiler** enforces the `ThreadAware: Send` supertrait and, through the derive's +//! field-type bounds, that every relocated field is itself thread-aware. It cannot tell whether a +//! `#[thread_aware(skip)]` is *justified* - only that the resulting type is still `Send`. +//! * **The derive** emits each field-type predicate once and suppresses a bound the author already +//! wrote, so a correct `#[derive(ThreadAware)]` does not trip +//! `clippy::trait_duplication_in_bounds`. +//! * **Your tests** are the only thing that checks the property that actually matters: that +//! relocation reaches the state it is supposed to. Nothing else does. +//! +//! The through-line of this guide: a thread-aware type that does the wrong thing usually does it +//! silently. Author for that - prefer the derive, justify every `skip`, relocate clones and graphs +//! at their boundaries, and prove it with a test that observes relocation happening. diff --git a/crates/thread_aware/src/lib.rs b/crates/thread_aware/src/lib.rs index 682a33183..214167a77 100644 --- a/crates/thread_aware/src/lib.rs +++ b/crates/thread_aware/src/lib.rs @@ -133,6 +133,10 @@ extern crate std; mod wrappers; +/// A guide to authoring thread-aware types: how to implement, test, and debug them, and the +/// anti-patterns to avoid. See [the guide](_documentation). +pub mod _documentation; + pub mod closure; #[cfg(feature = "test-utils")] From 3cd9d33601da7c3d4e25459fdcc0d113e7549a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:48:46 -0300 Subject: [PATCH 2/9] style(thread_aware): wrap guide doctest asserts for rustfmt `format_code_in_doc_comments` measures doc-comment code at the reduced width left by the `//! ` prefix, so the two `assert_eq!` calls in the testing example must wrap. Matches `cargo +nightly fmt --config-path ./unstable-rustfmt.toml`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/thread_aware/src/_documentation/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/thread_aware/src/_documentation/mod.rs b/crates/thread_aware/src/_documentation/mod.rs index a62c886fe..55392c835 100644 --- a/crates/thread_aware/src/_documentation/mod.rs +++ b/crates/thread_aware/src/_documentation/mod.rs @@ -213,8 +213,14 @@ //! skipped: Tracker::default(), //! }; //! value.relocate(from, to); -//! assert_eq!(value.tracked.relocations, 1, "non-skipped fields must be relocated"); -//! assert_eq!(value.skipped.relocations, 0, "skipped fields must not be relocated"); +//! assert_eq!( +//! value.tracked.relocations, 1, +//! "non-skipped fields must be relocated" +//! ); +//! assert_eq!( +//! value.skipped.relocations, 0, +//! "skipped fields must not be relocated" +//! ); //! } //! ``` //! From 8e3237d795db2e2278969b0a1871c3c7571a84d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:55:08 -0300 Subject: [PATCH 3/9] docs(thread_aware): gate the authoring guide with cfg(any(doc, test)) Addresses review feedback on PR #742: export the `_documentation` module only for rustdoc and tests, matching the established pattern in `recoverable` and `fetch`, so it does not become part of the crate's public API in normal builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/thread_aware/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/thread_aware/src/lib.rs b/crates/thread_aware/src/lib.rs index 214167a77..b2b1788b9 100644 --- a/crates/thread_aware/src/lib.rs +++ b/crates/thread_aware/src/lib.rs @@ -135,6 +135,7 @@ mod wrappers; /// A guide to authoring thread-aware types: how to implement, test, and debug them, and the /// anti-patterns to avoid. See [the guide](_documentation). +#[cfg(any(doc, test))] pub mod _documentation; pub mod closure; From 4a654515aa898e9c0772f0bf64b9b85894e99d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Wed, 9 Sep 2026 11:51:08 -0300 Subject: [PATCH 4/9] docs(thread_aware): address review feedback on the authoring guide Per @martintmk's review of PR #742: - Trim "Why thread-awareness exists" to a pointer at the crate-level Theory of Operation instead of duplicating it. - Link "the derive macro" to the actual macro (`macro@crate::ThreadAware`). - Simplify "What the generated bounds mean" - defer the detail to the derive's Generic Bounds reference rather than restating it. - Add a "Per-worker state with `Arc`" section explaining when to reach for `Arc` (separate per-worker instances) vs `PerProcess`/`PerNumaNode`. - Make "Testing" more concise and lead with the `test-utils` `Relocator` helper. - Drop "Debugging and telemetry" - the telemetry story is not defined yet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/thread_aware/src/_documentation/mod.rs | 81 +++++++------------ 1 file changed, 30 insertions(+), 51 deletions(-) diff --git a/crates/thread_aware/src/_documentation/mod.rs b/crates/thread_aware/src/_documentation/mod.rs index 55392c835..f3b7af3bf 100644 --- a/crates/thread_aware/src/_documentation/mod.rs +++ b/crates/thread_aware/src/_documentation/mod.rs @@ -15,26 +15,20 @@ //! //! # Why thread-awareness exists //! -//! Oxidizer runtimes are thread-per-core: each worker owns its slice of the machine, and shared -//! state that silently spans cores turns into cross-NUMA traffic and lock contention. A -//! thread-aware type is told, through [`relocate`](crate::ThreadAware::relocate), that it has just -//! moved from one worker to another, and is given the chance to *rebind* its affinity-bearing -//! state - reconnect to the destination's I/O scheduler, re-home an allocation in the local NUMA -//! node, or detach from memory it was sharing with the previous worker. -//! -//! Relocation is a **performance cooperation**, never a correctness guarantee. A type must remain -//! correct if `relocate` is called at the wrong moment, called with the wrong threads, or never -//! called at all - see [Performance vs. Correctness](crate#performance-vs-correctness). That -//! single fact drives most of the guidance below: because nothing enforces relocation, a type that -//! *silently* fails to relocate is the failure mode to design against. +//! The crate-level [Theory of Operation](crate#theory-of-operation) covers what relocation is and +//! why thread-per-core runtimes need it. The one idea this guide leans on: relocation is a +//! **performance cooperation, never a correctness guarantee** (see +//! [Performance vs. Correctness](crate#performance-vs-correctness)). Nothing enforces it, so the +//! failure mode to design against is a type that *silently* fails to relocate. //! //! # Authoring a thread-aware type //! //! ## Prefer the derive //! -//! In almost all cases, implement [`ThreadAware`](crate::ThreadAware) with the derive macro. It -//! generates a [`relocate`](crate::ThreadAware::relocate) that forwards the notification to every -//! field, which is exactly what a compound type owes its parts: +//! In almost all cases, implement [`ThreadAware`](crate::ThreadAware) with +//! [the derive macro](macro@crate::ThreadAware). It generates a +//! [`relocate`](crate::ThreadAware::relocate) that forwards the notification to every field, which +//! is exactly what a compound type owes its parts: //! //! ```rust //! use thread_aware::{Thread, ThreadAware}; @@ -79,15 +73,10 @@ //! //! ## What the generated bounds mean //! -//! The derive bounds the **field type**, not the parameters inside it. For every relocated field -//! whose type mentions a generic parameter, it emits `where : ThreadAware` - the exact -//! obligation the generated body discharges when it relocates that field. So a `Vec` field -//! yields `where Vec: ThreadAware`, and a `Wrapper` field yields -//! `where Wrapper: ThreadAware`, governed by that wrapper's own impl rather than by a bound on -//! `T`. A field whose -//! type reaches no parameter, and a marker payload behind a function pointer -//! (`PhantomData`), owe no bound at all. See -//! [the derive's reference](crate::ThreadAware#generic-bounds) for the full rules. +//! You rarely need to reason about this: the derive adds exactly the `ThreadAware` bounds its +//! generated body needs and no more, so a correct type "just derives". When it matters - a generic +//! wrapper, or a marker field that should stay bound-free - the derive's +//! [Generic Bounds](macro@crate::ThreadAware#generic-bounds) reference has the rules. //! //! ## Implementing the trait by hand //! @@ -111,6 +100,16 @@ //! } //! ``` //! +//! ## Per-worker state with `Arc` +//! +//! When several workers share a value but each should keep its *own* instance - a per-core cache, a +//! pool you do not want contended across cores - wrap it in the strategy-partitioned +//! [`Arc`](crate::Arc). Relocation materializes a separate `T` for the destination +//! worker (lazily, on first use there), so the sharing is per-worker instead of process-wide. Reach +//! for [`Arc`](crate::Arc), which behaves as a vanilla `Arc`, when one shared +//! instance is what you want, and [`Arc`](crate::Arc) for one instance per NUMA +//! node. This is also the usual bridge to a type that does not implement `ThreadAware` itself. +//! //! # Choosing an implementation //! //! | You have… | Reach for | Because | @@ -126,10 +125,6 @@ //! which worker they are on. Wrapping a type that *does* implement the trait is discouraged: it //! silences that type's own relocation (a performance loss, not a correctness bug). //! -//! The strategy-partitioned [`Arc`](crate::Arc) is the usual bridge to a type that does not -//! implement the trait itself: an `Arc` gives each worker its own `Foo`, while an -//! `Arc` shares one - the same `Arc` API, differing only in what relocation does. -//! //! # Anti-patterns //! //! These are the shapes that compile, satisfy `T: ThreadAware`, and still leave state stranded on @@ -173,17 +168,16 @@ //! When work crosses into a worker from the outside - an FFI entry, a hand-off from a foreign //! thread - relocate the entire long-lived dependency graph **once**, at that boundary, rather than //! special-casing each affinity-bearing dependency downstream. Make the graph's root `ThreadAware` -//! (by -//! derive) so a single `relocate` at the entry rebinds every affinity-bearing resource beneath it. +//! (by derive) so a single `relocate` at the entry rebinds every affinity-bearing resource beneath +//! it. //! Relocating a subtree while its parent was built from a stale clone (see above) is how affinity //! goes stale in practice. //! //! # Testing //! -//! Because relocation is silent when it is wrong, test it by observation, not by trusting that the -//! derive did the right thing. The reliable pattern is a leaf type whose `relocate` records that it -//! was called, composed into the type under test; after one relocation, assert that every -//! non-skipped field was reached and every skipped field was not. +//! Relocation is silent when it is wrong, so test it by observation. Compose a leaf type whose +//! `relocate` records that it ran, relocate the type under test once, and assert that every +//! non-skipped field was reached and every skipped one was not: //! //! ```rust //! use thread_aware::{Thread, ThreadAware}; @@ -224,23 +218,8 @@ //! } //! ``` //! -//! Construct the [`Thread`](crate::Thread) values a real test needs with -//! [`ThreadBuilder`](crate::ThreadBuilder) (available with the default `std` feature). The -//! `test-utils` feature additionally offers a [`Relocator`](crate::Relocator) helper for driving -//! relocations in tests. -//! -//! # Debugging and telemetry -//! -//! When a value seems bound to the wrong worker, the question is almost always *was `relocate` -//! called, and did it reach this field?* The `Tracker` pattern above answers it in a test; in a -//! running system, a runtime that detects affinity-bearing state being touched from the wrong -//! worker is the signal to watch - for example, a debug-build warning such as a `*.thread_mismatch` -//! event with a backtrace at the offending access. Treat such a warning as a missing or too-late -//! `relocate`, most often a [stale clone](#clone-does-not-relocate). -//! -//! Remember that the *absence* of a warning does not prove correctness: relocation is best-effort, -//! so a value can be on the wrong worker with no diagnostic at all. Coverage of the relocation path -//! belongs in your tests, not in production telemetry. +//! The `test-utils` feature's [`Relocator`](crate::Relocator) drives relocations without hand-built +//! [`Thread`](crate::Thread) values, which is usually what a real test wants. //! //! # Validating correctness //! From 840cede5109021aa60d3d2cd98eb006fe51440fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:10:35 -0300 Subject: [PATCH 5/9] docs(thread_aware): keep guide intra-doc links accurate across feature configs Addresses Copilot review comments on PR #742: - Use the `derive@` disambiguator for the derive-macro links (matches the repo convention, e.g. `internity`), replacing `macro@`. - The `Arc` strategy section pointed at `crate::Arc` / `crate::PerThread` / `crate::PerNumaNode`, which are `std`-gated, so the links broke under `--no-default-features`. Name the `std` feature and drop the feature-gated intra-doc links in favour of plain code spans. - `Relocator` is `test-utils`-gated; its intra-doc link broke in doc builds without that feature. Reword to a plain code span. Doctests still pass under default and all-feature builds; the guide no longer contributes any broken-intra-doc-link warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/thread_aware/src/_documentation/mod.rs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/thread_aware/src/_documentation/mod.rs b/crates/thread_aware/src/_documentation/mod.rs index f3b7af3bf..49dc77e1d 100644 --- a/crates/thread_aware/src/_documentation/mod.rs +++ b/crates/thread_aware/src/_documentation/mod.rs @@ -26,7 +26,7 @@ //! ## Prefer the derive //! //! In almost all cases, implement [`ThreadAware`](crate::ThreadAware) with -//! [the derive macro](macro@crate::ThreadAware). It generates a +//! [the derive macro](derive@crate::ThreadAware). It generates a //! [`relocate`](crate::ThreadAware::relocate) that forwards the notification to every field, which //! is exactly what a compound type owes its parts: //! @@ -76,7 +76,7 @@ //! You rarely need to reason about this: the derive adds exactly the `ThreadAware` bounds its //! generated body needs and no more, so a correct type "just derives". When it matters - a generic //! wrapper, or a marker field that should stay bound-free - the derive's -//! [Generic Bounds](macro@crate::ThreadAware#generic-bounds) reference has the rules. +//! [Generic Bounds](derive@crate::ThreadAware#generic-bounds) reference has the rules. //! //! ## Implementing the trait by hand //! @@ -103,12 +103,12 @@ //! ## Per-worker state with `Arc` //! //! When several workers share a value but each should keep its *own* instance - a per-core cache, a -//! pool you do not want contended across cores - wrap it in the strategy-partitioned -//! [`Arc`](crate::Arc). Relocation materializes a separate `T` for the destination -//! worker (lazily, on first use there), so the sharing is per-worker instead of process-wide. Reach -//! for [`Arc`](crate::Arc), which behaves as a vanilla `Arc`, when one shared -//! instance is what you want, and [`Arc`](crate::Arc) for one instance per NUMA -//! node. This is also the usual bridge to a type that does not implement `ThreadAware` itself. +//! pool you do not want contended across cores - wrap it in the strategy-partitioned `Arc` +//! that the crate's `std` feature provides (`thread_aware::Arc`). With the `PerThread` strategy, +//! relocation materializes a separate `T` for the destination worker (lazily, on first use there), +//! so the sharing is per-worker instead of process-wide. Use `PerProcess`, which behaves as a +//! vanilla `Arc`, when one shared instance is what you want, and `PerNumaNode` for one instance per +//! NUMA node. This is also the usual bridge to a type that does not implement `ThreadAware` itself. //! //! # Choosing an implementation //! @@ -117,8 +117,8 @@ //! | A compound of thread-aware fields | `#[derive(ThreadAware)]` | Forwards relocation to each field. | //! | A field with genuine per-core behavior | a hand-written impl | Only you know what "rebind" means. | //! | A foreign type that carries no affinity | [`Unaware`](crate::Unaware) | A `MoveAsIs`: implements the trait as a no-op. | -//! | Shared state that should differ per worker | [`Arc`](crate::Arc) | Materializes a separate `T` per destination. | -//! | Shared state that is the same everywhere | [`Arc`](crate::Arc) | Behaves as a vanilla `Arc`. | +//! | Shared state that should differ per worker | `Arc` (`std`) | Materializes a separate `T` per destination. | +//! | Shared state that is the same everywhere | `Arc` (`std`) | Behaves as a vanilla `Arc`. | //! //! [`Unaware`](crate::Unaware) wraps a value and satisfies `ThreadAware` without reacting to //! relocation - use it for inert, foreign, or allocation-free values that legitimately do not care @@ -218,8 +218,8 @@ //! } //! ``` //! -//! The `test-utils` feature's [`Relocator`](crate::Relocator) drives relocations without hand-built -//! [`Thread`](crate::Thread) values, which is usually what a real test wants. +//! The `test-utils` feature adds a `Relocator` (`thread_aware::Relocator`) that drives relocations +//! without hand-built [`Thread`](crate::Thread) values, which is usually what a real test wants. //! //! # Validating correctness //! From cec7a5ec7a64df61ed41ad982b9a3041de7b81e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:32:15 -0300 Subject: [PATCH 6/9] docs(thread_aware): drop the last std-gated Arc intra-doc link in the guide Follow-up to 2ce36aaa: the "Skipping a field" section still linked `[Arc](crate::Arc)`, which is `std`-gated and breaks under `--no-default-features`. Replace it with a plain code span that names the `std` feature, matching the treatment applied to the other `Arc` references. The guide now contributes no broken-intra-doc-link warnings in the `--no-default-features` doc build; doctests still pass under default and all-feature builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/thread_aware/src/_documentation/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/thread_aware/src/_documentation/mod.rs b/crates/thread_aware/src/_documentation/mod.rs index 49dc77e1d..fdd231f4b 100644 --- a/crates/thread_aware/src/_documentation/mod.rs +++ b/crates/thread_aware/src/_documentation/mod.rs @@ -68,8 +68,9 @@ //! ``` //! //! `skip` is a claim that a field genuinely has nothing to rebind. It is not an escape hatch for -//! "this field does not implement `ThreadAware` yet" - reach for [`Unaware`](crate::Unaware) or -//! [`Arc`](crate::Arc) for that, so the intent is visible in the type. +//! "this field does not implement `ThreadAware` yet" - reach for [`Unaware`](crate::Unaware) or the +//! strategy-partitioned `Arc` (with the `std` feature) for that, so the intent is visible in the +//! type. //! //! ## What the generated bounds mean //! From 60330240f4838c3faf0dff0e8078b69963a956dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Fri, 11 Sep 2026 07:27:23 -0300 Subject: [PATCH 7/9] docs(thread_aware): link facade types to docs.rs in the authoring guide Reference the facade types the guide mentions - the ThreadAware derive, Unaware, Arc, and Relocator - by docs.rs URL rather than intra-doc links, matching the pattern thread_aware_core already uses for these same types. This avoids a dev-dependency on thread_aware (and the dependency cycle it would create if the guide ever moves to thread_aware_core), and keeps every link resolvable regardless of which features the doc build enables - Arc is std-gated and Relocator is test-utils-gated, so intra-doc links to them broke under --no-default-features. Core types (the ThreadAware trait, Thread, relocate) stay as intra-doc links since they resolve in either crate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/thread_aware/src/_documentation/mod.rs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/thread_aware/src/_documentation/mod.rs b/crates/thread_aware/src/_documentation/mod.rs index fdd231f4b..cf448a1fc 100644 --- a/crates/thread_aware/src/_documentation/mod.rs +++ b/crates/thread_aware/src/_documentation/mod.rs @@ -26,9 +26,9 @@ //! ## Prefer the derive //! //! In almost all cases, implement [`ThreadAware`](crate::ThreadAware) with -//! [the derive macro](derive@crate::ThreadAware). It generates a -//! [`relocate`](crate::ThreadAware::relocate) that forwards the notification to every field, which -//! is exactly what a compound type owes its parts: +//! [the derive macro](https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html). It +//! generates a [`relocate`](crate::ThreadAware::relocate) that forwards the notification to every +//! field, which is exactly what a compound type owes its parts: //! //! ```rust //! use thread_aware::{Thread, ThreadAware}; @@ -68,7 +68,8 @@ //! ``` //! //! `skip` is a claim that a field genuinely has nothing to rebind. It is not an escape hatch for -//! "this field does not implement `ThreadAware` yet" - reach for [`Unaware`](crate::Unaware) or the +//! "this field does not implement `ThreadAware` yet" - reach for +//! [`Unaware`](https://docs.rs/thread_aware/latest/thread_aware/struct.Unaware.html) or the //! strategy-partitioned `Arc` (with the `std` feature) for that, so the intent is visible in the //! type. //! @@ -77,7 +78,8 @@ //! You rarely need to reason about this: the derive adds exactly the `ThreadAware` bounds its //! generated body needs and no more, so a correct type "just derives". When it matters - a generic //! wrapper, or a marker field that should stay bound-free - the derive's -//! [Generic Bounds](derive@crate::ThreadAware#generic-bounds) reference has the rules. +//! [Generic Bounds](https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html#generic-bounds) +//! reference has the rules. //! //! ## Implementing the trait by hand //! @@ -105,7 +107,8 @@ //! //! When several workers share a value but each should keep its *own* instance - a per-core cache, a //! pool you do not want contended across cores - wrap it in the strategy-partitioned `Arc` -//! that the crate's `std` feature provides (`thread_aware::Arc`). With the `PerThread` strategy, +//! ([`thread_aware::Arc`](https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html), with +//! the crate's `std` feature). With the `PerThread` strategy, //! relocation materializes a separate `T` for the destination worker (lazily, on first use there), //! so the sharing is per-worker instead of process-wide. Use `PerProcess`, which behaves as a //! vanilla `Arc`, when one shared instance is what you want, and `PerNumaNode` for one instance per @@ -117,11 +120,12 @@ //! |---|---|---| //! | A compound of thread-aware fields | `#[derive(ThreadAware)]` | Forwards relocation to each field. | //! | A field with genuine per-core behavior | a hand-written impl | Only you know what "rebind" means. | -//! | A foreign type that carries no affinity | [`Unaware`](crate::Unaware) | A `MoveAsIs`: implements the trait as a no-op. | +//! | A foreign type that carries no affinity | [`Unaware`](https://docs.rs/thread_aware/latest/thread_aware/struct.Unaware.html) | A `MoveAsIs`: implements the trait as a no-op. | //! | Shared state that should differ per worker | `Arc` (`std`) | Materializes a separate `T` per destination. | //! | Shared state that is the same everywhere | `Arc` (`std`) | Behaves as a vanilla `Arc`. | //! -//! [`Unaware`](crate::Unaware) wraps a value and satisfies `ThreadAware` without reacting to +//! [`Unaware`](https://docs.rs/thread_aware/latest/thread_aware/struct.Unaware.html) wraps a value +//! and satisfies `ThreadAware` without reacting to //! relocation - use it for inert, foreign, or allocation-free values that legitimately do not care //! which worker they are on. Wrapping a type that *does* implement the trait is discouraged: it //! silences that type's own relocation (a performance loss, not a correctness bug). @@ -219,8 +223,10 @@ //! } //! ``` //! -//! The `test-utils` feature adds a `Relocator` (`thread_aware::Relocator`) that drives relocations -//! without hand-built [`Thread`](crate::Thread) values, which is usually what a real test wants. +//! The `test-utils` feature adds a +//! [`Relocator`](https://docs.rs/thread_aware/latest/thread_aware/struct.Relocator.html) that drives +//! relocations without hand-built [`Thread`](crate::Thread) values, which is usually what a real +//! test wants. //! //! # Validating correctness //! From 5fddfd7bb95c55500e0db2ad5b01a5186fd2e1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:19:26 -0300 Subject: [PATCH 8/9] docs(thread_aware): gate the authoring guide on the derive feature Addresses a Copilot review comment on PR #742: the guide's examples all use `#[derive(ThreadAware)]`, which is only re-exported with the `derive` feature, but the module was included for `cfg(any(doc, test))` even when that feature is off - so the doctests would not compile in a build without the optional macro. Add `feature = "derive"` to the module's cfg. The guide (and its doctests) is present exactly when the derive it documents is available - in the default and all-feature builds - and simply absent otherwise, which is what `docs/feature-gated-doctests.md` requires. Verified: guide doctests run under default features and are absent (no failure) under --no-default-features. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/thread_aware/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/thread_aware/src/lib.rs b/crates/thread_aware/src/lib.rs index b2b1788b9..ffb54c545 100644 --- a/crates/thread_aware/src/lib.rs +++ b/crates/thread_aware/src/lib.rs @@ -135,7 +135,11 @@ mod wrappers; /// A guide to authoring thread-aware types: how to implement, test, and debug them, and the /// anti-patterns to avoid. See [the guide](_documentation). -#[cfg(any(doc, test))] +/// +/// Gated on `derive` because every example is built around `#[derive(ThreadAware)]`, which is only +/// available with that feature; this keeps the guide's doctests valid in a build without it (they +/// are simply absent) per `docs/feature-gated-doctests.md`. +#[cfg(all(any(doc, test), feature = "derive"))] pub mod _documentation; pub mod closure; From 550a1fec35191dac4d8696a51ca2bcf2b3391480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pato=20Sanda=C3=B1a?= <1194304+psandana@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:28:48 -0300 Subject: [PATCH 9/9] docs(thread_aware): reword to avoid the doctests spelling flag --- crates/thread_aware/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/thread_aware/src/lib.rs b/crates/thread_aware/src/lib.rs index ffb54c545..4ab6459cb 100644 --- a/crates/thread_aware/src/lib.rs +++ b/crates/thread_aware/src/lib.rs @@ -137,7 +137,7 @@ mod wrappers; /// anti-patterns to avoid. See [the guide](_documentation). /// /// Gated on `derive` because every example is built around `#[derive(ThreadAware)]`, which is only -/// available with that feature; this keeps the guide's doctests valid in a build without it (they +/// available with that feature; this keeps the guide's examples valid in a build without it (they /// are simply absent) per `docs/feature-gated-doctests.md`. #[cfg(all(any(doc, test), feature = "derive"))] pub mod _documentation;