Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_o3db_sync/tests/delete_replay.rs

6.1 KiB, 12 runs

created by r1870400018:13683, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A deleted key must not take the rest of the database with it.
2//!
3//! A deletion is appended to the data file as a tombstone under the deleted key. On the next
4//! start, with no index to read, the database rebuilds its index by replaying that file from the
5//! first byte, and it reads every record the same way: a cache hash, the key, the chunk index, the
6//! metadata, a checksum, then the value and its checksum. A tombstone written to any other shape
7//! desynchronises the replay at the point it appears, and every record after it -- however many,
8//! however recent -- is silently lost.
9//!
10//! `basic` already deletes the index files and restarts, but it never deletes a *key* first, so a
11//! tombstone never reached the replay under test. This test writes on both sides of a deletion,
12//! forces the rebuild, and insists that everything except the deleted key comes back.
13//!
14//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
15//! Anthropic Claude
16
17use oxedyne_fe2o3_core::{
18 prelude::*,
19 alt::Override,
20 rand::Rand,
21};
22use oxedyne_fe2o3_crypto::enc::EncryptionScheme;
23use oxedyne_fe2o3_hash::{
24 csum::ChecksumScheme,
25 hash::HashScheme,
26};
27use oxedyne_fe2o3_iop_db::api::{
28 Database,
29 RestSchemesOverride,
30};
31use oxedyne_fe2o3_jdat::prelude::*;
32use oxedyne_fe2o3_o3db_sync::{
33 base::index::ZoneInd,
34 data::core::RestSchemesInput,
35 file::zdir::ZoneDir,
36 test::{
37 file::delete_all_index_files,
38 setup,
39 },
40};
41
42use std::{
43 collections::BTreeMap,
44 path::Path,
45 thread,
46 time::Duration,
47};
48
49
50// Keys written before the deletion.
51const BEFORE: [&str; 3] = ["before:1", "before:2", "before:3"];
52// The key that is deleted.
53const DOOMED: &str = "doomed:1";
54// Keys written after the deletion. These are the ones a badly framed tombstone
55// loses.
56const AFTER: [&str; 3] = ["after:1", "after:2", "after:3"];
57
58pub fn test_delete_replay(_filter: &'static str) -> Outcome<()> {
59
60 res!(std::fs::create_dir_all("./test_db_delete_replay"));
61 let db_root = res!(Path::new("./test_db_delete_replay").canonicalize());
62
63 let mut enckey = [0u8; 32];
64 Rand::fill_u8(&mut enckey);
65 let aes_gcm = res!(EncryptionScheme::new_aes_256_gcm_with_key(&enckey[..]));
66 let crc32 = ChecksumScheme::new_crc32();
67 let schms2: RestSchemesOverride<EncryptionScheme, HashScheme> =
68 RestSchemesOverride::default().set_encrypter(Override::Default(aes_gcm.clone()));
69 let schms2 = Some(&schms2);
70 let user = setup::Uid::default();
71 let schms_input = RestSchemesInput::new(
72 Some(aes_gcm.clone()),
73 None::<HashScheme>,
74 None::<HashScheme>,
75 Some(crc32.clone()),
76 );
77
78 let mut cfg = res!(setup::default_cfg());
79 // Force every write to stable storage before it is acknowledged, so that what this test
80 // measures is the replay of the data file, not the buffering policy in front of it.
81 cfg.sync_on_write = true;
82 // Keep every zone inside this test's own root. The default configuration sends zone 1 to a
83 // container shared with the other tests, where this test's records would mix with theirs.
84 cfg.zone_overrides = DaticleMap::new();
85 let zdirs: BTreeMap<ZoneInd, ZoneDir>;
86
87 // ── Session 1: write, delete, write again ────────────────────
88 {
89 test!(sync_log::stream(), "Session 1: writing on both sides of a deletion.");
90 let mut db = res!(setup::start_db(
91 db_root.clone(),
92 Some(cfg.clone()),
93 schms_input.clone(),
94 None,
95 true,
96 true, // wipe: start from nothing
97 ));
98
99 for k in BEFORE {
100 res!(db.insert(dat!(k), dat!(fmt!("value of {}", k)), user, schms2));
101 }
102 res!(db.insert(dat!(DOOMED), dat!("this value is about to be deleted"), user, schms2));
103
104 // The tombstone. Everything written after this point is what a misframed one destroys.
105 let was_present = res!(db.delete(&dat!(DOOMED), user, schms2));
106 req!(true, was_present);
107
108 for k in AFTER {
109 res!(db.insert(dat!(k), dat!(fmt!("value of {}", k)), user, schms2));
110 }
111
112 zdirs = res!(db.api().get_zone_dirs());
113 res!(db.shutdown());
114 }
115
116 thread::sleep(Duration::from_secs(1));
117
118 // Force the rebuild: with no index files, the data file is replayed from the first byte.
119 res!(delete_all_index_files(&zdirs));
120
121 // ── Session 2: everything but the deleted key must return ────
122 {
123 test!(sync_log::stream(), "Session 2: index files removed, so the data file is replayed.");
124 let db = res!(setup::start_db(
125 db_root.clone(),
126 Some(cfg.clone()),
127 schms_input.clone(),
128 None,
129 true,
130 false, // do not wipe: read what session 1 left
131 ));
132
133 for k in BEFORE {
134 match res!(db.get(&dat!(k), schms2)) {
135 Some((v, _)) => req!(dat!(fmt!("value of {}", k)), v),
136 None => return Err(err!(
137 "The key {:?} was written before the deletion and is now missing: the \
138 replay of the data file did not reach it.", k;
139 Test, Missing, Data)),
140 }
141 }
142
143 // The point of the test: a tombstone must not swallow what follows it.
144 for k in AFTER {
145 match res!(db.get(&dat!(k), schms2)) {
146 Some((v, _)) => req!(dat!(fmt!("value of {}", k)), v),
147 None => return Err(err!(
148 "The key {:?} was written AFTER the deletion and is now missing. The \
149 tombstone is framed differently from an insertion, so the replay lost its \
150 place at the tombstone and never read the records beyond it.", k;
151 Test, Missing, Data)),
152 }
153 }
154
155 // And the deleted key must stay deleted.
156 if res!(db.get(&dat!(DOOMED), schms2)).is_some() {
157 return Err(err!(
158 "The key {:?} was deleted, but came back after the rebuild.", DOOMED;
159 Test, Invalid, Data));
160 }
161
162 res!(db.shutdown());
163 }
164
165 Ok(())
166}