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	Progress, Result,
11	arrayvec::ArrayVec,
12	err, implement, info,
13	smallvec::SmallVec,
14	utils::{
15		BoolExt, ReadyExt, TryReadyExt,
16		math::u64_from_usize_saturating,
17		stream::{BroadbandExt, IterStream, TryIgnore},
18	},
19	warn,
20};
21use tuwunel_database::{Database, Get, Handle, Map, SEP};
22
23use super::Reason;
24use crate::{
25	Services,
26	rooms::{pdu_metadata::typed_relations::Key as RelationKey, state_compressor::StateDiff},
27};
28
29/// Owned copy of a reverse-map identity, used to dereference a loser.
30///
31/// Sized for the common modern event id; longer identities spill.
32type Identity = SmallVec<[u8; 48]>;
33
34/// The `relatesto_typed` rows to rewrite, each with its stale child value.
35///
36/// The key is copied verbatim and rewritten at its own row; one wider than
37/// the writer's fixed length cannot be a relation row and is skipped.
38pub(super) type Relations = Vec<(RelationKey, u64)>;
39
40/// Bitmap over the short id space, one bit per id up to the global counter.
41///
42/// Out-of-range bits are silently absent: setting one is a no-op and
43/// testing one is false.
44type Bits = Vec<u64>;
45
46/// One short id paired with the identity its row names.
47type Candidate = (u64, Identity);
48
49/// Reverse rows no forward value claims, paired with the identities they
50/// name.
51type Candidates = Vec<Candidate>;
52
53/// One family's resolution: losers, the winner each maps to, the rows a
54/// promotion can heal, and the count the dereference could not settle.
55///
56/// Only a row proved absent is promotable; an unsettled one may still hold
57/// a live forward row, so it refuses instead.
58type Resolution = (Vec<u64>, BTreeMap<u64, u64>, Candidates, u64);
59
60/// What dereferencing one candidate's identity proved.
61///
62/// Absent is the only outcome a promotion may act on. A forward row whose
63/// value is not a short id remains unsettled, which is not the same as
64/// proving nothing is there; a failed read never produces an outcome.
65enum Resolved {
66	Winner(u64),
67	Absent,
68	Unsettled,
69}
70
71/// Exclusive upper bound on verifiable short ids.
72///
73/// Each scan bitmap costs one bit per id up to the global counter, and the
74/// deep sweep holds up to four at once. At or above this bound the scan
75/// reports unverifiable instead.
76const MAX_SHORT: u64 = 1 << 30;
77
78/// Format word leading every decline record.
79///
80/// A reader finding any other value here must treat the remaining words
81/// as opaque rather than assume this layout.
82const RECORD_FORMAT: u64 = 1;
83
84/// The counters a declined repair stamps as the marker value.
85///
86/// Twenty-two words: the format, the reason, the global counter, seven
87/// counts per family (rows, losers, dangling, promotable, contended,
88/// unresolved, malformed) for events then statekeys, and the five deep
89/// counts (infected, infected parents, orphans, malformed diffs,
90/// colliding diffs). The reason alone decides which words were measured:
91/// an
92/// unverifiable scan measured nothing past the counter, a healable or
93/// family-anomalous verdict measured the families only, and only a deep
94/// decline measured everything, so an unmeasured zero is no measurement.
95/// A nonzero malformed-diffs word also voids the colliding word, whose
96/// census skips impeached framing. The value serializer packs the words
97/// as contiguous big-endian bytes.
98pub(super) type DeclineRecord = ArrayVec<u64, 22>;
99
100/// One family's residue: its losers, their winners, the rows a heal pass
101/// completes, and the counts that impugn the scan.
102///
103/// The losers include every row the dereference could not pair, so
104/// `winners` is total exactly when `promotable` is empty and `unresolved`
105/// is zero. A contended slot leaves both its claimants unhealed, since
106/// nothing in the residue names which one the allocator meant.
107#[derive(Default)]
108pub(super) struct Family {
109	pub(super) rows: u64,
110	pub(super) losers: Vec<u64>,
111	pub(super) winners: BTreeMap<u64, u64>,
112	pub(super) dangling: Candidates,
113	pub(super) promotable: Candidates,
114	pub(super) contended: u64,
115	pub(super) unresolved: u64,
116	pub(super) malformed: u64,
117}
118
119/// Everything one scan measured, and the worklists the repair consumes.
120///
121/// The deep counts stay zero unless the sweep ran: a loserless scan, a
122/// healable residue, and a family anomaly all decide the boot without
123/// reading the deeper indexes.
124#[derive(Default)]
125pub(super) struct Scan {
126	pub(super) counter: u64,
127	pub(super) events: Family,
128	pub(super) statekeys: Family,
129	pub(super) dirty: u64,
130	pub(super) entries: u64,
131	pub(super) infected: BTreeSet<u64>,
132	pub(super) infected_parents: u64,
133	pub(super) orphans: u64,
134	pub(super) malformed_diffs: u64,
135	pub(super) colliding_diffs: u64,
136	pub(super) moves: Vec<u64>,
137	pub(super) relations: Relations,
138	pub(super) strays: u64,
139	pub(super) unverifiable: bool,
140}
141
142/// Statediff walk context: the bitmaps each row's entries are tested
143/// against.
144///
145/// Folded with a [`Counts`] accumulator over every
146/// `shortstatehash_statediff` row by [`Diffs::row`].
147struct Diffs<'a> {
148	counter: u64,
149	event_stale: &'a [u64],
150	statekey_stale: &'a [u64],
151	event_reverse: &'a [u64],
152	statekey_reverse: &'a [u64],
153}
154
155/// Counts the statediff walk accumulates.
156///
157/// `malformed` covers rows the framing rejects, entries included; the
158/// ghost tallies surface in the sweep's log and the remaining fields
159/// mirror their [`Scan`] counterparts.
160#[derive(Default)]
161struct Counts {
162	infected: BTreeSet<u64>,
163	infected_parents: u64,
164	ghosts: u64,
165	removed_ghosts: u64,
166	orphans: u64,
167	malformed: u64,
168}
169
170/// The `sroomid` field of a stored notification value.
171///
172/// A mirror of the pusher's stored shape just wide enough for the stray
173/// census; every other field is ignored.
174#[derive(Deserialize)]
175struct Notification {
176	sroomid: u64,
177}
178
179/// Measures short id injectivity across both families.
180///
181/// Each reverse map streams before its forward map, so a concurrent
182/// allocation surfaces only on the forward side and cannot be flagged
183/// stale. The deeper indexes are read only when a loser exists and no
184/// family-level verdict already decided the boot.
185#[tracing::instrument(level = "debug", skip_all)]
186pub(super) async fn scan(services: &Services) -> Result<Scan> {
187	info!("Scanning ShortID columns for duplicate values...");
188
189	let counter = services.globals.current_count();
190
191	if counter >= MAX_SHORT {
192		warn!(
193			%counter,
194			"Short id space too large to verify injectivity; refusing the destructive repair."
195		);
196		return Ok(Scan {
197			counter,
198			unverifiable: true,
199			..Default::default()
200		});
201	}
202
203	let words = usize::try_from((counter / 64).saturating_add(1))
204		.map_err(|_| err!("short id bitmap exceeds the address width"))?;
205
206	let progress = &services.server.progress;
207
208	progress.begin("fix_short_injectivity: event short ids");
209	let (events, event_reverse) =
210		family(services, "eventid_shorteventid", "shorteventid_eventid", counter, words).await?;
211
212	progress.begin("fix_short_injectivity: state key short ids");
213	let (statekeys, statekey_reverse) =
214		family(services, "statekey_shortstatekey", "shortstatekey_statekey", counter, words)
215			.await?;
216
217	if events.losers.is_empty() && statekeys.losers.is_empty() {
218		return Ok(Scan {
219			counter,
220			events,
221			statekeys,
222			..Default::default()
223		});
224	}
225
226	let families = Scan {
227		counter,
228		events,
229		statekeys,
230		..Default::default()
231	};
232
233	// Both a heal and a family anomaly decide the boot without a deep
234	// count, so neither pays for one.
235	if families.family_anomalous() || families.healable() {
236		return Ok(families);
237	}
238
239	let Scan { events, statekeys, .. } = families;
240
241	progress.begin("fix_short_injectivity: deep indexes");
242	let swept =
243		sweep(services, &events, &statekeys, event_reverse, statekey_reverse, counter).await?;
244
245	progress.begin("fix_short_injectivity: colliding state diffs");
246
247	// Malformed framing already refuses the lane, and the typed read would
248	// trust the framing the walk just impeached.
249	let colliding_diffs = match swept.malformed_diffs {
250		| 0 => colliding_diffs(services, &swept.infected).await?,
251		| _ => 0,
252	};
253
254	Ok(Scan {
255		counter,
256		events,
257		statekeys,
258		colliding_diffs,
259		..swept
260	})
261}
262
263/// Whether any count impugns the repair's own input.
264///
265/// Any anomaly refuses the destructive repair lane; the cache-clearing
266/// lane is unconditionally safe and proceeds regardless. A malformed diff
267/// row leaves the statediff walk's own output in doubt, a child of an
268/// infected state would need its whole chain rederived, and an infected
269/// row whose stored runs already intersect is malformed input no writer
270/// produces, so all three gate alongside the family anomalies.
271#[implement(Scan)]
272pub(super) fn anomalous(&self) -> bool {
273	self.family_anomalous()
274		|| self.infected_parents > 0
275		|| self.malformed_diffs > 0
276		|| self.colliding_diffs > 0
277}
278
279/// Whether either short id family carries an unhandled shape.
280///
281/// This predicate uses only the family passes, so callers can distinguish
282/// their early verdict from counters a deep sweep never measured.
283#[implement(Scan)]
284pub(super) fn family_anomalous(&self) -> bool {
285	self.events.anomalous() || self.statekeys.anomalous()
286}
287
288/// Whether a heal pass has rows to complete in either family.
289///
290/// A healable scan decides nothing else until it rescans, since a heal
291/// changes the losers and winners every deeper measure is taken against.
292#[implement(Scan)]
293pub(super) fn healable(&self) -> bool { self.events.healable() || self.statekeys.healable() }
294
295/// Packs the verdict's counters into the value a decline stamps.
296///
297/// The record outlives the boot log, handing a follow-up migration and
298/// any support thread the counters as they stood before the stamp.
299/// [`DeclineRecord`] owns the layout; a length past the integer range
300/// saturates rather than wrapping.
301#[implement(Scan)]
302pub(super) fn decline_record(&self, reason: Reason) -> DeclineRecord {
303	let count = u64_from_usize_saturating;
304
305	let record: [u64; 22] = [
306		RECORD_FORMAT,
307		reason.into(),
308		self.counter,
309		self.events.rows,
310		count(self.events.losers.len()),
311		count(self.events.dangling.len()),
312		count(self.events.promotable.len()),
313		self.events.contended,
314		self.events.unresolved,
315		self.events.malformed,
316		self.statekeys.rows,
317		count(self.statekeys.losers.len()),
318		count(self.statekeys.dangling.len()),
319		count(self.statekeys.promotable.len()),
320		self.statekeys.contended,
321		self.statekeys.unresolved,
322		self.statekeys.malformed,
323		count(self.infected.len()),
324		self.infected_parents,
325		self.orphans,
326		self.malformed_diffs,
327		self.colliding_diffs,
328	];
329
330	record.into()
331}
332
333/// Whether this family carries a shape the repair does not handle.
334///
335/// A contended slot has two claimants and nothing to break the tie, an
336/// unresolved row was never proved absent, and a malformed key leaves the
337/// bitmaps too incomplete for any other verdict to stand.
338#[implement(Family)]
339fn anomalous(&self) -> bool { self.contended > 0 || self.unresolved > 0 || self.malformed > 0 }
340
341/// Whether this family has rows a heal pass completes.
342///
343/// Anything impugning the family withholds both classes: the same
344/// bitmaps that name a dangling winner are the ones a malformed key
345/// leaves incomplete.
346#[implement(Family)]
347pub(super) fn healable(&self) -> bool {
348	!self.anomalous() && (!self.dangling.is_empty() || !self.promotable.is_empty())
349}
350
351/// Scans one family in two passes, and a third only where the bitmaps
352/// disagree.
353///
354/// The reverse bitmap completes before the forward stream begins, so a
355/// concurrent allocation surfaces only forward-side and cannot be counted
356/// dangling or stale. The third pass names each loser and its identity; on
357/// a clean family the bitmap difference proves there are none, so it never
358/// runs. Returns the family and its reverse-key bitmap, which the deep sweep
359/// reuses to detect orphaned statediff entries.
360#[tracing::instrument(
361	level = "debug",
362	skip_all,
363	fields(
364		%forward,
365		%reverse,
366	),
367)]
368async fn family(
369	services: &Services,
370	forward: &'static str,
371	reverse: &'static str,
372	counter: u64,
373	words: usize,
374) -> Result<(Family, Bits)> {
375	let db = &services.db;
376	let progress = &services.server.progress;
377
378	progress.enter("reverse rows");
379	let (reverse_bits, rows, reverse_malformed) =
380		reverse_bitmap(&db[reverse], words, progress).await?;
381
382	progress.enter("forward rows");
383	let (forward_bits, mut dangling, forward_malformed) =
384		dangling_winners(&db[forward], &reverse_bits, counter, words, progress).await?;
385
386	progress.enter("unclaimed reverse rows");
387
388	// A set reverse bit no forward value claims is what the pass collects.
389	let candidates = match any_unclaimed(&reverse_bits, &forward_bits, counter) {
390		| false => Candidates::new(),
391		| true => loser_candidates(&db[reverse], &forward_bits, counter, progress).await?,
392	};
393
394	drop(forward_bits);
395
396	progress.enter("candidate identities");
397	let (losers, winners, mut promotable, unresolved) =
398		resolve(&db[forward], &candidates, progress).await?;
399
400	let contended = contenders(&mut dangling, by_short)
401		.saturating_add(contenders(&mut promotable, by_identity));
402
403	let family = Family {
404		rows,
405		losers,
406		winners,
407		dangling,
408		promotable,
409		contended,
410		unresolved,
411		malformed: reverse_malformed.saturating_add(forward_malformed),
412	};
413
414	info!(
415		%forward,
416		%reverse,
417		rows = family.rows,
418		losers = family.losers.len(),
419		dangling = family.dangling.len(),
420		promotable = family.promotable.len(),
421		contended = family.contended,
422		unresolved = family.unresolved,
423		malformed = family.malformed,
424		"Finished scanning column pair."
425	);
426
427	Ok((family, reverse_bits))
428}
429
430/// Streams a reverse map into its keyset bitmap, its row count, and its
431/// count of keys that are not an 8-byte short id.
432///
433/// The rows are counted here rather than in the loser pass, which a clean
434/// family skips.
435async fn reverse_bitmap(
436	map: &Arc<Map>,
437	words: usize,
438	progress: &Progress,
439) -> Result<(Bits, u64, u64)> {
440	map.raw_keys()
441		.ready_try_fold((vec![0_u64; words], 0_u64, 0_u64), |(mut bits, rows, malformed), key| {
442			progress.advance();
443
444			let rows = rows.saturating_add(1);
445
446			Ok(match short_of(key) {
447				| None => (bits, rows, malformed.saturating_add(1)),
448				| Some(short) => {
449					set_bit(&mut bits, short);
450
451					(bits, rows, malformed)
452				},
453			})
454		})
455		.await
456}
457
458/// Streams a forward map against the reverse bitmap for dangling winners.
459///
460/// A dangling winner is a forward value no reverse row answers for.
461/// Values past the counter are concurrent allocations, not danglings. Each
462/// one carries the identity its forward row is keyed by, which is the
463/// reverse row a heal reinstates.
464async fn dangling_winners(
465	map: &Arc<Map>,
466	reverse_bits: &[u64],
467	counter: u64,
468	words: usize,
469	progress: &Progress,
470) -> Result<(Bits, Candidates, u64)> {
471	map.raw_stream()
472		.ready_try_fold(
473			(vec![0_u64; words], Candidates::new(), 0_u64),
474			|(mut bits, mut dangling, malformed), (key, value)| {
475				progress.advance();
476
477				Ok(match short_of(value) {
478					| None => (bits, dangling, malformed.saturating_add(1)),
479					| Some(short) => {
480						if short <= counter && !get_bit(reverse_bits, short) {
481							dangling.push((short, Identity::from_slice(key)));
482						}
483
484						set_bit(&mut bits, short);
485
486						(bits, dangling, malformed)
487					},
488				})
489			},
490		)
491		.await
492}
493
494/// Whether any reverse key's short id went unclaimed by a forward value.
495///
496/// The bitmaps round up to a whole word, so ids past the counter are
497/// addressable in the last one and are masked off. The mask keeps the
498/// counter's own bit, matching the bound the loser pass applies.
499fn any_unclaimed(reverse_bits: &[u64], forward_bits: &[u64], counter: u64) -> bool {
500	let last = usize::try_from(counter / 64).unwrap_or(usize::MAX);
501	let tail = u64::MAX >> 63_u64.saturating_sub(counter % 64);
502
503	debug_assert_eq!(reverse_bits.len(), last.saturating_add(1), "bitmap spans the counter");
504	debug_assert_eq!(forward_bits.len(), reverse_bits.len(), "bitmaps span one id space");
505
506	reverse_bits
507		.iter()
508		.copied()
509		.zip(forward_bits.iter().copied())
510		.enumerate()
511		.any(|(word, (reverse, forward))| {
512			let mask = match word < last {
513				| true => u64::MAX,
514				| false => tail,
515			};
516
517			(reverse & !forward & mask) != 0
518		})
519}
520
521/// Collects reverse keys no forward value claims.
522///
523/// The identity each row names rides along for the dereference pass.
524async fn loser_candidates(
525	map: &Arc<Map>,
526	forward_bits: &[u64],
527	counter: u64,
528	progress: &Progress,
529) -> Result<Candidates> {
530	map.raw_stream()
531		.ready_try_fold(Candidates::new(), |mut candidates, (key, value)| {
532			progress.advance();
533
534			let unclaimed =
535				short_of(key).filter(|short| *short <= counter && !get_bit(forward_bits, *short));
536
537			if let Some(short) = unclaimed {
538				candidates.push((short, Identity::from_slice(value)));
539			}
540
541			Ok(candidates)
542		})
543		.await
544}
545
546/// Dereferences each candidate's identity to split losers from winners.
547///
548/// The identity a loser's reverse row names must hold a live forward row,
549/// whose value is the winner. A candidate resolving to itself was a
550/// concurrent allocation, not a loser.
551async fn resolve(
552	map: &Arc<Map>,
553	candidates: &[(u64, Identity)],
554	progress: &Progress,
555) -> Result<Resolution> {
556	let (mut losers, winners, promotable, unsettled, paired) = candidates
557		.iter()
558		.map(candidate_identity)
559		.stream()
560		.get(map)
561		.zip(candidates.iter().stream())
562		.map(|(result, candidate)| resolution(result).map(|resolved| (resolved, candidate)))
563		.ready_try_fold(
564			(Vec::new(), BTreeMap::new(), Candidates::new(), 0_u64, 0_usize),
565			|(mut losers, mut winners, mut promotable, unsettled, paired),
566			 (resolved, candidate)| {
567				progress.advance();
568
569				let paired = paired.saturating_add(1);
570				let loser = candidate_short(candidate);
571
572				Ok(match resolved {
573					| Resolved::Winner(winner) if winner == loser =>
574						(losers, winners, promotable, unsettled, paired),
575					| Resolved::Winner(winner) => {
576						losers.push(loser);
577						winners.insert(loser, winner);
578
579						(losers, winners, promotable, unsettled, paired)
580					},
581					| Resolved::Absent => {
582						losers.push(loser);
583						promotable.push(candidate.clone());
584
585						(losers, winners, promotable, unsettled, paired)
586					},
587					| Resolved::Unsettled => {
588						losers.push(loser);
589
590						(losers, winners, promotable, unsettled.saturating_add(1), paired)
591					},
592				})
593			},
594		)
595		.await?;
596
597	// A successful batch preserves cardinality. Retain a fail-closed tail
598	// defense in case an adapter ever truncates a clean lookup stream.
599	let tail = candidates.get(paired..).unwrap_or_default();
600	losers.extend(tail.iter().map(candidate_short));
601
602	let unresolved = unsettled.saturating_add(u64_from_usize_saturating(tail.len()));
603
604	Ok((losers, winners, promotable, unresolved))
605}
606
607/// Counts candidates contending for a slot another candidate already
608/// claims.
609///
610/// Sorting is what makes contenders adjacent; the comparator names the
611/// half of the pair that decides the slot, the short id for a
612/// reinstatement and the identity for a promotion.
613fn contenders<F>(candidates: &mut [Candidate], cmp: F) -> u64
614where
615	F: Fn(&Candidate, &Candidate) -> Ordering,
616{
617	candidates.sort_unstable_by(&cmp);
618
619	let contenders = candidates
620		.windows(2)
621		.filter(|pair| cmp(&pair[0], &pair[1]).is_eq())
622		.count();
623
624	u64_from_usize_saturating(contenders)
625}
626
627// Named for the higher-ranked closure generality the dereference stream
628// needs; an inline closure pins the item lifetimes.
629fn candidate_identity((_, identity): &Candidate) -> &Identity { identity }
630
631fn candidate_short((short, _): &Candidate) -> u64 { *short }
632
633fn by_short(a: &Candidate, b: &Candidate) -> Ordering { a.0.cmp(&b.0) }
634
635fn by_identity(a: &Candidate, b: &Candidate) -> Ordering { a.1.cmp(&b.1) }
636
637// A failed read is not an absent row; only the not-found error proves the
638// forward row is missing, and a promotion is a write.
639fn resolution(result: Result<Handle<'_>>) -> Result<Resolved> {
640	match result {
641		| Ok(handle) => Ok(short_of(&handle).map_or(Resolved::Unsettled, Resolved::Winner)),
642		| Err(error) if error.is_not_found() => Ok(Resolved::Absent),
643		| Err(error) => Err(error),
644	}
645}
646
647/// Reads the deeper indexes for the worklists the repair consumes.
648///
649/// Statediff entries are tested against both families and chain-cache
650/// rows against either. The caller runs this only when a loser exists and
651/// no family-level verdict already decided the boot.
652#[tracing::instrument(level = "debug", skip_all)]
653async fn sweep(
654	services: &Services,
655	events: &Family,
656	statekeys: &Family,
657	event_reverse: Bits,
658	statekey_reverse: Bits,
659	counter: u64,
660) -> Result<Scan> {
661	let db = &services.db;
662	let progress = &services.server.progress;
663	let words = event_reverse.len();
664	let event_stale = bits_of(&events.losers, words);
665	let statekey_stale = bits_of(&statekeys.losers, words);
666
667	let walk = Diffs {
668		counter,
669		event_stale: &event_stale,
670		statekey_stale: &statekey_stale,
671		event_reverse: &event_reverse,
672		statekey_reverse: &statekey_reverse,
673	};
674
675	progress.enter("state diff rows");
676	let counts = db["shortstatehash_statediff"]
677		.raw_stream()
678		.ready_try_fold(Counts::default(), |counts, (key, value)| {
679			progress.advance();
680
681			Ok(walk.row(counts, key, value))
682		})
683		.await?;
684
685	drop(event_reverse);
686	drop(statekey_reverse);
687
688	// A key or value that is stale or not a whole number of short ids
689	// poisons the row either way.
690	progress.enter("auth chain rows");
691	let (dirty, entries) = db["authchainkey_authchain"]
692		.raw_stream()
693		.ready_try_fold((0_u64, 0_u64), |(dirty, entries), (key, chain)| {
694			progress.advance();
695
696			let hit = disposable(key, &event_stale, &statekey_stale)
697				|| disposable(chain, &event_stale, &statekey_stale);
698
699			Ok((dirty.saturating_add(u64::from(hit)), entries.saturating_add(1)))
700		})
701		.await?;
702
703	// ready_try_fold rather than ready_try_filter_map: the higher-ranked
704	// adapter fails the boot coroutine's Send obligation over cursor-borrowed
705	// items.
706	progress.enter("event state rows");
707	let moves: Vec<u64> = db["shorteventid_shortstatehash"]
708		.raw_keys()
709		.ready_try_fold(Vec::new(), |mut moves, key| {
710			progress.advance();
711
712			if let Some(loser) = short_of(key).filter(|short| get_bit(&event_stale, *short)) {
713				moves.push(loser);
714			}
715
716			Ok(moves)
717		})
718		.await?;
719
720	progress.enter("typed relation rows");
721	let relations: Relations = db["relatesto_typed"]
722		.raw_stream()
723		.ready_try_fold(Relations::new(), |mut relations, (key, value)| {
724			progress.advance();
725
726			let dirty = short_of(value)
727				.filter(|loser| get_bit(&event_stale, *loser))
728				.zip(RelationKey::try_from(key).ok());
729
730			if let Some((loser, key)) = dirty {
731				relations.push((key, loser));
732			}
733
734			Ok(relations)
735		})
736		.await?;
737
738	// dirty and entries read zero on a boot whose one-time chain clear ran
739	// first; split-marker upgrades report the live dirt instead.
740	warn!(
741		dirty,
742		entries,
743		infected = counts.infected.len(),
744		infected_parents = counts.infected_parents,
745		ghosts = counts.ghosts,
746		removed_ghosts = counts.removed_ghosts,
747		orphans = counts.orphans,
748		malformed_diffs = counts.malformed,
749		moves = moves.len(),
750		relations = relations.len(),
751		"Swept the deeper short id indexes."
752	);
753
754	Ok(Scan {
755		dirty,
756		entries,
757		infected: counts.infected,
758		infected_parents: counts.infected_parents,
759		orphans: counts.orphans,
760		malformed_diffs: counts.malformed,
761		moves,
762		relations,
763		..Default::default()
764	})
765}
766
767impl Diffs<'_> {
768	/// Folds one statediff row through the walk.
769	///
770	/// The value carries an 8-byte parent, then 16-byte entries of a
771	/// statekey and an event half, an added run first and a removed run
772	/// only behind an 8-byte zero sentinel. The sentinel shifts entry
773	/// alignment by 8, so the walk is sequential rather than chunked.
774	fn row(&self, mut counts: Counts, key: &[u8], value: &[u8]) -> Counts {
775		let (Some(row), Some(parent)) = (short_of(key), value.get(0..8).and_then(short_of))
776		else {
777			counts.malformed = counts.malformed.saturating_add(1);
778			return counts;
779		};
780
781		// Rows stream in ascending shortstatehash order, and the writer can
782		// only name an already allocated parent. Failing this premise closed
783		// keeps the one-pass descendant check sound.
784		if parent != 0 && parent >= row {
785			counts.malformed = counts.malformed.saturating_add(1);
786			return counts;
787		}
788
789		if parent != 0 && counts.infected.contains(&parent) {
790			counts.infected_parents = counts.infected_parents.saturating_add(1);
791		}
792
793		let mut removed_run = false;
794		let mut removed = 0_u64;
795		let mut at = 8_usize;
796
797		while at < value.len() {
798			if !removed_run && value[at..].starts_with(&0_u64.to_be_bytes()) {
799				removed_run = true;
800				at = at.saturating_add(8);
801				continue;
802			}
803
804			let entries = (
805				value
806					.get(at..at.saturating_add(8))
807					.and_then(short_of),
808				value
809					.get(at.saturating_add(8)..at.saturating_add(16))
810					.and_then(short_of),
811			);
812
813			let (Some(statekey), Some(event)) = entries else {
814				counts.malformed = counts.malformed.saturating_add(1);
815				return counts;
816			};
817
818			removed = removed.saturating_add(u64::from(removed_run));
819
820			if get_bit(self.statekey_stale, statekey) || get_bit(self.event_stale, event) {
821				counts.infected.insert(row);
822				counts.ghosts = counts.ghosts.saturating_add(1);
823				counts.removed_ghosts = counts
824					.removed_ghosts
825					.saturating_add(u64::from(removed_run));
826			}
827
828			let orphaned = (statekey <= self.counter
829				&& !get_bit(self.statekey_reverse, statekey))
830				|| (event <= self.counter && !get_bit(self.event_reverse, event));
831
832			counts.orphans = counts.orphans.saturating_add(u64::from(orphaned));
833			at = at.saturating_add(16);
834		}
835
836		// The writer gates the sentinel on a nonempty removed run.
837		if removed_run && removed == 0 {
838			counts.malformed = counts.malformed.saturating_add(1);
839		}
840
841		counts
842	}
843}
844
845/// Counts infected rows whose stored runs already intersect.
846///
847/// No writer emits a row whose added and removed runs share an entry, so
848/// a hit is malformed input of unknown provenance and refuses the
849/// destructive lane rather than being rewritten. The population is the
850/// repair's own worklist, read back through the same typed round-trip the
851/// patch trusts.
852#[tracing::instrument(level = "debug", skip_all)]
853async fn colliding_diffs(services: &Services, infected: &BTreeSet<u64>) -> Result<u64> {
854	infected
855		.iter()
856		.copied()
857		.stream()
858		.broad_then(async |state| {
859			services
860				.state_compressor
861				.get_statediff(state)
862				.await
863		})
864		.ready_try_fold(0_u64, |colliding, diff| {
865			Ok(colliding.saturating_add(u64::from(intersecting(&diff))))
866		})
867		.await
868}
869
870/// Whether one diff row's stored runs share an entry.
871fn intersecting(diff: &StateDiff) -> bool { !diff.added.is_disjoint(&diff.removed) }
872
873/// Counts shortroomid references with no forward row.
874///
875/// Purged rooms and losing allocations both produce them; no repair step
876/// touches a shortroomid family, so the count reports and gates nothing.
877/// Retained (unused) as the only in-tree measurement of the stray class,
878/// one restored call away from a repair that learns to act on it.
879#[tracing::instrument(level = "debug", skip_all)]
880#[expect(dead_code)]
881async fn strays(db: &Database, counter: u64, words: usize) -> u64 {
882	let rooms = db["roomid_shortroomid"]
883		.raw_stream()
884		.ignore_err()
885		.ready_fold(vec![0_u64; words], |mut bits, (_, value)| {
886			if let Some(short) = short_of(value) {
887				set_bit(&mut bits, short);
888			}
889
890			bits
891		})
892		.await;
893
894	let stray = |short: Option<u64>| {
895		u64::from(short.is_some_and(|short| short <= counter && !get_bit(&rooms, short)))
896	};
897
898	let strays = db["pduid_pdu"]
899		.raw_keys()
900		.ignore_err()
901		.ready_fold(0_u64, |strays, key| {
902			strays.saturating_add(stray(key.get(0..8).and_then(short_of)))
903		})
904		.await;
905
906	// The search key carries the shortroomid twice: as the prefix and again
907	// inside the pdu id behind the separator-terminated word.
908	let strays = db["tokenids"]
909		.raw_keys()
910		.ignore_err()
911		.ready_fold(strays, |strays, key| {
912			let prefix = key.get(0..8).and_then(short_of);
913			let embedded = key.get(8..).and_then(pdu_shortroomid);
914
915			strays
916				.saturating_add(stray(prefix))
917				.saturating_add(stray(embedded))
918		})
919		.await;
920
921	// Sending-queue keys hold a pdu id behind the destination only when
922	// the value is empty; nonempty rows queue EDUs.
923	let current = db["servercurrentevent_data"]
924		.raw_stream()
925		.ignore_err();
926
927	let strays = db["servernameevent_data"]
928		.raw_stream()
929		.ignore_err()
930		.chain(current)
931		.ready_fold(strays, |strays, (key, value)| {
932			let pdu = value.is_empty().and_then(|| pdu_shortroomid(key));
933
934			strays.saturating_add(stray(pdu))
935		})
936		.await;
937
938	db["useridcount_notification"]
939		.raw_stream()
940		.ignore_err()
941		.ready_fold(strays, |strays, (_, value)| {
942			let sroomid = serde_json::from_slice(value)
943				.ok()
944				.map(|notification: Notification| notification.sroomid);
945
946			strays.saturating_add(stray(sroomid))
947		})
948		.await
949}
950
951/// Extracts the shortroomid of a pdu id sitting behind a separator.
952///
953/// The pdu id must have the 16-byte normal or 24-byte backfilled width;
954/// anything else yields nothing.
955fn pdu_shortroomid(bytes: &[u8]) -> Option<u64> {
956	let sep = bytes.iter().position(|&byte| byte == SEP)?;
957	let id = bytes.get(sep.saturating_add(1)..)?;
958
959	(id.len() == 16 || id.len() == 24)
960		.and_then(|| id.get(0..8))
961		.and_then(short_of)
962}
963
964pub(super) fn short_of(bytes: &[u8]) -> Option<u64> {
965	bytes.try_into().ok().map(u64::from_be_bytes)
966}
967
968fn bits_of(shorts: &[u64], words: usize) -> Bits {
969	shorts
970		.iter()
971		.fold(vec![0_u64; words], |mut bits, short| {
972			set_bit(&mut bits, *short);
973
974			bits
975		})
976}
977
978fn disposable(bytes: &[u8], event_stale: &[u64], statekey_stale: &[u64]) -> bool {
979	!bytes.len().is_multiple_of(size_of::<u64>())
980		|| references(bytes, event_stale, statekey_stale)
981}
982
983fn references(bytes: &[u8], event_stale: &[u64], statekey_stale: &[u64]) -> bool {
984	let mut shorts = bytes
985		.as_chunks::<{ size_of::<u64>() }>()
986		.0
987		.iter()
988		.copied()
989		.map(u64::from_be_bytes);
990
991	Iterator::any(&mut shorts, |short| {
992		get_bit(event_stale, short) || get_bit(statekey_stale, short)
993	})
994}
995
996fn set_bit(bits: &mut [u64], index: u64) {
997	if let Some(word) = usize::try_from(index / 64)
998		.ok()
999		.and_then(|word| bits.get_mut(word))
1000	{
1001		*word |= 1_u64 << (index % 64);
1002	}
1003}
1004
1005fn get_bit(bits: &[u64], index: u64) -> bool {
1006	usize::try_from(index / 64)
1007		.ok()
1008		.and_then(|word| bits.get(word))
1009		.is_some_and(|word| word & (1_u64 << (index % 64)) != 0)
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014	use std::{collections::BTreeSet, sync::Arc};
1015
1016	use tuwunel_core::err;
1017	use tuwunel_database::serialize_to_vec;
1018
1019	use super::{
1020		Candidate, Counts, Diffs, Family, Identity, Reason, Resolved, Scan, StateDiff,
1021		by_identity, by_short, contenders, intersecting, resolution,
1022	};
1023
1024	fn candidate(short: u64, identity: &[u8]) -> Candidate {
1025		(short, Identity::from_slice(identity))
1026	}
1027
1028	fn statediff(parent: u64, statekey: u64, event: u64) -> [u8; 24] {
1029		let mut value = [0_u8; 24];
1030		value[..8].copy_from_slice(&parent.to_be_bytes());
1031		value[8..16].copy_from_slice(&statekey.to_be_bytes());
1032		value[16..].copy_from_slice(&event.to_be_bytes());
1033
1034		value
1035	}
1036
1037	#[test]
1038	fn contenders_counts_two_forward_rows_claiming_one_short() {
1039		let mut dangling = vec![candidate(7, b"$a"), candidate(7, b"$b"), candidate(9, b"$c")];
1040
1041		assert_eq!(contenders(&mut dangling, by_short), 1);
1042	}
1043
1044	#[test]
1045	fn contenders_counts_two_reverse_rows_naming_one_identity() {
1046		let mut promotable = vec![candidate(7, b"$a"), candidate(9, b"$a"), candidate(11, b"$b")];
1047
1048		assert_eq!(contenders(&mut promotable, by_identity), 1);
1049	}
1050
1051	#[test]
1052	fn contenders_is_zero_when_every_slot_is_claimed_once() {
1053		let mut dangling = vec![candidate(9, b"$a"), candidate(7, b"$b")];
1054
1055		assert_eq!(contenders(&mut dangling, by_short), 0);
1056	}
1057
1058	#[test]
1059	fn a_lone_dangling_winner_heals_without_refusing() {
1060		let family = Family {
1061			dangling: vec![candidate(7, b"$a")],
1062			..Default::default()
1063		};
1064
1065		assert!(family.healable());
1066		assert!(!family.anomalous());
1067	}
1068
1069	#[test]
1070	fn a_contended_short_refuses_instead_of_healing() {
1071		let family = Family {
1072			dangling: vec![candidate(7, b"$a"), candidate(7, b"$b")],
1073			contended: 1,
1074			..Default::default()
1075		};
1076
1077		assert!(!family.healable());
1078		assert!(family.anomalous());
1079	}
1080
1081	#[test]
1082	fn a_malformed_key_withholds_the_heal() {
1083		let family = Family {
1084			dangling: vec![candidate(7, b"$a")],
1085			malformed: 1,
1086			..Default::default()
1087		};
1088
1089		assert!(!family.healable());
1090	}
1091
1092	#[test]
1093	fn an_unresolved_row_withholds_the_promotion() {
1094		let family = Family {
1095			promotable: vec![candidate(7, b"$a")],
1096			unresolved: 1,
1097			..Default::default()
1098		};
1099
1100		assert!(!family.healable());
1101		assert!(family.anomalous());
1102	}
1103
1104	#[test]
1105	fn a_forward_read_error_propagates() {
1106		let result = resolution(Err(err!(Database("test read failure"))));
1107
1108		assert!(result.is_err());
1109	}
1110
1111	#[test]
1112	fn a_missing_forward_row_remains_absent() {
1113		let result = resolution(Err(err!(Request(NotFound("test row")))));
1114
1115		assert!(matches!(result, Ok(Resolved::Absent)));
1116	}
1117
1118	#[test]
1119	fn a_descendant_of_an_infected_state_refuses_the_repair() {
1120		let event_stale = [1_u64 << 7];
1121		let empty = [0_u64];
1122		let walk = Diffs {
1123			counter: 0,
1124			event_stale: &event_stale,
1125			statekey_stale: &empty,
1126			event_reverse: &empty,
1127			statekey_reverse: &empty,
1128		};
1129
1130		let infected = statediff(0, 3, 7);
1131		let child = statediff(10, 4, 8);
1132		let counts = walk.row(Counts::default(), &10_u64.to_be_bytes(), &infected);
1133		let counts = walk.row(counts, &11_u64.to_be_bytes(), &child);
1134
1135		assert!(counts.infected.contains(&10));
1136		assert_eq!(counts.infected_parents, 1);
1137
1138		let scan = Scan {
1139			events: Family { losers: vec![7], ..Default::default() },
1140			infected_parents: counts.infected_parents,
1141			..Default::default()
1142		};
1143
1144		assert!(scan.anomalous());
1145	}
1146
1147	#[test]
1148	fn an_intersecting_infected_row_refuses_the_repair() {
1149		let scan = Scan {
1150			events: Family { losers: vec![7], ..Default::default() },
1151			colliding_diffs: 1,
1152			..Default::default()
1153		};
1154
1155		assert!(scan.anomalous());
1156		assert!(!scan.family_anomalous());
1157	}
1158
1159	#[test]
1160	fn runs_sharing_an_entry_intersect() {
1161		let diff = StateDiff {
1162			parent: None,
1163			added: Arc::new(BTreeSet::from([[1_u8; 16], [2_u8; 16]])),
1164			removed: Arc::new(BTreeSet::from([[2_u8; 16], [3_u8; 16]])),
1165		};
1166
1167		assert!(intersecting(&diff));
1168	}
1169
1170	#[test]
1171	fn disjoint_runs_do_not_intersect() {
1172		let diff = StateDiff {
1173			parent: None,
1174			added: Arc::new(BTreeSet::from([[1_u8; 16]])),
1175			removed: Arc::new(BTreeSet::from([[2_u8; 16]])),
1176		};
1177
1178		assert!(!intersecting(&diff));
1179	}
1180
1181	#[test]
1182	fn a_decline_record_serializes_as_contiguous_be_words() {
1183		let scan = Scan {
1184			counter: 7,
1185			colliding_diffs: 9,
1186			..Default::default()
1187		};
1188
1189		let record = scan.decline_record(Reason::Unverifiable);
1190		let bytes = serialize_to_vec(&record).expect("record serializes");
1191
1192		let expected: Vec<u8> = record
1193			.iter()
1194			.flat_map(|word| word.to_be_bytes())
1195			.collect();
1196
1197		assert_eq!(bytes, expected);
1198		assert_eq!(bytes.len(), 22 * 8);
1199	}
1200
1201	#[test]
1202	fn a_decline_record_packs_the_counters_in_layout_order() {
1203		let scan = Scan {
1204			counter: 2,
1205			events: Family {
1206				rows: 10,
1207				losers: vec![0; 11],
1208				dangling: vec![candidate(0, b"$a"); 12],
1209				promotable: vec![candidate(0, b"$a"); 13],
1210				contended: 14,
1211				unresolved: 15,
1212				malformed: 16,
1213				..Default::default()
1214			},
1215			statekeys: Family {
1216				rows: 20,
1217				losers: vec![0; 21],
1218				dangling: vec![candidate(0, b"$a"); 22],
1219				promotable: vec![candidate(0, b"$a"); 23],
1220				contended: 24,
1221				unresolved: 25,
1222				malformed: 26,
1223				..Default::default()
1224			},
1225			infected: (0..30).collect(),
1226			infected_parents: 31,
1227			orphans: 32,
1228			malformed_diffs: 33,
1229			colliding_diffs: 34,
1230			..Default::default()
1231		};
1232
1233		let record = scan.decline_record(Reason::DeepAnomalous);
1234		let expected: [u64; 22] = [
1235			1, 4, 2, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 30, 31, 32, 33, 34,
1236		];
1237
1238		assert_eq!(record.as_slice(), expected.as_slice());
1239	}
1240
1241	#[test]
1242	fn decline_reasons_keep_their_record_numbers() {
1243		assert_eq!(u64::from(Reason::Unverifiable), 1);
1244		assert_eq!(u64::from(Reason::Healable), 2);
1245		assert_eq!(u64::from(Reason::FamilyAnomalous), 3);
1246		assert_eq!(u64::from(Reason::DeepAnomalous), 4);
1247	}
1248
1249	#[test]
1250	fn a_statediff_parent_must_precede_its_child() {
1251		let empty = [0_u64];
1252		let walk = Diffs {
1253			counter: 0,
1254			event_stale: &empty,
1255			statekey_stale: &empty,
1256			event_reverse: &empty,
1257			statekey_reverse: &empty,
1258		};
1259		let value = statediff(11, 3, 8);
1260
1261		let counts = walk.row(Counts::default(), &10_u64.to_be_bytes(), &value);
1262
1263		assert_eq!(counts.malformed, 1);
1264	}
1265}