@feltdb/core 0.8.5 → 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.
Files changed (29) hide show
  1. package/dist/create/package-versions.js +1 -1
  2. package/dist/create/server-source/crates/feltdb/src/lib.rs +1173 -72
  3. package/dist/create/server-source/crates/feltdb/src/operation.rs +39 -0
  4. package/dist/create/server-source/crates/feltdb/src/state_model.rs +52 -0
  5. package/dist/create/server-source/crates/feltdb/src/storage.rs +9 -3
  6. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +132 -0
  7. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +96 -0
  8. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +272 -0
  9. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +467 -0
  10. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +26 -5
  11. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +445 -0
  12. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +518 -0
  13. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +191 -0
  14. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +278 -0
  15. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +1153 -0
  16. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +771 -0
  17. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +490 -157
  18. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +417 -0
  19. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +442 -0
  20. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +14 -0
  21. package/dist/create/server-source/crates/feltdb-server/src/main.rs +369 -41
  22. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +21 -0
  23. package/dist/studio-app/assets/{feltdb_wasm-DaNwCLRX.js → feltdb_wasm-C1VhI-U5.js} +1 -1
  24. package/dist/studio-app/assets/feltdb_wasm_bg-C8HXbAXb.wasm +0 -0
  25. package/dist/studio-app/assets/{index-j8IlhNqJ.js → index-Bbos1m2U.js} +1 -1
  26. package/dist/studio-app/index.html +1 -1
  27. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  28. package/package.json +1 -1
  29. package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
