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.
- package/dist/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/tests/common/mod.rs +96 -0
- package/dist/server-source/crates/feltdb/tests/managed_incident_regression.rs +191 -0
- package/dist/server-source/crates/feltdb/tests/production_certification.rs +1153 -0
- package/dist/server-source/crates/feltdb/tests/production_contract.rs +771 -0
- package/package.json +1 -1
package/dist/package-versions.js
CHANGED
|
@@ -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.
|
|
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
|
+
}
|