@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,392 @@
|
|
|
1
|
+
//! Durable format versioning: FeltDB must fail closed when the durable meaning
|
|
2
|
+
//! of its own data is unknown.
|
|
3
|
+
//!
|
|
4
|
+
//! The production invariant this file exists to enforce:
|
|
5
|
+
//!
|
|
6
|
+
//! > **A durable database must never silently discard persisted information
|
|
7
|
+
//! > because the running FeltDB understands a different durable format.**
|
|
8
|
+
//!
|
|
9
|
+
//! Before this, a database written before revisions became resource-scoped
|
|
10
|
+
//! opened cleanly and reported no history at all. The records were still on
|
|
11
|
+
//! disk; every path that read them deserialized into the current shape and
|
|
12
|
+
//! discarded what failed. The database looked healthy and had lost its history
|
|
13
|
+
//! from the application's point of view. That is failing *open*.
|
|
14
|
+
//!
|
|
15
|
+
//! See `docs/architecture/durable-format-versioning.md`.
|
|
16
|
+
|
|
17
|
+
use feltdb::state_model::StateStore;
|
|
18
|
+
use feltdb::{
|
|
19
|
+
inspect_durable_format, DurableFormat, FeltDb, FlowError, FormatCompatibility,
|
|
20
|
+
DURABLE_FORMAT_VERSION,
|
|
21
|
+
};
|
|
22
|
+
use serde_json::{json, Value};
|
|
23
|
+
use std::io::Write;
|
|
24
|
+
use std::path::Path;
|
|
25
|
+
use std::sync::Arc;
|
|
26
|
+
use tempfile::TempDir;
|
|
27
|
+
|
|
28
|
+
/// Write a durable log by hand, one JSON record per line.
|
|
29
|
+
fn write_log(path: &Path, records: &[Value]) {
|
|
30
|
+
let mut file = std::fs::File::create(path).unwrap();
|
|
31
|
+
for record in records {
|
|
32
|
+
writeln!(file, "{}", serde_json::to_string(record).unwrap()).unwrap();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// A data row as the log stores it.
|
|
37
|
+
fn data_row(key: &str, value: Value) -> Value {
|
|
38
|
+
let capability = key.split_once(':').map(|(c, _)| c).unwrap_or("default");
|
|
39
|
+
json!({
|
|
40
|
+
"capability": capability,
|
|
41
|
+
"key": key,
|
|
42
|
+
"rust_type": "serde_json::value::Value",
|
|
43
|
+
"value": value,
|
|
44
|
+
"unix_ms": 1,
|
|
45
|
+
"content_hash": null,
|
|
46
|
+
"flow_ref": null,
|
|
47
|
+
"deleted": false,
|
|
48
|
+
"operation": null
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// A revision record in the pre-resource-scoped shape: content-only identity,
|
|
53
|
+
/// no resource, no sequence.
|
|
54
|
+
fn legacy_revision(id: &str, content: &str, parent: Option<&str>) -> Value {
|
|
55
|
+
json!({
|
|
56
|
+
"capability": "state",
|
|
57
|
+
"key": format!("state:revision:{id}"),
|
|
58
|
+
"rust_type": "feltdb::state_model::StateRevision",
|
|
59
|
+
"value": {
|
|
60
|
+
"id": id,
|
|
61
|
+
"content": content,
|
|
62
|
+
"parent_id": parent,
|
|
63
|
+
"authority": "before-the-model",
|
|
64
|
+
"timestamp_ms": 0,
|
|
65
|
+
"metadata": {}
|
|
66
|
+
},
|
|
67
|
+
"unix_ms": 1,
|
|
68
|
+
"content_hash": null,
|
|
69
|
+
"flow_ref": null,
|
|
70
|
+
"deleted": false,
|
|
71
|
+
"operation": null
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// A real legacy database: application state **and** revision history.
|
|
76
|
+
fn legacy_database(path: &Path) {
|
|
77
|
+
write_log(
|
|
78
|
+
path,
|
|
79
|
+
&[
|
|
80
|
+
data_row("tasks:1", json!({"title": "B"})),
|
|
81
|
+
data_row("tasks:2", json!({"title": "C"})),
|
|
82
|
+
legacy_revision("aaa", r#"{"title":"A"}"#, None),
|
|
83
|
+
legacy_revision("bbb", r#"{"title":"B"}"#, Some("aaa")),
|
|
84
|
+
],
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// The regression that matters
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/// **The #307 U2 failure, encoded.**
|
|
93
|
+
///
|
|
94
|
+
/// A database written before revisions belonged to a resource must not open as
|
|
95
|
+
/// though it were healthy and empty of history.
|
|
96
|
+
///
|
|
97
|
+
/// This is the most important test in the change; the implementation details
|
|
98
|
+
/// are secondary to it.
|
|
99
|
+
#[test]
|
|
100
|
+
fn pre_306_database_does_not_open_as_empty_history() {
|
|
101
|
+
let directory = TempDir::new().unwrap();
|
|
102
|
+
let path = directory.path().join("legacy.log");
|
|
103
|
+
legacy_database(&path);
|
|
104
|
+
let before = std::fs::read(&path).unwrap();
|
|
105
|
+
|
|
106
|
+
// 1. An explicit compatibility result, from a read-only examination.
|
|
107
|
+
let compatibility = inspect_durable_format(&path).unwrap();
|
|
108
|
+
let FormatCompatibility::Incompatible(incompatibility) = compatibility else {
|
|
109
|
+
panic!("the legacy format must be reported incompatible, got {compatibility:?}");
|
|
110
|
+
};
|
|
111
|
+
assert_eq!(incompatibility.found, DurableFormat::Unversioned);
|
|
112
|
+
assert_eq!(incompatibility.required, DURABLE_FORMAT_VERSION);
|
|
113
|
+
assert!(
|
|
114
|
+
incompatibility.reason.contains("resource"),
|
|
115
|
+
"the reason names the actual problem: {}",
|
|
116
|
+
incompatibility.reason
|
|
117
|
+
);
|
|
118
|
+
assert!(
|
|
119
|
+
!incompatibility.action.is_empty(),
|
|
120
|
+
"the operator is told what to do"
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// 2. The open is refused, rather than succeeding with nothing to show.
|
|
124
|
+
match FeltDb::open(&path) {
|
|
125
|
+
Err(FlowError::IncompatibleFormat(reported)) => {
|
|
126
|
+
assert_eq!(*reported, incompatibility, "open agrees with the probe");
|
|
127
|
+
let rendered = FlowError::IncompatibleFormat(reported).to_string();
|
|
128
|
+
assert!(rendered.contains("INCOMPATIBLE_DURABLE_FORMAT"));
|
|
129
|
+
assert!(rendered.contains("No data was read or modified"));
|
|
130
|
+
}
|
|
131
|
+
Err(other) => panic!("wrong error: {other}"),
|
|
132
|
+
Ok(_) => panic!("the legacy database opened — this is the defect"),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 3. Nothing was touched.
|
|
136
|
+
assert_eq!(
|
|
137
|
+
std::fs::read(&path).unwrap(),
|
|
138
|
+
before,
|
|
139
|
+
"a refused open must not modify a single byte"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Lossless migration is refused rather than attempted, and the reason is a
|
|
144
|
+
/// property of the data rather than a shortage of effort.
|
|
145
|
+
///
|
|
146
|
+
/// A legacy revision recorded `id`, `content`, `parent_id`, `authority`,
|
|
147
|
+
/// `timestamp_ms` and `metadata`. It did **not** record which resource it was a
|
|
148
|
+
/// revision of — that absence is the finding the resource-scoped model was
|
|
149
|
+
/// adopted to fix. So which history each legacy revision belongs to cannot be
|
|
150
|
+
/// recovered from the data, and any migration would have to invent it.
|
|
151
|
+
///
|
|
152
|
+
/// Inventing it would produce a database that looks migrated and is wrong,
|
|
153
|
+
/// which is worse than one that refuses.
|
|
154
|
+
#[test]
|
|
155
|
+
fn migration_is_refused_because_the_resource_was_never_recorded() {
|
|
156
|
+
let directory = TempDir::new().unwrap();
|
|
157
|
+
let path = directory.path().join("legacy.log");
|
|
158
|
+
legacy_database(&path);
|
|
159
|
+
|
|
160
|
+
let FormatCompatibility::Incompatible(incompatibility) = inspect_durable_format(&path).unwrap()
|
|
161
|
+
else {
|
|
162
|
+
panic!("expected incompatible");
|
|
163
|
+
};
|
|
164
|
+
assert!(
|
|
165
|
+
incompatibility
|
|
166
|
+
.reason
|
|
167
|
+
.contains("cannot be migrated without inventing information"),
|
|
168
|
+
"the refusal explains why migration is not merely unimplemented: {}",
|
|
169
|
+
incompatibility.reason
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// The legacy records are still readable as bytes by anything that
|
|
173
|
+
// understands them — nothing was deleted in the name of tidiness.
|
|
174
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
175
|
+
assert!(text.contains("state:revision:aaa"));
|
|
176
|
+
assert!(text.contains("state:revision:bbb"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Symmetry: no build silently interprets a format it does not understand
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/// **Downgrade protection.** A database from a newer FeltDB is refused.
|
|
184
|
+
///
|
|
185
|
+
/// The invariant is symmetric, and this is the half a rollback hits: an
|
|
186
|
+
/// operator reverting a deployment must not have the older build reinterpret
|
|
187
|
+
/// data written by the newer one.
|
|
188
|
+
#[test]
|
|
189
|
+
fn a_newer_durable_format_is_refused() {
|
|
190
|
+
let directory = TempDir::new().unwrap();
|
|
191
|
+
let path = directory.path().join("future.log");
|
|
192
|
+
write_log(
|
|
193
|
+
&path,
|
|
194
|
+
&[
|
|
195
|
+
json!({"record_type": "feltdb.format.v1", "format_version": DURABLE_FORMAT_VERSION + 1}),
|
|
196
|
+
data_row("tasks:1", json!({"title": "from the future"})),
|
|
197
|
+
],
|
|
198
|
+
);
|
|
199
|
+
let before = std::fs::read(&path).unwrap();
|
|
200
|
+
|
|
201
|
+
let FormatCompatibility::Incompatible(incompatibility) = inspect_durable_format(&path).unwrap()
|
|
202
|
+
else {
|
|
203
|
+
panic!("a newer format must be refused");
|
|
204
|
+
};
|
|
205
|
+
assert_eq!(
|
|
206
|
+
incompatibility.found,
|
|
207
|
+
DurableFormat::Versioned(DURABLE_FORMAT_VERSION + 1)
|
|
208
|
+
);
|
|
209
|
+
assert!(incompatibility.reason.contains("newer"));
|
|
210
|
+
|
|
211
|
+
assert!(matches!(
|
|
212
|
+
FeltDb::open(&path),
|
|
213
|
+
Err(FlowError::IncompatibleFormat(_))
|
|
214
|
+
));
|
|
215
|
+
assert_eq!(std::fs::read(&path).unwrap(), before, "untouched");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// An older declared format is refused too, so the check is a version
|
|
219
|
+
/// comparison rather than a special case for one legacy shape.
|
|
220
|
+
#[test]
|
|
221
|
+
fn an_older_declared_format_is_refused() {
|
|
222
|
+
let directory = TempDir::new().unwrap();
|
|
223
|
+
let path = directory.path().join("old.log");
|
|
224
|
+
write_log(
|
|
225
|
+
&path,
|
|
226
|
+
&[json!({"record_type": "feltdb.format.v1", "format_version": 1})],
|
|
227
|
+
);
|
|
228
|
+
let FormatCompatibility::Incompatible(incompatibility) = inspect_durable_format(&path).unwrap()
|
|
229
|
+
else {
|
|
230
|
+
panic!("an older format must be refused");
|
|
231
|
+
};
|
|
232
|
+
assert_eq!(incompatibility.found, DurableFormat::Versioned(1));
|
|
233
|
+
assert!(matches!(
|
|
234
|
+
FeltDb::open(&path),
|
|
235
|
+
Err(FlowError::IncompatibleFormat(_))
|
|
236
|
+
));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// What must keep working
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/// A new database states its format, and reopening it is compatible.
|
|
244
|
+
#[test]
|
|
245
|
+
fn a_new_database_declares_its_format() {
|
|
246
|
+
let directory = TempDir::new().unwrap();
|
|
247
|
+
let path = directory.path().join("new.log");
|
|
248
|
+
let db = FeltDb::open(&path).unwrap();
|
|
249
|
+
db.insert("tasks:1", json!({"n": 1})).unwrap();
|
|
250
|
+
|
|
251
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
252
|
+
let first = text.lines().next().unwrap();
|
|
253
|
+
let record: Value = serde_json::from_str(first).unwrap();
|
|
254
|
+
assert_eq!(record["record_type"], json!("feltdb.format.v1"));
|
|
255
|
+
assert_eq!(record["format_version"], json!(DURABLE_FORMAT_VERSION));
|
|
256
|
+
|
|
257
|
+
drop(db);
|
|
258
|
+
assert_eq!(
|
|
259
|
+
inspect_durable_format(&path).unwrap(),
|
|
260
|
+
FormatCompatibility::Compatible(DurableFormat::Versioned(DURABLE_FORMAT_VERSION))
|
|
261
|
+
);
|
|
262
|
+
let reopened = FeltDb::open(&path).unwrap();
|
|
263
|
+
let value: Value = reopened.get("tasks:1").unwrap().unwrap();
|
|
264
|
+
assert_eq!(value["n"], json!(1));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// A database that predates format versioning but holds nothing whose meaning
|
|
268
|
+
/// changed opens normally.
|
|
269
|
+
///
|
|
270
|
+
/// The refusal is targeted at data whose interpretation actually differs, not
|
|
271
|
+
/// at every database that lacks a version record. Refusing those would make the
|
|
272
|
+
/// safety check an upgrade wall.
|
|
273
|
+
#[test]
|
|
274
|
+
fn an_unversioned_database_without_legacy_revisions_still_opens() {
|
|
275
|
+
let directory = TempDir::new().unwrap();
|
|
276
|
+
let path = directory.path().join("plain.log");
|
|
277
|
+
write_log(
|
|
278
|
+
&path,
|
|
279
|
+
&[
|
|
280
|
+
data_row("tasks:1", json!({"title": "A"})),
|
|
281
|
+
data_row("tasks:2", json!({"title": "B"})),
|
|
282
|
+
],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
assert_eq!(
|
|
286
|
+
inspect_durable_format(&path).unwrap(),
|
|
287
|
+
FormatCompatibility::Compatible(DurableFormat::Unversioned)
|
|
288
|
+
);
|
|
289
|
+
let db = FeltDb::open(&path).unwrap();
|
|
290
|
+
let value: Value = db.get("tasks:1").unwrap().unwrap();
|
|
291
|
+
assert_eq!(value["title"], json!("A"));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// Compaction rewrites the log with the format stated in its header, so a
|
|
295
|
+
/// compacted database is self-describing too.
|
|
296
|
+
#[test]
|
|
297
|
+
fn a_compacted_log_declares_its_format() {
|
|
298
|
+
let directory = TempDir::new().unwrap();
|
|
299
|
+
let path = directory.path().join("compacted.log");
|
|
300
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
301
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
302
|
+
db.update("tasks:1", json!({"n": 1})).unwrap();
|
|
303
|
+
|
|
304
|
+
let versions = db.operation_versions().unwrap();
|
|
305
|
+
db.acknowledge_peer_versions("peer-1".to_string(), versions)
|
|
306
|
+
.unwrap();
|
|
307
|
+
db.compact_operation_log(&["peer-1".to_string()]).unwrap();
|
|
308
|
+
drop(db);
|
|
309
|
+
|
|
310
|
+
assert_eq!(
|
|
311
|
+
inspect_durable_format(&path).unwrap(),
|
|
312
|
+
FormatCompatibility::Compatible(DurableFormat::Versioned(DURABLE_FORMAT_VERSION))
|
|
313
|
+
);
|
|
314
|
+
let reopened = Arc::new(FeltDb::open(&path).unwrap());
|
|
315
|
+
let store = StateStore::with_feltdb(reopened.clone()).unwrap();
|
|
316
|
+
assert_eq!(
|
|
317
|
+
store.history_of("tasks:1").len(),
|
|
318
|
+
2,
|
|
319
|
+
"history survived compaction and reopen"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Snapshots
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
/// A snapshot carries its own format, and one from another format is refused.
|
|
328
|
+
///
|
|
329
|
+
/// Snapshots are durable interchange artifacts, not merely protocol values:
|
|
330
|
+
/// they carry whole rows, revisions included. Leaving that path unversioned
|
|
331
|
+
/// would have reopened the same hole the on-disk version closes.
|
|
332
|
+
#[test]
|
|
333
|
+
fn a_snapshot_from_another_format_is_refused() {
|
|
334
|
+
let directory = TempDir::new().unwrap();
|
|
335
|
+
let source = Arc::new(FeltDb::open(directory.path().join("source.log")).unwrap());
|
|
336
|
+
source.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
337
|
+
source.update("tasks:1", json!({"n": 1})).unwrap();
|
|
338
|
+
|
|
339
|
+
let snapshot = source.export_snapshot().unwrap();
|
|
340
|
+
assert_eq!(snapshot.format_version, DURABLE_FORMAT_VERSION);
|
|
341
|
+
|
|
342
|
+
// A matching snapshot installs, history included.
|
|
343
|
+
let target = Arc::new(FeltDb::open(directory.path().join("target.log")).unwrap());
|
|
344
|
+
target.install_snapshot(snapshot.clone()).unwrap();
|
|
345
|
+
let store = StateStore::with_feltdb(target.clone()).unwrap();
|
|
346
|
+
assert_eq!(store.history_of("tasks:1").len(), 2);
|
|
347
|
+
|
|
348
|
+
// One claiming another format does not.
|
|
349
|
+
let mut foreign = snapshot.clone();
|
|
350
|
+
foreign.format_version = DURABLE_FORMAT_VERSION + 1;
|
|
351
|
+
let other = Arc::new(FeltDb::open(directory.path().join("other.log")).unwrap());
|
|
352
|
+
assert!(matches!(
|
|
353
|
+
other.install_snapshot(foreign),
|
|
354
|
+
Err(FlowError::IncompatibleFormat(_))
|
|
355
|
+
));
|
|
356
|
+
|
|
357
|
+
// And neither does one serialized before snapshots carried a version.
|
|
358
|
+
let mut unversioned = snapshot;
|
|
359
|
+
unversioned.format_version = 0;
|
|
360
|
+
assert!(matches!(
|
|
361
|
+
other.install_snapshot(unversioned),
|
|
362
|
+
Err(FlowError::IncompatibleFormat(_))
|
|
363
|
+
));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// The server boundary
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
/// An incompatible database cannot reach a running server, so it cannot be
|
|
371
|
+
/// advertised as healthy.
|
|
372
|
+
///
|
|
373
|
+
/// This is the one health interaction in scope: `/health` is not redesigned
|
|
374
|
+
/// here, and does not need to be, because the database never opens. The check
|
|
375
|
+
/// is that the failure is a returned error a caller must handle — not a panic,
|
|
376
|
+
/// and not a degraded-but-running database.
|
|
377
|
+
#[test]
|
|
378
|
+
fn an_incompatible_database_cannot_be_opened_by_a_service() {
|
|
379
|
+
let directory = TempDir::new().unwrap();
|
|
380
|
+
let path = directory.path().join("legacy.log");
|
|
381
|
+
legacy_database(&path);
|
|
382
|
+
|
|
383
|
+
let outcome = FeltDb::open(&path);
|
|
384
|
+
assert!(
|
|
385
|
+
outcome.is_err(),
|
|
386
|
+
"startup fails closed rather than serving an empty history"
|
|
387
|
+
);
|
|
388
|
+
// The error is actionable enough to put in a startup log.
|
|
389
|
+
let message = outcome.err().unwrap().to_string();
|
|
390
|
+
assert!(message.contains("INCOMPATIBLE_DURABLE_FORMAT"));
|
|
391
|
+
assert!(message.contains("format version"));
|
|
392
|
+
}
|