tuwunel_service/migrations/injectivity/
repair.rs1use std::{collections::BTreeMap, sync::Arc};
2
3use tuwunel_core::{
4 Result, debug, info,
5 smallvec::SmallVec,
6 utils::{ReadyExt, hash::sha256::Digest, stream::TryIgnore},
7 warn,
8};
9use tuwunel_database::{Map, Txn};
10
11use super::{
12 clear_chains,
13 scan::{Family, Scan, short_of},
14};
15use crate::{
16 Services,
17 rooms::state_compressor::{
18 CompressedState, CompressedStateEvent, StateDiff, compress_state_event,
19 parse_compressed_state_event,
20 },
21};
22
23type Digests = BTreeMap<u64, SmallVec<[Digest; 1]>>;
28
29#[derive(Default)]
35struct Healed {
36 reinstated: usize,
37 promoted: usize,
38}
39
40#[tracing::instrument(level = "debug", skip_all)]
53pub(super) fn heal(services: &Services, scan: &Scan) -> bool {
54 if scan.unverifiable || !scan.healable() {
55 return false;
56 }
57
58 let db = &services.db;
59 let mut txn = db.txn();
60
61 let event_reverse = &db["shorteventid_eventid"];
62 let event_forward = &db["eventid_shorteventid"];
63 let events = heal_family(&mut txn, event_reverse, event_forward, &scan.events);
64
65 let statekey_reverse = &db["shortstatekey_statekey"];
66 let statekey_forward = &db["statekey_shortstatekey"];
67 let statekeys = heal_family(&mut txn, statekey_reverse, statekey_forward, &scan.statekeys);
68
69 info!(
70 reinstated_events = events.reinstated,
71 promoted_events = events.promoted,
72 reinstated_statekeys = statekeys.reinstated,
73 promoted_statekeys = statekeys.promoted,
74 "Completing torn short id writes; rescanning to re-measure what they explain."
75 );
76
77 txn.execute();
78
79 true
80}
81
82fn heal_family(txn: &mut Txn, reverse: &Map, forward: &Map, family: &Family) -> Healed {
87 if !family.healable() {
88 return Healed::default();
89 }
90
91 for (short, identity) in &family.dangling {
92 txn.insert_raw(reverse, short.to_be_bytes(), identity.as_slice());
93 }
94
95 for (short, identity) in &family.promotable {
96 txn.insert_raw(forward, identity.as_slice(), short.to_be_bytes());
97 }
98
99 Healed {
100 reinstated: family.dangling.len(),
101 promoted: family.promotable.len(),
102 }
103}
104
105#[tracing::instrument(level = "debug", skip_all)]
113pub(super) async fn repair(services: &Services, scan: &Scan) -> Result<bool> {
114 if scan.unverifiable {
115 return Ok(false);
116 }
117
118 if scan.strays > 0 {
119 warn!(
120 stray_references = scan.strays,
121 "Short room id references without a forward row exist; nothing repairs them."
122 );
123 }
124
125 if scan.dirty > 0 {
126 warn!(
127 dirty_entries = scan.dirty,
128 total_entries = scan.entries,
129 "Cached auth chains contain malformed or stale short id data; clearing the auth \
130 chain cache."
131 );
132
133 clear_chains(services).await;
134 }
135
136 if scan.healable() {
141 warn!(
142 dangling_events = scan.events.dangling.len(),
143 dangling_statekeys = scan.statekeys.dangling.len(),
144 promotable_events = scan.events.promotable.len(),
145 promotable_statekeys = scan.statekeys.promotable.len(),
146 "Short id heals did not settle; refusing the repair while residue stays healable."
147 );
148
149 return Ok(false);
150 }
151
152 if scan.events.losers.is_empty() && scan.statekeys.losers.is_empty() {
153 match scan.anomalous() {
154 | false => info!("Short id mappings verified injective."),
155 | true => warn!(
156 dangling_events = scan.events.dangling.len(),
157 dangling_statekeys = scan.statekeys.dangling.len(),
158 contended_events = scan.events.contended,
159 contended_statekeys = scan.statekeys.contended,
160 unresolved_events = scan.events.unresolved,
161 unresolved_statekeys = scan.statekeys.unresolved,
162 malformed_event_keys = scan.events.malformed,
163 malformed_statekey_keys = scan.statekeys.malformed,
164 "Short id anomalies exist with no stale mappings to repair; not scanning again."
165 ),
166 }
167
168 return Ok(true);
169 }
170
171 if scan.anomalous() {
172 warn!(
173 dangling_events = scan.events.dangling.len(),
174 dangling_statekeys = scan.statekeys.dangling.len(),
175 promotable_events = scan.events.promotable.len(),
176 promotable_statekeys = scan.statekeys.promotable.len(),
177 contended_events = scan.events.contended,
178 contended_statekeys = scan.statekeys.contended,
179 unresolved_events = scan.events.unresolved,
180 unresolved_statekeys = scan.statekeys.unresolved,
181 malformed_event_keys = scan.events.malformed,
182 malformed_statekey_keys = scan.statekeys.malformed,
183 orphan_entries = scan.orphans,
184 missing_parents = scan.missing_parents,
185 infected_parents = scan.infected_parents,
186 malformed_diffs = scan.malformed_diffs,
187 "Refusing the destructive short id repair; the nonzero counts name shapes it does \
188 not handle. Please report this line upstream, since the scan repeats each boot \
189 until a release handles them."
190 );
191
192 return Ok(false);
193 }
194
195 patch_statediffs(services, scan).await?;
196 move_keys(services, scan).await?;
197 delete_losers(services, scan);
198
199 Ok(true)
200}
201
202#[tracing::instrument(level = "debug", skip_all)]
210async fn patch_statediffs(services: &Services, scan: &Scan) -> Result {
211 if scan.infected.is_empty() {
212 return Ok(());
213 }
214
215 let digests: Digests = services.db["statehash_shortstatehash"]
216 .raw_stream()
217 .ignore_err()
218 .ready_fold(Digests::new(), |mut digests, (key, value)| {
219 let infected = short_of(value)
220 .filter(|state| scan.infected.contains(state))
221 .and_then(|state| key.try_into().ok().map(|digest| (state, digest)));
222
223 if let Some((state, digest)) = infected {
224 digests.entry(state).or_default().push(digest);
225 }
226
227 digests
228 })
229 .await;
230
231 for &state in &scan.infected {
234 patch_state(services, scan, &digests, state).await?;
235 }
236
237 Ok(())
238}
239
240#[tracing::instrument(
245 level = "debug",
246 skip_all,
247 fields(
248 %state,
249 ),
250)]
251async fn patch_state(services: &Services, scan: &Scan, digests: &Digests, state: u64) -> Result {
252 let diff = services
253 .state_compressor
254 .get_statediff(state)
255 .await?;
256
257 let (added, added_changes) = patch(&diff.added, scan);
258 let (removed, removed_changes) = patch(&diff.removed, scan);
259
260 if removed_changes > 0 {
261 warn!(
262 %state,
263 entries = removed_changes,
264 "Patched ghost entries inside a removed run; the state resolves differently \
265 now that the removal matches."
266 );
267 }
268
269 let shrunk = diff
270 .added
271 .len()
272 .saturating_add(diff.removed.len())
273 .saturating_sub(added.len())
274 .saturating_sub(removed.len());
275
276 if shrunk > 0 {
277 info!(
278 %state,
279 entries = shrunk,
280 "Patching converged duplicate entries; the state shrank."
281 );
282 }
283
284 let patched = StateDiff {
285 parent: diff.parent,
286 added: Arc::new(added),
287 removed: Arc::new(removed),
288 };
289
290 let statehashes = &services.db["statehash_shortstatehash"];
291 let mut txn = services.db.txn();
292
293 services
296 .state_compressor
297 .save_statediff(&mut txn, state, &patched);
298
299 digests
300 .get(&state)
301 .into_iter()
302 .flatten()
303 .for_each(|digest| txn.del_raw(statehashes, digest));
304
305 txn.execute();
306
307 info!(
308 %state,
309 entries = added_changes.saturating_add(removed_changes),
310 "Patched stale short ids out of a compressed state."
311 );
312
313 Ok(())
314}
315
316fn patch(entries: &CompressedState, scan: &Scan) -> (CompressedState, u64) {
321 entries
322 .iter()
323 .fold((CompressedState::new(), 0_u64), |(mut patched, changes), entry| {
324 let winner = winner_of(entry, scan);
325 let changes = changes.saturating_add(u64::from(winner.is_some()));
326
327 patched.insert(winner.unwrap_or(*entry));
328
329 (patched, changes)
330 })
331}
332
333fn winner_of(entry: &CompressedStateEvent, scan: &Scan) -> Option<CompressedStateEvent> {
337 let (statekey, event) = parse_compressed_state_event(*entry);
338 let winner_statekey = scan.statekeys.winners.get(&statekey).copied();
339 let winner_event = scan.events.winners.get(&event).copied();
340
341 (winner_statekey.is_some() || winner_event.is_some()).then(|| {
342 compress_state_event(winner_statekey.unwrap_or(statekey), winner_event.unwrap_or(event))
343 })
344}
345
346#[tracing::instrument(level = "debug", skip_all)]
352async fn move_keys(services: &Services, scan: &Scan) -> Result {
353 if scan.moves.is_empty() && scan.relations.is_empty() {
354 return Ok(());
355 }
356
357 let states = &services.db["shorteventid_shortstatehash"];
360 let mut txn = services.db.txn();
361
362 for &loser in &scan.moves {
363 let Some(&winner) = scan.events.winners.get(&loser) else {
364 continue;
365 };
366
367 move_state_row(states, &mut txn, loser, winner).await?;
368 }
369
370 let relations = &services.db["relatesto_typed"];
371
372 for (key, loser) in &scan.relations {
373 let Some(&winner) = scan.events.winners.get(loser) else {
374 continue;
375 };
376
377 txn.insert_raw(relations, key, winner.to_be_bytes());
378 }
379
380 info!(
381 moves = scan.moves.len(),
382 relations = scan.relations.len(),
383 "Rewrote loser-keyed and loser-valued rows."
384 );
385
386 txn.execute();
387
388 Ok(())
389}
390
391async fn move_state_row(states: &Arc<Map>, txn: &mut Txn, loser: u64, winner: u64) -> Result {
396 match states.get(&winner.to_be_bytes()).await {
397 | Ok(_) =>
398 debug!(loser, winner, "Dropping a loser-keyed state row; the winner has its own."),
399 | Err(error) if error.is_not_found() => {
400 let value = states.get(&loser.to_be_bytes()).await?;
401
402 txn.insert_raw(states, winner.to_be_bytes(), &*value);
403 },
404 | Err(error) => return Err(error),
405 }
406
407 txn.del_raw(states, loser.to_be_bytes());
408
409 Ok(())
410}
411
412fn delete_losers(services: &Services, scan: &Scan) {
418 info!(
419 stale_events = scan.events.losers.len(),
420 stale_statekeys = scan.statekeys.losers.len(),
421 "Deleting stale short id reverse rows."
422 );
423
424 let _cork = services.db.cork_and_sync();
425
426 let events = &services.db["shorteventid_eventid"];
427
428 for loser in &scan.events.losers {
429 events.remove(&loser.to_be_bytes());
430 }
431
432 let statekeys = &services.db["shortstatekey_statekey"];
433
434 for loser in &scan.statekeys.losers {
435 statekeys.remove(&loser.to_be_bytes());
436 }
437}