@feltdb/core 0.8.4 → 0.8.6
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/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1648 -46
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation.rs +39 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1565 -536
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +9 -3
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
- package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +132 -0
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +272 -0
- package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +467 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +309 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +445 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +518 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +278 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1365 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +417 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +442 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +16 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +695 -47
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +21 -0
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-C1VhI-U5.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8HXbAXb.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-Bbos1m2U.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
//! Durable backup, verification and restore.
|
|
2
|
+
//!
|
|
3
|
+
//! The question this answers is not "can we serialize the database" — the
|
|
4
|
+
//! in-process snapshot already round-tripped. It is:
|
|
5
|
+
//!
|
|
6
|
+
//! > **Can an operator produce a durable backup artifact, verify it
|
|
7
|
+
//! > independently, restore it into a fresh database, and prove that the
|
|
8
|
+
//! > restored database has the same authoritative state and history?**
|
|
9
|
+
//!
|
|
10
|
+
//! The distinction the whole file turns on:
|
|
11
|
+
//!
|
|
12
|
+
//! > **Verification must not merely prove that the file parses. It must prove
|
|
13
|
+
//! > that the resulting database means the same thing.**
|
|
14
|
+
//!
|
|
15
|
+
//! So a backup carries two digests. The content digest proves the bytes are
|
|
16
|
+
//! intact. The **meaning** digest covers what the database *is* — every live
|
|
17
|
+
//! record, the revision graph with its resources, parents, sequences and
|
|
18
|
+
//! authorities, and every retention policy and horizon — and is recomputed from
|
|
19
|
+
//! the *restored database* and compared. A restore that produced a
|
|
20
|
+
//! plausible-but-different database fails there.
|
|
21
|
+
//!
|
|
22
|
+
//! See `docs/architecture/durable-backup.md`.
|
|
23
|
+
|
|
24
|
+
use feltdb::state_model::{ParentLookup, RetentionPolicy, StateStore};
|
|
25
|
+
use feltdb::{verify_backup, FeltDb, FlowError, BACKUP_VERSION, DURABLE_FORMAT_VERSION};
|
|
26
|
+
use serde_json::{json, Value};
|
|
27
|
+
use std::path::Path;
|
|
28
|
+
use std::sync::Arc;
|
|
29
|
+
use tempfile::TempDir;
|
|
30
|
+
|
|
31
|
+
/// A database with everything a backup has to carry: plain state, a bounded
|
|
32
|
+
/// history, an unbounded one, a fork, and expired ancestry.
|
|
33
|
+
fn populated(directory: &Path) -> (Arc<FeltDb>, StateStore) {
|
|
34
|
+
let db = Arc::new(FeltDb::open(directory.join("source.log")).unwrap());
|
|
35
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
36
|
+
|
|
37
|
+
// A resource under retention, with history expired behind a horizon.
|
|
38
|
+
store
|
|
39
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(3))
|
|
40
|
+
.unwrap();
|
|
41
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
42
|
+
for n in 1..12 {
|
|
43
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A resource with unbounded history.
|
|
47
|
+
db.insert("tasks:2", json!({"n": 0})).unwrap();
|
|
48
|
+
for n in 1..4 {
|
|
49
|
+
db.update("tasks:2", json!({ "n": n })).unwrap();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A forked history, committed explicitly.
|
|
53
|
+
let base = store
|
|
54
|
+
.create("docs:1", r#"{"v":0}"#.into(), "alice".into())
|
|
55
|
+
.unwrap();
|
|
56
|
+
store
|
|
57
|
+
.commit(r#"{"v":1}"#.into(), &base, "alice".into())
|
|
58
|
+
.unwrap();
|
|
59
|
+
store
|
|
60
|
+
.commit(r#"{"v":2}"#.into(), &base, "bob".into())
|
|
61
|
+
.unwrap();
|
|
62
|
+
|
|
63
|
+
(db, store)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Everything about a database a backup is supposed to preserve.
|
|
67
|
+
#[derive(Debug, PartialEq)]
|
|
68
|
+
struct Meaning {
|
|
69
|
+
current: Vec<(String, String)>,
|
|
70
|
+
resources: Vec<String>,
|
|
71
|
+
revisions: Vec<(String, String, Option<String>, u64, String, String)>,
|
|
72
|
+
retention: Vec<(String, Option<usize>, u64)>,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn meaning(db: &Arc<FeltDb>, store: &StateStore) -> Meaning {
|
|
76
|
+
let mut current = Vec::new();
|
|
77
|
+
for key in ["tasks:1", "tasks:2"] {
|
|
78
|
+
if let Some(value) = db.get::<Value>(key).unwrap() {
|
|
79
|
+
current.push((key.to_string(), value.to_string()));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let resources = store.resources();
|
|
83
|
+
let mut revisions = Vec::new();
|
|
84
|
+
for resource in &resources {
|
|
85
|
+
for revision in store.history_of(resource) {
|
|
86
|
+
revisions.push((
|
|
87
|
+
revision.resource.clone(),
|
|
88
|
+
revision.id.as_hex().to_string(),
|
|
89
|
+
revision
|
|
90
|
+
.parent_id
|
|
91
|
+
.as_ref()
|
|
92
|
+
.map(|id| id.as_hex().to_string()),
|
|
93
|
+
revision.sequence,
|
|
94
|
+
revision.content_id.as_hex().to_string(),
|
|
95
|
+
revision.authority.clone(),
|
|
96
|
+
));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
revisions.sort();
|
|
100
|
+
let retention = resources
|
|
101
|
+
.iter()
|
|
102
|
+
.map(|resource| {
|
|
103
|
+
(
|
|
104
|
+
resource.clone(),
|
|
105
|
+
store.retention_policy(resource).keep_last,
|
|
106
|
+
store.retention_horizon(resource),
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
.collect();
|
|
110
|
+
Meaning {
|
|
111
|
+
current,
|
|
112
|
+
resources: resources.clone(),
|
|
113
|
+
revisions,
|
|
114
|
+
retention,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn expect_rejected(outcome: Result<impl Sized, FlowError>) -> feltdb::BackupProblem {
|
|
119
|
+
match outcome {
|
|
120
|
+
Err(FlowError::BackupRejected(problem)) => *problem,
|
|
121
|
+
Err(other) => panic!("expected a backup rejection, got {other}"),
|
|
122
|
+
Ok(_) => panic!("the damaged backup was accepted — this is the defect"),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// The artifact
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
/// A backup is a self-contained artifact, and it does not need the database it
|
|
131
|
+
/// came from.
|
|
132
|
+
#[test]
|
|
133
|
+
fn a_backup_is_self_contained_and_outlives_its_source() {
|
|
134
|
+
let directory = TempDir::new().unwrap();
|
|
135
|
+
let (db, store) = populated(directory.path());
|
|
136
|
+
let expected = meaning(&db, &store);
|
|
137
|
+
|
|
138
|
+
let backup = directory.path().join("db.feltbackup");
|
|
139
|
+
let written = db.write_backup(&backup).unwrap();
|
|
140
|
+
assert_eq!(written.backup_version, BACKUP_VERSION);
|
|
141
|
+
assert_eq!(written.format_version, DURABLE_FORMAT_VERSION);
|
|
142
|
+
assert!(written.rows > 0);
|
|
143
|
+
|
|
144
|
+
// The source is gone entirely.
|
|
145
|
+
drop(store);
|
|
146
|
+
drop(db);
|
|
147
|
+
std::fs::remove_file(directory.path().join("source.log")).unwrap();
|
|
148
|
+
|
|
149
|
+
// The artifact still verifies, and still restores.
|
|
150
|
+
let verified = verify_backup(&backup).unwrap();
|
|
151
|
+
assert_eq!(verified, written);
|
|
152
|
+
|
|
153
|
+
let target = directory.path().join("restored.log");
|
|
154
|
+
let (restored, verification) = FeltDb::restore_backup(&backup, &target).unwrap();
|
|
155
|
+
assert_eq!(verification.meaning_digest, written.meaning_digest);
|
|
156
|
+
|
|
157
|
+
let restored = Arc::new(restored);
|
|
158
|
+
let restored_store = StateStore::with_feltdb(restored.clone()).unwrap();
|
|
159
|
+
assert_eq!(meaning(&restored, &restored_store), expected);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Everything the model was built to hold survives a round trip: resource
|
|
163
|
+
/// identity, revision identity, ancestry, forks, retention and provenance.
|
|
164
|
+
#[test]
|
|
165
|
+
fn a_restore_preserves_state_history_forks_retention_and_provenance() {
|
|
166
|
+
let directory = TempDir::new().unwrap();
|
|
167
|
+
let (db, store) = populated(directory.path());
|
|
168
|
+
let backup = directory.path().join("db.feltbackup");
|
|
169
|
+
db.write_backup(&backup).unwrap();
|
|
170
|
+
|
|
171
|
+
let target = directory.path().join("restored.log");
|
|
172
|
+
let (restored, _) = FeltDb::restore_backup(&backup, &target).unwrap();
|
|
173
|
+
let restored = Arc::new(restored);
|
|
174
|
+
let restored_store = StateStore::with_feltdb(restored.clone()).unwrap();
|
|
175
|
+
|
|
176
|
+
// Current state.
|
|
177
|
+
let current: Value = restored.get("tasks:1").unwrap().unwrap();
|
|
178
|
+
assert_eq!(current["n"], json!(11));
|
|
179
|
+
|
|
180
|
+
// Retention state, including the horizon.
|
|
181
|
+
assert_eq!(
|
|
182
|
+
restored_store.retention_policy("tasks:1"),
|
|
183
|
+
RetentionPolicy::keep_last(3)
|
|
184
|
+
);
|
|
185
|
+
assert_eq!(
|
|
186
|
+
restored_store.retention_horizon("tasks:1"),
|
|
187
|
+
store.retention_horizon("tasks:1")
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// The bounded history is exactly what it was, expiry included.
|
|
191
|
+
let before = store.history_of("tasks:1");
|
|
192
|
+
let after = restored_store.history_of("tasks:1");
|
|
193
|
+
assert_eq!(after.len(), 3);
|
|
194
|
+
assert_eq!(
|
|
195
|
+
after.iter().map(|r| r.id.clone()).collect::<Vec<_>>(),
|
|
196
|
+
before.iter().map(|r| r.id.clone()).collect::<Vec<_>>(),
|
|
197
|
+
"revision identities are preserved, not recomputed"
|
|
198
|
+
);
|
|
199
|
+
assert_eq!(
|
|
200
|
+
after[0].sequence, before[0].sequence,
|
|
201
|
+
"sequences survive, so history did not restart"
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Ancestry, and the expiry boundary that goes with it.
|
|
205
|
+
assert!(matches!(
|
|
206
|
+
restored_store.parent_of(&after[0].id),
|
|
207
|
+
Some(ParentLookup::Expired(_))
|
|
208
|
+
));
|
|
209
|
+
assert!(matches!(
|
|
210
|
+
restored_store.parent_of(&after[1].id),
|
|
211
|
+
Some(ParentLookup::Revision(_))
|
|
212
|
+
));
|
|
213
|
+
|
|
214
|
+
// The fork, both heads and their shared base.
|
|
215
|
+
let docs = restored_store.history_of("docs:1");
|
|
216
|
+
assert_eq!(docs.len(), 3, "base plus two heads");
|
|
217
|
+
let heads: Vec<_> = docs.iter().filter(|r| r.sequence == 1).collect();
|
|
218
|
+
assert_eq!(heads.len(), 2, "the fork survived");
|
|
219
|
+
assert_eq!(heads[0].parent_id, heads[1].parent_id);
|
|
220
|
+
|
|
221
|
+
// Authority provenance.
|
|
222
|
+
let mut authorities: Vec<&str> = heads.iter().map(|r| r.authority.as_str()).collect();
|
|
223
|
+
authorities.sort();
|
|
224
|
+
assert_eq!(authorities, vec!["alice", "bob"]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// A restored database is a live database: it continues history rather than
|
|
228
|
+
/// restarting it.
|
|
229
|
+
#[test]
|
|
230
|
+
fn a_restored_database_continues_history() {
|
|
231
|
+
let directory = TempDir::new().unwrap();
|
|
232
|
+
let (db, store) = populated(directory.path());
|
|
233
|
+
let head_before = store.head_of("tasks:2").unwrap();
|
|
234
|
+
let backup = directory.path().join("db.feltbackup");
|
|
235
|
+
db.write_backup(&backup).unwrap();
|
|
236
|
+
|
|
237
|
+
let target = directory.path().join("restored.log");
|
|
238
|
+
let (restored, _) = FeltDb::restore_backup(&backup, &target).unwrap();
|
|
239
|
+
let restored = Arc::new(restored);
|
|
240
|
+
restored.update("tasks:2", json!({"n": 99})).unwrap();
|
|
241
|
+
|
|
242
|
+
let restored_store = StateStore::with_feltdb(restored.clone()).unwrap();
|
|
243
|
+
let head_after = restored_store.head_of("tasks:2").unwrap();
|
|
244
|
+
assert_eq!(head_after.sequence, head_before.sequence + 1);
|
|
245
|
+
assert_eq!(head_after.parent_id.as_ref(), Some(&head_before.id));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Verification, without a live database
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/// Verification reads the artifact and never opens or writes a database.
|
|
253
|
+
#[test]
|
|
254
|
+
fn verification_inspects_the_artifact_without_mutating_it() {
|
|
255
|
+
let directory = TempDir::new().unwrap();
|
|
256
|
+
let (db, _store) = populated(directory.path());
|
|
257
|
+
let backup = directory.path().join("db.feltbackup");
|
|
258
|
+
let written = db.write_backup(&backup).unwrap();
|
|
259
|
+
|
|
260
|
+
let before = std::fs::read(&backup).unwrap();
|
|
261
|
+
let verified = verify_backup(&backup).unwrap();
|
|
262
|
+
assert_eq!(verified.meaning_digest, written.meaning_digest);
|
|
263
|
+
assert_eq!(
|
|
264
|
+
std::fs::read(&backup).unwrap(),
|
|
265
|
+
before,
|
|
266
|
+
"verifying must not modify the artifact"
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// And nothing was restored as a side effect.
|
|
270
|
+
assert!(!directory.path().join("restored.log").exists());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// **The negative control.** One altered byte must refuse, not restore a
|
|
274
|
+
/// plausible-but-wrong database.
|
|
275
|
+
#[test]
|
|
276
|
+
fn a_single_altered_byte_is_refused() {
|
|
277
|
+
let directory = TempDir::new().unwrap();
|
|
278
|
+
let (db, _store) = populated(directory.path());
|
|
279
|
+
let backup = directory.path().join("db.feltbackup");
|
|
280
|
+
db.write_backup(&backup).unwrap();
|
|
281
|
+
|
|
282
|
+
// Change one digit inside a stored value: still valid JSON, still a valid
|
|
283
|
+
// record, and a different database.
|
|
284
|
+
let text = std::fs::read_to_string(&backup).unwrap();
|
|
285
|
+
let altered = text.replacen(r#""n":11"#, r#""n":12"#, 1);
|
|
286
|
+
assert_ne!(altered, text, "the fixture must actually change something");
|
|
287
|
+
std::fs::write(&backup, &altered).unwrap();
|
|
288
|
+
|
|
289
|
+
let problem = expect_rejected(verify_backup(&backup));
|
|
290
|
+
assert!(
|
|
291
|
+
problem.reason.contains("digest") || problem.reason.contains("mean"),
|
|
292
|
+
"rejected on meaning, not on syntax: {}",
|
|
293
|
+
problem.reason
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
let target = directory.path().join("restored.log");
|
|
297
|
+
expect_rejected(FeltDb::restore_backup(&backup, &target));
|
|
298
|
+
assert!(
|
|
299
|
+
!target.exists(),
|
|
300
|
+
"a refused restore leaves no half-built database"
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// A truncated artifact is refused, at the header and in the body.
|
|
305
|
+
#[test]
|
|
306
|
+
fn a_truncated_backup_is_refused() {
|
|
307
|
+
let directory = TempDir::new().unwrap();
|
|
308
|
+
let (db, _store) = populated(directory.path());
|
|
309
|
+
let backup = directory.path().join("db.feltbackup");
|
|
310
|
+
db.write_backup(&backup).unwrap();
|
|
311
|
+
let full = std::fs::read_to_string(&backup).unwrap();
|
|
312
|
+
|
|
313
|
+
// Body cut short: the header's record count no longer matches.
|
|
314
|
+
let mut lines: Vec<&str> = full.lines().collect();
|
|
315
|
+
lines.truncate(lines.len() - 2);
|
|
316
|
+
std::fs::write(&backup, lines.join("\n")).unwrap();
|
|
317
|
+
let problem = expect_rejected(verify_backup(&backup));
|
|
318
|
+
assert!(
|
|
319
|
+
problem.reason.contains("declares"),
|
|
320
|
+
"the count mismatch is what catches it: {}",
|
|
321
|
+
problem.reason
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// Header cut short.
|
|
325
|
+
std::fs::write(&backup, &full[..40]).unwrap();
|
|
326
|
+
let problem = expect_rejected(verify_backup(&backup));
|
|
327
|
+
assert_eq!(problem.line_number, Some(1));
|
|
328
|
+
|
|
329
|
+
// Nothing at all.
|
|
330
|
+
std::fs::write(&backup, "").unwrap();
|
|
331
|
+
let problem = expect_rejected(verify_backup(&backup));
|
|
332
|
+
assert!(problem.reason.contains("empty"));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// A malformed record inside the artifact is refused, and located.
|
|
336
|
+
#[test]
|
|
337
|
+
fn a_malformed_record_in_a_backup_is_refused() {
|
|
338
|
+
let directory = TempDir::new().unwrap();
|
|
339
|
+
let (db, _store) = populated(directory.path());
|
|
340
|
+
let backup = directory.path().join("db.feltbackup");
|
|
341
|
+
db.write_backup(&backup).unwrap();
|
|
342
|
+
|
|
343
|
+
let mut lines: Vec<String> = std::fs::read_to_string(&backup)
|
|
344
|
+
.unwrap()
|
|
345
|
+
.lines()
|
|
346
|
+
.map(str::to_string)
|
|
347
|
+
.collect();
|
|
348
|
+
lines[2] = "{not valid json}".to_string();
|
|
349
|
+
std::fs::write(&backup, lines.join("\n")).unwrap();
|
|
350
|
+
|
|
351
|
+
let problem = expect_rejected(verify_backup(&backup));
|
|
352
|
+
assert_eq!(problem.line_number, Some(3));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// A backup from another backup version, or another durable format, is refused.
|
|
356
|
+
#[test]
|
|
357
|
+
fn an_incompatible_backup_version_is_refused() {
|
|
358
|
+
let directory = TempDir::new().unwrap();
|
|
359
|
+
let (db, _store) = populated(directory.path());
|
|
360
|
+
let backup = directory.path().join("db.feltbackup");
|
|
361
|
+
db.write_backup(&backup).unwrap();
|
|
362
|
+
let full = std::fs::read_to_string(&backup).unwrap();
|
|
363
|
+
|
|
364
|
+
for (field, replacement) in [
|
|
365
|
+
("\"backup_version\":1", "\"backup_version\":2"),
|
|
366
|
+
(
|
|
367
|
+
&format!("\"format_version\":{DURABLE_FORMAT_VERSION}"),
|
|
368
|
+
&format!("\"format_version\":{}", DURABLE_FORMAT_VERSION + 1),
|
|
369
|
+
),
|
|
370
|
+
] {
|
|
371
|
+
let altered = full.replacen(field, replacement, 1);
|
|
372
|
+
assert_ne!(altered, full, "fixture must change {field}");
|
|
373
|
+
std::fs::write(&backup, &altered).unwrap();
|
|
374
|
+
let problem = expect_rejected(verify_backup(&backup));
|
|
375
|
+
assert_eq!(problem.line_number, Some(1));
|
|
376
|
+
assert!(problem.reason.contains("version"));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/// A file that is not a FeltDB backup is refused rather than guessed at.
|
|
381
|
+
#[test]
|
|
382
|
+
fn a_foreign_file_is_not_mistaken_for_a_backup() {
|
|
383
|
+
let directory = TempDir::new().unwrap();
|
|
384
|
+
let backup = directory.path().join("not-a-backup.feltbackup");
|
|
385
|
+
std::fs::write(&backup, "{\"hello\":\"world\"}\n").unwrap();
|
|
386
|
+
let problem = expect_rejected(verify_backup(&backup));
|
|
387
|
+
assert_eq!(problem.line_number, Some(1));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Restore safety
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
/// Restore refuses to write over an existing database.
|
|
395
|
+
///
|
|
396
|
+
/// A restore that could overwrite the thing an operator is trying to recover is
|
|
397
|
+
/// not a recovery tool.
|
|
398
|
+
#[test]
|
|
399
|
+
fn a_restore_will_not_overwrite_an_existing_database() {
|
|
400
|
+
let directory = TempDir::new().unwrap();
|
|
401
|
+
let (db, _store) = populated(directory.path());
|
|
402
|
+
let backup = directory.path().join("db.feltbackup");
|
|
403
|
+
db.write_backup(&backup).unwrap();
|
|
404
|
+
|
|
405
|
+
let occupied = directory.path().join("occupied.log");
|
|
406
|
+
let existing = FeltDb::open(&occupied).unwrap();
|
|
407
|
+
existing
|
|
408
|
+
.insert("tasks:9", json!({"important": true}))
|
|
409
|
+
.unwrap();
|
|
410
|
+
let before = std::fs::read(&occupied).unwrap();
|
|
411
|
+
drop(existing);
|
|
412
|
+
|
|
413
|
+
let problem = expect_rejected(FeltDb::restore_backup(&backup, &occupied));
|
|
414
|
+
assert!(problem.reason.contains("already exists"));
|
|
415
|
+
assert_eq!(
|
|
416
|
+
std::fs::read(&occupied).unwrap(),
|
|
417
|
+
before,
|
|
418
|
+
"the existing database was not touched"
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// A restored database is itself backup-able, and the second backup means the
|
|
423
|
+
/// same as the first.
|
|
424
|
+
///
|
|
425
|
+
/// This is the property that makes a backup chain trustworthy: restoring and
|
|
426
|
+
/// re-backing up is not lossy.
|
|
427
|
+
#[test]
|
|
428
|
+
fn a_backup_of_a_restore_means_the_same_thing() {
|
|
429
|
+
let directory = TempDir::new().unwrap();
|
|
430
|
+
let (db, _store) = populated(directory.path());
|
|
431
|
+
let first = directory.path().join("first.feltbackup");
|
|
432
|
+
let original = db.write_backup(&first).unwrap();
|
|
433
|
+
|
|
434
|
+
let target = directory.path().join("restored.log");
|
|
435
|
+
let (restored, _) = FeltDb::restore_backup(&first, &target).unwrap();
|
|
436
|
+
|
|
437
|
+
let second = directory.path().join("second.feltbackup");
|
|
438
|
+
let again = restored.write_backup(&second).unwrap();
|
|
439
|
+
|
|
440
|
+
assert_eq!(
|
|
441
|
+
again.meaning_digest, original.meaning_digest,
|
|
442
|
+
"a round trip does not change what the database means"
|
|
443
|
+
);
|
|
444
|
+
assert_eq!(again.rows, original.rows);
|
|
445
|
+
}
|