Skip to main content

tollgate_core/
sharding.rs

1//! Request-local affinity for opt-in instance-local sharding.
2//!
3//! The value is deliberately an affinity hint, not a CPU identity. Tokio
4//! tasks may move, operating-system threads may migrate between cores, and
5//! stable core identifiers are not portable. Assigning each participating OS
6//! thread one process-local number gives worker-thread workloads the property
7//! that matters here: a thread's routine writes land on the *same* cache
8//! lines every time.
9//!
10//! Whether they land on lines *no other thread writes* is a separate claim,
11//! and this module does not make it (GL-124). Affinities come from one
12//! process-global counter shared by every component and every thread that
13//! reaches one, and [`Locality::index`] reduces them onto each component's own
14//! shard count — so two threads whose numbers are congruent modulo that count
15//! share every sharded structure they touch. The counter is never recycled, so
16//! a thread that took a number and exited keeps pushing the live ones apart.
17//!
18//! Distinctness therefore holds while the affinities handed out do not
19//! outnumber the shards, which is a property of the deployment rather than one
20//! this module can enforce: it does not choose how many threads serve requests,
21//! and cannot know which of them still exist. What it can do is report, and
22//! [`LocalSharding::occupancy`] is that report.
23
24use std::cell::Cell;
25use std::num::NonZeroUsize;
26use std::sync::atomic::{AtomicUsize, Ordering};
27
28/// Number of instance-local shards used by hot-path state.
29///
30/// [`SINGLE`](Self::SINGLE) preserves the original layout and behavior.
31/// Larger values are an explicit deployment choice for accounts that
32/// genuinely saturate several worker threads; they trade per-account memory
33/// for less cache-line sharing.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct LocalSharding {
36    shards: NonZeroUsize,
37    /// How to reduce a locality onto these shards, decided once here rather
38    /// than at each lookup (GL-111).
39    ///
40    /// `shards - 1` when the count is a power of two, so the reduction is a
41    /// mask; [`NOT_A_MASK`](Self::NOT_A_MASK) otherwise, so it is a modulo.
42    /// A sentinel rather than an `Option` because this value is copied on
43    /// every sharded lookup: the option is sixteen bytes and pushes the whole
44    /// struct past what fits in registers, and `usize::MAX` cannot collide
45    /// with a real mask, which would need `2^64` shards. The shard count is
46    /// fixed for the process lifetime, which makes this a stored fact and not
47    /// a test to re-run per request — and the test was not free: written as
48    /// `if shards.is_power_of_two()` inside `Locality::index`, LLVM speculated
49    /// both arms and emitted the 64-bit division *unconditionally*, selecting
50    /// between it and the mask afterwards. Every sharded lookup in the
51    /// workspace paid for a division it discarded.
52    mask: usize,
53}
54
55impl LocalSharding {
56    pub const SINGLE: Self = Self::new(NonZeroUsize::MIN);
57
58    /// No mask exists for this shard count, so the reduction is a modulo.
59    ///
60    /// Unreachable as a real mask: `shards - 1` equals `usize::MAX` only for
61    /// `2^64` shards, which no allocation can hold.
62    const NOT_A_MASK: usize = usize::MAX;
63
64    #[must_use]
65    pub const fn new(shards: NonZeroUsize) -> Self {
66        Self {
67            shards,
68            mask: if shards.get().is_power_of_two() {
69                shards.get() - 1
70            } else {
71                Self::NOT_A_MASK
72            },
73        }
74    }
75
76    /// Match the host's advertised parallelism. This is a control-plane
77    /// helper; it is never called from the request path.
78    #[must_use]
79    pub fn available_parallelism() -> Self {
80        Self::new(std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN))
81    }
82
83    #[must_use]
84    pub const fn get(self) -> usize {
85        self.shards.get()
86    }
87
88    /// How this layout is holding up against the affinities handed out.
89    ///
90    /// Metrics only, like [`CapacityOccupancy`]: no decision reads it, and it
91    /// is a control-plane call rather than a request-path one.
92    ///
93    /// [`CapacityOccupancy`]: https://docs.rs/tollgate-admission
94    #[must_use]
95    pub fn occupancy(self) -> ShardOccupancy {
96        ShardOccupancy {
97            shards: self.get(),
98            affinities_assigned: Locality::assigned(),
99        }
100    }
101}
102
103/// What an instance's shard layout is actually carrying.
104///
105/// Counts only. There is no per-shard breakdown and no thread identity,
106/// because neither is knowable: affinities are never recycled, so the process
107/// can say how many it handed out but not which threads still hold them.
108///
109/// That is enough to answer the question an operator has. Affinities come from
110/// one `fetch_add`, so `affinities_assigned` of `n` means exactly the values
111/// `0..n` were handed out, and reducing those onto `shards` is arithmetic
112/// rather than estimation — see [`crowded_shards`](Self::crowded_shards).
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct ShardOccupancy {
115    /// The effective shard count this instance runs.
116    pub shards: usize,
117    /// How many affinities the *process* has handed out, across every
118    /// component and every thread that ever reached one — not how many threads
119    /// are alive, and not how many serve requests.
120    pub affinities_assigned: usize,
121}
122
123impl ShardOccupancy {
124    /// How many shards carry more than one affinity.
125    ///
126    /// The values handed out are `0..affinities_assigned`, so shard `i` carries
127    /// every `j` below that bound with `j % shards == i`. Each shard therefore
128    /// carries either `n / shards` or one more than that, and the ones carrying
129    /// more are the first `n % shards`. Counting the shards left above one
130    /// collapses to the expression below, which is why this is exact and not a
131    /// sample.
132    #[must_use]
133    pub fn crowded_shards(self) -> usize {
134        self.shards
135            .min(self.affinities_assigned.saturating_sub(self.shards))
136    }
137
138    /// Whether any shard carries more than one affinity.
139    ///
140    /// True means this instance has handed out more affinities than it has
141    /// shards, so some threads provably share sharded state — the contention
142    /// the layout was enabled to remove, looking exactly like ordinary load.
143    /// It does not mean two *live request-serving* threads collided: an
144    /// affinity a departed thread took still counts, because it still displaces
145    /// the ones that came after it.
146    #[must_use]
147    pub fn is_crowded(self) -> bool {
148        self.affinities_assigned > self.shards
149    }
150}
151
152impl Default for LocalSharding {
153    fn default() -> Self {
154        Self::SINGLE
155    }
156}
157
158static NEXT_LOCALITY: AtomicUsize = AtomicUsize::new(0);
159
160thread_local! {
161    static LOCALITY: Cell<usize> = Cell::new(NEXT_LOCALITY.fetch_add(1, Ordering::Relaxed));
162}
163
164/// Opaque process-local affinity assigned once to each participating thread.
165///
166/// It carries no authorization or accounting meaning. Components reduce it
167/// modulo their effective shard count, so one lookup can consistently select
168/// the lease, rate-limit, and observability shards for a request.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct Locality(usize);
171
172impl Locality {
173    /// The affinity a control-plane read uses.
174    ///
175    /// Reading published state is not request work and must not spend a
176    /// number: every affinity the control plane takes displaces a
177    /// request-serving thread onto a shard one of its peers already holds, and
178    /// `SnapshotManager` observing its own publications is exactly how that
179    /// happened (GL-124).
180    ///
181    /// It aliases shard zero under every layout, deliberately. A reader that
182    /// wants the generation, the validity bound or the presence of an entry
183    /// gets the same answer from any shard, so there is nothing to choose
184    /// between them — and a constant cannot drift the way "whichever number
185    /// this thread happens to hold" does.
186    pub const OBSERVER: Self = Self(0);
187
188    #[inline]
189    #[must_use]
190    pub fn current() -> Self {
191        LOCALITY.with(|locality| Self(locality.get()))
192    }
193
194    /// How many affinities this process has handed out.
195    ///
196    /// Control plane only — no policy decision reads it, and it is relaxed
197    /// because it answers "roughly how crowded is this instance", never
198    /// "which shard is this request on".
199    #[must_use]
200    pub fn assigned() -> usize {
201        NEXT_LOCALITY.load(Ordering::Relaxed)
202    }
203
204    #[inline]
205    #[must_use]
206    pub fn index(self, sharding: LocalSharding) -> usize {
207        // The choice was made when the sharding was built; this reads it.
208        if sharding.mask == LocalSharding::NOT_A_MASK {
209            self.0 % sharding.shards
210        } else {
211            self.0 & sharding.mask
212        }
213    }
214
215    #[cfg(test)]
216    pub(crate) const fn for_test(value: usize) -> Self {
217        Self(value)
218    }
219}
220
221#[cfg(test)]
222#[path = "../tests/support/isolated.rs"]
223mod isolated;
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn single_sharding_always_selects_the_only_shard() {
231        assert_eq!(Locality::current().index(LocalSharding::SINGLE), 0);
232    }
233
234    #[test]
235    fn one_thread_keeps_one_affinity() {
236        assert_eq!(Locality::current(), Locality::current());
237    }
238
239    #[test]
240    fn host_parallelism_helper_preserves_the_advertised_count() {
241        let expected = std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN);
242        assert_eq!(LocalSharding::available_parallelism().get(), expected.get());
243    }
244
245    #[test]
246    fn power_of_two_and_arbitrary_counts_select_the_same_modulo_index() {
247        let locality = Locality(13);
248        assert_eq!(
249            locality.index(LocalSharding::new(NonZeroUsize::new(8).unwrap())),
250            5
251        );
252        assert_eq!(
253            locality.index(LocalSharding::new(NonZeroUsize::new(5).unwrap())),
254            3
255        );
256    }
257
258    /// The stored reduction computes the same index the modulo always did,
259    /// for every shard count and on both arms (GL-111).
260    ///
261    /// The mask is an optimisation of `% shards`, so the definition it has to
262    /// agree with is `% shards` — stated here against the arithmetic rather
263    /// than against a table of expected answers, which would only pin the
264    /// examples someone thought to write down.
265    #[test]
266    fn a_masked_reduction_agrees_with_the_modulo_it_replaces() {
267        for shards in 1..=64usize {
268            let sharding = LocalSharding::new(NonZeroUsize::new(shards).unwrap());
269            assert_eq!(sharding.get(), shards, "the count itself must not move");
270            for value in [0usize, 1, 7, 13, 64, 255, 4_096, usize::MAX - 1, usize::MAX] {
271                assert_eq!(
272                    Locality(value).index(sharding),
273                    value % shards,
274                    "{value} on {shards} shards"
275                );
276                assert!(Locality(value).index(sharding) < shards);
277            }
278        }
279    }
280
281    /// Both arms exist and each is taken by the counts it is for.
282    ///
283    /// Without this the masked path could quietly become the only one — or
284    /// stop being taken at all — while every index above still agreed, since
285    /// the two arms are defined to produce the same answer.
286    #[test]
287    fn a_power_of_two_count_masks_and_any_other_divides() {
288        for shards in [1usize, 2, 4, 8, 16, 1_024] {
289            let sharding = LocalSharding::new(NonZeroUsize::new(shards).unwrap());
290            assert_eq!(
291                sharding.mask,
292                shards - 1,
293                "{shards} is a power of two and must reduce by mask"
294            );
295            assert_ne!(sharding.mask, LocalSharding::NOT_A_MASK);
296        }
297        for shards in [3usize, 5, 6, 7, 10, 100] {
298            let sharding = LocalSharding::new(NonZeroUsize::new(shards).unwrap());
299            assert_eq!(
300                sharding.mask,
301                LocalSharding::NOT_A_MASK,
302                "{shards} is not a power of two and must reduce by modulo"
303            );
304        }
305    }
306
307    /// The sentinel cannot collide with a real mask.
308    ///
309    /// `shards - 1` reaches `usize::MAX` only at `2^64` shards, which
310    /// `NonZeroUsize` can express and no allocation can hold — so the encoding
311    /// is safe by arithmetic, and this says so rather than leaving it to the
312    /// comment.
313    #[test]
314    fn the_modulo_sentinel_is_not_a_reachable_mask() {
315        // The largest power of two a `usize` can hold, and the largest shard
316        // count anything could allocate for.
317        let largest = NonZeroUsize::new(1usize << (usize::BITS - 1)).unwrap();
318        let sharding = LocalSharding::new(largest);
319        assert_ne!(sharding.mask, LocalSharding::NOT_A_MASK);
320        assert_eq!(sharding.mask, largest.get() - 1);
321
322        // And `SINGLE` masks with zero, which is what makes every locality
323        // select shard zero without a division.
324        assert_eq!(LocalSharding::SINGLE.mask, 0);
325        assert_eq!(LocalSharding::SINGLE.get(), 1);
326    }
327
328    /// Equality still means "the same sharding", now that a derived field
329    /// rides along with the count.
330    #[test]
331    fn shardings_compare_by_the_count_they_were_built_from() {
332        let four = LocalSharding::new(NonZeroUsize::new(4).unwrap());
333        assert_eq!(four, LocalSharding::new(NonZeroUsize::new(4).unwrap()));
334        assert_ne!(four, LocalSharding::new(NonZeroUsize::new(5).unwrap()));
335        assert_eq!(LocalSharding::SINGLE, LocalSharding::default());
336    }
337
338    /// Counted against the residues the counter actually produces, not against
339    /// a table of expected answers.
340    ///
341    /// `crowded_shards` is a closed form for "how many shards receive more
342    /// than one of `0..n`", so the thing it has to agree with is that tally —
343    /// written out here the slow way for every shard count and every load up
344    /// to three times it, including the boundaries on either side of `n ==
345    /// shards` where the answer turns over.
346    #[test]
347    fn crowded_shards_counts_the_residues_the_counter_hands_out() {
348        for shards in 1..=16usize {
349            let sharding = LocalSharding::new(NonZeroUsize::new(shards).unwrap());
350            for assigned in 0..=(3 * shards) {
351                let occupancy = ShardOccupancy {
352                    shards,
353                    affinities_assigned: assigned,
354                };
355                let mut carried = vec![0usize; shards];
356                for affinity in 0..assigned {
357                    carried[Locality(affinity).index(sharding)] += 1;
358                }
359                let expected = carried.iter().filter(|held| **held > 1).count();
360                assert_eq!(
361                    occupancy.crowded_shards(),
362                    expected,
363                    "{assigned} affinities on {shards} shard(s) crowd {expected} of them"
364                );
365                assert_eq!(
366                    occupancy.is_crowded(),
367                    expected > 0,
368                    "{assigned} on {shards}: crowding must agree with the count"
369                );
370                assert!(occupancy.crowded_shards() <= shards);
371            }
372        }
373    }
374
375    /// The case the layout is bought for, stated on its own so a regression
376    /// that crowded *every* instance could not hide inside the sweep above.
377    #[test]
378    fn a_layout_with_room_reports_every_affinity_distinct() {
379        for shards in 1..=16usize {
380            let sharding = LocalSharding::new(NonZeroUsize::new(shards).unwrap());
381            for assigned in 0..=shards {
382                let occupancy = ShardOccupancy {
383                    shards,
384                    affinities_assigned: assigned,
385                };
386                assert!(
387                    !occupancy.is_crowded(),
388                    "{assigned} affinities fit {shards} shard(s)"
389                );
390                assert_eq!(occupancy.crowded_shards(), 0);
391            }
392            // And one past the count is the first crowding, whatever the size.
393            let over = ShardOccupancy {
394                shards,
395                affinities_assigned: shards + 1,
396            };
397            assert!(over.is_crowded());
398            assert_eq!(over.crowded_shards(), 1);
399            assert_eq!(sharding.get(), shards);
400        }
401    }
402
403    /// Reading the report is not itself a claim on an affinity.
404    ///
405    /// Isolated from other tests' claims, and read on an untouched thread:
406    /// even one accidental first-use claim must fail this witness.
407    #[test]
408    fn occupancy_reports_the_counter_without_consuming_from_it() {
409        if isolated::rerun_in_child() {
410            return;
411        }
412        let sharding = LocalSharding::new(NonZeroUsize::new(4).unwrap());
413        assert_eq!(Locality::assigned(), 0);
414        for expected in 0..=5 {
415            // The reporting thread never claims an affinity, even after
416            // other threads have advanced the live counter past crowding.
417            for _ in 0..2 {
418                let now = sharding.occupancy();
419                assert_eq!(now.shards, 4);
420                assert_eq!(now.affinities_assigned, expected, "the counter is live");
421                assert_eq!(Locality::assigned(), expected, "reporting spends nothing");
422            }
423            std::thread::spawn(Locality::current).join().unwrap();
424        }
425    }
426
427    /// The observer affinity is a constant, spends nothing, and lands on the
428    /// same shard under every layout.
429    #[test]
430    fn the_observer_affinity_costs_nothing_and_never_moves() {
431        if isolated::rerun_in_child() {
432            return;
433        }
434        let before = Locality::assigned();
435        assert_eq!(before, 0, "the thread has never claimed an affinity");
436        for shards in 1..=16usize {
437            let sharding = LocalSharding::new(NonZeroUsize::new(shards).unwrap());
438            for _ in 0..64 {
439                assert_eq!(Locality::OBSERVER.index(sharding), 0, "{shards} shards");
440            }
441        }
442        assert_eq!(Locality::assigned(), before, "observing spends nothing");
443    }
444}