@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,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
|
+
}
|
|
@@ -166,7 +166,8 @@ fn a_scan_result_survives_concurrent_mutation() {
|
|
|
166
166
|
// The scan returns owned records, so mutation after the lock is released
|
|
167
167
|
// cannot invalidate or rewrite what the caller already holds.
|
|
168
168
|
for index in 0..200 {
|
|
169
|
-
db.delete(&format!("items:record-{index:05}"))
|
|
169
|
+
db.delete(&format!("items:record-{index:05}"))
|
|
170
|
+
.expect("delete");
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
assert_eq!(matches.len(), 50);
|
|
@@ -41,7 +41,8 @@ fn database() -> (TempDir, FeltDb) {
|
|
|
41
41
|
/// A database with `status` and `tenantId` indexed on `items`.
|
|
42
42
|
fn indexed() -> (TempDir, FeltDb) {
|
|
43
43
|
let (directory, db) = database();
|
|
44
|
-
db.create_equality_index("items", "status")
|
|
44
|
+
db.create_equality_index("items", "status")
|
|
45
|
+
.expect("declare");
|
|
45
46
|
db.create_equality_index("items", "tenantId")
|
|
46
47
|
.expect("declare");
|
|
47
48
|
(directory, db)
|
|
@@ -53,7 +54,9 @@ fn indexed() -> (TempDir, FeltDb) {
|
|
|
53
54
|
fn assert_index_matches_state(db: &FeltDb, after: &str) {
|
|
54
55
|
match db.verify_equality_index().expect("verify") {
|
|
55
56
|
Ok(()) => {}
|
|
56
|
-
Err(divergence) =>
|
|
57
|
+
Err(divergence) => {
|
|
58
|
+
panic!("index diverged from authoritative state after {after}: {divergence}")
|
|
59
|
+
}
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -67,7 +70,11 @@ fn ids(db: &FeltDb, field: &str, value: Value) -> Vec<String> {
|
|
|
67
70
|
candidates(db, field, value)
|
|
68
71
|
.expect("the field is indexed and the value is indexable")
|
|
69
72
|
.into_iter()
|
|
70
|
-
.map(|key|
|
|
73
|
+
.map(|key| {
|
|
74
|
+
key.split_once(':')
|
|
75
|
+
.map(|(_, id)| id.to_string())
|
|
76
|
+
.unwrap_or(key)
|
|
77
|
+
})
|
|
71
78
|
.collect()
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -155,7 +162,11 @@ fn an_update_that_leaves_the_indexed_field_alone_leaves_the_index_alone() {
|
|
|
155
162
|
db.update("items:a", &json!({ "status": "active", "note": "after" }))
|
|
156
163
|
.expect("update");
|
|
157
164
|
|
|
158
|
-
assert_eq!(
|
|
165
|
+
assert_eq!(
|
|
166
|
+
snapshot(&db),
|
|
167
|
+
before,
|
|
168
|
+
"an unrelated field change is invisible to the index"
|
|
169
|
+
);
|
|
159
170
|
assert_index_matches_state(&db, "an unrelated field changing");
|
|
160
171
|
}
|
|
161
172
|
|
|
@@ -260,7 +271,11 @@ fn unindexable_values_are_excluded_rather_than_coerced() {
|
|
|
260
271
|
put(&db, "object", json!({ "status": { "state": "active" } }));
|
|
261
272
|
put(&db, "array", json!({ "status": ["active"] }));
|
|
262
273
|
put(&db, "scalar", json!({ "status": "active" }));
|
|
263
|
-
put(
|
|
274
|
+
put(
|
|
275
|
+
&db,
|
|
276
|
+
"not-an-object",
|
|
277
|
+
json!("items are usually objects, but need not be"),
|
|
278
|
+
);
|
|
264
279
|
|
|
265
280
|
assert_eq!(
|
|
266
281
|
ids(&db, "status", json!("active")),
|
|
@@ -351,8 +366,16 @@ fn conditional_creation_maintains_the_index_when_it_wins_and_not_when_it_loses()
|
|
|
351
366
|
key: "items:a".into(),
|
|
352
367
|
value: Some(json!({ "status": "active" })),
|
|
353
368
|
};
|
|
354
|
-
db.apply_atomic_transaction_guarded(
|
|
355
|
-
|
|
369
|
+
db.apply_atomic_transaction_guarded(
|
|
370
|
+
"pr35-put-1",
|
|
371
|
+
None,
|
|
372
|
+
None,
|
|
373
|
+
&[],
|
|
374
|
+
&[absent.clone()],
|
|
375
|
+
&[write.clone()],
|
|
376
|
+
None,
|
|
377
|
+
)
|
|
378
|
+
.expect("the record is absent, so the creation wins");
|
|
356
379
|
assert_eq!(ids(&db, "status", json!("active")), vec!["a".to_string()]);
|
|
357
380
|
assert_index_matches_state(&db, "a conditional creation that won");
|
|
358
381
|
|
|
@@ -520,14 +543,17 @@ fn declaring_an_index_populates_it_from_state_that_already_exists() {
|
|
|
520
543
|
None,
|
|
521
544
|
"before declaration there is no index to consult",
|
|
522
545
|
);
|
|
523
|
-
assert!(db
|
|
546
|
+
assert!(db
|
|
547
|
+
.create_equality_index("items", "status")
|
|
548
|
+
.expect("declare"));
|
|
524
549
|
assert_eq!(
|
|
525
550
|
ids(&db, "status", json!("active")),
|
|
526
551
|
vec!["a".to_string()],
|
|
527
552
|
"declaration derives the index from the records already held",
|
|
528
553
|
);
|
|
529
554
|
assert!(
|
|
530
|
-
!db.create_equality_index("items", "status")
|
|
555
|
+
!db.create_equality_index("items", "status")
|
|
556
|
+
.expect("declare"),
|
|
531
557
|
"re-declaring an existing index reports that it created nothing",
|
|
532
558
|
);
|
|
533
559
|
assert_index_matches_state(&db, "declaring an index over existing state");
|
|
@@ -548,8 +574,11 @@ fn an_explicit_rebuild_reproduces_the_maintained_index_exactly() {
|
|
|
548
574
|
);
|
|
549
575
|
}
|
|
550
576
|
db.delete("items:record-000").expect("delete");
|
|
551
|
-
db.update(
|
|
552
|
-
|
|
577
|
+
db.update(
|
|
578
|
+
"items:record-001",
|
|
579
|
+
&json!({ "status": "active", "tenantId": "tenant-9" }),
|
|
580
|
+
)
|
|
581
|
+
.expect("update");
|
|
553
582
|
|
|
554
583
|
let maintained = snapshot(&db);
|
|
555
584
|
db.rebuild_equality_indexes().expect("rebuild");
|
|
@@ -567,8 +596,10 @@ fn reopening_the_database_rebuilds_the_index_from_durable_state() {
|
|
|
567
596
|
|
|
568
597
|
let before = {
|
|
569
598
|
let db = FeltDb::open(&path).expect("open");
|
|
570
|
-
db.create_equality_index("items", "status")
|
|
571
|
-
|
|
599
|
+
db.create_equality_index("items", "status")
|
|
600
|
+
.expect("declare");
|
|
601
|
+
db.create_equality_index("items", "tenantId")
|
|
602
|
+
.expect("declare");
|
|
572
603
|
for step in 0..48 {
|
|
573
604
|
db.insert(
|
|
574
605
|
&format!("items:record-{step:03}"),
|
|
@@ -577,8 +608,11 @@ fn reopening_the_database_rebuilds_the_index_from_durable_state() {
|
|
|
577
608
|
.expect("insert");
|
|
578
609
|
}
|
|
579
610
|
db.delete("items:record-005").expect("delete");
|
|
580
|
-
db.update(
|
|
581
|
-
|
|
611
|
+
db.update(
|
|
612
|
+
"items:record-006",
|
|
613
|
+
&json!({ "status": "active", "tenantId": "tenant-0" }),
|
|
614
|
+
)
|
|
615
|
+
.expect("update");
|
|
582
616
|
db.apply_atomic_transaction_guarded(
|
|
583
617
|
"pr35-restart-tx",
|
|
584
618
|
None,
|
|
@@ -587,7 +621,10 @@ fn reopening_the_database_rebuilds_the_index_from_durable_state() {
|
|
|
587
621
|
&[],
|
|
588
622
|
&[
|
|
589
623
|
mutation("items:record-007", None),
|
|
590
|
-
mutation(
|
|
624
|
+
mutation(
|
|
625
|
+
"items:record-100",
|
|
626
|
+
Some(json!({ "status": "active", "tenantId": "tenant-1" })),
|
|
627
|
+
),
|
|
591
628
|
],
|
|
592
629
|
None,
|
|
593
630
|
)
|
|
@@ -600,8 +637,12 @@ fn reopening_the_database_rebuilds_the_index_from_durable_state() {
|
|
|
600
637
|
reopened.equality_indexes().expect("indexes").is_empty(),
|
|
601
638
|
"an index is derived state: nothing about it is read back from the log",
|
|
602
639
|
);
|
|
603
|
-
reopened
|
|
604
|
-
|
|
640
|
+
reopened
|
|
641
|
+
.create_equality_index("items", "status")
|
|
642
|
+
.expect("declare");
|
|
643
|
+
reopened
|
|
644
|
+
.create_equality_index("items", "tenantId")
|
|
645
|
+
.expect("declare");
|
|
605
646
|
|
|
606
647
|
assert_eq!(
|
|
607
648
|
snapshot(&reopened),
|
|
@@ -699,8 +740,11 @@ fn indexed_execution_returns_exactly_what_the_scan_returns() {
|
|
|
699
740
|
|
|
700
741
|
// The conjunction actually narrows rather than merely re-filtering: the
|
|
701
742
|
// intersection is smaller than either side.
|
|
702
|
-
let both = indexed_query(
|
|
703
|
-
|
|
743
|
+
let both = indexed_query(
|
|
744
|
+
&db,
|
|
745
|
+
&[("status", json!("active")), ("tenantId", json!("tenant-1"))],
|
|
746
|
+
)
|
|
747
|
+
.expect("both fields are indexed");
|
|
704
748
|
let one = indexed_query(&db, &[("status", json!("active"))]).expect("indexed");
|
|
705
749
|
assert!(!both.is_empty() && both.len() < one.len());
|
|
706
750
|
}
|
|
@@ -718,8 +762,11 @@ fn the_index_selects_candidates_and_the_predicate_still_decides() {
|
|
|
718
762
|
"the index answers with candidates, both of which satisfy only the indexed condition",
|
|
719
763
|
);
|
|
720
764
|
|
|
721
|
-
let result = indexed_query(
|
|
722
|
-
|
|
765
|
+
let result = indexed_query(
|
|
766
|
+
&db,
|
|
767
|
+
&[("status", json!("active")), ("role", json!("admin"))],
|
|
768
|
+
)
|
|
769
|
+
.expect("indexed");
|
|
723
770
|
assert_eq!(
|
|
724
771
|
result,
|
|
725
772
|
vec!["items:a".to_string()],
|
|
@@ -834,14 +881,19 @@ fn concurrent_mutation_and_indexed_query_stay_coherent() {
|
|
|
834
881
|
// mean a partially committed transaction became queryable.
|
|
835
882
|
for (sibling, count) in halves {
|
|
836
883
|
if count != 2 {
|
|
837
|
-
failures.push(format!(
|
|
884
|
+
failures.push(format!(
|
|
885
|
+
"transaction pair {sibling} was observed {count} times, not 2"
|
|
886
|
+
));
|
|
838
887
|
}
|
|
839
888
|
}
|
|
840
889
|
observations += 1;
|
|
841
890
|
}
|
|
842
891
|
|
|
843
892
|
running.store(false, Ordering::Relaxed);
|
|
844
|
-
let cycles: usize = writers
|
|
893
|
+
let cycles: usize = writers
|
|
894
|
+
.into_iter()
|
|
895
|
+
.map(|handle| handle.join().expect("writer"))
|
|
896
|
+
.sum();
|
|
845
897
|
|
|
846
898
|
assert!(cycles > 0, "the writers actually mutated the collection");
|
|
847
899
|
assert!(observations > 0, "the reader actually queried");
|
|
@@ -881,7 +933,10 @@ fn indexed_and_scan_execution_agree_on_a_concurrently_built_state() {
|
|
|
881
933
|
for conditions in [
|
|
882
934
|
vec![("status", json!("active"))],
|
|
883
935
|
vec![("tenantId", json!("tenant-3"))],
|
|
884
|
-
vec![
|
|
936
|
+
vec![
|
|
937
|
+
("status", json!("archived")),
|
|
938
|
+
("tenantId", json!("tenant-2")),
|
|
939
|
+
],
|
|
885
940
|
] {
|
|
886
941
|
assert_eq!(
|
|
887
942
|
indexed_query(&db, &conditions).expect("indexed"),
|
|
@@ -17,10 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
use feltdb::{
|
|
19
19
|
state_contract::{
|
|
20
|
-
|
|
21
|
-
FieldConstraints,
|
|
22
|
-
AuthorizationContext, CanonicalQuery, begin_read, execute_query,
|
|
23
|
-
QueryFilter,
|
|
20
|
+
begin_read, execute_query, AuthorizationContext, CanonicalQuery, CollectionSchema,
|
|
21
|
+
FieldConstraints, FieldSchema, FieldType, PrimitiveType, QueryFilter, StateSchema,
|
|
24
22
|
},
|
|
25
23
|
FeltDb,
|
|
26
24
|
};
|
|
@@ -324,8 +322,7 @@ fn pr7_phase3_self_authorization_proof() -> Result<(), Box<dyn std::error::Error
|
|
|
324
322
|
field_projections: Default::default(),
|
|
325
323
|
};
|
|
326
324
|
|
|
327
|
-
let _bob_context = begin_read(&db, &schema, "app", bob_auth)
|
|
328
|
-
.map_err(|e| format!("{:?}", e))?;
|
|
325
|
+
let _bob_context = begin_read(&db, &schema, "app", bob_auth).map_err(|e| format!("{:?}", e))?;
|
|
329
326
|
|
|
330
327
|
// Query as Bob with self(user) policy
|
|
331
328
|
let _query = CanonicalQuery {
|
|
@@ -360,8 +357,8 @@ fn pr7_phase3_self_authorization_proof() -> Result<(), Box<dyn std::error::Error
|
|
|
360
357
|
field_projections: Default::default(),
|
|
361
358
|
};
|
|
362
359
|
|
|
363
|
-
let _alice_context =
|
|
364
|
-
.map_err(|e| format!("{:?}", e))?;
|
|
360
|
+
let _alice_context =
|
|
361
|
+
begin_read(&db, &schema, "app", alice_auth).map_err(|e| format!("{:?}", e))?;
|
|
365
362
|
|
|
366
363
|
println!(" • Alice is member of Org A");
|
|
367
364
|
println!(" • Invitation.organization = {}", org_a_id);
|