@feltdb/core 0.8.3 → 0.8.5
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/cli/commands.js +4 -1
- package/dist/cli/provisioning-neutrality.js +79 -0
- package/dist/collection.d.ts +43 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +192 -22
- package/dist/create/create.js +25 -21
- package/dist/create/managed-account.js +11 -0
- 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/equality_index.rs +595 -0
- 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 +1050 -117
- 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_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 +90 -72
- 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_execution_diagnostics.rs +126 -0
- 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 +305 -6
- 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 +1514 -537
- 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/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/sync.rs +12 -0
- 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 +442 -0
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -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/pr34_query_collection.rs +234 -0
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +947 -0
- 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 +1032 -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/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-server/src/app_state.rs +2 -1
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +1137 -29
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +800 -34
- package/dist/db.d.ts +33 -34
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +74 -20
- package/dist/deployment.d.ts +30 -0
- package/dist/deployment.d.ts.map +1 -0
- package/dist/deployment.js +130 -0
- package/dist/embedded-transaction.d.ts +22 -4
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +51 -5
- package/dist/feltdb.d.ts +14 -2
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.js +1 -1
- package/dist/http-client.d.ts +14 -0
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +23 -5
- package/dist/http-db.d.ts +119 -1
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +346 -31
- package/dist/index-core.d.ts +2 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +35 -21
- package/dist/managed-recovery.d.ts +192 -0
- package/dist/managed-recovery.d.ts.map +1 -0
- package/dist/managed-recovery.js +242 -0
- package/dist/memory-db.js +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-DB8cX151.js → feltdb_wasm-DaNwCLRX.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
- package/dist/studio-app/assets/index-j8IlhNqJ.js +29 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +30 -0
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +41 -0
- 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-ClhDHp0S.wasm +0 -0
- package/dist/studio-app/assets/index-B0k4UAlI.js +0 -29
|
@@ -41,6 +41,7 @@ pub mod durable_dedup_set;
|
|
|
41
41
|
pub mod durable_operation_identity;
|
|
42
42
|
pub mod durable_operation_log;
|
|
43
43
|
pub mod durable_sync;
|
|
44
|
+
pub mod equality_index;
|
|
44
45
|
mod execution;
|
|
45
46
|
pub mod in_process_transport;
|
|
46
47
|
pub mod indexing;
|
|
@@ -98,6 +99,7 @@ mod phase_5_soak;
|
|
|
98
99
|
pub mod policy_evaluation;
|
|
99
100
|
pub mod production_api;
|
|
100
101
|
mod provenance;
|
|
102
|
+
pub mod query_execution_diagnostics;
|
|
101
103
|
pub mod query_performance;
|
|
102
104
|
mod references;
|
|
103
105
|
pub mod replay_fuzzing;
|
|
@@ -107,11 +109,14 @@ pub mod replication_manager;
|
|
|
107
109
|
pub mod replication_protocol;
|
|
108
110
|
mod routing;
|
|
109
111
|
pub mod sharding;
|
|
112
|
+
pub mod state_conflict_contract;
|
|
110
113
|
pub mod state_contract;
|
|
114
|
+
pub mod state_diff_contract;
|
|
111
115
|
pub mod state_facade;
|
|
112
116
|
pub mod state_hash;
|
|
113
117
|
pub mod state_model;
|
|
114
118
|
pub mod state_transition_store;
|
|
119
|
+
pub mod state_trigger;
|
|
115
120
|
mod storage;
|
|
116
121
|
pub mod submission;
|
|
117
122
|
mod sync;
|
|
@@ -124,6 +129,7 @@ mod transaction_invariants;
|
|
|
124
129
|
pub mod transaction_preconditions;
|
|
125
130
|
pub mod transactions;
|
|
126
131
|
mod trigger;
|
|
132
|
+
pub mod trigger_contract;
|
|
127
133
|
pub mod worker_mesh;
|
|
128
134
|
mod workflow;
|
|
129
135
|
#[cfg(test)]
|
|
@@ -131,6 +137,7 @@ mod workflow_acceptance_tests;
|
|
|
131
137
|
mod workflow_integration;
|
|
132
138
|
mod workflow_runtime;
|
|
133
139
|
pub mod workload;
|
|
140
|
+
pub mod workload_diagnostics;
|
|
134
141
|
|
|
135
142
|
pub use acquisition::{
|
|
136
143
|
AcquisitionDeduplicator, AcquisitionPolicy, AcquisitionResult, ContentRequest, ContentResponse,
|
|
@@ -182,6 +189,7 @@ pub use distributed_transactions::{
|
|
|
182
189
|
DistributedTransactionExecutor, EnvelopeId, ReplicaState, ReplicationMessage,
|
|
183
190
|
TransactionEnvelope,
|
|
184
191
|
};
|
|
192
|
+
pub use equality_index::{EqualityIndex, EqualityIndexStats, IndexKey};
|
|
185
193
|
pub use execution::{Execution, ExecutionQueue, ExecutionStatus, RetryPolicy};
|
|
186
194
|
pub use indexing::{IndexConfig, IndexManager, IndexStats, IndexType};
|
|
187
195
|
pub use materialization::{MaterializationHandler, ReactiveCollectionIntegration};
|
|
@@ -218,14 +226,27 @@ pub use sharding::{
|
|
|
218
226
|
HotspotAlert, RebalanceOperation, ShardDistributionSummary, ShardId, ShardKey, ShardManager,
|
|
219
227
|
ShardMetrics, ShardRange, ShardingStrategy,
|
|
220
228
|
};
|
|
229
|
+
pub use state_conflict_contract::{
|
|
230
|
+
classify_states, StateConflictInput, StateConflictOutput, STATE_CONFLICT_CONTRACT_ID,
|
|
231
|
+
STATE_CONFLICT_CONTRACT_VERSION,
|
|
232
|
+
};
|
|
233
|
+
pub use state_diff_contract::{
|
|
234
|
+
diff_states, StateDiffInput, StateDiffOutput, STATE_DIFF_CONTRACT_ID,
|
|
235
|
+
STATE_DIFF_CONTRACT_VERSION,
|
|
236
|
+
};
|
|
221
237
|
pub use state_facade::FeltDBStateSystem;
|
|
222
238
|
pub use state_hash::{CanonicalState, StateHash};
|
|
223
239
|
pub use state_model::{
|
|
224
|
-
|
|
240
|
+
apply_reconciliation_plan, path_relation, paths_overlap, reconcile, resolve_path, ChangeKind,
|
|
241
|
+
ConflictClass, ConflictClassification, PathComponent, PathConflict, PathRelation,
|
|
225
242
|
ReconciliationPlan, Relationship, SemanticChange, SemanticDiff, StateId,
|
|
226
243
|
StateReconciliationResult, StateRevision, StateStore, StateTopology, STATE_MODEL_VERSION,
|
|
227
244
|
};
|
|
228
245
|
pub use state_transition_store::{MemoryStateTransitionStore, StateTransitionRecord};
|
|
246
|
+
pub use state_trigger::{
|
|
247
|
+
StateTrigger, StateTriggerChangeKind, StateTriggerError, StateTriggerPredicate,
|
|
248
|
+
StateTriggerStore, TriggeredWorkload,
|
|
249
|
+
};
|
|
229
250
|
pub use storage::{CheckpointData, FileStorage, MemoryStorage, Storage};
|
|
230
251
|
pub use submission::{SubmissionManager, SubmissionMetrics};
|
|
231
252
|
pub use sync::{ChangeLog, Conflict, ConflictDetector, PeerState, SyncMessage, SyncState};
|
|
@@ -235,6 +256,12 @@ pub use transactions::{
|
|
|
235
256
|
StateTransition, TransactionExecutor, TransitionResult,
|
|
236
257
|
};
|
|
237
258
|
pub use trigger::{Trigger, TriggerFilter, TriggerRegistry};
|
|
259
|
+
pub use trigger_contract::{
|
|
260
|
+
canonical_input_bytes, canonical_output_bytes, evaluate_canonical, evaluate_trigger,
|
|
261
|
+
ContractChange, ContractChangeKind, ContractErrorBody, ContractErrorCode, ContractPredicate,
|
|
262
|
+
ContractRuntime, ContractTrigger, MatchOutcome, MatchReason, TriggerMatchInput,
|
|
263
|
+
TriggerMatchOutput, TRIGGER_MATCH_CONTRACT_ID, TRIGGER_MATCH_CONTRACT_VERSION,
|
|
264
|
+
};
|
|
238
265
|
pub use workflow::{
|
|
239
266
|
BlockedReason, WorkflowGraph, WorkflowInstance, WorkflowNode, WorkflowOperation, WorkflowRef,
|
|
240
267
|
WorkflowState, WorkflowStepRef, WorkflowStepState,
|
|
@@ -251,7 +278,7 @@ use serde_json::Value;
|
|
|
251
278
|
use sha2::{Digest, Sha256};
|
|
252
279
|
use std::any::type_name;
|
|
253
280
|
use std::collections::hash_map::DefaultHasher;
|
|
254
|
-
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
281
|
+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
|
255
282
|
use std::fmt::{Display, Formatter};
|
|
256
283
|
use std::fs::{self, OpenOptions};
|
|
257
284
|
use std::hash::{Hash, Hasher};
|
|
@@ -281,6 +308,12 @@ pub enum FlowError {
|
|
|
281
308
|
expected: u64,
|
|
282
309
|
actual: u64,
|
|
283
310
|
},
|
|
311
|
+
/// The durable database is in a format this build does not understand.
|
|
312
|
+
///
|
|
313
|
+
/// Returned by [`FeltDb::open`] **before any record is interpreted**, and
|
|
314
|
+
/// before anything is written. A database that returns this error is
|
|
315
|
+
/// untouched.
|
|
316
|
+
IncompatibleFormat(Box<FormatIncompatibility>),
|
|
284
317
|
}
|
|
285
318
|
|
|
286
319
|
impl Display for FlowError {
|
|
@@ -295,6 +328,9 @@ impl Display for FlowError {
|
|
|
295
328
|
f,
|
|
296
329
|
"REVISION_CONFLICT: expected authority revision {expected}, current revision is {actual}"
|
|
297
330
|
),
|
|
331
|
+
FlowError::IncompatibleFormat(incompatibility) => {
|
|
332
|
+
write!(f, "INCOMPATIBLE_DURABLE_FORMAT: {incompatibility}")
|
|
333
|
+
}
|
|
298
334
|
}
|
|
299
335
|
}
|
|
300
336
|
}
|
|
@@ -352,6 +388,140 @@ struct Inner {
|
|
|
352
388
|
transaction_payload_hashes: HashMap<String, String>,
|
|
353
389
|
transaction_revisions: HashMap<String, (u64, u64)>,
|
|
354
390
|
collection_cardinality: HashMap<String, u64>,
|
|
391
|
+
/// Derived equality index over `rows`. Never durable, never replicated, and
|
|
392
|
+
/// never consulted for anything but candidate selection: it is maintained
|
|
393
|
+
/// inside the same state mutation boundary as `rows` and rebuilt from
|
|
394
|
+
/// `rows` after recovery.
|
|
395
|
+
equality_index: EqualityIndex,
|
|
396
|
+
/// Each resource's newest revision, as `(id, sequence)`.
|
|
397
|
+
///
|
|
398
|
+
/// Derived from the `state` rows exactly like `equality_index` is derived
|
|
399
|
+
/// from `rows`: never durable, rebuilt after recovery. It exists so that
|
|
400
|
+
/// minting a revision does not have to scan a resource's history to find
|
|
401
|
+
/// the parent it descends from.
|
|
402
|
+
revision_heads: HashMap<String, (state_model::StateId, u64)>,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// The state lock, held with its wait and hold time attributable.
|
|
406
|
+
///
|
|
407
|
+
/// FeltDB serializes every read and every write behind one mutex, so "how long
|
|
408
|
+
/// did an operation take" and "how long did it make everything else wait" are
|
|
409
|
+
/// different questions. PR34 showed the second one mattered: a query holding the
|
|
410
|
+
/// lock while it cloned a collection inflated the cost of unrelated writes.
|
|
411
|
+
///
|
|
412
|
+
/// This guard answers both. It records the time spent *waiting* to acquire —
|
|
413
|
+
/// which is pure contention, the one span where no work happens at all — and the
|
|
414
|
+
/// time spent *holding*, which bounds how long anything else can be forced to
|
|
415
|
+
/// wait. Both are off unless `FELTDB_WORKLOAD_DIAGNOSTICS=1`, where acquisition
|
|
416
|
+
/// costs one relaxed atomic load and no clock read.
|
|
417
|
+
///
|
|
418
|
+
/// It dereferences to [`Inner`], so every existing use site reads unchanged.
|
|
419
|
+
struct StateGuard<'a> {
|
|
420
|
+
inner: std::sync::MutexGuard<'a, Inner>,
|
|
421
|
+
held_since: Option<std::time::Instant>,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
impl std::ops::Deref for StateGuard<'_> {
|
|
425
|
+
type Target = Inner;
|
|
426
|
+
fn deref(&self) -> &Inner {
|
|
427
|
+
&self.inner
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
impl std::ops::DerefMut for StateGuard<'_> {
|
|
432
|
+
fn deref_mut(&mut self) -> &mut Inner {
|
|
433
|
+
&mut self.inner
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
impl Drop for StateGuard<'_> {
|
|
438
|
+
fn drop(&mut self) {
|
|
439
|
+
if let Some(since) = self.held_since {
|
|
440
|
+
workload_diagnostics::record(
|
|
441
|
+
workload_diagnostics::Phase::StateLockHold,
|
|
442
|
+
since.elapsed().as_nanos().min(u64::MAX as u128) as u64,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/// The single state mutation boundary for authoritative records.
|
|
449
|
+
///
|
|
450
|
+
/// Every path that changes a record — insert, update, delete, CAS, transaction
|
|
451
|
+
/// commit, replicated operation apply, log replay, snapshot install — goes
|
|
452
|
+
/// through these three methods, and they change the record map and the derived
|
|
453
|
+
/// equality index together while the caller still holds the state lock. That is
|
|
454
|
+
/// the whole atomicity argument: there is no window in which a committed record
|
|
455
|
+
/// exists without its index entries, or an index entry survives its record,
|
|
456
|
+
/// because no code path can write one without the other.
|
|
457
|
+
///
|
|
458
|
+
/// The index is deliberately maintained here rather than through an independent
|
|
459
|
+
/// index API. An independent API would have to be called, correctly, by nine
|
|
460
|
+
/// separate mutation paths; a missed call would be a silent divergence that only
|
|
461
|
+
/// a query would reveal. Routing every write through one boundary makes the
|
|
462
|
+
/// omission impossible to write in the first place.
|
|
463
|
+
impl Inner {
|
|
464
|
+
/// Write one record, replacing any record already at its key.
|
|
465
|
+
///
|
|
466
|
+
/// Returns `true` when this created a record rather than replacing one, which
|
|
467
|
+
/// is what collection cardinality accounting needs.
|
|
468
|
+
fn put_row(&mut self, row: StoredRow) -> bool {
|
|
469
|
+
let capability = row.capability.clone();
|
|
470
|
+
let key = row.key.clone();
|
|
471
|
+
let indexed = self.equality_index.indexes_collection(&capability);
|
|
472
|
+
let mutation = workload_diagnostics::span(workload_diagnostics::Phase::RecordMutation);
|
|
473
|
+
let bucket = self.rows.entry(capability.clone()).or_default();
|
|
474
|
+
let previous = bucket.insert(key.clone(), row);
|
|
475
|
+
drop(mutation);
|
|
476
|
+
if !indexed {
|
|
477
|
+
return previous.is_none();
|
|
478
|
+
}
|
|
479
|
+
// From here on the work exists only because an index does. Measuring it
|
|
480
|
+
// apart from the record write is what makes "did index maintenance
|
|
481
|
+
// become a write-side cost?" a question with an answer.
|
|
482
|
+
let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
|
|
483
|
+
// Old entries first, then new ones. Doing it in this order means a
|
|
484
|
+
// record whose indexed field did not change is removed from and
|
|
485
|
+
// re-added to the same bucket, which is a no-op rather than a gap.
|
|
486
|
+
if let Some(previous) = &previous {
|
|
487
|
+
self.equality_index
|
|
488
|
+
.remove_record(&capability, &key, &previous.value);
|
|
489
|
+
}
|
|
490
|
+
// Disjoint field borrows: the record map is read while the index is
|
|
491
|
+
// written. Nothing is cloned to maintain the index.
|
|
492
|
+
let Inner {
|
|
493
|
+
rows,
|
|
494
|
+
equality_index,
|
|
495
|
+
..
|
|
496
|
+
} = self;
|
|
497
|
+
if let Some(current) = rows.get(&capability).and_then(|rows| rows.get(&key)) {
|
|
498
|
+
equality_index.insert_record(&capability, &key, ¤t.value);
|
|
499
|
+
}
|
|
500
|
+
previous.is_none()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/// Remove one record. Returns `true` when a record was actually removed.
|
|
504
|
+
fn remove_row(&mut self, capability: &str, key: &str) -> bool {
|
|
505
|
+
let mutation = workload_diagnostics::span(workload_diagnostics::Phase::RecordMutation);
|
|
506
|
+
let Some(removed) = self
|
|
507
|
+
.rows
|
|
508
|
+
.get_mut(capability)
|
|
509
|
+
.and_then(|bucket| bucket.remove(key))
|
|
510
|
+
else {
|
|
511
|
+
return false;
|
|
512
|
+
};
|
|
513
|
+
drop(mutation);
|
|
514
|
+
let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
|
|
515
|
+
self.equality_index
|
|
516
|
+
.remove_record(capability, key, &removed.value);
|
|
517
|
+
true
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/// Drop all records, as a snapshot install does. The index follows.
|
|
521
|
+
fn clear_rows(&mut self) {
|
|
522
|
+
self.rows.clear();
|
|
523
|
+
self.equality_index.clear();
|
|
524
|
+
}
|
|
355
525
|
}
|
|
356
526
|
|
|
357
527
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
@@ -601,11 +771,107 @@ pub struct StoredRow {
|
|
|
601
771
|
pub operation: Option<Operation>,
|
|
602
772
|
}
|
|
603
773
|
|
|
774
|
+
/// The durable format this build writes and understands.
|
|
775
|
+
///
|
|
776
|
+
/// Independent of the package version on purpose: an application release that
|
|
777
|
+
/// changes nothing about persistence must not appear to change the database.
|
|
778
|
+
///
|
|
779
|
+
/// - **1** — everything before revisions became resource-scoped. Never written
|
|
780
|
+
/// by any build; it names the era that predates format versioning.
|
|
781
|
+
/// - **2** — resource-scoped revisions: a `StateRevision` carries `resource`,
|
|
782
|
+
/// `content_id` and `sequence`.
|
|
783
|
+
pub const DURABLE_FORMAT_VERSION: u32 = 2;
|
|
784
|
+
|
|
785
|
+
/// The log record that carries the format version.
|
|
786
|
+
const FORMAT_RECORD_TYPE: &str = "feltdb.format.v1";
|
|
787
|
+
|
|
788
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
789
|
+
struct FormatRecord {
|
|
790
|
+
record_type: String,
|
|
791
|
+
format_version: u32,
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/// What format a durable database is in.
|
|
795
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
796
|
+
pub enum DurableFormat {
|
|
797
|
+
/// A format record states the version.
|
|
798
|
+
Versioned(u32),
|
|
799
|
+
/// No format record: the database predates format versioning.
|
|
800
|
+
///
|
|
801
|
+
/// This is a version, not an absence of one. It is the only case where the
|
|
802
|
+
/// records themselves are examined to decide compatibility, and that
|
|
803
|
+
/// examination is a deliberate one-time probe rather than a deserialization
|
|
804
|
+
/// attempt whose failure is discarded.
|
|
805
|
+
Unversioned,
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
impl Display for DurableFormat {
|
|
809
|
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
810
|
+
match self {
|
|
811
|
+
DurableFormat::Versioned(version) => write!(f, "version {version}"),
|
|
812
|
+
DurableFormat::Unversioned => write!(f, "unversioned (predates format versioning)"),
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/// Why a durable database cannot be opened, and what to do about it.
|
|
818
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
819
|
+
pub struct FormatIncompatibility {
|
|
820
|
+
/// What the database is.
|
|
821
|
+
pub found: DurableFormat,
|
|
822
|
+
/// What this build requires.
|
|
823
|
+
pub required: u32,
|
|
824
|
+
/// What is wrong, in terms of the data rather than the code.
|
|
825
|
+
pub reason: String,
|
|
826
|
+
/// What the operator can do.
|
|
827
|
+
pub action: String,
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
impl Display for FormatIncompatibility {
|
|
831
|
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
832
|
+
write!(
|
|
833
|
+
f,
|
|
834
|
+
"durable format {} is incompatible with this FeltDB, which requires \
|
|
835
|
+
format version {}; {}. No data was read or modified. {}",
|
|
836
|
+
self.found, self.required, self.reason, self.action
|
|
837
|
+
)
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/// The result of examining a durable database without opening it.
|
|
842
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
843
|
+
pub enum FormatCompatibility {
|
|
844
|
+
/// Safe to open.
|
|
845
|
+
Compatible(DurableFormat),
|
|
846
|
+
/// Must not be opened.
|
|
847
|
+
Incompatible(FormatIncompatibility),
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
impl FormatCompatibility {
|
|
851
|
+
/// Whether opening this database is safe.
|
|
852
|
+
pub fn is_compatible(&self) -> bool {
|
|
853
|
+
matches!(self, FormatCompatibility::Compatible(_))
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
604
857
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
605
858
|
pub struct DatabaseSnapshot {
|
|
606
859
|
pub rows: Vec<StoredRow>,
|
|
607
860
|
pub versions: HashMap<String, u64>,
|
|
608
861
|
pub content_hash: String,
|
|
862
|
+
/// The durable format the rows are in.
|
|
863
|
+
///
|
|
864
|
+
/// A snapshot carries whole `StoredRow`s, revisions included, so it is a
|
|
865
|
+
/// durable interchange artifact and not merely a protocol value: installing
|
|
866
|
+
/// one from a different format would reintroduce exactly the silent
|
|
867
|
+
/// misinterpretation the on-disk version exists to prevent.
|
|
868
|
+
///
|
|
869
|
+
/// Defaults to 0 so a snapshot serialized before versioning still
|
|
870
|
+
/// deserializes; 0 is treated as unversioned and refused rather than
|
|
871
|
+
/// assumed current. `content_hash` deliberately does not cover this field,
|
|
872
|
+
/// so hashes computed before it existed remain valid.
|
|
873
|
+
#[serde(default)]
|
|
874
|
+
pub format_version: u32,
|
|
609
875
|
}
|
|
610
876
|
|
|
611
877
|
impl DatabaseSnapshot {
|
|
@@ -633,6 +899,10 @@ struct LogHeader {
|
|
|
633
899
|
record_type: String,
|
|
634
900
|
local_sequence: u64,
|
|
635
901
|
observed_versions: HashMap<String, u64>,
|
|
902
|
+
/// Present since format versioning. Absent in headers written before it,
|
|
903
|
+
/// which deserialize as 0 and are classified as unversioned.
|
|
904
|
+
#[serde(default)]
|
|
905
|
+
format_version: u32,
|
|
636
906
|
}
|
|
637
907
|
|
|
638
908
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
@@ -646,14 +916,222 @@ pub fn open<P: AsRef<Path>>(path: P) -> Result<FeltDb> {
|
|
|
646
916
|
FeltDb::open(path)
|
|
647
917
|
}
|
|
648
918
|
|
|
919
|
+
/// Capabilities whose rows are not themselves versioned.
|
|
920
|
+
///
|
|
921
|
+
/// `state` holds the revisions, and `_retention` holds the watermark that says
|
|
922
|
+
/// how much of each resource's history survives. Minting a revision of either
|
|
923
|
+
/// would mint a revision of the mint.
|
|
924
|
+
const UNVERSIONED_CAPABILITIES: &[&str] = &["state", "_retention"];
|
|
925
|
+
|
|
926
|
+
/// Whether a write to this capability creates a revision.
|
|
927
|
+
fn is_versioned(capability: &str) -> bool {
|
|
928
|
+
!UNVERSIONED_CAPABILITIES.contains(&capability)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/// Rebuild the derived head index from the durable revisions.
|
|
932
|
+
///
|
|
933
|
+
/// Called after recovery, for the same reason `equality_index` is: the index is
|
|
934
|
+
/// a convenience over `rows` and must never be the authority for what `rows`
|
|
935
|
+
/// contains.
|
|
936
|
+
fn rebuild_revision_heads(inner: &mut Inner) {
|
|
937
|
+
let mut heads: HashMap<String, (state_model::StateId, u64)> = HashMap::new();
|
|
938
|
+
if let Some(bucket) = inner.rows.get("state") {
|
|
939
|
+
for row in bucket.values() {
|
|
940
|
+
if row.deleted || !row.key.starts_with("state:revision:") {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
let Ok(revision) =
|
|
944
|
+
serde_json::from_value::<state_model::StateRevision>(row.value.clone())
|
|
945
|
+
else {
|
|
946
|
+
continue;
|
|
947
|
+
};
|
|
948
|
+
let entry = heads
|
|
949
|
+
.entry(revision.resource.clone())
|
|
950
|
+
.or_insert_with(|| (revision.id.clone(), revision.sequence));
|
|
951
|
+
if revision.sequence >= entry.1 {
|
|
952
|
+
*entry = (revision.id.clone(), revision.sequence);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
inner.revision_heads = heads;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/// Write one derived row inside an already-held state lock.
|
|
960
|
+
///
|
|
961
|
+
/// Deliberately **not** a mutation: no sequence number, no vector clock tick,
|
|
962
|
+
/// no entry in the change log. A revision is a *derived* record of a mutation
|
|
963
|
+
/// that already has all three, and giving it its own would make every write
|
|
964
|
+
/// advance replication twice and double the operation stream.
|
|
965
|
+
///
|
|
966
|
+
/// It is still durable — the row is appended to the log and enters `rows`
|
|
967
|
+
/// inside the same lock as the state it records, so a reader never observes a
|
|
968
|
+
/// mutation whose revision is missing, and a reopened database has both.
|
|
969
|
+
///
|
|
970
|
+
/// The consequence is that revisions are **local and derived**, not replicated.
|
|
971
|
+
/// That is sound in principle, because a revision's identity is a function of
|
|
972
|
+
/// the resource, the content, the parent and the sequence — all of which a peer
|
|
973
|
+
/// that applies the same operation stream has. Whether the sync-apply path
|
|
974
|
+
/// actually mints is a separate question this change does not answer.
|
|
975
|
+
fn append_derived_row_locked(
|
|
976
|
+
inner: &mut Inner,
|
|
977
|
+
capability: &str,
|
|
978
|
+
key: &str,
|
|
979
|
+
value: Value,
|
|
980
|
+
) -> Result<()> {
|
|
981
|
+
let row = StoredRow {
|
|
982
|
+
capability: capability.to_string(),
|
|
983
|
+
key: key.to_string(),
|
|
984
|
+
rust_type: "feltdb::state_model::StateRevision".to_string(),
|
|
985
|
+
value,
|
|
986
|
+
unix_ms: now_ms(),
|
|
987
|
+
content_hash: None,
|
|
988
|
+
flow_ref: None,
|
|
989
|
+
deleted: false,
|
|
990
|
+
operation: None,
|
|
991
|
+
};
|
|
992
|
+
append_event(&inner.path, &row)?;
|
|
993
|
+
inner.put_row(row);
|
|
994
|
+
Ok(())
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/// Mint a revision for an authoritative write, under the caller's lock.
|
|
998
|
+
///
|
|
999
|
+
/// This is where the resource-scoped model meets the write path. The resource
|
|
1000
|
+
/// is the record's key, the parent is that resource's current head, and the
|
|
1001
|
+
/// sequence is allocated from the parent — so a resource that returns to a value
|
|
1002
|
+
/// it previously held records a *new* revision rather than resurrecting the old
|
|
1003
|
+
/// one.
|
|
1004
|
+
///
|
|
1005
|
+
/// Retention is applied here too, by the same decision function the explicit
|
|
1006
|
+
/// store API uses. Bounding history at the moment history is created is what
|
|
1007
|
+
/// keeps automatic minting from being an unbounded-history architecture.
|
|
1008
|
+
fn mint_revision_locked(inner: &mut Inner, resource: &str, content: String) -> Result<()> {
|
|
1009
|
+
let parent = inner.revision_heads.get(resource).cloned();
|
|
1010
|
+
let (parent_id, sequence) = match &parent {
|
|
1011
|
+
Some((id, sequence)) => (Some(id.clone()), sequence + 1),
|
|
1012
|
+
None => (None, 0),
|
|
1013
|
+
};
|
|
1014
|
+
let authority = inner.instance_id.clone();
|
|
1015
|
+
let revision = state_model::StateRevision::at(
|
|
1016
|
+
resource.to_string(),
|
|
1017
|
+
content,
|
|
1018
|
+
parent_id,
|
|
1019
|
+
sequence,
|
|
1020
|
+
authority,
|
|
1021
|
+
);
|
|
1022
|
+
let id = revision.id.clone();
|
|
1023
|
+
let key = format!("state:revision:{}", id.as_hex());
|
|
1024
|
+
let value = serde_json::to_value(&revision)?;
|
|
1025
|
+
append_derived_row_locked(inner, "state", &key, value)?;
|
|
1026
|
+
inner
|
|
1027
|
+
.revision_heads
|
|
1028
|
+
.insert(resource.to_string(), (id, sequence));
|
|
1029
|
+
apply_retention_locked(inner, resource)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/// Expire whatever the resource's configured policy no longer retains.
|
|
1033
|
+
///
|
|
1034
|
+
/// The common path is one map lookup: with no policy configured there is
|
|
1035
|
+
/// nothing to scan. A resource's history is only walked once a policy exists to
|
|
1036
|
+
/// bound it.
|
|
1037
|
+
fn apply_retention_locked(inner: &mut Inner, resource: &str) -> Result<()> {
|
|
1038
|
+
let retention_key = format!("_retention:{resource}");
|
|
1039
|
+
let Some(state_row) = inner
|
|
1040
|
+
.rows
|
|
1041
|
+
.get("_retention")
|
|
1042
|
+
.and_then(|bucket| bucket.get(&retention_key))
|
|
1043
|
+
else {
|
|
1044
|
+
return Ok(());
|
|
1045
|
+
};
|
|
1046
|
+
let Ok(mut state) = serde_json::from_value::<RetentionRecord>(state_row.value.clone()) else {
|
|
1047
|
+
return Ok(());
|
|
1048
|
+
};
|
|
1049
|
+
if state.policy.keep_last.is_none() {
|
|
1050
|
+
return Ok(());
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
let mut history: Vec<(state_model::StateId, u64)> = Vec::new();
|
|
1054
|
+
if let Some(bucket) = inner.rows.get("state") {
|
|
1055
|
+
for row in bucket.values() {
|
|
1056
|
+
if row.deleted || !row.key.starts_with("state:revision:") {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if let Ok(revision) =
|
|
1060
|
+
serde_json::from_value::<state_model::StateRevision>(row.value.clone())
|
|
1061
|
+
{
|
|
1062
|
+
if revision.resource == resource {
|
|
1063
|
+
history.push((revision.id, revision.sequence));
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
history.sort_by_key(|(_, sequence)| *sequence);
|
|
1069
|
+
|
|
1070
|
+
let (expire, horizon) = state_model::revisions_to_expire(&history, state.policy.keep_last);
|
|
1071
|
+
if expire.is_empty() {
|
|
1072
|
+
return Ok(());
|
|
1073
|
+
}
|
|
1074
|
+
for id in &expire {
|
|
1075
|
+
let key = format!("state:revision:{}", id.as_hex());
|
|
1076
|
+
if let Some(bucket) = inner.rows.get_mut("state") {
|
|
1077
|
+
bucket.remove(&key);
|
|
1078
|
+
}
|
|
1079
|
+
let tombstone = StoredRow {
|
|
1080
|
+
capability: "state".to_string(),
|
|
1081
|
+
key,
|
|
1082
|
+
rust_type: "feltdb::state_model::StateRevision".to_string(),
|
|
1083
|
+
value: Value::Null,
|
|
1084
|
+
unix_ms: now_ms(),
|
|
1085
|
+
content_hash: None,
|
|
1086
|
+
flow_ref: None,
|
|
1087
|
+
deleted: true,
|
|
1088
|
+
operation: None,
|
|
1089
|
+
};
|
|
1090
|
+
append_event(&inner.path, &tombstone)?;
|
|
1091
|
+
}
|
|
1092
|
+
if let Some(horizon) = horizon {
|
|
1093
|
+
state.horizon = horizon;
|
|
1094
|
+
}
|
|
1095
|
+
let value = serde_json::to_value(&state)?;
|
|
1096
|
+
append_derived_row_locked(inner, "_retention", &retention_key, value)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/// The durable shape of a resource's retention state.
|
|
1100
|
+
///
|
|
1101
|
+
/// Mirrors `state_model`'s private record so the write boundary can read and
|
|
1102
|
+
/// advance the horizon without the store type.
|
|
1103
|
+
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
|
1104
|
+
struct RetentionRecord {
|
|
1105
|
+
policy: state_model::RetentionPolicy,
|
|
1106
|
+
horizon: u64,
|
|
1107
|
+
}
|
|
1108
|
+
|
|
649
1109
|
impl FeltDb {
|
|
650
1110
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
651
1111
|
let path = path.as_ref().to_path_buf();
|
|
1112
|
+
|
|
1113
|
+
// Fail closed. The format is decided before any record is interpreted
|
|
1114
|
+
// and before anything is written, so a database this build does not
|
|
1115
|
+
// understand is refused with its bytes untouched rather than opened
|
|
1116
|
+
// with whatever happened to deserialize.
|
|
1117
|
+
if let FormatCompatibility::Incompatible(incompatibility) = inspect_durable_format(&path)? {
|
|
1118
|
+
return Err(FlowError::IncompatibleFormat(Box::new(incompatibility)));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
652
1121
|
if let Some(parent) = path.parent() {
|
|
653
1122
|
fs::create_dir_all(parent)?;
|
|
654
1123
|
}
|
|
655
1124
|
if !path.exists() {
|
|
656
|
-
OpenOptions::new().create(true).append(true).open(&path)?;
|
|
1125
|
+
let mut file = OpenOptions::new().create(true).append(true).open(&path)?;
|
|
1126
|
+
// A new database states its format in its first record, so it never
|
|
1127
|
+
// has to be inferred again.
|
|
1128
|
+
write_json_line(
|
|
1129
|
+
&mut file,
|
|
1130
|
+
&FormatRecord {
|
|
1131
|
+
record_type: FORMAT_RECORD_TYPE.to_string(),
|
|
1132
|
+
format_version: DURABLE_FORMAT_VERSION,
|
|
1133
|
+
},
|
|
1134
|
+
)?;
|
|
657
1135
|
}
|
|
658
1136
|
|
|
659
1137
|
// Generate instance ID from path hash
|
|
@@ -673,6 +1151,7 @@ impl FeltDb {
|
|
|
673
1151
|
inner.sync_state = SyncState::new(instance_id);
|
|
674
1152
|
replay_log(&mut inner)?;
|
|
675
1153
|
load_sync_metadata(&mut inner)?;
|
|
1154
|
+
rebuild_revision_heads(&mut inner);
|
|
676
1155
|
|
|
677
1156
|
let (event_tx, _) = broadcast::channel(1024);
|
|
678
1157
|
|
|
@@ -722,7 +1201,7 @@ impl FeltDb {
|
|
|
722
1201
|
.to_string();
|
|
723
1202
|
let rust_type = type_name::<Value>().to_string();
|
|
724
1203
|
let (result, event) = {
|
|
725
|
-
let mut inner = self.
|
|
1204
|
+
let mut inner = self.state();
|
|
726
1205
|
let Some(current) = inner.rows.get(&capability).and_then(|rows| rows.get(key)) else {
|
|
727
1206
|
return Ok(JsonCasResult::NotFound);
|
|
728
1207
|
};
|
|
@@ -822,11 +1301,10 @@ impl FeltDb {
|
|
|
822
1301
|
append_event(&inner.path, &row)?;
|
|
823
1302
|
inner.authority_revision += 1;
|
|
824
1303
|
inner.change_log.add_operation(operation);
|
|
825
|
-
inner
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
.insert(key.to_string(), row.clone());
|
|
1304
|
+
inner.put_row(row.clone());
|
|
1305
|
+
if is_versioned(&capability) {
|
|
1306
|
+
mint_revision_locked(&mut inner, key, value.to_string())?;
|
|
1307
|
+
}
|
|
830
1308
|
let event = ChangeEvent {
|
|
831
1309
|
capability,
|
|
832
1310
|
key: key.to_string(),
|
|
@@ -975,7 +1453,7 @@ impl FeltDb {
|
|
|
975
1453
|
.map(|(cap, _)| cap)
|
|
976
1454
|
.unwrap_or("default")
|
|
977
1455
|
.to_string();
|
|
978
|
-
let inner = self.
|
|
1456
|
+
let inner = self.state();
|
|
979
1457
|
let value = inner
|
|
980
1458
|
.rows
|
|
981
1459
|
.get(&capability)
|
|
@@ -991,7 +1469,7 @@ impl FeltDb {
|
|
|
991
1469
|
F: Fn(&T) -> bool,
|
|
992
1470
|
{
|
|
993
1471
|
let rust_type = type_name::<T>().to_string();
|
|
994
|
-
let mut inner = self.
|
|
1472
|
+
let mut inner = self.state();
|
|
995
1473
|
*inner
|
|
996
1474
|
.query_count_by_type
|
|
997
1475
|
.entry(rust_type.clone())
|
|
@@ -1024,19 +1502,19 @@ impl FeltDb {
|
|
|
1024
1502
|
|
|
1025
1503
|
pub fn is_auto_indexed<T>(&self) -> bool {
|
|
1026
1504
|
let rust_type = type_name::<T>().to_string();
|
|
1027
|
-
let inner = self.
|
|
1505
|
+
let inner = self.state();
|
|
1028
1506
|
inner.adaptive_indexes.contains(&rust_type)
|
|
1029
1507
|
}
|
|
1030
1508
|
|
|
1031
1509
|
/// Get the instance ID for this FeltDB instance
|
|
1032
1510
|
pub fn instance_id(&self) -> Result<String> {
|
|
1033
|
-
let inner = self.
|
|
1511
|
+
let inner = self.state();
|
|
1034
1512
|
Ok(inner.instance_id.clone())
|
|
1035
1513
|
}
|
|
1036
1514
|
|
|
1037
1515
|
/// Get the current sequence number for this instance
|
|
1038
1516
|
pub fn sequence(&self) -> Result<u64> {
|
|
1039
|
-
let inner = self.
|
|
1517
|
+
let inner = self.state();
|
|
1040
1518
|
Ok(inner.sequence)
|
|
1041
1519
|
}
|
|
1042
1520
|
|
|
@@ -1044,13 +1522,13 @@ impl FeltDb {
|
|
|
1044
1522
|
/// In multi-process topology callers must use `AuthorityClient::read` to
|
|
1045
1523
|
/// cross the authority boundary and obtain a current authoritative value.
|
|
1046
1524
|
pub fn current_revision(&self) -> Result<u64> {
|
|
1047
|
-
Ok(self.
|
|
1525
|
+
Ok(self.state().authority_revision)
|
|
1048
1526
|
}
|
|
1049
1527
|
|
|
1050
1528
|
/// Capture revision and rows under one lock. Authority reads must never
|
|
1051
1529
|
/// pair metadata from one commit with state from another.
|
|
1052
1530
|
pub(crate) fn authority_state(&self) -> (u64, Vec<StoredRow>) {
|
|
1053
|
-
let inner = self.
|
|
1531
|
+
let inner = self.state();
|
|
1054
1532
|
let rows = inner
|
|
1055
1533
|
.rows
|
|
1056
1534
|
.values()
|
|
@@ -1062,7 +1540,7 @@ impl FeltDb {
|
|
|
1062
1540
|
/// Get the maintained cardinality (record count) for a collection capability.
|
|
1063
1541
|
/// Returns the count of non-deleted records in the collection.
|
|
1064
1542
|
pub fn collection_cardinality(&self, capability: &str) -> Result<u64> {
|
|
1065
|
-
let inner = self.
|
|
1543
|
+
let inner = self.state();
|
|
1066
1544
|
Ok(inner
|
|
1067
1545
|
.collection_cardinality
|
|
1068
1546
|
.get(capability)
|
|
@@ -1073,7 +1551,7 @@ impl FeltDb {
|
|
|
1073
1551
|
/// Diagnostic: List all collection capabilities and their cardinalities.
|
|
1074
1552
|
/// Used to detect capability format mismatches.
|
|
1075
1553
|
pub fn list_cardinalities(&self) -> Result<Vec<(String, u64)>> {
|
|
1076
|
-
let inner = self.
|
|
1554
|
+
let inner = self.state();
|
|
1077
1555
|
let mut result: Vec<_> = inner
|
|
1078
1556
|
.collection_cardinality
|
|
1079
1557
|
.iter()
|
|
@@ -1086,7 +1564,7 @@ impl FeltDb {
|
|
|
1086
1564
|
/// Diagnostic: Count actual rows for a given capability pattern.
|
|
1087
1565
|
/// Used to verify that rows exist even if cardinality is 0.
|
|
1088
1566
|
pub fn diagnostic_row_count(&self, capability_pattern: &str) -> Result<usize> {
|
|
1089
|
-
let inner = self.
|
|
1567
|
+
let inner = self.state();
|
|
1090
1568
|
Ok(inner
|
|
1091
1569
|
.rows
|
|
1092
1570
|
.iter()
|
|
@@ -1098,7 +1576,7 @@ impl FeltDb {
|
|
|
1098
1576
|
/// Diagnostic: Get actual persisted capability strings matching a pattern.
|
|
1099
1577
|
/// Used to identify capability identity formats in use.
|
|
1100
1578
|
pub fn diagnostic_capability_keys(&self, capability_pattern: &str) -> Result<Vec<String>> {
|
|
1101
|
-
let inner = self.
|
|
1579
|
+
let inner = self.state();
|
|
1102
1580
|
let mut keys: Vec<_> = inner
|
|
1103
1581
|
.rows
|
|
1104
1582
|
.keys()
|
|
@@ -1195,7 +1673,7 @@ impl FeltDb {
|
|
|
1195
1673
|
audit: Option<Value>,
|
|
1196
1674
|
) -> Result<AtomicCommit> {
|
|
1197
1675
|
let (commit, events) = {
|
|
1198
|
-
let mut inner = self.
|
|
1676
|
+
let mut inner = self.state();
|
|
1199
1677
|
if inner.applied_transactions.contains(transaction_id) {
|
|
1200
1678
|
if let Some(expected_hash) = payload_hash {
|
|
1201
1679
|
if inner
|
|
@@ -1385,6 +1863,14 @@ impl FeltDb {
|
|
|
1385
1863
|
&advanced
|
|
1386
1864
|
};
|
|
1387
1865
|
|
|
1866
|
+
// Transaction bookkeeping is measured as one span per commit even
|
|
1867
|
+
// though it brackets two regions: everything before the durable
|
|
1868
|
+
// append, and the revision, dedup and payload-hash accounting after
|
|
1869
|
+
// the rows are applied. The append itself is `Persistence` and the
|
|
1870
|
+
// apply loop is `RecordMutation` + `IndexMaintenance`, so summing
|
|
1871
|
+
// this with those does not double-count any of them.
|
|
1872
|
+
let mut bookkeeping_nanos = 0u64;
|
|
1873
|
+
let prepare_began = workload_diagnostics::enabled().then(std::time::Instant::now);
|
|
1388
1874
|
let base_revision = inner.authority_revision;
|
|
1389
1875
|
if let Some(expected) = expected_parent {
|
|
1390
1876
|
if expected != base_revision {
|
|
@@ -1480,6 +1966,9 @@ impl FeltDb {
|
|
|
1480
1966
|
rows: rows.clone(),
|
|
1481
1967
|
audit,
|
|
1482
1968
|
};
|
|
1969
|
+
if let Some(began) = prepare_began {
|
|
1970
|
+
bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
|
1971
|
+
}
|
|
1483
1972
|
append_transaction(&inner.path, &record)?;
|
|
1484
1973
|
inner.authority_revision = base_revision + 1;
|
|
1485
1974
|
inner.sequence = next_sequence;
|
|
@@ -1488,9 +1977,7 @@ impl FeltDb {
|
|
|
1488
1977
|
inner.change_log.add_operation(operation);
|
|
1489
1978
|
}
|
|
1490
1979
|
if row.deleted {
|
|
1491
|
-
|
|
1492
|
-
bucket.remove(&row.key);
|
|
1493
|
-
}
|
|
1980
|
+
inner.remove_row(&row.capability, &row.key);
|
|
1494
1981
|
// Decrement cardinality on delete
|
|
1495
1982
|
let count = inner
|
|
1496
1983
|
.collection_cardinality
|
|
@@ -1498,17 +1985,10 @@ impl FeltDb {
|
|
|
1498
1985
|
.or_default();
|
|
1499
1986
|
*count = count.saturating_sub(1);
|
|
1500
1987
|
} else {
|
|
1501
|
-
//
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
.is_some_and(|bucket| bucket.contains_key(&row.key));
|
|
1506
|
-
|
|
1507
|
-
inner
|
|
1508
|
-
.rows
|
|
1509
|
-
.entry(row.capability.clone())
|
|
1510
|
-
.or_default()
|
|
1511
|
-
.insert(row.key.clone(), row.clone());
|
|
1988
|
+
// Record and index change together, inside the commit that
|
|
1989
|
+
// is already holding the state lock: a transaction that is
|
|
1990
|
+
// refused leaves neither, and one that commits publishes both.
|
|
1991
|
+
let is_insert = inner.put_row(row.clone());
|
|
1512
1992
|
|
|
1513
1993
|
// Increment cardinality only on insert, not update
|
|
1514
1994
|
if is_insert {
|
|
@@ -1520,6 +2000,7 @@ impl FeltDb {
|
|
|
1520
2000
|
}
|
|
1521
2001
|
}
|
|
1522
2002
|
}
|
|
2003
|
+
let tail_began = workload_diagnostics::enabled().then(std::time::Instant::now);
|
|
1523
2004
|
inner.sync_state.merge_vector_clock(&vector_clock);
|
|
1524
2005
|
inner.applied_transactions.insert(transaction_id.into());
|
|
1525
2006
|
let commit_revision = inner.authority_revision;
|
|
@@ -1531,6 +2012,13 @@ impl FeltDb {
|
|
|
1531
2012
|
.transaction_payload_hashes
|
|
1532
2013
|
.insert(transaction_id.into(), hash.into());
|
|
1533
2014
|
}
|
|
2015
|
+
if let Some(began) = tail_began {
|
|
2016
|
+
bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
|
2017
|
+
}
|
|
2018
|
+
workload_diagnostics::record(
|
|
2019
|
+
workload_diagnostics::Phase::TransactionCommit,
|
|
2020
|
+
bookkeeping_nanos,
|
|
2021
|
+
);
|
|
1534
2022
|
let events = rows
|
|
1535
2023
|
.iter()
|
|
1536
2024
|
.map(|row| ChangeEvent {
|
|
@@ -1603,6 +2091,9 @@ impl FeltDb {
|
|
|
1603
2091
|
) -> Result<()> {
|
|
1604
2092
|
let rust_type = type_name::<T>().to_string();
|
|
1605
2093
|
let value_json = serde_json::to_value(value)?;
|
|
2094
|
+
// Canonical because `serde_json` is built without `preserve_order`, so
|
|
2095
|
+
// `Map` is a `BTreeMap` and `to_string` emits keys in sorted order.
|
|
2096
|
+
let canonical = value_json.to_string();
|
|
1606
2097
|
let mut row = StoredRow {
|
|
1607
2098
|
capability: capability.clone(),
|
|
1608
2099
|
key: key.clone(),
|
|
@@ -1616,7 +2107,7 @@ impl FeltDb {
|
|
|
1616
2107
|
};
|
|
1617
2108
|
|
|
1618
2109
|
{
|
|
1619
|
-
let mut inner = self.
|
|
2110
|
+
let mut inner = self.state();
|
|
1620
2111
|
|
|
1621
2112
|
// Increment sequence number
|
|
1622
2113
|
inner.sequence += 1;
|
|
@@ -1645,11 +2136,10 @@ impl FeltDb {
|
|
|
1645
2136
|
|
|
1646
2137
|
append_event(&inner.path, &row)?;
|
|
1647
2138
|
inner.authority_revision += 1;
|
|
1648
|
-
inner
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
.insert(key.clone(), row);
|
|
2139
|
+
inner.put_row(row);
|
|
2140
|
+
if is_versioned(&capability) {
|
|
2141
|
+
mint_revision_locked(&mut inner, &key, canonical)?;
|
|
2142
|
+
}
|
|
1653
2143
|
let suffix = key.rsplit_once(':').map(|(_, right)| right);
|
|
1654
2144
|
if let Some(id) = suffix.and_then(|s| s.parse::<u64>().ok()) {
|
|
1655
2145
|
let entry = inner.key_counters.entry(capability.clone()).or_default();
|
|
@@ -1676,6 +2166,9 @@ impl FeltDb {
|
|
|
1676
2166
|
) -> Result<()> {
|
|
1677
2167
|
let rust_type = type_name::<T>().to_string();
|
|
1678
2168
|
let value_json = serde_json::to_value(value)?;
|
|
2169
|
+
// Canonical because `serde_json` is built without `preserve_order`, so
|
|
2170
|
+
// `Map` is a `BTreeMap` and `to_string` emits keys in sorted order.
|
|
2171
|
+
let canonical = value_json.to_string();
|
|
1679
2172
|
let mut row = StoredRow {
|
|
1680
2173
|
capability: capability.clone(),
|
|
1681
2174
|
key: key.clone(),
|
|
@@ -1689,7 +2182,7 @@ impl FeltDb {
|
|
|
1689
2182
|
};
|
|
1690
2183
|
|
|
1691
2184
|
{
|
|
1692
|
-
let mut inner = self.
|
|
2185
|
+
let mut inner = self.state();
|
|
1693
2186
|
|
|
1694
2187
|
// Increment sequence number
|
|
1695
2188
|
inner.sequence += 1;
|
|
@@ -1718,11 +2211,10 @@ impl FeltDb {
|
|
|
1718
2211
|
|
|
1719
2212
|
append_event(&inner.path, &row)?;
|
|
1720
2213
|
inner.authority_revision += 1;
|
|
1721
|
-
inner
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
.insert(key.clone(), row);
|
|
2214
|
+
inner.put_row(row);
|
|
2215
|
+
if is_versioned(&capability) {
|
|
2216
|
+
mint_revision_locked(&mut inner, &key, canonical)?;
|
|
2217
|
+
}
|
|
1726
2218
|
}
|
|
1727
2219
|
|
|
1728
2220
|
let event = ChangeEvent {
|
|
@@ -1738,7 +2230,7 @@ impl FeltDb {
|
|
|
1738
2230
|
|
|
1739
2231
|
fn delete_internal(&self, capability: String, key: String) -> Result<()> {
|
|
1740
2232
|
{
|
|
1741
|
-
let mut inner = self.
|
|
2233
|
+
let mut inner = self.state();
|
|
1742
2234
|
|
|
1743
2235
|
// Get the row to determine its type
|
|
1744
2236
|
let rust_type = inner
|
|
@@ -1788,10 +2280,8 @@ impl FeltDb {
|
|
|
1788
2280
|
append_event(&inner.path, &tombstone)?;
|
|
1789
2281
|
inner.authority_revision += 1;
|
|
1790
2282
|
|
|
1791
|
-
// Remove from in-memory store
|
|
1792
|
-
|
|
1793
|
-
bucket.remove(&key);
|
|
1794
|
-
}
|
|
2283
|
+
// Remove from in-memory store, index entries included.
|
|
2284
|
+
inner.remove_row(&capability, &key);
|
|
1795
2285
|
}
|
|
1796
2286
|
|
|
1797
2287
|
let event = ChangeEvent {
|
|
@@ -1831,7 +2321,7 @@ impl FeltDb {
|
|
|
1831
2321
|
let rust_type = type_name::<T>().to_string();
|
|
1832
2322
|
|
|
1833
2323
|
let initial_results = {
|
|
1834
|
-
let mut inner = self.
|
|
2324
|
+
let mut inner = self.state();
|
|
1835
2325
|
*inner
|
|
1836
2326
|
.query_count_by_type
|
|
1837
2327
|
.entry(rust_type.clone())
|
|
@@ -1870,7 +2360,7 @@ impl FeltDb {
|
|
|
1870
2360
|
|
|
1871
2361
|
/// Get current sync state information
|
|
1872
2362
|
pub fn sync_info(&self) -> Result<SyncState> {
|
|
1873
|
-
let inner = self.
|
|
2363
|
+
let inner = self.state();
|
|
1874
2364
|
Ok(inner.sync_state.clone())
|
|
1875
2365
|
}
|
|
1876
2366
|
|
|
@@ -1880,13 +2370,330 @@ impl FeltDb {
|
|
|
1880
2370
|
}
|
|
1881
2371
|
|
|
1882
2372
|
/// List the canonical values in one collection/capability namespace.
|
|
2373
|
+
///
|
|
2374
|
+
/// This clones the whole collection, so its cost is linear in collection
|
|
2375
|
+
/// size. Readers that discard most of what they receive — a filtered query,
|
|
2376
|
+
/// for example — should use [`FeltDb::query_collection`] instead, which
|
|
2377
|
+
/// evaluates borrowed rows and clones only what it keeps.
|
|
1883
2378
|
pub fn list_collection(&self, capability: &str) -> Result<Vec<StoredRow>> {
|
|
1884
|
-
let inner = self.
|
|
1885
|
-
|
|
2379
|
+
let inner = self.state();
|
|
2380
|
+
let rows: Vec<StoredRow> = inner
|
|
1886
2381
|
.rows
|
|
1887
2382
|
.get(capability)
|
|
1888
2383
|
.map(|rows| rows.values().cloned().collect())
|
|
1889
|
-
.unwrap_or_default()
|
|
2384
|
+
.unwrap_or_default();
|
|
2385
|
+
query_execution_diagnostics::record_full_materialization(rows.len());
|
|
2386
|
+
Ok(rows)
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
/// Evaluate a predicate over one collection without materializing it.
|
|
2390
|
+
///
|
|
2391
|
+
/// The predicate observes each live row *borrowed* from state, so a record
|
|
2392
|
+
/// the predicate rejects is never cloned. Only retained rows are cloned,
|
|
2393
|
+
/// which happens while state protection is still held: the returned rows are
|
|
2394
|
+
/// owned, so nothing the caller keeps can reference storage a concurrent
|
|
2395
|
+
/// mutation may move or drop after the lock is released.
|
|
2396
|
+
///
|
|
2397
|
+
/// Traversal is the collection's own record-key order, the same order
|
|
2398
|
+
/// [`FeltDb::list_collection_page`] walks. `limit` bounds accumulation and
|
|
2399
|
+
/// stops the traversal as soon as it is satisfied; because early stopping
|
|
2400
|
+
/// truncates in record-key order, a caller that must order results by some
|
|
2401
|
+
/// other field has to pass `None` and bound its own already-filtered set.
|
|
2402
|
+
///
|
|
2403
|
+
/// The predicate runs under the state lock, so it must not call back into
|
|
2404
|
+
/// the database or block.
|
|
2405
|
+
pub fn query_collection<F>(
|
|
2406
|
+
&self,
|
|
2407
|
+
capability: &str,
|
|
2408
|
+
limit: Option<usize>,
|
|
2409
|
+
mut predicate: F,
|
|
2410
|
+
) -> Result<Vec<StoredRow>>
|
|
2411
|
+
where
|
|
2412
|
+
F: FnMut(&StoredRow) -> bool,
|
|
2413
|
+
{
|
|
2414
|
+
let mut matched = Vec::new();
|
|
2415
|
+
let mut visited = 0usize;
|
|
2416
|
+
{
|
|
2417
|
+
let inner = self.state();
|
|
2418
|
+
let _query = workload_diagnostics::span(workload_diagnostics::Phase::ScanQuery);
|
|
2419
|
+
if let Some(rows) = inner.rows.get(capability) {
|
|
2420
|
+
for row in rows.values() {
|
|
2421
|
+
if limit.is_some_and(|bound| matched.len() >= bound) {
|
|
2422
|
+
break;
|
|
2423
|
+
}
|
|
2424
|
+
visited += 1;
|
|
2425
|
+
// The same predicate phase the indexed execution reports to,
|
|
2426
|
+
// so the two executions' predicate cost is directly
|
|
2427
|
+
// comparable rather than being two different measurements.
|
|
2428
|
+
let _predicate = workload_diagnostics::span(
|
|
2429
|
+
workload_diagnostics::Phase::PredicateEvaluation,
|
|
2430
|
+
);
|
|
2431
|
+
if predicate(row) {
|
|
2432
|
+
drop(_predicate);
|
|
2433
|
+
matched.push(row.clone());
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
query_execution_diagnostics::record_scan(visited, matched.len());
|
|
2439
|
+
Ok(matched)
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// ----------------------------------------------------------------------
|
|
2443
|
+
// Equality index — lifecycle and execution.
|
|
2444
|
+
//
|
|
2445
|
+
// The index is derived state. Every method below either declares what is
|
|
2446
|
+
// derived, re-derives it from authoritative records, or reads candidate
|
|
2447
|
+
// record keys out of it. None of them is a source of truth, and none of
|
|
2448
|
+
// them can answer a query on its own: `query_collection_by_equality` still
|
|
2449
|
+
// evaluates the caller's full predicate against authoritative records.
|
|
2450
|
+
// ----------------------------------------------------------------------
|
|
2451
|
+
|
|
2452
|
+
/// Acquire the state lock, attributing the wait and the hold.
|
|
2453
|
+
///
|
|
2454
|
+
/// Every path in this file that touches authoritative state goes through
|
|
2455
|
+
/// here, which is what makes lock contention a measured quantity rather than
|
|
2456
|
+
/// a residual inferred from wall-clock time.
|
|
2457
|
+
fn state(&self) -> StateGuard<'_> {
|
|
2458
|
+
if !workload_diagnostics::enabled() {
|
|
2459
|
+
return StateGuard {
|
|
2460
|
+
inner: self.inner.lock().expect("lock poisoned"),
|
|
2461
|
+
held_since: None,
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
let requested = std::time::Instant::now();
|
|
2465
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
2466
|
+
let acquired = std::time::Instant::now();
|
|
2467
|
+
workload_diagnostics::record(
|
|
2468
|
+
workload_diagnostics::Phase::StateLockWait,
|
|
2469
|
+
acquired
|
|
2470
|
+
.duration_since(requested)
|
|
2471
|
+
.as_nanos()
|
|
2472
|
+
.min(u64::MAX as u128) as u64,
|
|
2473
|
+
);
|
|
2474
|
+
StateGuard {
|
|
2475
|
+
inner,
|
|
2476
|
+
held_since: Some(acquired),
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
/// Declare an equality index on one collection field and populate it from
|
|
2481
|
+
/// authoritative state. Returns whether this call created the index.
|
|
2482
|
+
///
|
|
2483
|
+
/// Creation and population happen under one lock, so an index is never
|
|
2484
|
+
/// observable in a half-built state. Declaring an index that already exists
|
|
2485
|
+
/// is not an error and does not rebuild it.
|
|
2486
|
+
///
|
|
2487
|
+
/// `recordId` is refused: on the query surface it is authority metadata that
|
|
2488
|
+
/// shadows a document field of the same name on object-valued records only,
|
|
2489
|
+
/// so an index over the document field would answer a different question
|
|
2490
|
+
/// from the one the predicate asks.
|
|
2491
|
+
pub fn create_equality_index(&self, collection: &str, field: &str) -> Result<bool> {
|
|
2492
|
+
let mut inner = self.state();
|
|
2493
|
+
let created = inner
|
|
2494
|
+
.equality_index
|
|
2495
|
+
.declare(collection, field)
|
|
2496
|
+
.map_err(FlowError::CapabilityError)?;
|
|
2497
|
+
if created {
|
|
2498
|
+
let Inner {
|
|
2499
|
+
rows,
|
|
2500
|
+
equality_index,
|
|
2501
|
+
..
|
|
2502
|
+
} = &mut *inner;
|
|
2503
|
+
equality_index.rebuild_field(collection, field, rows);
|
|
2504
|
+
}
|
|
2505
|
+
Ok(created)
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
/// Drop an equality index and its buckets. Returns whether one existed.
|
|
2509
|
+
pub fn drop_equality_index(&self, collection: &str, field: &str) -> Result<bool> {
|
|
2510
|
+
let mut inner = self.state();
|
|
2511
|
+
Ok(inner.equality_index.undeclare(collection, field))
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/// Every declared index, as `(collection, field)` in a stable order.
|
|
2515
|
+
pub fn equality_indexes(&self) -> Result<Vec<(String, String)>> {
|
|
2516
|
+
let inner = self.state();
|
|
2517
|
+
Ok(inner.equality_index.declarations())
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/// Re-derive every declared index from authoritative records.
|
|
2521
|
+
///
|
|
2522
|
+
/// This is the recovery path. An index is never read from disk and never
|
|
2523
|
+
/// received from a peer: after a restart, a snapshot install, or any other
|
|
2524
|
+
/// reload of authoritative state, the index is whatever this produces.
|
|
2525
|
+
pub fn rebuild_equality_indexes(&self) -> Result<EqualityIndexStats> {
|
|
2526
|
+
let mut inner = self.state();
|
|
2527
|
+
let Inner {
|
|
2528
|
+
rows,
|
|
2529
|
+
equality_index,
|
|
2530
|
+
..
|
|
2531
|
+
} = &mut *inner;
|
|
2532
|
+
equality_index.rebuild(rows);
|
|
2533
|
+
Ok(equality_index.stats())
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/// Index size, as counts. Never a value, never a record id.
|
|
2537
|
+
pub fn equality_index_stats(&self) -> Result<EqualityIndexStats> {
|
|
2538
|
+
let inner = self.state();
|
|
2539
|
+
Ok(inner.equality_index.stats())
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
/// A fully ordered rendering of the live index.
|
|
2543
|
+
///
|
|
2544
|
+
/// Test instrumentation. It exists so a test can compare the live index
|
|
2545
|
+
/// against one rebuilt from authoritative state; nothing in execution reads
|
|
2546
|
+
/// it, and it is not part of any product surface.
|
|
2547
|
+
#[doc(hidden)]
|
|
2548
|
+
pub fn equality_index_snapshot(
|
|
2549
|
+
&self,
|
|
2550
|
+
) -> Result<BTreeMap<String, BTreeMap<String, BTreeMap<String, Vec<String>>>>> {
|
|
2551
|
+
let inner = self.state();
|
|
2552
|
+
Ok(inner.equality_index.snapshot())
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/// The index invariant: the live index equals one derived from records now.
|
|
2556
|
+
///
|
|
2557
|
+
/// Returns a description of the first divergence rather than a bare bool, so
|
|
2558
|
+
/// a failing assertion says what diverged. This is the strongest statement
|
|
2559
|
+
/// available about index maintenance, because it compares the maintained
|
|
2560
|
+
/// structure against the authoritative state it claims to describe rather
|
|
2561
|
+
/// than against a second copy of its own bookkeeping.
|
|
2562
|
+
pub fn verify_equality_index(&self) -> Result<std::result::Result<(), String>> {
|
|
2563
|
+
let inner = self.state();
|
|
2564
|
+
let live = inner.equality_index.snapshot();
|
|
2565
|
+
let mut expected = inner.equality_index.clone();
|
|
2566
|
+
expected.clear();
|
|
2567
|
+
expected.rebuild(&inner.rows);
|
|
2568
|
+
let expected = expected.snapshot();
|
|
2569
|
+
if live == expected {
|
|
2570
|
+
return Ok(Ok(()));
|
|
2571
|
+
}
|
|
2572
|
+
for (collection, fields) in expected.iter() {
|
|
2573
|
+
for (field, buckets) in fields {
|
|
2574
|
+
let observed = live.get(collection).and_then(|fields| fields.get(field));
|
|
2575
|
+
if observed != Some(buckets) {
|
|
2576
|
+
return Ok(Err(format!(
|
|
2577
|
+
"index for {collection}.{field} diverges from authoritative state: \
|
|
2578
|
+
{} value buckets held, {} derived",
|
|
2579
|
+
observed.map(BTreeMap::len).unwrap_or(0),
|
|
2580
|
+
buckets.len(),
|
|
2581
|
+
)));
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
Ok(Err(
|
|
2586
|
+
"the live index holds entries no authoritative record derives".to_string(),
|
|
2587
|
+
))
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
/// Candidate record keys for one equality condition, copied out of the index.
|
|
2591
|
+
///
|
|
2592
|
+
/// `None` means the index cannot answer that condition — the field is not
|
|
2593
|
+
/// indexed, or the value is not an indexable scalar — and the caller must
|
|
2594
|
+
/// fall back to a scan. `Some(empty)` is an answer, not a failure.
|
|
2595
|
+
///
|
|
2596
|
+
/// The keys are copied while the state lock is held. Nothing borrowed from
|
|
2597
|
+
/// the index or from the record map escapes this call.
|
|
2598
|
+
pub fn equality_index_candidates(
|
|
2599
|
+
&self,
|
|
2600
|
+
collection: &str,
|
|
2601
|
+
field: &str,
|
|
2602
|
+
value: &Value,
|
|
2603
|
+
) -> Result<Option<Vec<String>>> {
|
|
2604
|
+
let inner = self.state();
|
|
2605
|
+
Ok(inner
|
|
2606
|
+
.equality_index
|
|
2607
|
+
.candidates(collection, field, value)
|
|
2608
|
+
.map(|keys| keys.iter().cloned().collect()))
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
/// Execute a bounded query through the equality index when one applies.
|
|
2612
|
+
///
|
|
2613
|
+
/// `equalities` are the query's equality conditions, in the caller's order.
|
|
2614
|
+
/// At least one of them must be indexed and hold an indexable value, or this
|
|
2615
|
+
/// returns `Ok(None)` and the caller falls back to [`FeltDb::query_collection`].
|
|
2616
|
+
/// Returning `None` rather than an empty result is the difference between
|
|
2617
|
+
/// "the index does not apply" and "nothing matches", and conflating the two
|
|
2618
|
+
/// would silently drop records.
|
|
2619
|
+
///
|
|
2620
|
+
/// When the index does apply, every applicable condition contributes a
|
|
2621
|
+
/// candidate set and the sets are intersected smallest-first. The result is
|
|
2622
|
+
/// still only *candidates*: `predicate` — the caller's existing, unmodified
|
|
2623
|
+
/// predicate — decides what matches, evaluated against authoritative records.
|
|
2624
|
+
/// The index therefore cannot change what a query returns, only how many
|
|
2625
|
+
/// records the predicate has to look at.
|
|
2626
|
+
///
|
|
2627
|
+
/// Candidate keys, the record lookups they drive and the predicate all run
|
|
2628
|
+
/// under one acquisition of the state lock, so a query observes one coherent
|
|
2629
|
+
/// committed state exactly as a scan does. Records are cloned only when the
|
|
2630
|
+
/// predicate keeps them, and nothing borrowed from state outlives the lock.
|
|
2631
|
+
pub fn query_collection_by_equality<F>(
|
|
2632
|
+
&self,
|
|
2633
|
+
capability: &str,
|
|
2634
|
+
equalities: &[(&str, &Value)],
|
|
2635
|
+
mut predicate: F,
|
|
2636
|
+
) -> Result<Option<Vec<StoredRow>>>
|
|
2637
|
+
where
|
|
2638
|
+
F: FnMut(&StoredRow) -> bool,
|
|
2639
|
+
{
|
|
2640
|
+
let mut matched = Vec::new();
|
|
2641
|
+
let mut examined = 0usize;
|
|
2642
|
+
let mut evaluated = 0usize;
|
|
2643
|
+
{
|
|
2644
|
+
let inner = self.state();
|
|
2645
|
+
let _query = workload_diagnostics::span(workload_diagnostics::Phase::IndexedQuery);
|
|
2646
|
+
// Candidate selection, timed apart from record access: PR36 has to
|
|
2647
|
+
// be able to say whether the index lookup itself has become a cost,
|
|
2648
|
+
// and "index query time" alone could not distinguish a slow lookup
|
|
2649
|
+
// from a large candidate set.
|
|
2650
|
+
let lookup = workload_diagnostics::span(workload_diagnostics::Phase::IndexLookup);
|
|
2651
|
+
let mut applicable: Vec<&BTreeSet<String>> = Vec::new();
|
|
2652
|
+
for (field, value) in equalities {
|
|
2653
|
+
if let Some(keys) = inner.equality_index.candidates(capability, field, value) {
|
|
2654
|
+
applicable.push(keys);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
if applicable.is_empty() {
|
|
2658
|
+
drop(lookup);
|
|
2659
|
+
drop(_query);
|
|
2660
|
+
drop(inner);
|
|
2661
|
+
query_execution_diagnostics::record_index_miss();
|
|
2662
|
+
return Ok(None);
|
|
2663
|
+
}
|
|
2664
|
+
// Smallest first. This is not cost estimation: a set's length is
|
|
2665
|
+
// already held by the set, so intersecting from the smallest is
|
|
2666
|
+
// simply the cheaper order to do the same work.
|
|
2667
|
+
applicable.sort_by_key(|keys| keys.len());
|
|
2668
|
+
drop(lookup);
|
|
2669
|
+
let (smallest, rest) = applicable.split_first().expect("non-empty");
|
|
2670
|
+
let bucket = inner.rows.get(capability);
|
|
2671
|
+
for key in smallest.iter() {
|
|
2672
|
+
if !rest.iter().all(|keys| keys.contains(key)) {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
examined += 1;
|
|
2676
|
+
let retrieval =
|
|
2677
|
+
workload_diagnostics::span(workload_diagnostics::Phase::CandidateRetrieval);
|
|
2678
|
+
let row = bucket.and_then(|rows| rows.get(key));
|
|
2679
|
+
drop(retrieval);
|
|
2680
|
+
let Some(row) = row else {
|
|
2681
|
+
// An index entry with no record behind it would be a
|
|
2682
|
+
// maintenance defect. Skipping it keeps the query correct
|
|
2683
|
+
// rather than surfacing a record that does not exist.
|
|
2684
|
+
continue;
|
|
2685
|
+
};
|
|
2686
|
+
evaluated += 1;
|
|
2687
|
+
let _predicate =
|
|
2688
|
+
workload_diagnostics::span(workload_diagnostics::Phase::PredicateEvaluation);
|
|
2689
|
+
if predicate(row) {
|
|
2690
|
+
drop(_predicate);
|
|
2691
|
+
matched.push(row.clone());
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
query_execution_diagnostics::record_indexed_query(examined, evaluated, matched.len());
|
|
2696
|
+
Ok(Some(matched))
|
|
1890
2697
|
}
|
|
1891
2698
|
|
|
1892
2699
|
/// Traverse one collection in immutable record-key order without materializing it.
|
|
@@ -1896,7 +2703,7 @@ impl FeltDb {
|
|
|
1896
2703
|
after: Option<&str>,
|
|
1897
2704
|
limit: usize,
|
|
1898
2705
|
) -> Result<Vec<StoredRow>> {
|
|
1899
|
-
let inner = self.
|
|
2706
|
+
let inner = self.state();
|
|
1900
2707
|
let Some(rows) = inner.rows.get(capability) else {
|
|
1901
2708
|
return Ok(vec![]);
|
|
1902
2709
|
};
|
|
@@ -1916,7 +2723,7 @@ impl FeltDb {
|
|
|
1916
2723
|
|
|
1917
2724
|
/// Fetch one live collection record without materializing collection state.
|
|
1918
2725
|
pub fn get_collection_record(&self, capability: &str, key: &str) -> Result<Option<StoredRow>> {
|
|
1919
|
-
let inner = self.
|
|
2726
|
+
let inner = self.state();
|
|
1920
2727
|
Ok(inner
|
|
1921
2728
|
.rows
|
|
1922
2729
|
.get(capability)
|
|
@@ -1932,14 +2739,14 @@ impl FeltDb {
|
|
|
1932
2739
|
|
|
1933
2740
|
/// Add a peer for synchronization
|
|
1934
2741
|
pub fn add_sync_peer(&self, peer_id: String) -> Result<()> {
|
|
1935
|
-
let mut inner = self.
|
|
2742
|
+
let mut inner = self.state();
|
|
1936
2743
|
inner.sync_state.add_peer(peer_id);
|
|
1937
2744
|
Ok(())
|
|
1938
2745
|
}
|
|
1939
2746
|
|
|
1940
2747
|
/// Remove a peer from synchronization
|
|
1941
2748
|
pub fn remove_sync_peer(&self, peer_id: &str) -> Result<()> {
|
|
1942
|
-
let mut inner = self.
|
|
2749
|
+
let mut inner = self.state();
|
|
1943
2750
|
inner.sync_state.remove_peer(peer_id);
|
|
1944
2751
|
Ok(())
|
|
1945
2752
|
}
|
|
@@ -1950,7 +2757,7 @@ impl FeltDb {
|
|
|
1950
2757
|
_peer_id: &str,
|
|
1951
2758
|
since_sequence: u64,
|
|
1952
2759
|
) -> Result<Vec<Operation>> {
|
|
1953
|
-
let inner = self.
|
|
2760
|
+
let inner = self.state();
|
|
1954
2761
|
let pending: Vec<Operation> = inner
|
|
1955
2762
|
.change_log
|
|
1956
2763
|
.pending_operations()
|
|
@@ -1965,18 +2772,18 @@ impl FeltDb {
|
|
|
1965
2772
|
&self,
|
|
1966
2773
|
versions: &std::collections::HashMap<String, u64>,
|
|
1967
2774
|
) -> Result<Vec<Operation>> {
|
|
1968
|
-
let inner = self.
|
|
2775
|
+
let inner = self.state();
|
|
1969
2776
|
Ok(inner.change_log.operations_since(versions))
|
|
1970
2777
|
}
|
|
1971
2778
|
|
|
1972
2779
|
/// Return the highest durable operation sequence observed from each origin.
|
|
1973
2780
|
pub fn operation_versions(&self) -> Result<std::collections::HashMap<String, u64>> {
|
|
1974
|
-
let inner = self.
|
|
2781
|
+
let inner = self.state();
|
|
1975
2782
|
Ok(inner.change_log.versions())
|
|
1976
2783
|
}
|
|
1977
2784
|
|
|
1978
2785
|
pub fn export_snapshot(&self) -> Result<DatabaseSnapshot> {
|
|
1979
|
-
let inner = self.
|
|
2786
|
+
let inner = self.state();
|
|
1980
2787
|
let mut rows: Vec<StoredRow> = inner
|
|
1981
2788
|
.rows
|
|
1982
2789
|
.values()
|
|
@@ -1992,13 +2799,14 @@ impl FeltDb {
|
|
|
1992
2799
|
rows,
|
|
1993
2800
|
versions,
|
|
1994
2801
|
content_hash,
|
|
2802
|
+
format_version: DURABLE_FORMAT_VERSION,
|
|
1995
2803
|
})
|
|
1996
2804
|
}
|
|
1997
2805
|
/// Current materialized rows including their causal operation envelopes.
|
|
1998
2806
|
/// State-contract execution uses this for record-version preconditions;
|
|
1999
2807
|
/// portable snapshots intentionally continue to strip those envelopes.
|
|
2000
2808
|
pub fn state_rows(&self) -> Result<Vec<StoredRow>> {
|
|
2001
|
-
let inner = self.
|
|
2809
|
+
let inner = self.state();
|
|
2002
2810
|
Ok(inner
|
|
2003
2811
|
.rows
|
|
2004
2812
|
.values()
|
|
@@ -2012,7 +2820,7 @@ impl FeltDb {
|
|
|
2012
2820
|
/// Selecting the capability buckets before cloning prevents one application's
|
|
2013
2821
|
/// read snapshot from materializing every other application's durable rows.
|
|
2014
2822
|
pub fn state_rows_for_namespace(&self, state_namespace: &str) -> Result<Vec<StoredRow>> {
|
|
2015
|
-
let inner = self.
|
|
2823
|
+
let inner = self.state();
|
|
2016
2824
|
let prefix = format!("{state_namespace}:");
|
|
2017
2825
|
Ok(inner
|
|
2018
2826
|
.rows
|
|
@@ -2023,15 +2831,33 @@ impl FeltDb {
|
|
|
2023
2831
|
}
|
|
2024
2832
|
|
|
2025
2833
|
pub fn can_install_snapshot(&self) -> Result<bool> {
|
|
2026
|
-
let inner = self.
|
|
2834
|
+
let inner = self.state();
|
|
2027
2835
|
Ok(!inner.bootstrapped && inner.rows.is_empty() && inner.change_log.pending.is_empty())
|
|
2028
2836
|
}
|
|
2029
2837
|
|
|
2030
2838
|
/// Install a verified bootstrap snapshot into a pristine node. A node with
|
|
2031
2839
|
/// local history must converge through operations rather than overwrite it.
|
|
2032
2840
|
pub fn install_snapshot(&self, snapshot: DatabaseSnapshot) -> Result<()> {
|
|
2841
|
+
// A snapshot carries whole rows, revisions included, so installing one
|
|
2842
|
+
// from another format would reintroduce exactly the silent
|
|
2843
|
+
// misinterpretation the on-disk version prevents.
|
|
2844
|
+
if snapshot.format_version != DURABLE_FORMAT_VERSION {
|
|
2845
|
+
return Err(FlowError::IncompatibleFormat(Box::new(
|
|
2846
|
+
FormatIncompatibility {
|
|
2847
|
+
found: if snapshot.format_version == 0 {
|
|
2848
|
+
DurableFormat::Unversioned
|
|
2849
|
+
} else {
|
|
2850
|
+
DurableFormat::Versioned(snapshot.format_version)
|
|
2851
|
+
},
|
|
2852
|
+
required: DURABLE_FORMAT_VERSION,
|
|
2853
|
+
reason: "the snapshot was produced in a different durable format".to_string(),
|
|
2854
|
+
action: "Take a fresh snapshot from a FeltDB build matching this one."
|
|
2855
|
+
.to_string(),
|
|
2856
|
+
},
|
|
2857
|
+
)));
|
|
2858
|
+
}
|
|
2033
2859
|
snapshot.verify()?;
|
|
2034
|
-
let mut inner = self.
|
|
2860
|
+
let mut inner = self.state();
|
|
2035
2861
|
if !inner.rows.is_empty() || !inner.change_log.pending.is_empty() {
|
|
2036
2862
|
return Err(FlowError::CapabilityError(
|
|
2037
2863
|
"snapshot bootstrap requires a pristine node".to_string(),
|
|
@@ -2058,6 +2884,7 @@ impl FeltDb {
|
|
|
2058
2884
|
&mut file,
|
|
2059
2885
|
&LogHeader {
|
|
2060
2886
|
record_type: "feltdb.snapshot.v1".to_string(),
|
|
2887
|
+
format_version: DURABLE_FORMAT_VERSION,
|
|
2061
2888
|
local_sequence: inner.sequence,
|
|
2062
2889
|
observed_versions: snapshot.versions.clone(),
|
|
2063
2890
|
},
|
|
@@ -2069,7 +2896,7 @@ impl FeltDb {
|
|
|
2069
2896
|
file.sync_all()?;
|
|
2070
2897
|
fs::rename(temporary, &inner.path)?;
|
|
2071
2898
|
|
|
2072
|
-
inner.
|
|
2899
|
+
inner.clear_rows();
|
|
2073
2900
|
inner.key_counters.clear();
|
|
2074
2901
|
for row in snapshot.rows {
|
|
2075
2902
|
if let Some(id) = row
|
|
@@ -2083,11 +2910,7 @@ impl FeltDb {
|
|
|
2083
2910
|
.or_default();
|
|
2084
2911
|
*counter = (*counter).max(id + 1);
|
|
2085
2912
|
}
|
|
2086
|
-
inner
|
|
2087
|
-
.rows
|
|
2088
|
-
.entry(row.capability.clone())
|
|
2089
|
-
.or_default()
|
|
2090
|
-
.insert(row.key.clone(), row);
|
|
2913
|
+
inner.put_row(row);
|
|
2091
2914
|
}
|
|
2092
2915
|
inner.change_log.observed_versions = snapshot.versions;
|
|
2093
2916
|
inner.bootstrapped = true;
|
|
@@ -2096,7 +2919,7 @@ impl FeltDb {
|
|
|
2096
2919
|
|
|
2097
2920
|
/// Acknowledge receipt of operations from a peer
|
|
2098
2921
|
pub fn acknowledge_peer_operations(&self, peer_id: String, sequence: u64) -> Result<()> {
|
|
2099
|
-
let mut inner = self.
|
|
2922
|
+
let mut inner = self.state();
|
|
2100
2923
|
inner.change_log.acknowledge_peer(peer_id, sequence);
|
|
2101
2924
|
persist_sync_metadata(&inner)?;
|
|
2102
2925
|
Ok(())
|
|
@@ -2108,25 +2931,31 @@ impl FeltDb {
|
|
|
2108
2931
|
peer_id: String,
|
|
2109
2932
|
versions: HashMap<String, u64>,
|
|
2110
2933
|
) -> Result<()> {
|
|
2111
|
-
let mut inner = self.
|
|
2934
|
+
let mut inner = self.state();
|
|
2112
2935
|
inner.change_log.acknowledge_versions(peer_id, versions);
|
|
2113
2936
|
persist_sync_metadata(&inner)
|
|
2114
2937
|
}
|
|
2115
2938
|
|
|
2116
2939
|
pub fn acknowledged_peer_versions(&self, peer_id: &str) -> Result<HashMap<String, u64>> {
|
|
2117
|
-
let inner = self.
|
|
2940
|
+
let inner = self.state();
|
|
2118
2941
|
Ok(inner.change_log.acknowledged_versions(peer_id))
|
|
2119
2942
|
}
|
|
2120
2943
|
|
|
2944
|
+
/// Earliest operation still available for incremental replay, by origin.
|
|
2945
|
+
pub fn retained_operation_floors(&self) -> Result<HashMap<String, u64>> {
|
|
2946
|
+
let inner = self.state();
|
|
2947
|
+
Ok(inner.change_log.retained_from())
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2121
2950
|
pub fn acknowledged_operation_count(&self, active_peers: &[String]) -> Result<usize> {
|
|
2122
|
-
let inner = self.
|
|
2951
|
+
let inner = self.state();
|
|
2123
2952
|
Ok(inner.change_log.acknowledged_operation_count(active_peers))
|
|
2124
2953
|
}
|
|
2125
2954
|
|
|
2126
2955
|
/// Atomically replace acknowledged history with a state snapshot and any
|
|
2127
2956
|
/// operations still needed by at least one configured peer.
|
|
2128
2957
|
pub fn compact_operation_log(&self, active_peers: &[String]) -> Result<usize> {
|
|
2129
|
-
let mut inner = self.
|
|
2958
|
+
let mut inner = self.state();
|
|
2130
2959
|
let removed = inner.change_log.prune_acknowledged(active_peers);
|
|
2131
2960
|
if removed == 0 {
|
|
2132
2961
|
return Ok(0);
|
|
@@ -2143,6 +2972,7 @@ impl FeltDb {
|
|
|
2143
2972
|
&mut file,
|
|
2144
2973
|
&LogHeader {
|
|
2145
2974
|
record_type: "feltdb.snapshot.v1".to_string(),
|
|
2975
|
+
format_version: DURABLE_FORMAT_VERSION,
|
|
2146
2976
|
local_sequence: inner.sequence,
|
|
2147
2977
|
observed_versions: inner.change_log.versions(),
|
|
2148
2978
|
},
|
|
@@ -2166,7 +2996,7 @@ impl FeltDb {
|
|
|
2166
2996
|
|
|
2167
2997
|
/// Register a peer in the distributed fabric
|
|
2168
2998
|
pub fn register_peer(&self, advertisement: PeerAdvertisement) -> Result<()> {
|
|
2169
|
-
let inner = self.
|
|
2999
|
+
let inner = self.state();
|
|
2170
3000
|
inner
|
|
2171
3001
|
.peer_registry
|
|
2172
3002
|
.register(advertisement)
|
|
@@ -2175,7 +3005,7 @@ impl FeltDb {
|
|
|
2175
3005
|
|
|
2176
3006
|
/// Get a peer's advertisement
|
|
2177
3007
|
pub fn get_peer(&self, peer_id: &PeerId) -> Result<Option<PeerAdvertisement>> {
|
|
2178
|
-
let inner = self.
|
|
3008
|
+
let inner = self.state();
|
|
2179
3009
|
inner
|
|
2180
3010
|
.peer_registry
|
|
2181
3011
|
.get(peer_id)
|
|
@@ -2184,7 +3014,7 @@ impl FeltDb {
|
|
|
2184
3014
|
|
|
2185
3015
|
/// Find all peers providing a namespace
|
|
2186
3016
|
pub fn peers_for_namespace(&self, namespace: &str) -> Result<Vec<PeerAdvertisement>> {
|
|
2187
|
-
let inner = self.
|
|
3017
|
+
let inner = self.state();
|
|
2188
3018
|
inner
|
|
2189
3019
|
.peer_registry
|
|
2190
3020
|
.peers_for_namespace(namespace)
|
|
@@ -2193,7 +3023,7 @@ impl FeltDb {
|
|
|
2193
3023
|
|
|
2194
3024
|
/// Find all peers providing a capability
|
|
2195
3025
|
pub fn peers_for_capability(&self, capability: &str) -> Result<Vec<PeerAdvertisement>> {
|
|
2196
|
-
let inner = self.
|
|
3026
|
+
let inner = self.state();
|
|
2197
3027
|
inner
|
|
2198
3028
|
.peer_registry
|
|
2199
3029
|
.peers_for_capability(capability)
|
|
@@ -2202,7 +3032,7 @@ impl FeltDb {
|
|
|
2202
3032
|
|
|
2203
3033
|
/// Get all registered peers
|
|
2204
3034
|
pub fn all_peers(&self) -> Result<Vec<PeerAdvertisement>> {
|
|
2205
|
-
let inner = self.
|
|
3035
|
+
let inner = self.state();
|
|
2206
3036
|
inner
|
|
2207
3037
|
.peer_registry
|
|
2208
3038
|
.all_peers()
|
|
@@ -2218,14 +3048,14 @@ impl FeltDb {
|
|
|
2218
3048
|
|
|
2219
3049
|
/// Get current sync state
|
|
2220
3050
|
pub fn sync_state(&self) -> Result<SyncState> {
|
|
2221
|
-
let inner = self.
|
|
3051
|
+
let inner = self.state();
|
|
2222
3052
|
Ok(inner.sync_state.clone())
|
|
2223
3053
|
}
|
|
2224
3054
|
|
|
2225
3055
|
/// Apply an operation from a remote peer
|
|
2226
3056
|
/// This merges the vector clock, resolves conflicts, and applies the mutation
|
|
2227
3057
|
pub fn apply_remote_operation(&self, op: Operation) -> Result<()> {
|
|
2228
|
-
let mut inner = self.
|
|
3058
|
+
let mut inner = self.state();
|
|
2229
3059
|
|
|
2230
3060
|
if op.content_hash != op.compute_content_hash() {
|
|
2231
3061
|
return Err(FlowError::CapabilityError(
|
|
@@ -2324,18 +3154,12 @@ impl FeltDb {
|
|
|
2324
3154
|
OperationType::Insert | OperationType::Update => {
|
|
2325
3155
|
append_event(&inner.path, &row)?;
|
|
2326
3156
|
inner.authority_revision += 1;
|
|
2327
|
-
inner
|
|
2328
|
-
.rows
|
|
2329
|
-
.entry(op_to_apply.capability.clone())
|
|
2330
|
-
.or_default()
|
|
2331
|
-
.insert(op_to_apply.key.clone(), row);
|
|
3157
|
+
inner.put_row(row);
|
|
2332
3158
|
}
|
|
2333
3159
|
OperationType::Delete => {
|
|
2334
3160
|
append_event(&inner.path, &row)?;
|
|
2335
3161
|
inner.authority_revision += 1;
|
|
2336
|
-
|
|
2337
|
-
bucket.remove(&op_to_apply.key);
|
|
2338
|
-
}
|
|
3162
|
+
inner.remove_row(&op_to_apply.capability, &op_to_apply.key);
|
|
2339
3163
|
}
|
|
2340
3164
|
}
|
|
2341
3165
|
if let Some(id) = op_to_apply
|
|
@@ -2366,7 +3190,7 @@ impl FeltDb {
|
|
|
2366
3190
|
|
|
2367
3191
|
/// Get the current vector clock state
|
|
2368
3192
|
pub fn get_vector_clock(&self) -> Result<Option<std::collections::HashMap<String, u64>>> {
|
|
2369
|
-
let inner = self.
|
|
3193
|
+
let inner = self.state();
|
|
2370
3194
|
Ok(inner.sync_state.vector_clock.clone())
|
|
2371
3195
|
}
|
|
2372
3196
|
}
|
|
@@ -2380,7 +3204,7 @@ pub struct Capability {
|
|
|
2380
3204
|
impl Capability {
|
|
2381
3205
|
pub fn insert<T: Serialize>(&self, value: T) -> Result<String> {
|
|
2382
3206
|
let key = {
|
|
2383
|
-
let mut inner = self.db.
|
|
3207
|
+
let mut inner = self.db.state();
|
|
2384
3208
|
let next = inner.key_counters.entry(self.name.clone()).or_default();
|
|
2385
3209
|
let key = format!("{}:{}", self.name, *next);
|
|
2386
3210
|
*next += 1;
|
|
@@ -2433,7 +3257,7 @@ impl Capability {
|
|
|
2433
3257
|
F: Fn(&T) -> bool,
|
|
2434
3258
|
{
|
|
2435
3259
|
let rust_type = type_name::<T>().to_string();
|
|
2436
|
-
let mut inner = self.db.
|
|
3260
|
+
let mut inner = self.db.state();
|
|
2437
3261
|
*inner
|
|
2438
3262
|
.query_count_by_type
|
|
2439
3263
|
.entry(rust_type.clone())
|
|
@@ -2487,7 +3311,7 @@ impl Capability {
|
|
|
2487
3311
|
let capability_name = self.name.clone();
|
|
2488
3312
|
|
|
2489
3313
|
let initial_results = {
|
|
2490
|
-
let mut inner = self.db.
|
|
3314
|
+
let mut inner = self.db.state();
|
|
2491
3315
|
*inner
|
|
2492
3316
|
.query_count_by_type
|
|
2493
3317
|
.entry(rust_type.clone())
|
|
@@ -2568,7 +3392,7 @@ where
|
|
|
2568
3392
|
}
|
|
2569
3393
|
|
|
2570
3394
|
let results = {
|
|
2571
|
-
let inner = self.db.
|
|
3395
|
+
let inner = self.db.state();
|
|
2572
3396
|
let mut out = Vec::new();
|
|
2573
3397
|
for bucket in inner.rows.values() {
|
|
2574
3398
|
for row in bucket.values() {
|
|
@@ -2639,7 +3463,7 @@ where
|
|
|
2639
3463
|
}
|
|
2640
3464
|
|
|
2641
3465
|
let results = {
|
|
2642
|
-
let inner = self.db.
|
|
3466
|
+
let inner = self.db.state();
|
|
2643
3467
|
let mut out = Vec::new();
|
|
2644
3468
|
if let Some(bucket) = inner.rows.get(&self.capability_name) {
|
|
2645
3469
|
for row in bucket.values() {
|
|
@@ -2677,6 +3501,115 @@ where
|
|
|
2677
3501
|
///
|
|
2678
3502
|
/// Damage anywhere earlier is not an interrupted write. Recovery refuses it
|
|
2679
3503
|
/// rather than silently dropping committed history.
|
|
3504
|
+
/// Examine a durable database's format **without opening or modifying it**.
|
|
3505
|
+
///
|
|
3506
|
+
/// This runs before any record is interpreted, and reads the file only. A
|
|
3507
|
+
/// database this reports as incompatible is left exactly as it was found.
|
|
3508
|
+
///
|
|
3509
|
+
/// The order matters: an explicit format record decides, and the records
|
|
3510
|
+
/// themselves are examined *only* when no format record exists — the
|
|
3511
|
+
/// unversioned era. Compatibility is never inferred from whether a
|
|
3512
|
+
/// deserialization happened to succeed.
|
|
3513
|
+
pub fn inspect_durable_format(path: &Path) -> Result<FormatCompatibility> {
|
|
3514
|
+
if !path.exists() {
|
|
3515
|
+
// A database that does not exist yet will be created in the current
|
|
3516
|
+
// format.
|
|
3517
|
+
return Ok(FormatCompatibility::Compatible(DurableFormat::Versioned(
|
|
3518
|
+
DURABLE_FORMAT_VERSION,
|
|
3519
|
+
)));
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
let lines = read_log_lines(path)?;
|
|
3523
|
+
let mut declared: Option<u32> = None;
|
|
3524
|
+
let mut legacy_revision = false;
|
|
3525
|
+
|
|
3526
|
+
for line in &lines {
|
|
3527
|
+
if line.trim().is_empty() {
|
|
3528
|
+
continue;
|
|
3529
|
+
}
|
|
3530
|
+
let Ok(value) = serde_json::from_str::<Value>(line) else {
|
|
3531
|
+
continue;
|
|
3532
|
+
};
|
|
3533
|
+
match value.get("record_type").and_then(Value::as_str) {
|
|
3534
|
+
Some(FORMAT_RECORD_TYPE) => {
|
|
3535
|
+
if let Some(version) = value.get("format_version").and_then(Value::as_u64) {
|
|
3536
|
+
declared = Some(version as u32);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
Some("feltdb.snapshot.v1" | "flowdb.snapshot.v1") => {
|
|
3540
|
+
match value.get("format_version").and_then(Value::as_u64) {
|
|
3541
|
+
Some(version) if version > 0 => declared = Some(version as u32),
|
|
3542
|
+
_ => {}
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
_ => {
|
|
3546
|
+
// The unversioned-era probe: a revision record that predates
|
|
3547
|
+
// resource-scoped identity. Deliberate and specific — it looks
|
|
3548
|
+
// for a named absence rather than trying to parse and shrugging
|
|
3549
|
+
// at failure.
|
|
3550
|
+
let is_revision = value
|
|
3551
|
+
.get("key")
|
|
3552
|
+
.and_then(Value::as_str)
|
|
3553
|
+
.is_some_and(|key| key.starts_with("state:revision:"));
|
|
3554
|
+
if is_revision
|
|
3555
|
+
&& value
|
|
3556
|
+
.get("value")
|
|
3557
|
+
.and_then(Value::as_object)
|
|
3558
|
+
.is_some_and(|revision| !revision.contains_key("resource"))
|
|
3559
|
+
{
|
|
3560
|
+
legacy_revision = true;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
if let Some(version) = declared {
|
|
3567
|
+
return Ok(match version.cmp(&DURABLE_FORMAT_VERSION) {
|
|
3568
|
+
std::cmp::Ordering::Equal => {
|
|
3569
|
+
FormatCompatibility::Compatible(DurableFormat::Versioned(version))
|
|
3570
|
+
}
|
|
3571
|
+
std::cmp::Ordering::Greater => {
|
|
3572
|
+
FormatCompatibility::Incompatible(FormatIncompatibility {
|
|
3573
|
+
found: DurableFormat::Versioned(version),
|
|
3574
|
+
required: DURABLE_FORMAT_VERSION,
|
|
3575
|
+
reason: "the database was written by a newer FeltDB".to_string(),
|
|
3576
|
+
action: "Run a FeltDB build that understands this format, or restore \
|
|
3577
|
+
a backup taken in an older format."
|
|
3578
|
+
.to_string(),
|
|
3579
|
+
})
|
|
3580
|
+
}
|
|
3581
|
+
std::cmp::Ordering::Less => FormatCompatibility::Incompatible(FormatIncompatibility {
|
|
3582
|
+
found: DurableFormat::Versioned(version),
|
|
3583
|
+
required: DURABLE_FORMAT_VERSION,
|
|
3584
|
+
reason: "the database was written in an older durable format".to_string(),
|
|
3585
|
+
action: "Migrate the database with a FeltDB build that supports both \
|
|
3586
|
+
formats, or restore a backup."
|
|
3587
|
+
.to_string(),
|
|
3588
|
+
}),
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
if legacy_revision {
|
|
3593
|
+
return Ok(FormatCompatibility::Incompatible(FormatIncompatibility {
|
|
3594
|
+
found: DurableFormat::Unversioned,
|
|
3595
|
+
required: DURABLE_FORMAT_VERSION,
|
|
3596
|
+
reason: "it holds revision records written before revisions belonged to a \
|
|
3597
|
+
resource, and those records cannot be migrated without inventing \
|
|
3598
|
+
information they never contained: a revision recorded no resource, \
|
|
3599
|
+
so which history each one belongs to is not recoverable from the data"
|
|
3600
|
+
.to_string(),
|
|
3601
|
+
action: "Export the application state you need from a FeltDB build that \
|
|
3602
|
+
understands the old format, then load it into a new database. The \
|
|
3603
|
+
existing file has not been modified."
|
|
3604
|
+
.to_string(),
|
|
3605
|
+
}));
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// No format record and no legacy revision: nothing in this database depends
|
|
3609
|
+
// on the difference.
|
|
3610
|
+
Ok(FormatCompatibility::Compatible(DurableFormat::Unversioned))
|
|
3611
|
+
}
|
|
3612
|
+
|
|
2680
3613
|
fn read_log_lines(path: &Path) -> Result<Vec<String>> {
|
|
2681
3614
|
let mut file = BufReader::new(OpenOptions::new().read(true).open(path)?);
|
|
2682
3615
|
let mut lines: Vec<(String, bool)> = Vec::new();
|
|
@@ -2733,6 +3666,10 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
2733
3666
|
snapshot_log = true;
|
|
2734
3667
|
continue;
|
|
2735
3668
|
}
|
|
3669
|
+
if value.get("record_type").and_then(Value::as_str) == Some(FORMAT_RECORD_TYPE) {
|
|
3670
|
+
// Already honoured by `inspect_durable_format` before the open.
|
|
3671
|
+
continue;
|
|
3672
|
+
}
|
|
2736
3673
|
if value.get("record_type").and_then(Value::as_str) == Some("feltdb.transaction.v1") {
|
|
2737
3674
|
let transaction: TransactionLogRecord = serde_json::from_value(value)?;
|
|
2738
3675
|
// Older records did not carry a commit revision. Their durable
|
|
@@ -2772,15 +3709,9 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
2772
3709
|
inner.change_log.add_operation(operation);
|
|
2773
3710
|
}
|
|
2774
3711
|
if row.deleted {
|
|
2775
|
-
|
|
2776
|
-
bucket.remove(&row.key);
|
|
2777
|
-
}
|
|
3712
|
+
inner.remove_row(&row.capability, &row.key);
|
|
2778
3713
|
} else {
|
|
2779
|
-
inner
|
|
2780
|
-
.rows
|
|
2781
|
-
.entry(row.capability.clone())
|
|
2782
|
-
.or_default()
|
|
2783
|
-
.insert(row.key.clone(), row);
|
|
3714
|
+
inner.put_row(row);
|
|
2784
3715
|
}
|
|
2785
3716
|
}
|
|
2786
3717
|
}
|
|
@@ -2817,15 +3748,9 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
2817
3748
|
*counter = (*counter).max(id + 1);
|
|
2818
3749
|
}
|
|
2819
3750
|
if row.deleted {
|
|
2820
|
-
|
|
2821
|
-
bucket.remove(&row.key);
|
|
2822
|
-
}
|
|
3751
|
+
inner.remove_row(&row.capability, &row.key);
|
|
2823
3752
|
} else {
|
|
2824
|
-
inner
|
|
2825
|
-
.rows
|
|
2826
|
-
.entry(row.capability.clone())
|
|
2827
|
-
.or_default()
|
|
2828
|
-
.insert(row.key.clone(), row);
|
|
3753
|
+
inner.put_row(row);
|
|
2829
3754
|
}
|
|
2830
3755
|
}
|
|
2831
3756
|
|
|
@@ -2904,6 +3829,10 @@ fn persist_sync_metadata(inner: &Inner) -> Result<()> {
|
|
|
2904
3829
|
}
|
|
2905
3830
|
|
|
2906
3831
|
fn append_event(path: &Path, event: &StoredRow) -> Result<()> {
|
|
3832
|
+
// Single-record durability: the line is written and flushed to the OS, but
|
|
3833
|
+
// not fsynced. `append_transaction` below does fsync. The asymmetry is
|
|
3834
|
+
// pre-existing and PR36 measures it rather than changing it.
|
|
3835
|
+
let _span = workload_diagnostics::span(workload_diagnostics::Phase::Persistence);
|
|
2907
3836
|
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
|
|
2908
3837
|
let mut line = serde_json::to_string(event)?;
|
|
2909
3838
|
line.push('\n');
|
|
@@ -2913,6 +3842,10 @@ fn append_event(path: &Path, event: &StoredRow) -> Result<()> {
|
|
|
2913
3842
|
}
|
|
2914
3843
|
|
|
2915
3844
|
fn append_transaction(path: &Path, transaction: &TransactionLogRecord) -> Result<()> {
|
|
3845
|
+
// A transaction commit fsyncs. This span therefore includes the fsync, which
|
|
3846
|
+
// is the point: it is the durability cost of a commit, measured rather than
|
|
3847
|
+
// assumed.
|
|
3848
|
+
let _span = workload_diagnostics::span(workload_diagnostics::Phase::Persistence);
|
|
2916
3849
|
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
|
|
2917
3850
|
write_json_line(&mut file, transaction)?;
|
|
2918
3851
|
file.sync_data()?;
|