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
29type Identity = SmallVec<[u8; 48]>;
33
34pub(super) type Relations = Vec<(RelationKey, u64)>;
39
40type Bits = Vec<u64>;
45
46type Candidate = (u64, Identity);
48
49type Candidates = Vec<Candidate>;
52
53type Resolution = (Vec<u64>, BTreeMap<u64, u64>, Candidates, u64);
59
60enum Resolved {
66 Winner(u64),
67 Absent,
68 Unsettled,
69}
70
71const MAX_SHORT: u64 = 1 << 30;
77
78const RECORD_FORMAT: u64 = 1;
83
84pub(super) type DeclineRecord = ArrayVec<u64, 22>;
99
100#[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#[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
142struct 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#[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#[derive(Deserialize)]
175struct Notification {
176 sroomid: u64,
177}
178
179#[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 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 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#[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#[implement(Scan)]
284pub(super) fn family_anomalous(&self) -> bool {
285 self.events.anomalous() || self.statekeys.anomalous()
286}
287
288#[implement(Scan)]
293pub(super) fn healable(&self) -> bool { self.events.healable() || self.statekeys.healable() }
294
295#[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#[implement(Family)]
339fn anomalous(&self) -> bool { self.contended > 0 || self.unresolved > 0 || self.malformed > 0 }
340
341#[implement(Family)]
347pub(super) fn healable(&self) -> bool {
348 !self.anomalous() && (!self.dangling.is_empty() || !self.promotable.is_empty())
349}
350
351#[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 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
430async 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
458async 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
494fn 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
521async 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
546async 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 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
607fn 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
627fn 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
637fn 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#[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 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 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 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 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 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 if removed_run && removed == 0 {
838 counts.malformed = counts.malformed.saturating_add(1);
839 }
840
841 counts
842 }
843}
844
845#[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
870fn intersecting(diff: &StateDiff) -> bool { !diff.added.is_disjoint(&diff.removed) }
872
873#[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 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 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
951fn 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}