@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,442 @@
|
|
|
1
|
+
//! Durability modes and the workload envelope.
|
|
2
|
+
//!
|
|
3
|
+
//! Two questions, in this order:
|
|
4
|
+
//!
|
|
5
|
+
//! > **What durability contract can FeltDB provide, and what does each one
|
|
6
|
+
//! > cost under a frozen workload?**
|
|
7
|
+
//!
|
|
8
|
+
//! The order matters. Choosing between `flush` and `fsync` first would be
|
|
9
|
+
//! picking an implementation before deciding what a successful write should
|
|
10
|
+
//! mean.
|
|
11
|
+
//!
|
|
12
|
+
//! # What this file asserts, and what it does not
|
|
13
|
+
//!
|
|
14
|
+
//! Measurements on a shared machine are noisy, so the **tests assert
|
|
15
|
+
//! relationships** — a barrier per write costs more than none, grouping sits
|
|
16
|
+
//! between them — and never absolute numbers. The numbers themselves belong in
|
|
17
|
+
//! `docs/architecture/workload-envelope.md`, recorded as *observed under this
|
|
18
|
+
//! frozen workload on this environment* rather than as a capacity claim.
|
|
19
|
+
//!
|
|
20
|
+
//! Nothing here proves power-loss durability. Issuing `fsync` is not evidence
|
|
21
|
+
//! that bytes reached a platter: that depends on the filesystem, the mount
|
|
22
|
+
//! options and the drive's write cache, none of which this database can
|
|
23
|
+
//! observe. D5 stays Unproven, and that is the honest end state.
|
|
24
|
+
|
|
25
|
+
use feltdb::state_model::{RetentionPolicy, StateStore};
|
|
26
|
+
use feltdb::{AtomicMutation, DurabilityMode, FeltDb};
|
|
27
|
+
use serde_json::json;
|
|
28
|
+
use std::collections::HashMap;
|
|
29
|
+
use std::path::Path;
|
|
30
|
+
use std::sync::Arc;
|
|
31
|
+
use std::time::{Duration, Instant};
|
|
32
|
+
use tempfile::TempDir;
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// The frozen workload
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/// A workload is fixed by its shape, not by a wall-clock budget, so two runs
|
|
39
|
+
/// measure the same thing.
|
|
40
|
+
#[derive(Debug, Clone, Copy)]
|
|
41
|
+
struct Workload {
|
|
42
|
+
name: &'static str,
|
|
43
|
+
writes: usize,
|
|
44
|
+
resources: usize,
|
|
45
|
+
payload_bytes: usize,
|
|
46
|
+
retention: Option<usize>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// What one run of a workload cost.
|
|
50
|
+
#[derive(Debug, Clone)]
|
|
51
|
+
struct Measurement {
|
|
52
|
+
elapsed: Duration,
|
|
53
|
+
log_bytes: u64,
|
|
54
|
+
revisions: usize,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl Measurement {
|
|
58
|
+
fn writes_per_second(&self, writes: usize) -> f64 {
|
|
59
|
+
writes as f64 / self.elapsed.as_secs_f64().max(f64::EPSILON)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn payload(index: usize, bytes: usize) -> serde_json::Value {
|
|
64
|
+
json!({ "n": index, "pad": "x".repeat(bytes) })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Run one workload under one durability mode. Deterministic in shape.
|
|
68
|
+
fn run(directory: &Path, workload: Workload, mode: DurabilityMode) -> Measurement {
|
|
69
|
+
let path = directory.join(format!("{}-{:?}.log", workload.name, mode));
|
|
70
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
71
|
+
db.set_durability_mode(mode);
|
|
72
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
73
|
+
|
|
74
|
+
for resource in 0..workload.resources {
|
|
75
|
+
let key = format!("bench:{resource}");
|
|
76
|
+
if let Some(keep) = workload.retention {
|
|
77
|
+
store
|
|
78
|
+
.set_retention_policy(&key, RetentionPolicy::keep_last(keep))
|
|
79
|
+
.unwrap();
|
|
80
|
+
}
|
|
81
|
+
db.insert(&key, payload(0, workload.payload_bytes)).unwrap();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let start = Instant::now();
|
|
85
|
+
for index in 1..=workload.writes {
|
|
86
|
+
let key = format!("bench:{}", index % workload.resources);
|
|
87
|
+
db.update(&key, payload(index, workload.payload_bytes))
|
|
88
|
+
.unwrap();
|
|
89
|
+
}
|
|
90
|
+
let elapsed = start.elapsed();
|
|
91
|
+
|
|
92
|
+
let revisions: usize = (0..workload.resources)
|
|
93
|
+
.map(|resource| store.history_of(&format!("bench:{resource}")).len())
|
|
94
|
+
.sum();
|
|
95
|
+
|
|
96
|
+
Measurement {
|
|
97
|
+
elapsed,
|
|
98
|
+
log_bytes: std::fs::metadata(&path).unwrap().len(),
|
|
99
|
+
revisions,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// The frozen matrix. Changing any row changes what every recorded number
|
|
104
|
+
/// means, so the table lives here rather than being chosen per test.
|
|
105
|
+
const WORKLOADS: &[Workload] = &[
|
|
106
|
+
Workload {
|
|
107
|
+
name: "sequential-unbounded",
|
|
108
|
+
writes: 300,
|
|
109
|
+
resources: 1,
|
|
110
|
+
payload_bytes: 100,
|
|
111
|
+
retention: None,
|
|
112
|
+
},
|
|
113
|
+
Workload {
|
|
114
|
+
name: "sequential-keep5",
|
|
115
|
+
writes: 300,
|
|
116
|
+
resources: 1,
|
|
117
|
+
payload_bytes: 100,
|
|
118
|
+
retention: Some(5),
|
|
119
|
+
},
|
|
120
|
+
Workload {
|
|
121
|
+
name: "sequential-keep50",
|
|
122
|
+
writes: 300,
|
|
123
|
+
resources: 1,
|
|
124
|
+
payload_bytes: 100,
|
|
125
|
+
retention: Some(50),
|
|
126
|
+
},
|
|
127
|
+
Workload {
|
|
128
|
+
name: "many-resources",
|
|
129
|
+
writes: 300,
|
|
130
|
+
resources: 50,
|
|
131
|
+
payload_bytes: 100,
|
|
132
|
+
retention: None,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Durability modes: the contract, then the cost
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/// **Every mode states what a successful write guarantees.**
|
|
141
|
+
///
|
|
142
|
+
/// The point of the enum is that the contract is readable rather than inferred
|
|
143
|
+
/// from an API name. `Flushed` does not claim stable storage; `Synced` claims a
|
|
144
|
+
/// barrier was *issued* and explicitly not that it landed; `Grouped` names the
|
|
145
|
+
/// window of acknowledged writes that may not have been through one.
|
|
146
|
+
#[test]
|
|
147
|
+
fn every_durability_mode_states_its_own_guarantee() {
|
|
148
|
+
let modes = [
|
|
149
|
+
DurabilityMode::Flushed,
|
|
150
|
+
DurabilityMode::Synced,
|
|
151
|
+
DurabilityMode::Grouped { every: 16 },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
for mode in modes {
|
|
155
|
+
let guarantee = mode.guarantee();
|
|
156
|
+
assert!(!guarantee.is_empty());
|
|
157
|
+
assert!(
|
|
158
|
+
!guarantee.contains("power-loss safe") && !guarantee.contains("guaranteed durable"),
|
|
159
|
+
"{mode:?} overstates: {guarantee}"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The window each mode leaves is explicit rather than implied.
|
|
164
|
+
assert_eq!(DurabilityMode::Flushed.unbarriered_window(), None);
|
|
165
|
+
assert_eq!(DurabilityMode::Synced.unbarriered_window(), Some(0));
|
|
166
|
+
assert_eq!(
|
|
167
|
+
DurabilityMode::Grouped { every: 16 }.unbarriered_window(),
|
|
168
|
+
Some(15)
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Only one mode claims a barrier per write, and it still does not claim the
|
|
172
|
+
// barrier reached the device.
|
|
173
|
+
assert!(DurabilityMode::Synced.guarantee().contains("barrier"));
|
|
174
|
+
assert!(DurabilityMode::Synced
|
|
175
|
+
.guarantee()
|
|
176
|
+
.contains("cannot observe"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// **The default is the weakest contract**, so nothing is silently promised.
|
|
180
|
+
#[test]
|
|
181
|
+
fn the_default_mode_is_the_one_whose_cost_was_already_measured() {
|
|
182
|
+
let directory = TempDir::new().unwrap();
|
|
183
|
+
let db = FeltDb::open(directory.path().join("default.log")).unwrap();
|
|
184
|
+
assert_eq!(db.durability_mode(), DurabilityMode::Flushed);
|
|
185
|
+
assert_eq!(DurabilityMode::default(), DurabilityMode::Flushed);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// **A stronger barrier costs more.**
|
|
189
|
+
///
|
|
190
|
+
/// Asserted as an ordering rather than as numbers, because the numbers depend
|
|
191
|
+
/// on the machine. Per-write `fsync` runs roughly three to four times slower
|
|
192
|
+
/// than flush across every row of the frozen matrix, which is large enough to
|
|
193
|
+
/// be a property rather than noise.
|
|
194
|
+
///
|
|
195
|
+
/// **Grouping is deliberately not asserted to sit between them.** The
|
|
196
|
+
/// measurements do not support it at this scale: on the recorded environment
|
|
197
|
+
/// grouped/32 came out *slower* than per-write `fsync` on one workload and
|
|
198
|
+
/// faster on the others. An assertion that can flip on a shared machine is
|
|
199
|
+
/// worse than no assertion, so the observation is recorded in the envelope
|
|
200
|
+
/// document and left unasserted here. What grouping buys — a smaller barrier
|
|
201
|
+
/// count for a stated window of unbarriered writes — is a contract property,
|
|
202
|
+
/// and that *is* asserted, above.
|
|
203
|
+
#[test]
|
|
204
|
+
fn a_barrier_per_write_costs_more_than_none() {
|
|
205
|
+
let directory = TempDir::new().unwrap();
|
|
206
|
+
let workload = WORKLOADS[0];
|
|
207
|
+
|
|
208
|
+
let flushed = run(directory.path(), workload, DurabilityMode::Flushed);
|
|
209
|
+
let synced = run(directory.path(), workload, DurabilityMode::Synced);
|
|
210
|
+
|
|
211
|
+
assert!(
|
|
212
|
+
synced.elapsed > flushed.elapsed,
|
|
213
|
+
"a barrier per write must cost something: synced {:?} vs flushed {:?}",
|
|
214
|
+
synced.elapsed,
|
|
215
|
+
flushed.elapsed
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Durability is the only thing that changed. The database is identical.
|
|
219
|
+
assert_eq!(flushed.revisions, synced.revisions);
|
|
220
|
+
assert_eq!(flushed.log_bytes, synced.log_bytes);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Changing the mode changes the contract and nothing else: the same writes
|
|
224
|
+
/// produce the same database.
|
|
225
|
+
///
|
|
226
|
+
/// Run against **one path**, deliberately. An authority's identity is derived
|
|
227
|
+
/// from its path, and a revision records the authority that committed it, so
|
|
228
|
+
/// two databases in different directories never share a history digest even
|
|
229
|
+
/// with identical logical history. That is correct — provenance is part of what
|
|
230
|
+
/// happened — and it means a comparison like this has to reuse the path rather
|
|
231
|
+
/// than compare two locations.
|
|
232
|
+
#[test]
|
|
233
|
+
fn the_durability_mode_does_not_change_what_is_stored() {
|
|
234
|
+
let directory = TempDir::new().unwrap();
|
|
235
|
+
let path = directory.path().join("same.log");
|
|
236
|
+
let mut digests = Vec::new();
|
|
237
|
+
|
|
238
|
+
for mode in [
|
|
239
|
+
DurabilityMode::Flushed,
|
|
240
|
+
DurabilityMode::Synced,
|
|
241
|
+
DurabilityMode::Grouped { every: 4 },
|
|
242
|
+
] {
|
|
243
|
+
let _ = std::fs::remove_file(&path);
|
|
244
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
245
|
+
db.set_durability_mode(mode);
|
|
246
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
247
|
+
for n in 1..12 {
|
|
248
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
249
|
+
}
|
|
250
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
251
|
+
digests.push((db.state_digest().unwrap(), store.history_digest()));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
assert_eq!(digests[0], digests[1]);
|
|
255
|
+
assert_eq!(digests[0], digests[2]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// A grouped database still recovers cleanly, because the flush — not the
|
|
259
|
+
/// barrier — is what makes a record survive a process crash.
|
|
260
|
+
#[test]
|
|
261
|
+
fn every_mode_recovers_to_a_clean_prefix() {
|
|
262
|
+
for mode in [
|
|
263
|
+
DurabilityMode::Flushed,
|
|
264
|
+
DurabilityMode::Synced,
|
|
265
|
+
DurabilityMode::Grouped { every: 4 },
|
|
266
|
+
] {
|
|
267
|
+
let directory = TempDir::new().unwrap();
|
|
268
|
+
let path = directory.path().join("recover.log");
|
|
269
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
270
|
+
db.set_durability_mode(mode);
|
|
271
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
272
|
+
db.update("tasks:1", json!({"n": 1})).unwrap();
|
|
273
|
+
db.update("tasks:1", json!({"n": 2})).unwrap();
|
|
274
|
+
drop(db);
|
|
275
|
+
|
|
276
|
+
let full = std::fs::read(&path).unwrap();
|
|
277
|
+
for cut in (0..=full.len()).step_by(29) {
|
|
278
|
+
std::fs::write(&path, &full[..cut]).unwrap();
|
|
279
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
280
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
281
|
+
let value = db
|
|
282
|
+
.get::<serde_json::Value>("tasks:1")
|
|
283
|
+
.unwrap()
|
|
284
|
+
.map(|value| value["n"].as_i64().unwrap());
|
|
285
|
+
assert_eq!(
|
|
286
|
+
store.history_of("tasks:1").len(),
|
|
287
|
+
value.map(|n| n as usize + 1).unwrap_or(0),
|
|
288
|
+
"{mode:?} at cut {cut}"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// **Transactions already carry the strongest contract**, whatever the
|
|
295
|
+
/// single-record mode is.
|
|
296
|
+
///
|
|
297
|
+
/// `append_transaction` issues a barrier unconditionally. So a caller who needs
|
|
298
|
+
/// stable-storage semantics for a group of records has had a way to ask for it
|
|
299
|
+
/// all along, which is a real option in the trade-off rather than a workaround.
|
|
300
|
+
#[test]
|
|
301
|
+
fn a_transaction_commits_under_a_barrier_regardless_of_the_mode() {
|
|
302
|
+
let directory = TempDir::new().unwrap();
|
|
303
|
+
let path = directory.path().join("txn.log");
|
|
304
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
305
|
+
// Even at the weakest single-record contract.
|
|
306
|
+
db.set_durability_mode(DurabilityMode::Flushed);
|
|
307
|
+
|
|
308
|
+
db.apply_atomic_transaction(
|
|
309
|
+
"t1",
|
|
310
|
+
None,
|
|
311
|
+
&[],
|
|
312
|
+
&[AtomicMutation {
|
|
313
|
+
capability: "acct".into(),
|
|
314
|
+
key: "acct:a".into(),
|
|
315
|
+
value: Some(json!({"balance": 1})),
|
|
316
|
+
}],
|
|
317
|
+
None,
|
|
318
|
+
)
|
|
319
|
+
.unwrap();
|
|
320
|
+
|
|
321
|
+
let value: serde_json::Value = db.get("acct:a").unwrap().unwrap();
|
|
322
|
+
assert_eq!(value["balance"], json!(1));
|
|
323
|
+
assert_eq!(
|
|
324
|
+
db.durability_mode(),
|
|
325
|
+
DurabilityMode::Flushed,
|
|
326
|
+
"the transaction path is independent of the single-record contract"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// The envelope
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
/// **Retention cost grows with the window, and the log grows with retention.**
|
|
335
|
+
///
|
|
336
|
+
/// Both directions of the trade-off the retention work exposed, now measured
|
|
337
|
+
/// across the frozen matrix rather than one ad-hoc run. Asserted as ordering.
|
|
338
|
+
#[test]
|
|
339
|
+
fn the_retention_envelope_has_the_measured_shape() {
|
|
340
|
+
let directory = TempDir::new().unwrap();
|
|
341
|
+
|
|
342
|
+
let unbounded = run(directory.path(), WORKLOADS[0], DurabilityMode::Flushed);
|
|
343
|
+
let keep5 = run(directory.path(), WORKLOADS[1], DurabilityMode::Flushed);
|
|
344
|
+
let keep50 = run(directory.path(), WORKLOADS[2], DurabilityMode::Flushed);
|
|
345
|
+
|
|
346
|
+
assert_eq!(unbounded.revisions, 301, "unbounded keeps everything");
|
|
347
|
+
assert_eq!(keep5.revisions, 5);
|
|
348
|
+
assert_eq!(keep50.revisions, 50);
|
|
349
|
+
|
|
350
|
+
assert!(
|
|
351
|
+
keep50.elapsed > keep5.elapsed,
|
|
352
|
+
"a wider window costs more per write: keep50 {:?} vs keep5 {:?}",
|
|
353
|
+
keep50.elapsed,
|
|
354
|
+
keep5.elapsed
|
|
355
|
+
);
|
|
356
|
+
assert!(
|
|
357
|
+
keep5.log_bytes > unbounded.log_bytes,
|
|
358
|
+
"expiry tombstones inflate the log before compaction: {} vs {}",
|
|
359
|
+
keep5.log_bytes,
|
|
360
|
+
unbounded.log_bytes
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// Spreading writes over many resources does not change what is stored per
|
|
365
|
+
/// write, and history stays per-resource.
|
|
366
|
+
#[test]
|
|
367
|
+
fn many_resources_keep_independent_bounded_histories() {
|
|
368
|
+
let directory = TempDir::new().unwrap();
|
|
369
|
+
let measurement = run(directory.path(), WORKLOADS[3], DurabilityMode::Flushed);
|
|
370
|
+
assert_eq!(
|
|
371
|
+
measurement.revisions, 350,
|
|
372
|
+
"50 initial writes plus 300 updates, spread evenly"
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// Replaying a database costs time proportional to what it holds, and recovery
|
|
377
|
+
/// is part of the envelope an operator has to size for.
|
|
378
|
+
#[test]
|
|
379
|
+
fn restart_cost_is_measurable_and_bounded_by_retention() {
|
|
380
|
+
let directory = TempDir::new().unwrap();
|
|
381
|
+
|
|
382
|
+
// Two databases with the same writes and different retention.
|
|
383
|
+
let unbounded = run(directory.path(), WORKLOADS[0], DurabilityMode::Flushed);
|
|
384
|
+
let bounded = run(directory.path(), WORKLOADS[1], DurabilityMode::Flushed);
|
|
385
|
+
|
|
386
|
+
let reopen = |name: &str| {
|
|
387
|
+
let path = directory
|
|
388
|
+
.path()
|
|
389
|
+
.join(format!("{name}-{:?}.log", DurabilityMode::Flushed));
|
|
390
|
+
let start = Instant::now();
|
|
391
|
+
let db = FeltDb::open(&path).unwrap();
|
|
392
|
+
let elapsed = start.elapsed();
|
|
393
|
+
drop(db);
|
|
394
|
+
elapsed
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
let _ = reopen("sequential-unbounded");
|
|
398
|
+
let _ = reopen("sequential-keep5");
|
|
399
|
+
|
|
400
|
+
// The claim is that both reopen, and that retention bounds what has to be
|
|
401
|
+
// held afterwards — not a timing threshold, which would be noise.
|
|
402
|
+
assert!(unbounded.revisions > bounded.revisions * 10);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// The whole frozen matrix runs under every mode, which is what makes the
|
|
406
|
+
/// recorded table reproducible rather than a set of one-off numbers.
|
|
407
|
+
#[test]
|
|
408
|
+
fn the_frozen_matrix_runs_under_every_durability_mode() {
|
|
409
|
+
let directory = TempDir::new().unwrap();
|
|
410
|
+
let mut rows = 0;
|
|
411
|
+
|
|
412
|
+
for workload in WORKLOADS {
|
|
413
|
+
for mode in [
|
|
414
|
+
DurabilityMode::Flushed,
|
|
415
|
+
DurabilityMode::Grouped { every: 32 },
|
|
416
|
+
DurabilityMode::Synced,
|
|
417
|
+
] {
|
|
418
|
+
let measurement = run(directory.path(), *workload, mode);
|
|
419
|
+
assert!(measurement.writes_per_second(workload.writes) > 0.0);
|
|
420
|
+
assert!(measurement.log_bytes > 0);
|
|
421
|
+
rows += 1;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
assert_eq!(rows, WORKLOADS.len() * 3, "every cell of the matrix ran");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// A measured number is not a capacity claim, and the harness records the
|
|
429
|
+
/// workload alongside it so it cannot be quoted without one.
|
|
430
|
+
#[test]
|
|
431
|
+
fn a_measurement_carries_the_workload_that_produced_it() {
|
|
432
|
+
let directory = TempDir::new().unwrap();
|
|
433
|
+
let workload = WORKLOADS[1];
|
|
434
|
+
let measurement = run(directory.path(), workload, DurabilityMode::Flushed);
|
|
435
|
+
|
|
436
|
+
// Everything needed to reproduce or dispute the number.
|
|
437
|
+
assert_eq!(workload.writes, 300);
|
|
438
|
+
assert_eq!(workload.resources, 1);
|
|
439
|
+
assert_eq!(workload.payload_bytes, 100);
|
|
440
|
+
assert_eq!(workload.retention, Some(5));
|
|
441
|
+
assert!(measurement.writes_per_second(workload.writes).is_finite());
|
|
442
|
+
}
|
|
@@ -31,7 +31,7 @@ use feltdb::{
|
|
|
31
31
|
sync_contract::SyncStore,
|
|
32
32
|
worker_mesh::WorkerMeshStore,
|
|
33
33
|
workload::WorkloadStore,
|
|
34
|
-
FeltDb,
|
|
34
|
+
FeltDb, StateTriggerStore,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
#[derive(Clone)]
|
|
@@ -41,6 +41,7 @@ pub struct AppState {
|
|
|
41
41
|
pub namespace: Arc<str>,
|
|
42
42
|
pub ids: Arc<AtomicU64>,
|
|
43
43
|
pub auth_enabled: bool,
|
|
44
|
+
pub authentication_workers: Arc<tokio::sync::Semaphore>,
|
|
44
45
|
pub keys: KeyStore,
|
|
45
46
|
pub metrics: Metrics,
|
|
46
47
|
pub cluster: ClusterStore,
|
|
@@ -72,7 +73,21 @@ pub struct AppState {
|
|
|
72
73
|
pub grants: Arc<std::sync::Mutex<GrantStore>>,
|
|
73
74
|
pub sync: Arc<std::sync::Mutex<SyncStore>>,
|
|
74
75
|
pub workloads: Arc<std::sync::Mutex<WorkloadStore>>,
|
|
76
|
+
pub state_triggers: Arc<std::sync::Mutex<StateTriggerStore>>,
|
|
75
77
|
pub mesh: Arc<std::sync::Mutex<WorkerMeshStore>>,
|
|
76
78
|
pub readiness_probe: Arc<PathBuf>,
|
|
77
79
|
pub bounded_query_cursors: Arc<std::sync::Mutex<HashMap<String, BoundedQueryCursor>>>,
|
|
80
|
+
/// Bounds how many requests may be in flight at once.
|
|
81
|
+
///
|
|
82
|
+
/// Every read and write serializes on one lock inside the database, so
|
|
83
|
+
/// admitting unbounded concurrency does not increase throughput — it only
|
|
84
|
+
/// converts a queue the server could reject quickly into one it holds until
|
|
85
|
+
/// clients give up. Shedding at the door turns a pile-up into a fast 503
|
|
86
|
+
/// with `Retry-After`.
|
|
87
|
+
pub admission: Arc<tokio::sync::Semaphore>,
|
|
88
|
+
/// How long a request may take before the server abandons it.
|
|
89
|
+
///
|
|
90
|
+
/// Without this a slow operation runs until the *client* times out, and the
|
|
91
|
+
/// work keeps running after the client has gone.
|
|
92
|
+
pub request_deadline: std::time::Duration,
|
|
78
93
|
}
|