@@ -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
+ }
@@ -0,0 +1,278 @@
1
+ //! Operational health.
2
+ //!
3
+ //! Everything from the format work onward proved what FeltDB *does*. This file
4
+ //! asks a different question:
5
+ //!
6
+ //! > **Can an operator reliably tell what FeltDB knows about itself?**
7
+ //!
8
+ //! The rule the whole surface is built on:
9
+ //!
10
+ //! > **Health can expose a proven fact. It cannot create a stronger
11
+ //! > guarantee.**
12
+ //!
13
+ //! So `DatabaseHealth` reports the durable format an open accepted and whether
14
+ //! replay discarded an incomplete final append, and nothing else. There is no
15
+ //! replication health, no backup freshness and no durability claim, because
16
+ //! none of those has an operational contract yet and inventing one here would
17
+ //! reintroduce exactly the defect this replaces: a reassuring label that
18
+ //! observes nothing.
19
+ //!
20
+ //! See `docs/architecture/operational-health.md`.
21
+
22
+ use feltdb::{
23
+ inspect_durable_format, DatabaseHealth, DurableFormat, FeltDb, FlowError, LogRecovery,
24
+ StorageHealth, DURABLE_FORMAT_VERSION,
25
+ };
26
+ use serde_json::{json, Value};
27
+ use std::io::Write;
28
+ use std::path::Path;
29
+ use tempfile::TempDir;
30
+
31
+ fn three_writes(path: &Path) -> Vec<u8> {
32
+ let db = FeltDb::open(path).unwrap();
33
+ for n in 0..3 {
34
+ db.insert(&format!("tasks:{n}"), json!({ "n": n })).unwrap();
35
+ }
36
+ drop(db);
37
+ std::fs::read(path).unwrap()
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // The five questions health has to answer
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /// **A clean startup reports clean.**
45
+ #[test]
46
+ fn a_clean_open_reports_clean_storage() {
47
+ let directory = TempDir::new().unwrap();
48
+ let path = directory.path().join("clean.log");
49
+ three_writes(&path);
50
+
51
+ let db = FeltDb::open(&path).unwrap();
52
+ let health = db.health();
53
+
54
+ assert_eq!(health.storage, StorageHealth::Clean);
55
+ assert_eq!(health.recovery, LogRecovery::Clean);
56
+ assert_eq!(
57
+ health.durable_format,
58
+ DurableFormat::Versioned(DURABLE_FORMAT_VERSION)
59
+ );
60
+ assert!(health.is_nominal());
61
+ assert_eq!(health.storage.label(), "clean");
62
+ }
63
+
64
+ /// **The regression that matters.** A database that discarded a torn tail must
65
+ /// not report the same health as one that replayed cleanly.
66
+ ///
67
+ /// This is the operational consequence of the corruption work: the distinction
68
+ /// between a clean open and a recovered one was established there, and it is
69
+ /// worth nothing to an operator unless it reaches them.
70
+ #[test]
71
+ fn a_recovered_open_does_not_report_the_same_health_as_a_clean_one() {
72
+ let directory = TempDir::new().unwrap();
73
+ let clean_path = directory.path().join("clean.log");
74
+ let torn_path = directory.path().join("torn.log");
75
+ let full = three_writes(&clean_path);
76
+
77
+ // The same database, with its final append cut short.
78
+ std::fs::write(&torn_path, &full[..full.len() - 30]).unwrap();
79
+
80
+ let clean = FeltDb::open(&clean_path).unwrap().health();
81
+ let torn = FeltDb::open(&torn_path).unwrap().health();
82
+
83
+ assert_ne!(
84
+ clean, torn,
85
+ "a database that lost its last write must not look identical to one that did not"
86
+ );
87
+ assert!(clean.is_nominal());
88
+ assert!(!torn.is_nominal(), "recovered is not nominal");
89
+ assert_ne!(clean.storage.label(), torn.storage.label());
90
+
91
+ match torn.storage {
92
+ StorageHealth::RecoveredIncompleteWrite {
93
+ byte_offset,
94
+ discarded_bytes,
95
+ } => {
96
+ assert!(byte_offset > 0, "the operator is told where");
97
+ assert!(discarded_bytes > 0, "and how much");
98
+ }
99
+ other => panic!("expected a reported recovery, got {other:?}"),
100
+ }
101
+
102
+ // And it says so in words an operator can act on.
103
+ let described = torn.storage.to_string();
104
+ assert!(described.contains("incomplete append"));
105
+ assert!(described.contains("last write did not land"));
106
+ }
107
+
108
+ /// **A corrupt database does not become healthy.** It does not open at all.
109
+ ///
110
+ /// The ambiguity being removed here is "healthy process, unhealthy database":
111
+ /// there is no state in which a server is serving a database whose durable log
112
+ /// failed to replay.
113
+ #[test]
114
+ fn a_corrupt_database_never_reaches_a_health_report() {
115
+ let directory = TempDir::new().unwrap();
116
+ let path = directory.path().join("corrupt.log");
117
+ three_writes(&path);
118
+
119
+ let mut file = std::fs::OpenOptions::new()
120
+ .append(true)
121
+ .open(&path)
122
+ .unwrap();
123
+ writeln!(file, "this is not json at all").unwrap();
124
+ drop(file);
125
+
126
+ match FeltDb::open(&path) {
127
+ Err(FlowError::CorruptLogLine(_)) => {}
128
+ Err(other) => panic!("wrong error: {other}"),
129
+ Ok(_) => panic!("a corrupt database opened and could have reported health"),
130
+ }
131
+ }
132
+
133
+ /// **An incompatible database stays refused**, so a running server cannot be
134
+ /// advertising one.
135
+ #[test]
136
+ fn an_incompatible_database_never_reaches_a_health_report() {
137
+ let directory = TempDir::new().unwrap();
138
+ let path = directory.path().join("future.log");
139
+ std::fs::write(
140
+ &path,
141
+ format!(
142
+ "{}\n",
143
+ json!({
144
+ "record_type": "feltdb.format.v1",
145
+ "format_version": DURABLE_FORMAT_VERSION + 1
146
+ })
147
+ ),
148
+ )
149
+ .unwrap();
150
+
151
+ assert!(!inspect_durable_format(&path).unwrap().is_compatible());
152
+ assert!(matches!(
153
+ FeltDb::open(&path),
154
+ Err(FlowError::IncompatibleFormat(_))
155
+ ));
156
+ }
157
+
158
+ /// Health survives the thing it describes: reopening a repaired log reports
159
+ /// clean again, because the condition is genuinely gone.
160
+ ///
161
+ /// A status that stayed degraded forever would be as useless as one that never
162
+ /// was.
163
+ #[test]
164
+ fn health_reflects_the_current_open_not_the_database_s_past() {
165
+ let directory = TempDir::new().unwrap();
166
+ let path = directory.path().join("repaired.log");
167
+ let full = three_writes(&path);
168
+ std::fs::write(&path, &full[..full.len() - 30]).unwrap();
169
+
170
+ let recovered = FeltDb::open(&path).unwrap();
171
+ assert!(!recovered.health().is_nominal());
172
+ drop(recovered);
173
+
174
+ let reopened = FeltDb::open(&path).unwrap();
175
+ assert!(
176
+ reopened.health().is_nominal(),
177
+ "the incomplete append is gone, so the condition is too"
178
+ );
179
+ }
180
+
181
+ /// A database with no torn tail but written before format versioning is
182
+ /// reported as what it is, rather than as an error or as current.
183
+ #[test]
184
+ fn an_unversioned_database_reports_its_actual_format() {
185
+ let directory = TempDir::new().unwrap();
186
+ let path = directory.path().join("unversioned.log");
187
+ std::fs::write(
188
+ &path,
189
+ format!(
190
+ "{}\n",
191
+ json!({
192
+ "capability": "tasks", "key": "tasks:1",
193
+ "rust_type": "serde_json::value::Value", "value": {"n": 1},
194
+ "unix_ms": 1, "content_hash": null, "flow_ref": null,
195
+ "deleted": false, "operation": null
196
+ })
197
+ ),
198
+ )
199
+ .unwrap();
200
+
201
+ let health = FeltDb::open(&path).unwrap().health();
202
+ assert_eq!(health.durable_format, DurableFormat::Unversioned);
203
+ assert!(health.is_nominal(), "unversioned is not unhealthy");
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // What health deliberately does not say
208
+ // ---------------------------------------------------------------------------
209
+
210
+ /// **Health does not manufacture a guarantee the database does not have.**
211
+ ///
212
+ /// The defect being replaced was `storage: "durable"` — a string constant that
213
+ /// inspected nothing and asserted the one property still unproven. The
214
+ /// replacement is a small closed set of observed conditions, and every label in
215
+ /// it names what was seen rather than what would be reassuring.
216
+ ///
217
+ /// In particular nothing here claims durability against power loss, which
218
+ /// remains unproven; nothing claims the database is replicated, backed up, or
219
+ /// within any capacity; and there is no "probably fine".
220
+ #[test]
221
+ fn health_reports_only_conditions_that_were_observed() {
222
+ let directory = TempDir::new().unwrap();
223
+ let clean = directory.path().join("a.log");
224
+ let torn = directory.path().join("b.log");
225
+ let full = three_writes(&clean);
226
+ std::fs::write(&torn, &full[..full.len() - 30]).unwrap();
227
+
228
+ let labels: Vec<&str> = [&clean, &torn]
229
+ .iter()
230
+ .map(|path| FeltDb::open(path).unwrap().health().storage.label())
231
+ .collect();
232
+
233
+ // Exactly two conditions, both naming an observation.
234
+ assert_eq!(labels, vec!["clean", "recovered-incomplete-write"]);
235
+ for label in &labels {
236
+ assert!(
237
+ !label.contains("durable") && !label.contains("healthy"),
238
+ "'{label}' would assert more than was observed"
239
+ );
240
+ }
241
+ }
242
+
243
+ /// The health value is derived from the recovery the open recorded, not from
244
+ /// anything a caller can set.
245
+ #[test]
246
+ fn health_is_derived_from_the_open_rather_than_configured() {
247
+ let directory = TempDir::new().unwrap();
248
+ let path = directory.path().join("derived.log");
249
+ let full = three_writes(&path);
250
+ std::fs::write(&path, &full[..full.len() - 30]).unwrap();
251
+
252
+ let db = FeltDb::open(&path).unwrap();
253
+ let health: DatabaseHealth = db.health();
254
+
255
+ // The two agree because one is computed from the other.
256
+ match (&health.recovery, &health.storage) {
257
+ (
258
+ LogRecovery::RecoveredTornTail {
259
+ byte_offset: recovered_at,
260
+ discarded_bytes: recovered_bytes,
261
+ },
262
+ StorageHealth::RecoveredIncompleteWrite {
263
+ byte_offset,
264
+ discarded_bytes,
265
+ },
266
+ ) => {
267
+ assert_eq!(recovered_at, byte_offset);
268
+ assert_eq!(recovered_bytes, discarded_bytes);
269
+ }
270
+ other => panic!("recovery and storage disagree: {other:?}"),
271
+ }
272
+
273
+ // Writing more does not change what the open found.
274
+ db.insert("tasks:9", json!({"n": 9})).unwrap();
275
+ assert_eq!(db.health(), health);
276
+ let value: Value = db.get("tasks:9").unwrap().unwrap();
277
+ assert_eq!(value["n"], json!(9), "and the database is fully usable");
278
+ }