Skip to main content

tuwunel_service/migrations/injectivity/
scan.rs

1use std::{
2	cmp::Ordering,
3	collections::{BTreeMap, BTreeSet},
4	sync::Arc,
5};
6
7use futures::StreamExt;
8use serde::Deserialize;
9use tuwunel_core::{
10	Result, err, implement, info,
11	smallvec::SmallVec,
12	utils::{
13		BoolExt, ReadyExt,
14		stream::{IterStream, TryIgnore},
15	},
16	warn,
17};
18use tuwunel_database::{Database, Get, Handle, Map, SEP};
19
20use crate::{Services, rooms::pdu_metadata::typed_relations::Key as RelationKey};
21
22/// Owned copy of a reverse-map identity, used to dereference a loser.
23///
24/// Sized for the common modern event id; longer identities spill.
25type Identity = SmallVec<[u8; 48]>;
26
27/// The `relatesto_typed` rows to rewrite, each with its stale child value.
28///
29/// The key is copied verbatim and rewritten at its own row; one wider than
30/// the writer's fixed length cannot be a relation row and is skipped.
31pub(super) type Relations = Vec<(RelationKey, u64)>;
32
33/// Bitmap over the short id space, one bit per id up to the global counter.
34///
35/// Out-of-range bits are silently absent: setting one is a no-op and
36/// testing one is false.
37type Bits = Vec<u64>;
38
39/// One short id paired with the identity its row names.
40type Candidate = (u64, Identity);
41
42/// Reverse rows no forward value claims, paired with the identities they
43/// name.
44type Candidates = Vec<Candidate>;
45
46/// One family's resolution: losers, the winner each maps to, the rows a
47/// promotion can heal, and the count the dereference could not settle.
48///
49/// Only a row proved absent is promotable; an unsettled one may still hold
50/// a live forward row, so it refuses instead.
51type Resolution = (Vec<u64>, BTreeMap<u64, u64>, Candidates, u64);
52
53/// What dereferencing one candidate's identity proved.
54///
55/// Absent is the only outcome a promotion may act on. A failed read and a
56/// forward row whose value is not a short id both leave the row unsettled,
57/// which is not the same as proving nothing is there.
58enum Resolved {
59	Winner(u64),
60	Absent,
61	Unsettled,
62}
63
64/// Exclusive upper bound on verifiable short ids.
65///
66/// Each scan bitmap costs one bit per id up to the global counter, and the
67/// deep sweep holds up to five at once. At or above this bound the scan
68/// reports unverifiable instead.
69const MAX_SHORT: u64 = 1 << 30;
70
71/// One family's residue: its losers, their winners, the rows a heal pass
72/// completes, and the counts that impugn the scan.
73///
74/// The losers include every row the dereference could not pair, so
75/// `winners` is total exactly when `promotable` is empty and `unresolved`
76/// is zero. A contended slot leaves both its claimants unhealed, since
77/// nothing in the residue names which one the allocator meant.
78#[derive(Default)]
79pub(super) struct Family {
80	pub(super) rows: u64,
81	pub(super) losers: Vec<u64>,
82	pub(super) winners: BTreeMap<u64, u64>,
83	pub(super) dangling: Candidates,
84	pub(super) promotable: Candidates,
85	pub(super) contended: u64,
86	pub(super) unresolved: u64,
87	pub(super) malformed: u64,
88}
89
90/// Everything one scan measured, and the worklists the repair consumes.
91///
92/// The deep counts stay zero when neither family has a loser, since the
93/// deeper indexes are not read in that case.
94#[derive(Default)]
95pub(super) struct Scan {
96	pub(super) events: Family,
97	pub(super) statekeys: Family,
98	pub(super) dirty: u64,
99	pub(super) entries: u64,
100	pub(super) infected: BTreeSet<u64>,
101	pub(super) orphans: u64,
102	pub(super) missing_parents: u64,
103	pub(super) infected_parents: u64,
104	pub(super) malformed_diffs: u64,
105	pub(super) moves: Vec<u64>,
106	pub(super) relations: Relations,
107	pub(super) strays: u64,
108	pub(super) unverifiable: bool,
109}
110
111/// Statediff walk context: the bitmaps each row's entries are tested
112/// against.
113///
114/// Folded with a [`Counts`] accumulator over every
115/// `shortstatehash_statediff` row by [`Diffs::row`].
116struct Diffs<'a> {
117	counter: u64,
118	event_stale: &'a [u64],
119	statekey_stale: &'a [u64],
120	event_reverse: &'a [u64],
121	statekey_reverse: &'a [u64],
122}
123
124/// Counts the statediff walk accumulates.
125///
126/// `malformed` covers rows the framing rejects, entries included; the
127/// ghost tallies surface in the sweep's log and the remaining fields
128/// mirror their [`Scan`] counterparts.
129#[derive(Default)]
130struct Counts {
131	infected: BTreeSet<u64>,
132	ghosts: u64,
133	removed_ghosts: u64,
134	orphans: u64,
135	missing_parents: u64,
136	malformed: u64,
137}
138
139/// The `sroomid` field of a stored notification value.
140///
141/// A mirror of the pusher's stored shape just wide enough for the stray
142/// census; every other field is ignored.
143#[derive(Deserialize)]
144struct Notification {
145	sroomid: u64,
146}
147
148/// Measures short id injectivity across both families.
149///
150/// Each reverse map streams before its forward map, so a concurrent
151/// allocation surfaces only on the forward side and cannot be flagged
152/// stale. The deeper indexes are read only when a loser exists.
153#[tracing::instrument(level = "debug", skip_all)]
154pub(super) async fn scan(services: &Services) -> Result<Scan> {
155	info!("Scanning ShortID columns for duplicate values...");
156
157	let counter = services.globals.current_count();
158
159	if counter >= MAX_SHORT {
160		warn!(
161			%counter,
162			"Short id space too large to verify injectivity; stale auth chain caches, if \
163			 any, survive and can distort state resolution until `server clear-caches`."
164		);
165		return Ok(Scan { unverifiable: true, ..Default::default() });
166	}
167
168	let words = usize::try_from((counter / 64).saturating_add(1))
169		.map_err(|_| err!("short id bitmap exceeds the address width"))?;
170
171	let (events, event_reverse) =
172		family(services, "eventid_shorteventid", "shorteventid_eventid", counter, words).await;
173
174	let (statekeys, statekey_reverse) =
175		family(services, "statekey_shortstatekey", "shortstatekey_statekey", counter, words)
176			.await;
177
178	if events.losers.is_empty() && statekeys.losers.is_empty() {
179		return Ok(Scan { events, statekeys, ..Default::default() });
180	}
181
182	let swept =
183		sweep(services, &events, &statekeys, event_reverse, statekey_reverse, counter).await;
184
185	let scan = Scan { events, statekeys, ..swept };
186
187	// The stray census is the widest pass and gates nothing; it reports on
188	// the boot that acts and skips the rescans a refusal or a heal causes.
189	match scan.anomalous() || scan.healable() {
190		| true => Ok(scan),
191		| false => {
192			let strays = strays(&services.db, counter, words).await;
193
194			Ok(Scan { strays, ..scan })
195		},
196	}
197}
198
199/// Whether any count impugns the scan or exceeds what the repair handles.
200///
201/// Any anomaly refuses the destructive repair lane; the cache-clearing
202/// lane is unconditionally safe and proceeds regardless. The classes a
203/// heal pass completes are gone by the time this decides, so they are
204/// absent here.
205#[implement(Scan)]
206pub(super) fn anomalous(&self) -> bool {
207	self.events.anomalous()
208		|| self.statekeys.anomalous()
209		|| self.orphans > 0
210		|| self.missing_parents > 0
211		|| self.infected_parents > 0
212		|| self.malformed_diffs > 0
213}
214
215/// Whether a heal pass has rows to complete in either family.
216///
217/// The orphan and parent counts are taken against bitmaps a heal then
218/// changes, so a healable scan decides nothing else until it rescans.
219#[implement(Scan)]
220pub(super) fn healable(&self) -> bool { self.events.healable() || self.statekeys.healable() }
221
222/// Whether this family carries a shape the repair does not handle.
223///
224/// A contended slot has two claimants and nothing to break the tie, an
225/// unresolved row was never proved absent, and a malformed key leaves the
226/// bitmaps too incomplete for any other verdict to stand.
227#[implement(Family)]
228fn anomalous(&self) -> bool { self.contended > 0 || self.unresolved > 0 || self.malformed > 0 }
229
230/// Whether this family has rows a heal pass completes.
231///
232/// Anything impugning the family withholds both classes: the same
233/// bitmaps that name a dangling winner are the ones a malformed key
234/// leaves incomplete.
235#[implement(Family)]
236pub(super) fn healable(&self) -> bool {
237	!self.anomalous() && (!self.dangling.is_empty() || !self.promotable.is_empty())
238}
239
240/// Scans one family in two passes, and a third only where the bitmaps
241/// disagree.
242///
243/// The reverse bitmap completes before the forward stream begins, so a
244/// concurrent allocation surfaces only forward-side and cannot be counted
245/// dangling or stale. The third pass names each loser and its identity; on
246/// a clean family the bitmap difference proves there are none, so it never
247/// runs. Returns the family and its reverse-key bitmap, which the deep sweep
248/// reuses to detect orphaned statediff entries.
249#[tracing::instrument(
250	level = "debug",
251	skip_all,
252	fields(
253		%forward,
254		%reverse,
255	),
256)]
257async fn family(
258	services: &Services,
259	forward: &'static str,
260	reverse: &'static str,
261	counter: u64,
262	words: usize,
263) -> (Family, Bits) {
264	let db = &services.db;
265
266	let (reverse_bits, rows, reverse_malformed) = reverse_bitmap(&db[reverse], words).await;
267
268	let (forward_bits, mut dangling, forward_malformed) =
269		dangling_winners(&db[forward], &reverse_bits, counter, words).await;
270
271	// A set reverse bit no forward value claims is what the pass collects.
272	let candidates = match any_unclaimed(&reverse_bits, &forward_bits, counter) {
273		| false => Candidates::new(),
274		| true => loser_candidates(&db[reverse], &forward_bits, counter).await,
275	};
276
277	drop(forward_bits);
278
279	let (losers, winners, mut promotable, unresolved) = resolve(&db[forward], &candidates).await;
280
281	let contended = contenders(&mut dangling, by_short)
282		.saturating_add(contenders(&mut promotable, by_identity));
283
284	let family = Family {
285		rows,
286		losers,
287		winners,
288		dangling,
289		promotable,
290		contended,
291		unresolved,
292		malformed: reverse_malformed.saturating_add(forward_malformed),
293	};
294
295	info!(
296		%forward,
297		%reverse,
298		rows = family.rows,
299		losers = family.losers.len(),
300		dangling = family.dangling.len(),
301		promotable = family.promotable.len(),
302		contended = family.contended,
303		unresolved = family.unresolved,
304		malformed = family.malformed,
305		"Finished scanning column pair."
306	);
307
308	(family, reverse_bits)
309}
310
311/// Streams a reverse map into its keyset bitmap, its row count, and its
312/// count of keys that are not an 8-byte short id.
313///
314/// The rows are counted here rather than in the loser pass, which a clean
315/// family skips.
316async fn reverse_bitmap(map: &Arc<Map>, words: usize) -> (Bits, u64, u64) {
317	map.raw_keys()
318		.ignore_err()
319		.ready_fold((vec![0_u64; words], 0_u64, 0_u64), |(mut bits, rows, malformed), key| {
320			let rows = rows.saturating_add(1);
321
322			match short_of(key) {
323				| None => (bits, rows, malformed.saturating_add(1)),
324				| Some(short) => {
325					set_bit(&mut bits, short);
326
327					(bits, rows, malformed)
328				},
329			}
330		})
331		.await
332}
333
334/// Streams a forward map against the reverse bitmap for dangling winners.
335///
336/// A dangling winner is a forward value no reverse row answers for.
337/// Values past the counter are concurrent allocations, not danglings. Each
338/// one carries the identity its forward row is keyed by, which is the
339/// reverse row a heal reinstates.
340async fn dangling_winners(
341	map: &Arc<Map>,
342	reverse_bits: &[u64],
343	counter: u64,
344	words: usize,
345) -> (Bits, Candidates, u64) {
346	map.raw_stream()
347		.ignore_err()
348		.ready_fold(
349			(vec![0_u64; words], Candidates::new(), 0_u64),
350			|(mut bits, mut dangling, malformed), (key, value)| match short_of(value) {
351				| None => (bits, dangling, malformed.saturating_add(1)),
352				| Some(short) => {
353					if short <= counter && !get_bit(reverse_bits, short) {
354						dangling.push((short, Identity::from_slice(key)));
355					}
356
357					set_bit(&mut bits, short);
358
359					(bits, dangling, malformed)
360				},
361			},
362		)
363		.await
364}
365
366/// Whether any reverse key's short id went unclaimed by a forward value.
367///
368/// The bitmaps round up to a whole word, so ids past the counter are
369/// addressable in the last one and are masked off. The mask keeps the
370/// counter's own bit, matching the bound the loser pass applies.
371fn any_unclaimed(reverse_bits: &[u64], forward_bits: &[u64], counter: u64) -> bool {
372	let last = usize::try_from(counter / 64).unwrap_or(usize::MAX);
373	let tail = u64::MAX >> 63_u64.saturating_sub(counter % 64);
374
375	debug_assert_eq!(reverse_bits.len(), last.saturating_add(1), "bitmap spans the counter");
376	debug_assert_eq!(forward_bits.len(), reverse_bits.len(), "bitmaps span one id space");
377
378	reverse_bits
379		.iter()
380		.copied()
381		.zip(forward_bits.iter().copied())
382		.enumerate()
383		.any(|(word, (reverse, forward))| {
384			let mask = match word < last {
385				| true => u64::MAX,
386				| false => tail,
387			};
388
389			(reverse & !forward & mask) != 0
390		})
391}
392
393/// Collects reverse keys no forward value claims.
394///
395/// The identity each row names rides along for the dereference pass.
396async fn loser_candidates(map: &Arc<Map>, forward_bits: &[u64], counter: u64) -> Candidates {
397	map.raw_stream()
398		.ignore_err()
399		.ready_fold(Candidates::new(), |mut candidates, (key, value)| {
400			let unclaimed =
401				short_of(key).filter(|short| *short <= counter && !get_bit(forward_bits, *short));
402
403			if let Some(short) = unclaimed {
404				candidates.push((short, Identity::from_slice(value)));
405			}
406
407			candidates
408		})
409		.await
410}
411
412/// Dereferences each candidate's identity to split losers from winners.
413///
414/// The identity a loser's reverse row names must hold a live forward row,
415/// whose value is the winner. A candidate resolving to itself was a
416/// concurrent allocation, not a loser.
417async fn resolve(map: &Arc<Map>, candidates: &[(u64, Identity)]) -> Resolution {
418	let (mut losers, winners, promotable, unsettled, paired) = candidates
419		.iter()
420		.map(candidate_identity)
421		.stream()
422		.get(map)
423		.map(resolution)
424		.zip(candidates.iter().stream())
425		.ready_fold(
426			(Vec::new(), BTreeMap::new(), Candidates::new(), 0_u64, 0_usize),
427			|(mut losers, mut winners, mut promotable, unsettled, paired),
428			 (resolved, candidate)| {
429				let paired = paired.saturating_add(1);
430				let loser = candidate_short(candidate);
431
432				match resolved {
433					| Resolved::Winner(winner) if winner == loser =>
434						(losers, winners, promotable, unsettled, paired),
435					| Resolved::Winner(winner) => {
436						losers.push(loser);
437						winners.insert(loser, winner);
438
439						(losers, winners, promotable, unsettled, paired)
440					},
441					| Resolved::Absent => {
442						losers.push(loser);
443						promotable.push(candidate.clone());
444
445						(losers, winners, promotable, unsettled, paired)
446					},
447					| Resolved::Unsettled => {
448						losers.push(loser);
449
450						(losers, winners, promotable, unsettled.saturating_add(1), paired)
451					},
452				}
453			},
454		)
455		.await;
456
457	// A batched lookup can compress a failed chunk into one error item,
458	// desynchronizing the zip; the unpaired tail is undereferenced, so it refuses.
459	let tail = candidates.get(paired..).unwrap_or_default();
460	losers.extend(tail.iter().map(candidate_short));
461
462	let unresolved = unsettled.saturating_add(u64::try_from(tail.len()).unwrap_or(u64::MAX));
463
464	(losers, winners, promotable, unresolved)
465}
466
467/// Counts candidates contending for a slot another candidate already
468/// claims.
469///
470/// Sorting is what makes contenders adjacent; the comparator names the
471/// half of the pair that decides the slot, the short id for a
472/// reinstatement and the identity for a promotion.
473fn contenders<F>(candidates: &mut [Candidate], cmp: F) -> u64
474where
475	F: Fn(&Candidate, &Candidate) -> Ordering,
476{
477	candidates.sort_unstable_by(&cmp);
478
479	let contenders = candidates
480		.windows(2)
481		.filter(|pair| cmp(&pair[0], &pair[1]).is_eq())
482		.count();
483
484	u64::try_from(contenders).unwrap_or(u64::MAX)
485}
486
487// Named for the higher-ranked closure generality the dereference stream
488// needs; an inline closure pins the item lifetimes.
489fn candidate_identity((_, identity): &Candidate) -> &Identity { identity }
490
491fn candidate_short((short, _): &Candidate) -> u64 { *short }
492
493fn by_short(a: &Candidate, b: &Candidate) -> Ordering { a.0.cmp(&b.0) }
494
495fn by_identity(a: &Candidate, b: &Candidate) -> Ordering { a.1.cmp(&b.1) }
496
497// A failed read is not an absent row; only the not-found error proves the
498// forward row is missing, and a promotion is a write.
499fn resolution(result: Result<Handle<'_>>) -> Resolved {
500	match result {
501		| Ok(handle) => short_of(&handle).map_or(Resolved::Unsettled, Resolved::Winner),
502		| Err(error) if error.is_not_found() => Resolved::Absent,
503		| Err(_) => Resolved::Unsettled,
504	}
505}
506
507/// Reads the deeper indexes once a loser exists in either family.
508///
509/// Statediff entries are tested against both families and chain-cache rows
510/// against either, while the shortroomid families are counted for the
511/// report without gating any repair.
512#[tracing::instrument(level = "debug", skip_all)]
513async fn sweep(
514	services: &Services,
515	events: &Family,
516	statekeys: &Family,
517	event_reverse: Bits,
518	statekey_reverse: Bits,
519	counter: u64,
520) -> Scan {
521	let db = &services.db;
522	let words = event_reverse.len();
523	let event_stale = bits_of(&events.losers, words);
524	let statekey_stale = bits_of(&statekeys.losers, words);
525
526	let walk = Diffs {
527		counter,
528		event_stale: &event_stale,
529		statekey_stale: &statekey_stale,
530		event_reverse: &event_reverse,
531		statekey_reverse: &statekey_reverse,
532	};
533
534	let counts = diffs(db, words, walk).await;
535
536	drop(event_reverse);
537	drop(statekey_reverse);
538
539	// A descendant of an infected state would need re-derivation down the
540	// diff chain, which is not built; the anomaly refuses the destructive
541	// lane instead.
542	let infected_parents = match counts.infected.is_empty() {
543		| true => 0,
544		| false =>
545			db["shortstatehash_statediff"]
546				.raw_stream()
547				.ignore_err()
548				.ready_fold(0_u64, |descendants, (_, value)| {
549					let parent = value.get(0..8).and_then(short_of);
550
551					descendants.saturating_add(u64::from(
552						parent.is_some_and(|parent| counts.infected.contains(&parent)),
553					))
554				})
555				.await,
556	};
557
558	// A key or value that is stale or not a whole number of short ids
559	// poisons the row either way.
560	let (dirty, entries) = db["authchainkey_authchain"]
561		.raw_stream()
562		.ignore_err()
563		.ready_fold((0_u64, 0_u64), |(dirty, entries), (key, chain)| {
564			let hit = disposable(key, &event_stale, &statekey_stale)
565				|| disposable(chain, &event_stale, &statekey_stale);
566
567			(dirty.saturating_add(u64::from(hit)), entries.saturating_add(1))
568		})
569		.await;
570
571	// ready_fold rather than ready_filter_map: the higher-ranked adapter
572	// fails the boot coroutine's Send obligation over cursor-borrowed items.
573	let moves: Vec<u64> = db["shorteventid_shortstatehash"]
574		.raw_keys()
575		.ignore_err()
576		.ready_fold(Vec::new(), |mut moves, key| {
577			if let Some(loser) = short_of(key).filter(|short| get_bit(&event_stale, *short)) {
578				moves.push(loser);
579			}
580
581			moves
582		})
583		.await;
584
585	let relations: Relations = db["relatesto_typed"]
586		.raw_stream()
587		.ignore_err()
588		.ready_fold(Relations::new(), |mut relations, (key, value)| {
589			let dirty = short_of(value)
590				.filter(|loser| get_bit(&event_stale, *loser))
591				.zip(RelationKey::try_from(key).ok());
592
593			if let Some((loser, key)) = dirty {
594				relations.push((key, loser));
595			}
596
597			relations
598		})
599		.await;
600
601	// dirty and entries read zero on any boot whose chain clear ran first;
602	// a refusing database's later boots report the live dirt instead.
603	warn!(
604		dirty,
605		entries,
606		infected = counts.infected.len(),
607		ghosts = counts.ghosts,
608		removed_ghosts = counts.removed_ghosts,
609		orphans = counts.orphans,
610		missing_parents = counts.missing_parents,
611		infected_parents,
612		malformed_diffs = counts.malformed,
613		moves = moves.len(),
614		relations = relations.len(),
615		"Swept the deeper short id indexes."
616	);
617
618	Scan {
619		dirty,
620		entries,
621		infected: counts.infected,
622		orphans: counts.orphans,
623		missing_parents: counts.missing_parents,
624		infected_parents,
625		malformed_diffs: counts.malformed,
626		moves,
627		relations,
628		..Default::default()
629	}
630}
631
632/// Folds every statediff row through the walk, its parent keyset first.
633///
634/// The whole keyset must precede the row walk, a row's parent appearing
635/// anywhere in the file.
636async fn diffs(db: &Database, words: usize, walk: Diffs<'_>) -> Counts {
637	let parents = db["shortstatehash_statediff"]
638		.raw_keys()
639		.ignore_err()
640		.ready_fold(vec![0_u64; words], |mut bits, key| {
641			if let Some(short) = short_of(key) {
642				set_bit(&mut bits, short);
643			}
644
645			bits
646		})
647		.await;
648
649	db["shortstatehash_statediff"]
650		.raw_stream()
651		.ignore_err()
652		.ready_fold(Counts::default(), |counts, (key, value)| {
653			walk.row(counts, key, value, &parents)
654		})
655		.await
656}
657
658impl Diffs<'_> {
659	/// Folds one statediff row through the walk.
660	///
661	/// The value carries an 8-byte parent, then 16-byte entries of a
662	/// statekey and an event half, an added run first and a removed run
663	/// only behind an 8-byte zero sentinel. The sentinel shifts entry
664	/// alignment by 8, so the walk is sequential rather than chunked.
665	fn row(&self, mut counts: Counts, key: &[u8], value: &[u8], parents: &[u64]) -> Counts {
666		let (Some(row), Some(parent)) = (short_of(key), value.get(0..8).and_then(short_of))
667		else {
668			counts.malformed = counts.malformed.saturating_add(1);
669			return counts;
670		};
671
672		if parent != 0 && parent <= self.counter && !get_bit(parents, parent) {
673			counts.missing_parents = counts.missing_parents.saturating_add(1);
674		}
675
676		let mut removed_run = false;
677		let mut removed = 0_u64;
678		let mut at = 8_usize;
679
680		while at < value.len() {
681			if !removed_run && value[at..].starts_with(&0_u64.to_be_bytes()) {
682				removed_run = true;
683				at = at.saturating_add(8);
684				continue;
685			}
686
687			let entries = (
688				value
689					.get(at..at.saturating_add(8))
690					.and_then(short_of),
691				value
692					.get(at.saturating_add(8)..at.saturating_add(16))
693					.and_then(short_of),
694			);
695
696			let (Some(statekey), Some(event)) = entries else {
697				counts.malformed = counts.malformed.saturating_add(1);
698				return counts;
699			};
700
701			removed = removed.saturating_add(u64::from(removed_run));
702
703			if get_bit(self.statekey_stale, statekey) || get_bit(self.event_stale, event) {
704				counts.infected.insert(row);
705				counts.ghosts = counts.ghosts.saturating_add(1);
706				counts.removed_ghosts = counts
707					.removed_ghosts
708					.saturating_add(u64::from(removed_run));
709			}
710
711			let orphaned = (statekey <= self.counter
712				&& !get_bit(self.statekey_reverse, statekey))
713				|| (event <= self.counter && !get_bit(self.event_reverse, event));
714
715			counts.orphans = counts.orphans.saturating_add(u64::from(orphaned));
716			at = at.saturating_add(16);
717		}
718
719		// The writer gates the sentinel on a nonempty removed run.
720		if removed_run && removed == 0 {
721			counts.malformed = counts.malformed.saturating_add(1);
722		}
723
724		counts
725	}
726}
727
728/// Counts shortroomid references with no forward row.
729///
730/// Purged rooms and losing allocations both produce them; no repair step
731/// touches a shortroomid family, so the count reports and gates nothing.
732#[tracing::instrument(level = "debug", skip_all)]
733async fn strays(db: &Database, counter: u64, words: usize) -> u64 {
734	let rooms = db["roomid_shortroomid"]
735		.raw_stream()
736		.ignore_err()
737		.ready_fold(vec![0_u64; words], |mut bits, (_, value)| {
738			if let Some(short) = short_of(value) {
739				set_bit(&mut bits, short);
740			}
741
742			bits
743		})
744		.await;
745
746	let stray = |short: Option<u64>| {
747		u64::from(short.is_some_and(|short| short <= counter && !get_bit(&rooms, short)))
748	};
749
750	let strays = db["pduid_pdu"]
751		.raw_keys()
752		.ignore_err()
753		.ready_fold(0_u64, |strays, key| {
754			strays.saturating_add(stray(key.get(0..8).and_then(short_of)))
755		})
756		.await;
757
758	// The search key carries the shortroomid twice: as the prefix and again
759	// inside the pdu id behind the separator-terminated word.
760	let strays = db["tokenids"]
761		.raw_keys()
762		.ignore_err()
763		.ready_fold(strays, |strays, key| {
764			let prefix = key.get(0..8).and_then(short_of);
765			let embedded = key.get(8..).and_then(pdu_shortroomid);
766
767			strays
768				.saturating_add(stray(prefix))
769				.saturating_add(stray(embedded))
770		})
771		.await;
772
773	// Sending-queue keys hold a pdu id behind the destination only when
774	// the value is empty; nonempty rows queue EDUs.
775	let current = db["servercurrentevent_data"]
776		.raw_stream()
777		.ignore_err();
778
779	let strays = db["servernameevent_data"]
780		.raw_stream()
781		.ignore_err()
782		.chain(current)
783		.ready_fold(strays, |strays, (key, value)| {
784			let pdu = value.is_empty().and_then(|| pdu_shortroomid(key));
785
786			strays.saturating_add(stray(pdu))
787		})
788		.await;
789
790	db["useridcount_notification"]
791		.raw_stream()
792		.ignore_err()
793		.ready_fold(strays, |strays, (_, value)| {
794			let sroomid = serde_json::from_slice(value)
795				.ok()
796				.map(|notification: Notification| notification.sroomid);
797
798			strays.saturating_add(stray(sroomid))
799		})
800		.await
801}
802
803/// Extracts the shortroomid of a pdu id sitting behind a separator.
804///
805/// The pdu id must have the 16-byte normal or 24-byte backfilled width;
806/// anything else yields nothing.
807fn pdu_shortroomid(bytes: &[u8]) -> Option<u64> {
808	let sep = bytes.iter().position(|&byte| byte == SEP)?;
809	let id = bytes.get(sep.saturating_add(1)..)?;
810
811	(id.len() == 16 || id.len() == 24)
812		.and_then(|| id.get(0..8))
813		.and_then(short_of)
814}
815
816pub(super) fn short_of(bytes: &[u8]) -> Option<u64> {
817	bytes.try_into().ok().map(u64::from_be_bytes)
818}
819
820fn bits_of(shorts: &[u64], words: usize) -> Bits {
821	shorts
822		.iter()
823		.fold(vec![0_u64; words], |mut bits, short| {
824			set_bit(&mut bits, *short);
825
826			bits
827		})
828}
829
830fn disposable(bytes: &[u8], event_stale: &[u64], statekey_stale: &[u64]) -> bool {
831	!bytes.len().is_multiple_of(size_of::<u64>())
832		|| references(bytes, event_stale, statekey_stale)
833}
834
835fn references(bytes: &[u8], event_stale: &[u64], statekey_stale: &[u64]) -> bool {
836	bytes
837		.as_chunks::<{ size_of::<u64>() }>()
838		.0
839		.iter()
840		.copied()
841		.map(u64::from_be_bytes)
842		.any(|short| get_bit(event_stale, short) || get_bit(statekey_stale, short))
843}
844
845fn set_bit(bits: &mut [u64], index: u64) {
846	if let Some(word) = usize::try_from(index / 64)
847		.ok()
848		.and_then(|word| bits.get_mut(word))
849	{
850		*word |= 1_u64 << (index % 64);
851	}
852}
853
854fn get_bit(bits: &[u64], index: u64) -> bool {
855	usize::try_from(index / 64)
856		.ok()
857		.and_then(|word| bits.get(word))
858		.is_some_and(|word| word & (1_u64 << (index % 64)) != 0)
859}
860
861#[cfg(test)]
862mod tests {
863	use super::{Candidate, Family, Identity, by_identity, by_short, contenders};
864
865	fn candidate(short: u64, identity: &[u8]) -> Candidate {
866		(short, Identity::from_slice(identity))
867	}
868
869	#[test]
870	fn contenders_counts_two_forward_rows_claiming_one_short() {
871		let mut dangling = vec![candidate(7, b"$a"), candidate(7, b"$b"), candidate(9, b"$c")];
872
873		assert_eq!(contenders(&mut dangling, by_short), 1);
874	}
875
876	#[test]
877	fn contenders_counts_two_reverse_rows_naming_one_identity() {
878		let mut promotable = vec![candidate(7, b"$a"), candidate(9, b"$a"), candidate(11, b"$b")];
879
880		assert_eq!(contenders(&mut promotable, by_identity), 1);
881	}
882
883	#[test]
884	fn contenders_is_zero_when_every_slot_is_claimed_once() {
885		let mut dangling = vec![candidate(9, b"$a"), candidate(7, b"$b")];
886
887		assert_eq!(contenders(&mut dangling, by_short), 0);
888	}
889
890	#[test]
891	fn a_lone_dangling_winner_heals_without_refusing() {
892		let family = Family {
893			dangling: vec![candidate(7, b"$a")],
894			..Default::default()
895		};
896
897		assert!(family.healable());
898		assert!(!family.anomalous());
899	}
900
901	#[test]
902	fn a_contended_short_refuses_instead_of_healing() {
903		let family = Family {
904			dangling: vec![candidate(7, b"$a"), candidate(7, b"$b")],
905			contended: 1,
906			..Default::default()
907		};
908
909		assert!(!family.healable());
910		assert!(family.anomalous());
911	}
912
913	#[test]
914	fn a_malformed_key_withholds_the_heal() {
915		let family = Family {
916			dangling: vec![candidate(7, b"$a")],
917			malformed: 1,
918			..Default::default()
919		};
920
921		assert!(!family.healable());
922	}
923
924	#[test]
925	fn an_unresolved_row_withholds_the_promotion() {
926		let family = Family {
927			promotable: vec![candidate(7, b"$a")],
928			unresolved: 1,
929			..Default::default()
930		};
931
932		assert!(!family.healable());
933	}
934}