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}