Skip to content
Open
246 changes: 246 additions & 0 deletions crates/thread_aware/src/_documentation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should just link to main lib.rs docs, too much duplication

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed to a pointer at the crate-level Theory of Operation (53ca92f); dropped the duplication.

//!
//! 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](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};
//!
//! #[derive(ThreadAware)]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 180583e. Rather than shim each doctest, I gated the whole module on the derive feature: #[cfg(all(any(doc, test), feature = "derive"))]. Every example here is built around #[derive(ThreadAware)], so the guide is present exactly when the derive it documents is available (default and all-feature builds) and simply absent otherwise — which is what docs/feature-gated-doctests.md requires. Verified the guide doctests run under default features and are absent (no failure) under --no-default-features.

//! struct Connection {
//! pool: Vec<u8>,
//! 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<u8>,
//! // 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`](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.
//!
//! ## What the generated bounds mean

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be too much detail, I myself had trouble grasping what is this traying to say

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - cut it down to a short pointer at the derive's Generic Bounds reference (53ca92f).

//!
//! 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](https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html#generic-bounds)
//! reference has the rules.
//!
//! ## Implementing the trait by hand

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am missing thread_aware::Arc guide here, when to implement this. (when we want to maintain separated PerThread instances)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a "Per-worker state with Arc" section (53ca92f): when to reach for Arc<T, PerThread> to keep separate per-worker instances, vs PerProcess / PerNumaNode.

//!
//! 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<u8>,
//! }
//!
//! 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();
Comment on lines +99 to +101
//! }
//! }
//! ```
//!
//! ## 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<T, S>`
//! ([`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
//! 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 |
//! |---|---|---|
//! | 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<T>`](https://docs.rs/thread_aware/latest/thread_aware/struct.Unaware.html) | A `MoveAsIs<T>`: implements the trait as a no-op. |
//! | Shared state that should differ per worker | `Arc<T, PerThread>` (`std`) | Materializes a separate `T` per destination. |
//! | Shared state that is the same everywhere | `Arc<T, PerProcess>` (`std`) | Behaves as a vanilla `Arc`. |
//!
//! [`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).
//!
//! # 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check thread_aware::Relocator (under "test-util") that could be used for relocation testing. Also too much detail too, make it more concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made Testing more concise and now lead with the test-utils Relocator helper for driving relocations (53ca92f).

//!
//! 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};
//!
//! /// 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"
//! );
//! }
//! ```
//!
//! 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
//!
//! 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.
9 changes: 9 additions & 0 deletions crates/thread_aware/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ 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).
///
/// Gated on `derive` because every example is built around `#[derive(ThreadAware)]`, which is only
/// 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;
Comment thread
Copilot marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • this should go into thread_aware_core
  • rename to just documentation (also could you apply the same rename across all our crates)
  • protect with #[cfg(any(doc, test))] so this module doesn't become part of our public API

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've gated it with #[cfg(any(doc, test))] (705d1a9) so the module is doc/test-only and out of the normal public API, matching the existing _documentation modules in recoverable and fetch.

On the other two points:

  • Move to thread_aware_core: this guide is written around the thread_aware facade#[derive(ThreadAware)], Unaware, the strategy-partitioned Arc, ThreadBuilder — none of which live in thread_aware_core. Placing it there would need thread_aware as a dev-dependency of core (which risks the cyclic-deps gate) and would break the intra-doc links to those facade types. Keeping it where those APIs live seems most useful to readers. If you'd instead like a separate, trait-contract-focused guide in core, I'm happy to split it — just let me know.
  • Rename _documentationdocumentation across all crates: glad to, but since every crate currently uses the _documentation name it's a repo-wide convention change; I'd rather land it as its own sweep so it isn't tangled with this guide. I'll open a follow-up unless you'd prefer it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gated with #[cfg(any(doc, test))] (705d1a9). Opened AB#7857350 for the repo-wide _documentation -> documentation rename so it can land as its own sweep. On the core move: this guide leans on the thread_aware facade - the derive, Unaware, strategy Arc, Relocator - none of which live in thread_aware_core, so moving it there breaks those intra-doc links (and would need thread_aware as a dev-dependency of core, risking the cyclic-deps gate). Happy to instead split out a separate, trait-contract-focused guide for core if you'd prefer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update on the "move to thread_aware_core" point: I've switched the guide's references to the facade types (the ThreadAware derive, Unaware, Arc, Relocator) to docs.rs links (9760e99), matching the pattern thread_aware_core already uses for these same types ([arc]: https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html, [derive]: .../derive.ThreadAware.html).

That removes the blocker I raised earlier: the guide no longer needs any of those types in scope, so it could move to thread_aware_core without a dev-dependency on thread_aware (and the dependency cycle that would create). It also fixes the feature-gated-link breakage — Arc is std-gated and Relocator is test-utils-gated, so intra-doc links to them broke under --no-default-features; docs.rs links resolve in every config. The core types it still links by intra-doc (ThreadAware, Thread, relocate) resolve from either crate.

Happy to move the module to thread_aware_core in this PR if you'd prefer, or land it here and move it in the rename sweep (AB#7857350). Let me know which you'd like.


pub mod closure;

#[cfg(feature = "test-utils")]
Expand Down
Loading