create-feltdb 0.8.6 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  // One release train keeps generated applications installable. The repository
2
2
  // validation script checks these values against every workspace manifest.
3
- export const FELTDB_PACKAGE_VERSION = '0.8.6';
3
+ export const FELTDB_PACKAGE_VERSION = '0.8.7';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -0,0 +1,96 @@
1
+ //! Shared rules for reading the certification and contract documents.
2
+ //!
3
+ //! Both `production_certification.rs` and `production_contract.rs` check
4
+ //! published prose against an executable table, and both need the same answer
5
+ //! to one awkward question: when does a document *assert* a guarantee, as
6
+ //! opposed to naming one it is withholding? Defining that twice would be the
7
+ //! same mistake the certification exists to prevent — two files quietly
8
+ //! disagreeing — so it is defined once, here.
9
+
10
+ #![allow(dead_code)]
11
+
12
+ /// Split a markdown table row into its trimmed cells.
13
+ ///
14
+ /// Emphasis and code delimiters are stripped, so `**Proven**` and `` `E6` ``
15
+ /// compare equal to the plain text they render.
16
+ pub fn table_cells(line: &str) -> Option<Vec<String>> {
17
+ let line = line.trim();
18
+ if !line.starts_with('|') {
19
+ return None;
20
+ }
21
+ Some(
22
+ line.trim_matches('|')
23
+ .split('|')
24
+ .map(|cell| {
25
+ cell.trim()
26
+ .trim_matches('*')
27
+ .trim()
28
+ .trim_matches('`')
29
+ .to_string()
30
+ })
31
+ .collect(),
32
+ )
33
+ }
34
+
35
+ /// Does `text` *assert* `phrase`, as opposed to naming it?
36
+ ///
37
+ /// The distinction matters because the honest way to record a withheld
38
+ /// guarantee is to quote it: `workload-envelope.md` says a test asserts no
39
+ /// durability mode's wording contains "power-loss safe". A tripwire that
40
+ /// flagged any occurrence would punish the document for being precise, and the
41
+ /// cheapest way to satisfy it would be to stop naming the phrase at all — which
42
+ /// is the opposite of what the tripwire is for.
43
+ ///
44
+ /// So an occurrence inside a quotation (`"..."`) or a code span (`` `...` ``)
45
+ /// is a mention. Every other occurrence is an assertion. Quoting is judged per
46
+ /// line, because a stray delimiter must not silence the rest of the file.
47
+ pub fn asserts_phrase(text: &str, phrase: &str) -> bool {
48
+ text.lines().any(|line| {
49
+ if !line.contains(phrase) {
50
+ return false;
51
+ }
52
+ let mut unquoted = String::new();
53
+ let mut segment = String::new();
54
+ let mut quoted = false;
55
+ for character in line.chars() {
56
+ if character == '"' || character == '`' {
57
+ if !quoted {
58
+ // A quotation opens here, so the text before it is asserted.
59
+ unquoted.push_str(&segment);
60
+ unquoted.push('\u{0}');
61
+ }
62
+ segment.clear();
63
+ quoted = !quoted;
64
+ } else {
65
+ segment.push(character);
66
+ }
67
+ }
68
+ // A delimiter that is never closed quotes nothing.
69
+ unquoted.push_str(&segment);
70
+ unquoted.contains(phrase)
71
+ })
72
+ }
73
+
74
+ /// The document's prose, with rows that record a withheld verdict removed.
75
+ ///
76
+ /// A table row that names something next to a label withholding it — `Unproven`,
77
+ /// `Not certified` — is recording a gap, not asserting a guarantee. Without
78
+ /// this, a matrix would fail its own tripwire the moment it published a gap
79
+ /// precisely, and the cheapest way to pass would be to stop naming the gap.
80
+ ///
81
+ /// Rows whose verdict cell *grants* the thing are deliberately not exempt: if
82
+ /// one ever carried a forbidden phrase, the table and the phrase list would be
83
+ /// in genuine contradiction and the tripwire should say so.
84
+ pub fn prose_excluding_withheld_rows(document: &str, withheld: &[&str]) -> String {
85
+ document
86
+ .lines()
87
+ .filter(|line| match table_cells(line) {
88
+ None => true,
89
+ Some(cells) => !cells
90
+ .iter()
91
+ .any(|cell| withheld.iter().any(|label| cell == label)),
92
+ })
93
+ .collect::<Vec<_>>()
94
+ .join("\n")
95
+ .to_lowercase()
96
+ }
@@ -0,0 +1,191 @@
1
+ //! The managed incident, composed.
2
+ //!
3
+ //! The four remediations each have their own contract: `compaction_stall_contract`
4
+ //! covers the deferred rewrite, `bounded_read_contract` covers scoped reads, and
5
+ //! the server crate's `admission_tests` cover the refusal. Every one of them
6
+ //! exercises its fix in isolation, on an idle database.
7
+ //!
8
+ //! The incident was not any one of them in isolation. It was their composition:
9
+ //! a populated collection, a timer firing compaction into the single lock that
10
+ //! serializes every read and write, and scoped requests arriving throughout.
11
+ //! A database that satisfies each contract separately can still fail the shape
12
+ //! that actually happened, so the shape gets its own test.
13
+ //!
14
+ //! # What this file does not establish
15
+ //!
16
+ //! **It does not measure latency, and it does not prove the incident cannot
17
+ //! recur.** No assertion here bounds how long a read waits, and every read
18
+ //! still queues behind the same mutex. That is claim `M6`, and it is recorded
19
+ //! in the certification matrix as `Unproven` precisely because a test like this
20
+ //! one is not evidence for it. A timing assertion added here would either be
21
+ //! flaky or be tuned until it passed, and both are worse than an honest gap.
22
+ //!
23
+ //! What it does establish is that under the composed load, the timer does not
24
+ //! rewrite, scoped reads stay bounded, and no read observes a torn or missing
25
+ //! database.
26
+
27
+ use feltdb::{CompactionOutcome, CompactionPolicy, FeltDb};
28
+ use serde_json::json;
29
+ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
30
+ use std::sync::Arc;
31
+ use tempfile::TempDir;
32
+
33
+ const ROWS: usize = 800;
34
+
35
+ /// A database shaped like the affected instance: one large collection, every
36
+ /// operation acknowledged so a compaction always has work to do.
37
+ fn incident_shaped() -> (TempDir, Arc<FeltDb>) {
38
+ let directory = TempDir::new().unwrap();
39
+ let db = Arc::new(FeltDb::open(directory.path().join("incident.log")).unwrap());
40
+ db.set_compaction_policy(CompactionPolicy::default());
41
+
42
+ for index in 0..ROWS {
43
+ db.insert(
44
+ &format!("docs:{index:05}"),
45
+ json!({ "n": index, "body": "x".repeat(200) }),
46
+ )
47
+ .unwrap();
48
+ }
49
+ let versions = db.operation_versions().unwrap();
50
+ db.acknowledge_peer_versions("peer-1".to_string(), versions)
51
+ .unwrap();
52
+
53
+ (directory, db)
54
+ }
55
+
56
+ /// **Scoped reads stay correct and bounded while the compaction timer runs.**
57
+ ///
58
+ /// The compacting thread calls the same entry point the server's timer calls,
59
+ /// continuously, for the whole run. The reading thread does what the handlers
60
+ /// now do: a keyed lookup and a limited search, never a materializing scan.
61
+ ///
62
+ /// Three properties are asserted, and each one is a way the incident could
63
+ /// return:
64
+ ///
65
+ /// 1. every read returns the right answer — a compaction running concurrently
66
+ /// never exposes a partially pruned database;
67
+ /// 2. every bounded search visits exactly its limit — the amplifier is still
68
+ /// absent under load, not just on an idle database;
69
+ /// 3. the timer performs no durable rewrite — the expensive half stays deferred
70
+ /// when the policy says it should, and does not creep back in because
71
+ /// something else about the workload changed.
72
+ #[test]
73
+ fn scoped_reads_stay_correct_and_bounded_while_compaction_runs() {
74
+ let (_directory, db) = incident_shaped();
75
+
76
+ let stop = Arc::new(AtomicBool::new(false));
77
+ let rewrites = Arc::new(AtomicUsize::new(0));
78
+ let ticks = Arc::new(AtomicUsize::new(0));
79
+
80
+ let compactor = {
81
+ let db = db.clone();
82
+ let stop = stop.clone();
83
+ let rewrites = rewrites.clone();
84
+ let ticks = ticks.clone();
85
+ std::thread::spawn(move || {
86
+ while !stop.load(Ordering::Relaxed) {
87
+ let outcome = db
88
+ .maybe_compact_operation_log(&["peer-1".to_string()])
89
+ .expect("a timer-driven compaction does not fail");
90
+ ticks.fetch_add(1, Ordering::Relaxed);
91
+ if matches!(outcome, CompactionOutcome::Rewritten { .. }) {
92
+ rewrites.fetch_add(1, Ordering::Relaxed);
93
+ }
94
+ }
95
+ })
96
+ };
97
+
98
+ for round in 0..200 {
99
+ let key = format!("docs:{:05}", round % ROWS);
100
+ let row = db
101
+ .get_collection_record("docs", &key)
102
+ .unwrap()
103
+ .unwrap_or_else(|| panic!("{key} is readable while compaction runs"));
104
+ assert_eq!(row.value["n"], json!(round % ROWS));
105
+
106
+ let visited = AtomicUsize::new(0);
107
+ let matched = db
108
+ .query_collection("docs", Some(10), |row| {
109
+ visited.fetch_add(1, Ordering::Relaxed);
110
+ row.value["body"].is_string()
111
+ })
112
+ .unwrap();
113
+ assert_eq!(matched.len(), 10, "the limit is honoured under load");
114
+ assert_eq!(
115
+ visited.load(Ordering::Relaxed),
116
+ 10,
117
+ "and the search did not walk the collection"
118
+ );
119
+ }
120
+
121
+ stop.store(true, Ordering::Relaxed);
122
+ compactor.join().unwrap();
123
+
124
+ assert!(
125
+ ticks.load(Ordering::Relaxed) > 0,
126
+ "the compaction thread actually ran"
127
+ );
128
+ assert_eq!(
129
+ rewrites.load(Ordering::Relaxed),
130
+ 0,
131
+ "the timer rewrote the durable log during the run; the deferred-rewrite \
132
+ fix does not hold under the composed workload"
133
+ );
134
+ }
135
+
136
+ /// **The composed run leaves the database intact.**
137
+ ///
138
+ /// The incident's worst plausible outcome was not slowness, it was a database
139
+ /// damaged by a rewrite interleaved with traffic. After the concurrent run the
140
+ /// full collection is still present, still correct, and still there after a
141
+ /// reopen — including once a rewrite is forced to happen.
142
+ #[test]
143
+ fn a_concurrent_compaction_leaves_the_database_intact() {
144
+ let directory = TempDir::new().unwrap();
145
+ let path = directory.path().join("intact.log");
146
+ let db = Arc::new(FeltDb::open(&path).unwrap());
147
+ db.set_compaction_policy(CompactionPolicy::default());
148
+
149
+ for index in 0..ROWS {
150
+ db.insert(&format!("docs:{index:05}"), json!({ "n": index }))
151
+ .unwrap();
152
+ }
153
+ let versions = db.operation_versions().unwrap();
154
+ db.acknowledge_peer_versions("peer-1".to_string(), versions)
155
+ .unwrap();
156
+
157
+ let stop = Arc::new(AtomicBool::new(false));
158
+ let compactor = {
159
+ let db = db.clone();
160
+ let stop = stop.clone();
161
+ std::thread::spawn(move || {
162
+ while !stop.load(Ordering::Relaxed) {
163
+ db.maybe_compact_operation_log(&["peer-1".to_string()])
164
+ .unwrap();
165
+ }
166
+ })
167
+ };
168
+
169
+ for index in ROWS..ROWS + 200 {
170
+ db.insert(&format!("docs:{index:05}"), json!({ "n": index }))
171
+ .unwrap();
172
+ }
173
+
174
+ stop.store(true, Ordering::Relaxed);
175
+ compactor.join().unwrap();
176
+
177
+ // An explicit compaction still rewrites — the deferral is a policy, not a
178
+ // disablement — and the rewrite happens over a database that concurrent
179
+ // traffic has just been mutating.
180
+ db.compact_operation_log(&["peer-1".to_string()]).unwrap();
181
+ drop(db);
182
+
183
+ let reopened = FeltDb::open(&path).unwrap();
184
+ for index in 0..ROWS + 200 {
185
+ let value: serde_json::Value = reopened
186
+ .get(&format!("docs:{index:05}"))
187
+ .unwrap()
188
+ .unwrap_or_else(|| panic!("docs:{index:05} survived the composed run"));
189
+ assert_eq!(value["n"], json!(index));
190
+ }
191
+ }