@feltdb/core 0.8.3 → 0.8.4
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/crates/feltdb/src/equality_index.rs +595 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +547 -115
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +11 -2
- package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +126 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +292 -2
- package/dist/create/server-source/crates/feltdb/src/sync.rs +12 -0
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +443 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +233 -0
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +892 -0
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +1137 -29
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +474 -28
- 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-CVQWgXO-.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
- package/dist/studio-app/assets/index-DwgNAIIX.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/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;
|
|
@@ -131,6 +133,7 @@ mod workflow_acceptance_tests;
|
|
|
131
133
|
mod workflow_integration;
|
|
132
134
|
mod workflow_runtime;
|
|
133
135
|
pub mod workload;
|
|
136
|
+
pub mod workload_diagnostics;
|
|
134
137
|
|
|
135
138
|
pub use acquisition::{
|
|
136
139
|
AcquisitionDeduplicator, AcquisitionPolicy, AcquisitionResult, ContentRequest, ContentResponse,
|
|
@@ -182,6 +185,7 @@ pub use distributed_transactions::{
|
|
|
182
185
|
DistributedTransactionExecutor, EnvelopeId, ReplicaState, ReplicationMessage,
|
|
183
186
|
TransactionEnvelope,
|
|
184
187
|
};
|
|
188
|
+
pub use equality_index::{EqualityIndex, EqualityIndexStats, IndexKey};
|
|
185
189
|
pub use execution::{Execution, ExecutionQueue, ExecutionStatus, RetryPolicy};
|
|
186
190
|
pub use indexing::{IndexConfig, IndexManager, IndexStats, IndexType};
|
|
187
191
|
pub use materialization::{MaterializationHandler, ReactiveCollectionIntegration};
|
|
@@ -251,7 +255,7 @@ use serde_json::Value;
|
|
|
251
255
|
use sha2::{Digest, Sha256};
|
|
252
256
|
use std::any::type_name;
|
|
253
257
|
use std::collections::hash_map::DefaultHasher;
|
|
254
|
-
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
258
|
+
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
|
255
259
|
use std::fmt::{Display, Formatter};
|
|
256
260
|
use std::fs::{self, OpenOptions};
|
|
257
261
|
use std::hash::{Hash, Hasher};
|
|
@@ -352,6 +356,133 @@ struct Inner {
|
|
|
352
356
|
transaction_payload_hashes: HashMap<String, String>,
|
|
353
357
|
transaction_revisions: HashMap<String, (u64, u64)>,
|
|
354
358
|
collection_cardinality: HashMap<String, u64>,
|
|
359
|
+
/// Derived equality index over `rows`. Never durable, never replicated, and
|
|
360
|
+
/// never consulted for anything but candidate selection: it is maintained
|
|
361
|
+
/// inside the same state mutation boundary as `rows` and rebuilt from
|
|
362
|
+
/// `rows` after recovery.
|
|
363
|
+
equality_index: EqualityIndex,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// The state lock, held with its wait and hold time attributable.
|
|
367
|
+
///
|
|
368
|
+
/// FeltDB serializes every read and every write behind one mutex, so "how long
|
|
369
|
+
/// did an operation take" and "how long did it make everything else wait" are
|
|
370
|
+
/// different questions. PR34 showed the second one mattered: a query holding the
|
|
371
|
+
/// lock while it cloned a collection inflated the cost of unrelated writes.
|
|
372
|
+
///
|
|
373
|
+
/// This guard answers both. It records the time spent *waiting* to acquire —
|
|
374
|
+
/// which is pure contention, the one span where no work happens at all — and the
|
|
375
|
+
/// time spent *holding*, which bounds how long anything else can be forced to
|
|
376
|
+
/// wait. Both are off unless `FELTDB_WORKLOAD_DIAGNOSTICS=1`, where acquisition
|
|
377
|
+
/// costs one relaxed atomic load and no clock read.
|
|
378
|
+
///
|
|
379
|
+
/// It dereferences to [`Inner`], so every existing use site reads unchanged.
|
|
380
|
+
struct StateGuard<'a> {
|
|
381
|
+
inner: std::sync::MutexGuard<'a, Inner>,
|
|
382
|
+
held_since: Option<std::time::Instant>,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
impl std::ops::Deref for StateGuard<'_> {
|
|
386
|
+
type Target = Inner;
|
|
387
|
+
fn deref(&self) -> &Inner {
|
|
388
|
+
&self.inner
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
impl std::ops::DerefMut for StateGuard<'_> {
|
|
393
|
+
fn deref_mut(&mut self) -> &mut Inner {
|
|
394
|
+
&mut self.inner
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
impl Drop for StateGuard<'_> {
|
|
399
|
+
fn drop(&mut self) {
|
|
400
|
+
if let Some(since) = self.held_since {
|
|
401
|
+
workload_diagnostics::record(
|
|
402
|
+
workload_diagnostics::Phase::StateLockHold,
|
|
403
|
+
since.elapsed().as_nanos().min(u64::MAX as u128) as u64,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// The single state mutation boundary for authoritative records.
|
|
410
|
+
///
|
|
411
|
+
/// Every path that changes a record — insert, update, delete, CAS, transaction
|
|
412
|
+
/// commit, replicated operation apply, log replay, snapshot install — goes
|
|
413
|
+
/// through these three methods, and they change the record map and the derived
|
|
414
|
+
/// equality index together while the caller still holds the state lock. That is
|
|
415
|
+
/// the whole atomicity argument: there is no window in which a committed record
|
|
416
|
+
/// exists without its index entries, or an index entry survives its record,
|
|
417
|
+
/// because no code path can write one without the other.
|
|
418
|
+
///
|
|
419
|
+
/// The index is deliberately maintained here rather than through an independent
|
|
420
|
+
/// index API. An independent API would have to be called, correctly, by nine
|
|
421
|
+
/// separate mutation paths; a missed call would be a silent divergence that only
|
|
422
|
+
/// a query would reveal. Routing every write through one boundary makes the
|
|
423
|
+
/// omission impossible to write in the first place.
|
|
424
|
+
impl Inner {
|
|
425
|
+
/// Write one record, replacing any record already at its key.
|
|
426
|
+
///
|
|
427
|
+
/// Returns `true` when this created a record rather than replacing one, which
|
|
428
|
+
/// is what collection cardinality accounting needs.
|
|
429
|
+
fn put_row(&mut self, row: StoredRow) -> bool {
|
|
430
|
+
let capability = row.capability.clone();
|
|
431
|
+
let key = row.key.clone();
|
|
432
|
+
let indexed = self.equality_index.indexes_collection(&capability);
|
|
433
|
+
let mutation = workload_diagnostics::span(workload_diagnostics::Phase::RecordMutation);
|
|
434
|
+
let bucket = self.rows.entry(capability.clone()).or_default();
|
|
435
|
+
let previous = bucket.insert(key.clone(), row);
|
|
436
|
+
drop(mutation);
|
|
437
|
+
if !indexed {
|
|
438
|
+
return previous.is_none();
|
|
439
|
+
}
|
|
440
|
+
// From here on the work exists only because an index does. Measuring it
|
|
441
|
+
// apart from the record write is what makes "did index maintenance
|
|
442
|
+
// become a write-side cost?" a question with an answer.
|
|
443
|
+
let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
|
|
444
|
+
// Old entries first, then new ones. Doing it in this order means a
|
|
445
|
+
// record whose indexed field did not change is removed from and
|
|
446
|
+
// re-added to the same bucket, which is a no-op rather than a gap.
|
|
447
|
+
if let Some(previous) = &previous {
|
|
448
|
+
self.equality_index
|
|
449
|
+
.remove_record(&capability, &key, &previous.value);
|
|
450
|
+
}
|
|
451
|
+
// Disjoint field borrows: the record map is read while the index is
|
|
452
|
+
// written. Nothing is cloned to maintain the index.
|
|
453
|
+
let Inner {
|
|
454
|
+
rows,
|
|
455
|
+
equality_index,
|
|
456
|
+
..
|
|
457
|
+
} = self;
|
|
458
|
+
if let Some(current) = rows.get(&capability).and_then(|rows| rows.get(&key)) {
|
|
459
|
+
equality_index.insert_record(&capability, &key, ¤t.value);
|
|
460
|
+
}
|
|
461
|
+
previous.is_none()
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/// Remove one record. Returns `true` when a record was actually removed.
|
|
465
|
+
fn remove_row(&mut self, capability: &str, key: &str) -> bool {
|
|
466
|
+
let mutation = workload_diagnostics::span(workload_diagnostics::Phase::RecordMutation);
|
|
467
|
+
let Some(removed) = self
|
|
468
|
+
.rows
|
|
469
|
+
.get_mut(capability)
|
|
470
|
+
.and_then(|bucket| bucket.remove(key))
|
|
471
|
+
else {
|
|
472
|
+
return false;
|
|
473
|
+
};
|
|
474
|
+
drop(mutation);
|
|
475
|
+
let _index = workload_diagnostics::span(workload_diagnostics::Phase::IndexMaintenance);
|
|
476
|
+
self.equality_index
|
|
477
|
+
.remove_record(capability, key, &removed.value);
|
|
478
|
+
true
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// Drop all records, as a snapshot install does. The index follows.
|
|
482
|
+
fn clear_rows(&mut self) {
|
|
483
|
+
self.rows.clear();
|
|
484
|
+
self.equality_index.clear();
|
|
485
|
+
}
|
|
355
486
|
}
|
|
356
487
|
|
|
357
488
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
@@ -722,7 +853,7 @@ impl FeltDb {
|
|
|
722
853
|
.to_string();
|
|
723
854
|
let rust_type = type_name::<Value>().to_string();
|
|
724
855
|
let (result, event) = {
|
|
725
|
-
let mut inner = self.
|
|
856
|
+
let mut inner = self.state();
|
|
726
857
|
let Some(current) = inner.rows.get(&capability).and_then(|rows| rows.get(key)) else {
|
|
727
858
|
return Ok(JsonCasResult::NotFound);
|
|
728
859
|
};
|
|
@@ -822,11 +953,7 @@ impl FeltDb {
|
|
|
822
953
|
append_event(&inner.path, &row)?;
|
|
823
954
|
inner.authority_revision += 1;
|
|
824
955
|
inner.change_log.add_operation(operation);
|
|
825
|
-
inner
|
|
826
|
-
.rows
|
|
827
|
-
.entry(capability.clone())
|
|
828
|
-
.or_default()
|
|
829
|
-
.insert(key.to_string(), row.clone());
|
|
956
|
+
inner.put_row(row.clone());
|
|
830
957
|
let event = ChangeEvent {
|
|
831
958
|
capability,
|
|
832
959
|
key: key.to_string(),
|
|
@@ -975,7 +1102,7 @@ impl FeltDb {
|
|
|
975
1102
|
.map(|(cap, _)| cap)
|
|
976
1103
|
.unwrap_or("default")
|
|
977
1104
|
.to_string();
|
|
978
|
-
let inner = self.
|
|
1105
|
+
let inner = self.state();
|
|
979
1106
|
let value = inner
|
|
980
1107
|
.rows
|
|
981
1108
|
.get(&capability)
|
|
@@ -991,7 +1118,7 @@ impl FeltDb {
|
|
|
991
1118
|
F: Fn(&T) -> bool,
|
|
992
1119
|
{
|
|
993
1120
|
let rust_type = type_name::<T>().to_string();
|
|
994
|
-
let mut inner = self.
|
|
1121
|
+
let mut inner = self.state();
|
|
995
1122
|
*inner
|
|
996
1123
|
.query_count_by_type
|
|
997
1124
|
.entry(rust_type.clone())
|
|
@@ -1024,19 +1151,19 @@ impl FeltDb {
|
|
|
1024
1151
|
|
|
1025
1152
|
pub fn is_auto_indexed<T>(&self) -> bool {
|
|
1026
1153
|
let rust_type = type_name::<T>().to_string();
|
|
1027
|
-
let inner = self.
|
|
1154
|
+
let inner = self.state();
|
|
1028
1155
|
inner.adaptive_indexes.contains(&rust_type)
|
|
1029
1156
|
}
|
|
1030
1157
|
|
|
1031
1158
|
/// Get the instance ID for this FeltDB instance
|
|
1032
1159
|
pub fn instance_id(&self) -> Result<String> {
|
|
1033
|
-
let inner = self.
|
|
1160
|
+
let inner = self.state();
|
|
1034
1161
|
Ok(inner.instance_id.clone())
|
|
1035
1162
|
}
|
|
1036
1163
|
|
|
1037
1164
|
/// Get the current sequence number for this instance
|
|
1038
1165
|
pub fn sequence(&self) -> Result<u64> {
|
|
1039
|
-
let inner = self.
|
|
1166
|
+
let inner = self.state();
|
|
1040
1167
|
Ok(inner.sequence)
|
|
1041
1168
|
}
|
|
1042
1169
|
|
|
@@ -1044,13 +1171,13 @@ impl FeltDb {
|
|
|
1044
1171
|
/// In multi-process topology callers must use `AuthorityClient::read` to
|
|
1045
1172
|
/// cross the authority boundary and obtain a current authoritative value.
|
|
1046
1173
|
pub fn current_revision(&self) -> Result<u64> {
|
|
1047
|
-
Ok(self.
|
|
1174
|
+
Ok(self.state().authority_revision)
|
|
1048
1175
|
}
|
|
1049
1176
|
|
|
1050
1177
|
/// Capture revision and rows under one lock. Authority reads must never
|
|
1051
1178
|
/// pair metadata from one commit with state from another.
|
|
1052
1179
|
pub(crate) fn authority_state(&self) -> (u64, Vec<StoredRow>) {
|
|
1053
|
-
let inner = self.
|
|
1180
|
+
let inner = self.state();
|
|
1054
1181
|
let rows = inner
|
|
1055
1182
|
.rows
|
|
1056
1183
|
.values()
|
|
@@ -1062,7 +1189,7 @@ impl FeltDb {
|
|
|
1062
1189
|
/// Get the maintained cardinality (record count) for a collection capability.
|
|
1063
1190
|
/// Returns the count of non-deleted records in the collection.
|
|
1064
1191
|
pub fn collection_cardinality(&self, capability: &str) -> Result<u64> {
|
|
1065
|
-
let inner = self.
|
|
1192
|
+
let inner = self.state();
|
|
1066
1193
|
Ok(inner
|
|
1067
1194
|
.collection_cardinality
|
|
1068
1195
|
.get(capability)
|
|
@@ -1073,7 +1200,7 @@ impl FeltDb {
|
|
|
1073
1200
|
/// Diagnostic: List all collection capabilities and their cardinalities.
|
|
1074
1201
|
/// Used to detect capability format mismatches.
|
|
1075
1202
|
pub fn list_cardinalities(&self) -> Result<Vec<(String, u64)>> {
|
|
1076
|
-
let inner = self.
|
|
1203
|
+
let inner = self.state();
|
|
1077
1204
|
let mut result: Vec<_> = inner
|
|
1078
1205
|
.collection_cardinality
|
|
1079
1206
|
.iter()
|
|
@@ -1086,7 +1213,7 @@ impl FeltDb {
|
|
|
1086
1213
|
/// Diagnostic: Count actual rows for a given capability pattern.
|
|
1087
1214
|
/// Used to verify that rows exist even if cardinality is 0.
|
|
1088
1215
|
pub fn diagnostic_row_count(&self, capability_pattern: &str) -> Result<usize> {
|
|
1089
|
-
let inner = self.
|
|
1216
|
+
let inner = self.state();
|
|
1090
1217
|
Ok(inner
|
|
1091
1218
|
.rows
|
|
1092
1219
|
.iter()
|
|
@@ -1098,7 +1225,7 @@ impl FeltDb {
|
|
|
1098
1225
|
/// Diagnostic: Get actual persisted capability strings matching a pattern.
|
|
1099
1226
|
/// Used to identify capability identity formats in use.
|
|
1100
1227
|
pub fn diagnostic_capability_keys(&self, capability_pattern: &str) -> Result<Vec<String>> {
|
|
1101
|
-
let inner = self.
|
|
1228
|
+
let inner = self.state();
|
|
1102
1229
|
let mut keys: Vec<_> = inner
|
|
1103
1230
|
.rows
|
|
1104
1231
|
.keys()
|
|
@@ -1195,7 +1322,7 @@ impl FeltDb {
|
|
|
1195
1322
|
audit: Option<Value>,
|
|
1196
1323
|
) -> Result<AtomicCommit> {
|
|
1197
1324
|
let (commit, events) = {
|
|
1198
|
-
let mut inner = self.
|
|
1325
|
+
let mut inner = self.state();
|
|
1199
1326
|
if inner.applied_transactions.contains(transaction_id) {
|
|
1200
1327
|
if let Some(expected_hash) = payload_hash {
|
|
1201
1328
|
if inner
|
|
@@ -1385,6 +1512,14 @@ impl FeltDb {
|
|
|
1385
1512
|
&advanced
|
|
1386
1513
|
};
|
|
1387
1514
|
|
|
1515
|
+
// Transaction bookkeeping is measured as one span per commit even
|
|
1516
|
+
// though it brackets two regions: everything before the durable
|
|
1517
|
+
// append, and the revision, dedup and payload-hash accounting after
|
|
1518
|
+
// the rows are applied. The append itself is `Persistence` and the
|
|
1519
|
+
// apply loop is `RecordMutation` + `IndexMaintenance`, so summing
|
|
1520
|
+
// this with those does not double-count any of them.
|
|
1521
|
+
let mut bookkeeping_nanos = 0u64;
|
|
1522
|
+
let prepare_began = workload_diagnostics::enabled().then(std::time::Instant::now);
|
|
1388
1523
|
let base_revision = inner.authority_revision;
|
|
1389
1524
|
if let Some(expected) = expected_parent {
|
|
1390
1525
|
if expected != base_revision {
|
|
@@ -1480,6 +1615,9 @@ impl FeltDb {
|
|
|
1480
1615
|
rows: rows.clone(),
|
|
1481
1616
|
audit,
|
|
1482
1617
|
};
|
|
1618
|
+
if let Some(began) = prepare_began {
|
|
1619
|
+
bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
|
1620
|
+
}
|
|
1483
1621
|
append_transaction(&inner.path, &record)?;
|
|
1484
1622
|
inner.authority_revision = base_revision + 1;
|
|
1485
1623
|
inner.sequence = next_sequence;
|
|
@@ -1488,9 +1626,7 @@ impl FeltDb {
|
|
|
1488
1626
|
inner.change_log.add_operation(operation);
|
|
1489
1627
|
}
|
|
1490
1628
|
if row.deleted {
|
|
1491
|
-
|
|
1492
|
-
bucket.remove(&row.key);
|
|
1493
|
-
}
|
|
1629
|
+
inner.remove_row(&row.capability, &row.key);
|
|
1494
1630
|
// Decrement cardinality on delete
|
|
1495
1631
|
let count = inner
|
|
1496
1632
|
.collection_cardinality
|
|
@@ -1498,17 +1634,10 @@ impl FeltDb {
|
|
|
1498
1634
|
.or_default();
|
|
1499
1635
|
*count = count.saturating_sub(1);
|
|
1500
1636
|
} 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());
|
|
1637
|
+
// Record and index change together, inside the commit that
|
|
1638
|
+
// is already holding the state lock: a transaction that is
|
|
1639
|
+
// refused leaves neither, and one that commits publishes both.
|
|
1640
|
+
let is_insert = inner.put_row(row.clone());
|
|
1512
1641
|
|
|
1513
1642
|
// Increment cardinality only on insert, not update
|
|
1514
1643
|
if is_insert {
|
|
@@ -1520,6 +1649,7 @@ impl FeltDb {
|
|
|
1520
1649
|
}
|
|
1521
1650
|
}
|
|
1522
1651
|
}
|
|
1652
|
+
let tail_began = workload_diagnostics::enabled().then(std::time::Instant::now);
|
|
1523
1653
|
inner.sync_state.merge_vector_clock(&vector_clock);
|
|
1524
1654
|
inner.applied_transactions.insert(transaction_id.into());
|
|
1525
1655
|
let commit_revision = inner.authority_revision;
|
|
@@ -1531,6 +1661,13 @@ impl FeltDb {
|
|
|
1531
1661
|
.transaction_payload_hashes
|
|
1532
1662
|
.insert(transaction_id.into(), hash.into());
|
|
1533
1663
|
}
|
|
1664
|
+
if let Some(began) = tail_began {
|
|
1665
|
+
bookkeeping_nanos += began.elapsed().as_nanos().min(u64::MAX as u128) as u64;
|
|
1666
|
+
}
|
|
1667
|
+
workload_diagnostics::record(
|
|
1668
|
+
workload_diagnostics::Phase::TransactionCommit,
|
|
1669
|
+
bookkeeping_nanos,
|
|
1670
|
+
);
|
|
1534
1671
|
let events = rows
|
|
1535
1672
|
.iter()
|
|
1536
1673
|
.map(|row| ChangeEvent {
|
|
@@ -1616,7 +1753,7 @@ impl FeltDb {
|
|
|
1616
1753
|
};
|
|
1617
1754
|
|
|
1618
1755
|
{
|
|
1619
|
-
let mut inner = self.
|
|
1756
|
+
let mut inner = self.state();
|
|
1620
1757
|
|
|
1621
1758
|
// Increment sequence number
|
|
1622
1759
|
inner.sequence += 1;
|
|
@@ -1645,11 +1782,7 @@ impl FeltDb {
|
|
|
1645
1782
|
|
|
1646
1783
|
append_event(&inner.path, &row)?;
|
|
1647
1784
|
inner.authority_revision += 1;
|
|
1648
|
-
inner
|
|
1649
|
-
.rows
|
|
1650
|
-
.entry(capability.clone())
|
|
1651
|
-
.or_default()
|
|
1652
|
-
.insert(key.clone(), row);
|
|
1785
|
+
inner.put_row(row);
|
|
1653
1786
|
let suffix = key.rsplit_once(':').map(|(_, right)| right);
|
|
1654
1787
|
if let Some(id) = suffix.and_then(|s| s.parse::<u64>().ok()) {
|
|
1655
1788
|
let entry = inner.key_counters.entry(capability.clone()).or_default();
|
|
@@ -1689,7 +1822,7 @@ impl FeltDb {
|
|
|
1689
1822
|
};
|
|
1690
1823
|
|
|
1691
1824
|
{
|
|
1692
|
-
let mut inner = self.
|
|
1825
|
+
let mut inner = self.state();
|
|
1693
1826
|
|
|
1694
1827
|
// Increment sequence number
|
|
1695
1828
|
inner.sequence += 1;
|
|
@@ -1718,11 +1851,7 @@ impl FeltDb {
|
|
|
1718
1851
|
|
|
1719
1852
|
append_event(&inner.path, &row)?;
|
|
1720
1853
|
inner.authority_revision += 1;
|
|
1721
|
-
inner
|
|
1722
|
-
.rows
|
|
1723
|
-
.entry(capability.clone())
|
|
1724
|
-
.or_default()
|
|
1725
|
-
.insert(key.clone(), row);
|
|
1854
|
+
inner.put_row(row);
|
|
1726
1855
|
}
|
|
1727
1856
|
|
|
1728
1857
|
let event = ChangeEvent {
|
|
@@ -1738,7 +1867,7 @@ impl FeltDb {
|
|
|
1738
1867
|
|
|
1739
1868
|
fn delete_internal(&self, capability: String, key: String) -> Result<()> {
|
|
1740
1869
|
{
|
|
1741
|
-
let mut inner = self.
|
|
1870
|
+
let mut inner = self.state();
|
|
1742
1871
|
|
|
1743
1872
|
// Get the row to determine its type
|
|
1744
1873
|
let rust_type = inner
|
|
@@ -1788,10 +1917,8 @@ impl FeltDb {
|
|
|
1788
1917
|
append_event(&inner.path, &tombstone)?;
|
|
1789
1918
|
inner.authority_revision += 1;
|
|
1790
1919
|
|
|
1791
|
-
// Remove from in-memory store
|
|
1792
|
-
|
|
1793
|
-
bucket.remove(&key);
|
|
1794
|
-
}
|
|
1920
|
+
// Remove from in-memory store, index entries included.
|
|
1921
|
+
inner.remove_row(&capability, &key);
|
|
1795
1922
|
}
|
|
1796
1923
|
|
|
1797
1924
|
let event = ChangeEvent {
|
|
@@ -1831,7 +1958,7 @@ impl FeltDb {
|
|
|
1831
1958
|
let rust_type = type_name::<T>().to_string();
|
|
1832
1959
|
|
|
1833
1960
|
let initial_results = {
|
|
1834
|
-
let mut inner = self.
|
|
1961
|
+
let mut inner = self.state();
|
|
1835
1962
|
*inner
|
|
1836
1963
|
.query_count_by_type
|
|
1837
1964
|
.entry(rust_type.clone())
|
|
@@ -1870,7 +1997,7 @@ impl FeltDb {
|
|
|
1870
1997
|
|
|
1871
1998
|
/// Get current sync state information
|
|
1872
1999
|
pub fn sync_info(&self) -> Result<SyncState> {
|
|
1873
|
-
let inner = self.
|
|
2000
|
+
let inner = self.state();
|
|
1874
2001
|
Ok(inner.sync_state.clone())
|
|
1875
2002
|
}
|
|
1876
2003
|
|
|
@@ -1880,13 +2007,326 @@ impl FeltDb {
|
|
|
1880
2007
|
}
|
|
1881
2008
|
|
|
1882
2009
|
/// List the canonical values in one collection/capability namespace.
|
|
2010
|
+
///
|
|
2011
|
+
/// This clones the whole collection, so its cost is linear in collection
|
|
2012
|
+
/// size. Readers that discard most of what they receive — a filtered query,
|
|
2013
|
+
/// for example — should use [`FeltDb::query_collection`] instead, which
|
|
2014
|
+
/// evaluates borrowed rows and clones only what it keeps.
|
|
1883
2015
|
pub fn list_collection(&self, capability: &str) -> Result<Vec<StoredRow>> {
|
|
1884
|
-
let inner = self.
|
|
1885
|
-
|
|
2016
|
+
let inner = self.state();
|
|
2017
|
+
let rows: Vec<StoredRow> = inner
|
|
1886
2018
|
.rows
|
|
1887
2019
|
.get(capability)
|
|
1888
2020
|
.map(|rows| rows.values().cloned().collect())
|
|
1889
|
-
.unwrap_or_default()
|
|
2021
|
+
.unwrap_or_default();
|
|
2022
|
+
query_execution_diagnostics::record_full_materialization(rows.len());
|
|
2023
|
+
Ok(rows)
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/// Evaluate a predicate over one collection without materializing it.
|
|
2027
|
+
///
|
|
2028
|
+
/// The predicate observes each live row *borrowed* from state, so a record
|
|
2029
|
+
/// the predicate rejects is never cloned. Only retained rows are cloned,
|
|
2030
|
+
/// which happens while state protection is still held: the returned rows are
|
|
2031
|
+
/// owned, so nothing the caller keeps can reference storage a concurrent
|
|
2032
|
+
/// mutation may move or drop after the lock is released.
|
|
2033
|
+
///
|
|
2034
|
+
/// Traversal is the collection's own record-key order, the same order
|
|
2035
|
+
/// [`FeltDb::list_collection_page`] walks. `limit` bounds accumulation and
|
|
2036
|
+
/// stops the traversal as soon as it is satisfied; because early stopping
|
|
2037
|
+
/// truncates in record-key order, a caller that must order results by some
|
|
2038
|
+
/// other field has to pass `None` and bound its own already-filtered set.
|
|
2039
|
+
///
|
|
2040
|
+
/// The predicate runs under the state lock, so it must not call back into
|
|
2041
|
+
/// the database or block.
|
|
2042
|
+
pub fn query_collection<F>(
|
|
2043
|
+
&self,
|
|
2044
|
+
capability: &str,
|
|
2045
|
+
limit: Option<usize>,
|
|
2046
|
+
mut predicate: F,
|
|
2047
|
+
) -> Result<Vec<StoredRow>>
|
|
2048
|
+
where
|
|
2049
|
+
F: FnMut(&StoredRow) -> bool,
|
|
2050
|
+
{
|
|
2051
|
+
let mut matched = Vec::new();
|
|
2052
|
+
let mut visited = 0usize;
|
|
2053
|
+
{
|
|
2054
|
+
let inner = self.state();
|
|
2055
|
+
let _query = workload_diagnostics::span(workload_diagnostics::Phase::ScanQuery);
|
|
2056
|
+
if let Some(rows) = inner.rows.get(capability) {
|
|
2057
|
+
for row in rows.values() {
|
|
2058
|
+
if limit.is_some_and(|bound| matched.len() >= bound) {
|
|
2059
|
+
break;
|
|
2060
|
+
}
|
|
2061
|
+
visited += 1;
|
|
2062
|
+
// The same predicate phase the indexed execution reports to,
|
|
2063
|
+
// so the two executions' predicate cost is directly
|
|
2064
|
+
// comparable rather than being two different measurements.
|
|
2065
|
+
let _predicate =
|
|
2066
|
+
workload_diagnostics::span(workload_diagnostics::Phase::PredicateEvaluation);
|
|
2067
|
+
if predicate(row) {
|
|
2068
|
+
drop(_predicate);
|
|
2069
|
+
matched.push(row.clone());
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
query_execution_diagnostics::record_scan(visited, matched.len());
|
|
2075
|
+
Ok(matched)
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// ----------------------------------------------------------------------
|
|
2079
|
+
// Equality index — lifecycle and execution.
|
|
2080
|
+
//
|
|
2081
|
+
// The index is derived state. Every method below either declares what is
|
|
2082
|
+
// derived, re-derives it from authoritative records, or reads candidate
|
|
2083
|
+
// record keys out of it. None of them is a source of truth, and none of
|
|
2084
|
+
// them can answer a query on its own: `query_collection_by_equality` still
|
|
2085
|
+
// evaluates the caller's full predicate against authoritative records.
|
|
2086
|
+
// ----------------------------------------------------------------------
|
|
2087
|
+
|
|
2088
|
+
/// Acquire the state lock, attributing the wait and the hold.
|
|
2089
|
+
///
|
|
2090
|
+
/// Every path in this file that touches authoritative state goes through
|
|
2091
|
+
/// here, which is what makes lock contention a measured quantity rather than
|
|
2092
|
+
/// a residual inferred from wall-clock time.
|
|
2093
|
+
fn state(&self) -> StateGuard<'_> {
|
|
2094
|
+
if !workload_diagnostics::enabled() {
|
|
2095
|
+
return StateGuard {
|
|
2096
|
+
inner: self.inner.lock().expect("lock poisoned"),
|
|
2097
|
+
held_since: None,
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
let requested = std::time::Instant::now();
|
|
2101
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
2102
|
+
let acquired = std::time::Instant::now();
|
|
2103
|
+
workload_diagnostics::record(
|
|
2104
|
+
workload_diagnostics::Phase::StateLockWait,
|
|
2105
|
+
acquired.duration_since(requested).as_nanos().min(u64::MAX as u128) as u64,
|
|
2106
|
+
);
|
|
2107
|
+
StateGuard {
|
|
2108
|
+
inner,
|
|
2109
|
+
held_since: Some(acquired),
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
/// Declare an equality index on one collection field and populate it from
|
|
2114
|
+
/// authoritative state. Returns whether this call created the index.
|
|
2115
|
+
///
|
|
2116
|
+
/// Creation and population happen under one lock, so an index is never
|
|
2117
|
+
/// observable in a half-built state. Declaring an index that already exists
|
|
2118
|
+
/// is not an error and does not rebuild it.
|
|
2119
|
+
///
|
|
2120
|
+
/// `recordId` is refused: on the query surface it is authority metadata that
|
|
2121
|
+
/// shadows a document field of the same name on object-valued records only,
|
|
2122
|
+
/// so an index over the document field would answer a different question
|
|
2123
|
+
/// from the one the predicate asks.
|
|
2124
|
+
pub fn create_equality_index(&self, collection: &str, field: &str) -> Result<bool> {
|
|
2125
|
+
let mut inner = self.state();
|
|
2126
|
+
let created = inner
|
|
2127
|
+
.equality_index
|
|
2128
|
+
.declare(collection, field)
|
|
2129
|
+
.map_err(FlowError::CapabilityError)?;
|
|
2130
|
+
if created {
|
|
2131
|
+
let Inner {
|
|
2132
|
+
rows,
|
|
2133
|
+
equality_index,
|
|
2134
|
+
..
|
|
2135
|
+
} = &mut *inner;
|
|
2136
|
+
equality_index.rebuild_field(collection, field, rows);
|
|
2137
|
+
}
|
|
2138
|
+
Ok(created)
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
/// Drop an equality index and its buckets. Returns whether one existed.
|
|
2142
|
+
pub fn drop_equality_index(&self, collection: &str, field: &str) -> Result<bool> {
|
|
2143
|
+
let mut inner = self.state();
|
|
2144
|
+
Ok(inner.equality_index.undeclare(collection, field))
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/// Every declared index, as `(collection, field)` in a stable order.
|
|
2148
|
+
pub fn equality_indexes(&self) -> Result<Vec<(String, String)>> {
|
|
2149
|
+
let inner = self.state();
|
|
2150
|
+
Ok(inner.equality_index.declarations())
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/// Re-derive every declared index from authoritative records.
|
|
2154
|
+
///
|
|
2155
|
+
/// This is the recovery path. An index is never read from disk and never
|
|
2156
|
+
/// received from a peer: after a restart, a snapshot install, or any other
|
|
2157
|
+
/// reload of authoritative state, the index is whatever this produces.
|
|
2158
|
+
pub fn rebuild_equality_indexes(&self) -> Result<EqualityIndexStats> {
|
|
2159
|
+
let mut inner = self.state();
|
|
2160
|
+
let Inner {
|
|
2161
|
+
rows,
|
|
2162
|
+
equality_index,
|
|
2163
|
+
..
|
|
2164
|
+
} = &mut *inner;
|
|
2165
|
+
equality_index.rebuild(rows);
|
|
2166
|
+
Ok(equality_index.stats())
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/// Index size, as counts. Never a value, never a record id.
|
|
2170
|
+
pub fn equality_index_stats(&self) -> Result<EqualityIndexStats> {
|
|
2171
|
+
let inner = self.state();
|
|
2172
|
+
Ok(inner.equality_index.stats())
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/// A fully ordered rendering of the live index.
|
|
2176
|
+
///
|
|
2177
|
+
/// Test instrumentation. It exists so a test can compare the live index
|
|
2178
|
+
/// against one rebuilt from authoritative state; nothing in execution reads
|
|
2179
|
+
/// it, and it is not part of any product surface.
|
|
2180
|
+
#[doc(hidden)]
|
|
2181
|
+
pub fn equality_index_snapshot(
|
|
2182
|
+
&self,
|
|
2183
|
+
) -> Result<BTreeMap<String, BTreeMap<String, BTreeMap<String, Vec<String>>>>> {
|
|
2184
|
+
let inner = self.state();
|
|
2185
|
+
Ok(inner.equality_index.snapshot())
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
/// The index invariant: the live index equals one derived from records now.
|
|
2189
|
+
///
|
|
2190
|
+
/// Returns a description of the first divergence rather than a bare bool, so
|
|
2191
|
+
/// a failing assertion says what diverged. This is the strongest statement
|
|
2192
|
+
/// available about index maintenance, because it compares the maintained
|
|
2193
|
+
/// structure against the authoritative state it claims to describe rather
|
|
2194
|
+
/// than against a second copy of its own bookkeeping.
|
|
2195
|
+
pub fn verify_equality_index(&self) -> Result<std::result::Result<(), String>> {
|
|
2196
|
+
let inner = self.state();
|
|
2197
|
+
let live = inner.equality_index.snapshot();
|
|
2198
|
+
let mut expected = inner.equality_index.clone();
|
|
2199
|
+
expected.clear();
|
|
2200
|
+
expected.rebuild(&inner.rows);
|
|
2201
|
+
let expected = expected.snapshot();
|
|
2202
|
+
if live == expected {
|
|
2203
|
+
return Ok(Ok(()));
|
|
2204
|
+
}
|
|
2205
|
+
for (collection, fields) in expected.iter() {
|
|
2206
|
+
for (field, buckets) in fields {
|
|
2207
|
+
let observed = live.get(collection).and_then(|fields| fields.get(field));
|
|
2208
|
+
if observed != Some(buckets) {
|
|
2209
|
+
return Ok(Err(format!(
|
|
2210
|
+
"index for {collection}.{field} diverges from authoritative state: \
|
|
2211
|
+
{} value buckets held, {} derived",
|
|
2212
|
+
observed.map(BTreeMap::len).unwrap_or(0),
|
|
2213
|
+
buckets.len(),
|
|
2214
|
+
)));
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
Ok(Err(
|
|
2219
|
+
"the live index holds entries no authoritative record derives".to_string()
|
|
2220
|
+
))
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/// Candidate record keys for one equality condition, copied out of the index.
|
|
2224
|
+
///
|
|
2225
|
+
/// `None` means the index cannot answer that condition — the field is not
|
|
2226
|
+
/// indexed, or the value is not an indexable scalar — and the caller must
|
|
2227
|
+
/// fall back to a scan. `Some(empty)` is an answer, not a failure.
|
|
2228
|
+
///
|
|
2229
|
+
/// The keys are copied while the state lock is held. Nothing borrowed from
|
|
2230
|
+
/// the index or from the record map escapes this call.
|
|
2231
|
+
pub fn equality_index_candidates(
|
|
2232
|
+
&self,
|
|
2233
|
+
collection: &str,
|
|
2234
|
+
field: &str,
|
|
2235
|
+
value: &Value,
|
|
2236
|
+
) -> Result<Option<Vec<String>>> {
|
|
2237
|
+
let inner = self.state();
|
|
2238
|
+
Ok(inner
|
|
2239
|
+
.equality_index
|
|
2240
|
+
.candidates(collection, field, value)
|
|
2241
|
+
.map(|keys| keys.iter().cloned().collect()))
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/// Execute a bounded query through the equality index when one applies.
|
|
2245
|
+
///
|
|
2246
|
+
/// `equalities` are the query's equality conditions, in the caller's order.
|
|
2247
|
+
/// At least one of them must be indexed and hold an indexable value, or this
|
|
2248
|
+
/// returns `Ok(None)` and the caller falls back to [`FeltDb::query_collection`].
|
|
2249
|
+
/// Returning `None` rather than an empty result is the difference between
|
|
2250
|
+
/// "the index does not apply" and "nothing matches", and conflating the two
|
|
2251
|
+
/// would silently drop records.
|
|
2252
|
+
///
|
|
2253
|
+
/// When the index does apply, every applicable condition contributes a
|
|
2254
|
+
/// candidate set and the sets are intersected smallest-first. The result is
|
|
2255
|
+
/// still only *candidates*: `predicate` — the caller's existing, unmodified
|
|
2256
|
+
/// predicate — decides what matches, evaluated against authoritative records.
|
|
2257
|
+
/// The index therefore cannot change what a query returns, only how many
|
|
2258
|
+
/// records the predicate has to look at.
|
|
2259
|
+
///
|
|
2260
|
+
/// Candidate keys, the record lookups they drive and the predicate all run
|
|
2261
|
+
/// under one acquisition of the state lock, so a query observes one coherent
|
|
2262
|
+
/// committed state exactly as a scan does. Records are cloned only when the
|
|
2263
|
+
/// predicate keeps them, and nothing borrowed from state outlives the lock.
|
|
2264
|
+
pub fn query_collection_by_equality<F>(
|
|
2265
|
+
&self,
|
|
2266
|
+
capability: &str,
|
|
2267
|
+
equalities: &[(&str, &Value)],
|
|
2268
|
+
mut predicate: F,
|
|
2269
|
+
) -> Result<Option<Vec<StoredRow>>>
|
|
2270
|
+
where
|
|
2271
|
+
F: FnMut(&StoredRow) -> bool,
|
|
2272
|
+
{
|
|
2273
|
+
let mut matched = Vec::new();
|
|
2274
|
+
let mut examined = 0usize;
|
|
2275
|
+
let mut evaluated = 0usize;
|
|
2276
|
+
{
|
|
2277
|
+
let inner = self.state();
|
|
2278
|
+
let _query = workload_diagnostics::span(workload_diagnostics::Phase::IndexedQuery);
|
|
2279
|
+
// Candidate selection, timed apart from record access: PR36 has to
|
|
2280
|
+
// be able to say whether the index lookup itself has become a cost,
|
|
2281
|
+
// and "index query time" alone could not distinguish a slow lookup
|
|
2282
|
+
// from a large candidate set.
|
|
2283
|
+
let lookup = workload_diagnostics::span(workload_diagnostics::Phase::IndexLookup);
|
|
2284
|
+
let mut applicable: Vec<&BTreeSet<String>> = Vec::new();
|
|
2285
|
+
for (field, value) in equalities {
|
|
2286
|
+
if let Some(keys) = inner.equality_index.candidates(capability, field, value) {
|
|
2287
|
+
applicable.push(keys);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if applicable.is_empty() {
|
|
2291
|
+
drop(lookup);
|
|
2292
|
+
drop(_query);
|
|
2293
|
+
drop(inner);
|
|
2294
|
+
query_execution_diagnostics::record_index_miss();
|
|
2295
|
+
return Ok(None);
|
|
2296
|
+
}
|
|
2297
|
+
// Smallest first. This is not cost estimation: a set's length is
|
|
2298
|
+
// already held by the set, so intersecting from the smallest is
|
|
2299
|
+
// simply the cheaper order to do the same work.
|
|
2300
|
+
applicable.sort_by_key(|keys| keys.len());
|
|
2301
|
+
drop(lookup);
|
|
2302
|
+
let (smallest, rest) = applicable.split_first().expect("non-empty");
|
|
2303
|
+
let bucket = inner.rows.get(capability);
|
|
2304
|
+
for key in smallest.iter() {
|
|
2305
|
+
if !rest.iter().all(|keys| keys.contains(key)) {
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
2308
|
+
examined += 1;
|
|
2309
|
+
let retrieval =
|
|
2310
|
+
workload_diagnostics::span(workload_diagnostics::Phase::CandidateRetrieval);
|
|
2311
|
+
let row = bucket.and_then(|rows| rows.get(key));
|
|
2312
|
+
drop(retrieval);
|
|
2313
|
+
let Some(row) = row else {
|
|
2314
|
+
// An index entry with no record behind it would be a
|
|
2315
|
+
// maintenance defect. Skipping it keeps the query correct
|
|
2316
|
+
// rather than surfacing a record that does not exist.
|
|
2317
|
+
continue;
|
|
2318
|
+
};
|
|
2319
|
+
evaluated += 1;
|
|
2320
|
+
let _predicate =
|
|
2321
|
+
workload_diagnostics::span(workload_diagnostics::Phase::PredicateEvaluation);
|
|
2322
|
+
if predicate(row) {
|
|
2323
|
+
drop(_predicate);
|
|
2324
|
+
matched.push(row.clone());
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
query_execution_diagnostics::record_indexed_query(examined, evaluated, matched.len());
|
|
2329
|
+
Ok(Some(matched))
|
|
1890
2330
|
}
|
|
1891
2331
|
|
|
1892
2332
|
/// Traverse one collection in immutable record-key order without materializing it.
|
|
@@ -1896,7 +2336,7 @@ impl FeltDb {
|
|
|
1896
2336
|
after: Option<&str>,
|
|
1897
2337
|
limit: usize,
|
|
1898
2338
|
) -> Result<Vec<StoredRow>> {
|
|
1899
|
-
let inner = self.
|
|
2339
|
+
let inner = self.state();
|
|
1900
2340
|
let Some(rows) = inner.rows.get(capability) else {
|
|
1901
2341
|
return Ok(vec![]);
|
|
1902
2342
|
};
|
|
@@ -1916,7 +2356,7 @@ impl FeltDb {
|
|
|
1916
2356
|
|
|
1917
2357
|
/// Fetch one live collection record without materializing collection state.
|
|
1918
2358
|
pub fn get_collection_record(&self, capability: &str, key: &str) -> Result<Option<StoredRow>> {
|
|
1919
|
-
let inner = self.
|
|
2359
|
+
let inner = self.state();
|
|
1920
2360
|
Ok(inner
|
|
1921
2361
|
.rows
|
|
1922
2362
|
.get(capability)
|
|
@@ -1932,14 +2372,14 @@ impl FeltDb {
|
|
|
1932
2372
|
|
|
1933
2373
|
/// Add a peer for synchronization
|
|
1934
2374
|
pub fn add_sync_peer(&self, peer_id: String) -> Result<()> {
|
|
1935
|
-
let mut inner = self.
|
|
2375
|
+
let mut inner = self.state();
|
|
1936
2376
|
inner.sync_state.add_peer(peer_id);
|
|
1937
2377
|
Ok(())
|
|
1938
2378
|
}
|
|
1939
2379
|
|
|
1940
2380
|
/// Remove a peer from synchronization
|
|
1941
2381
|
pub fn remove_sync_peer(&self, peer_id: &str) -> Result<()> {
|
|
1942
|
-
let mut inner = self.
|
|
2382
|
+
let mut inner = self.state();
|
|
1943
2383
|
inner.sync_state.remove_peer(peer_id);
|
|
1944
2384
|
Ok(())
|
|
1945
2385
|
}
|
|
@@ -1950,7 +2390,7 @@ impl FeltDb {
|
|
|
1950
2390
|
_peer_id: &str,
|
|
1951
2391
|
since_sequence: u64,
|
|
1952
2392
|
) -> Result<Vec<Operation>> {
|
|
1953
|
-
let inner = self.
|
|
2393
|
+
let inner = self.state();
|
|
1954
2394
|
let pending: Vec<Operation> = inner
|
|
1955
2395
|
.change_log
|
|
1956
2396
|
.pending_operations()
|
|
@@ -1965,18 +2405,18 @@ impl FeltDb {
|
|
|
1965
2405
|
&self,
|
|
1966
2406
|
versions: &std::collections::HashMap<String, u64>,
|
|
1967
2407
|
) -> Result<Vec<Operation>> {
|
|
1968
|
-
let inner = self.
|
|
2408
|
+
let inner = self.state();
|
|
1969
2409
|
Ok(inner.change_log.operations_since(versions))
|
|
1970
2410
|
}
|
|
1971
2411
|
|
|
1972
2412
|
/// Return the highest durable operation sequence observed from each origin.
|
|
1973
2413
|
pub fn operation_versions(&self) -> Result<std::collections::HashMap<String, u64>> {
|
|
1974
|
-
let inner = self.
|
|
2414
|
+
let inner = self.state();
|
|
1975
2415
|
Ok(inner.change_log.versions())
|
|
1976
2416
|
}
|
|
1977
2417
|
|
|
1978
2418
|
pub fn export_snapshot(&self) -> Result<DatabaseSnapshot> {
|
|
1979
|
-
let inner = self.
|
|
2419
|
+
let inner = self.state();
|
|
1980
2420
|
let mut rows: Vec<StoredRow> = inner
|
|
1981
2421
|
.rows
|
|
1982
2422
|
.values()
|
|
@@ -1998,7 +2438,7 @@ impl FeltDb {
|
|
|
1998
2438
|
/// State-contract execution uses this for record-version preconditions;
|
|
1999
2439
|
/// portable snapshots intentionally continue to strip those envelopes.
|
|
2000
2440
|
pub fn state_rows(&self) -> Result<Vec<StoredRow>> {
|
|
2001
|
-
let inner = self.
|
|
2441
|
+
let inner = self.state();
|
|
2002
2442
|
Ok(inner
|
|
2003
2443
|
.rows
|
|
2004
2444
|
.values()
|
|
@@ -2012,7 +2452,7 @@ impl FeltDb {
|
|
|
2012
2452
|
/// Selecting the capability buckets before cloning prevents one application's
|
|
2013
2453
|
/// read snapshot from materializing every other application's durable rows.
|
|
2014
2454
|
pub fn state_rows_for_namespace(&self, state_namespace: &str) -> Result<Vec<StoredRow>> {
|
|
2015
|
-
let inner = self.
|
|
2455
|
+
let inner = self.state();
|
|
2016
2456
|
let prefix = format!("{state_namespace}:");
|
|
2017
2457
|
Ok(inner
|
|
2018
2458
|
.rows
|
|
@@ -2023,7 +2463,7 @@ impl FeltDb {
|
|
|
2023
2463
|
}
|
|
2024
2464
|
|
|
2025
2465
|
pub fn can_install_snapshot(&self) -> Result<bool> {
|
|
2026
|
-
let inner = self.
|
|
2466
|
+
let inner = self.state();
|
|
2027
2467
|
Ok(!inner.bootstrapped && inner.rows.is_empty() && inner.change_log.pending.is_empty())
|
|
2028
2468
|
}
|
|
2029
2469
|
|
|
@@ -2031,7 +2471,7 @@ impl FeltDb {
|
|
|
2031
2471
|
/// local history must converge through operations rather than overwrite it.
|
|
2032
2472
|
pub fn install_snapshot(&self, snapshot: DatabaseSnapshot) -> Result<()> {
|
|
2033
2473
|
snapshot.verify()?;
|
|
2034
|
-
let mut inner = self.
|
|
2474
|
+
let mut inner = self.state();
|
|
2035
2475
|
if !inner.rows.is_empty() || !inner.change_log.pending.is_empty() {
|
|
2036
2476
|
return Err(FlowError::CapabilityError(
|
|
2037
2477
|
"snapshot bootstrap requires a pristine node".to_string(),
|
|
@@ -2069,7 +2509,7 @@ impl FeltDb {
|
|
|
2069
2509
|
file.sync_all()?;
|
|
2070
2510
|
fs::rename(temporary, &inner.path)?;
|
|
2071
2511
|
|
|
2072
|
-
inner.
|
|
2512
|
+
inner.clear_rows();
|
|
2073
2513
|
inner.key_counters.clear();
|
|
2074
2514
|
for row in snapshot.rows {
|
|
2075
2515
|
if let Some(id) = row
|
|
@@ -2083,11 +2523,7 @@ impl FeltDb {
|
|
|
2083
2523
|
.or_default();
|
|
2084
2524
|
*counter = (*counter).max(id + 1);
|
|
2085
2525
|
}
|
|
2086
|
-
inner
|
|
2087
|
-
.rows
|
|
2088
|
-
.entry(row.capability.clone())
|
|
2089
|
-
.or_default()
|
|
2090
|
-
.insert(row.key.clone(), row);
|
|
2526
|
+
inner.put_row(row);
|
|
2091
2527
|
}
|
|
2092
2528
|
inner.change_log.observed_versions = snapshot.versions;
|
|
2093
2529
|
inner.bootstrapped = true;
|
|
@@ -2096,7 +2532,7 @@ impl FeltDb {
|
|
|
2096
2532
|
|
|
2097
2533
|
/// Acknowledge receipt of operations from a peer
|
|
2098
2534
|
pub fn acknowledge_peer_operations(&self, peer_id: String, sequence: u64) -> Result<()> {
|
|
2099
|
-
let mut inner = self.
|
|
2535
|
+
let mut inner = self.state();
|
|
2100
2536
|
inner.change_log.acknowledge_peer(peer_id, sequence);
|
|
2101
2537
|
persist_sync_metadata(&inner)?;
|
|
2102
2538
|
Ok(())
|
|
@@ -2108,25 +2544,31 @@ impl FeltDb {
|
|
|
2108
2544
|
peer_id: String,
|
|
2109
2545
|
versions: HashMap<String, u64>,
|
|
2110
2546
|
) -> Result<()> {
|
|
2111
|
-
let mut inner = self.
|
|
2547
|
+
let mut inner = self.state();
|
|
2112
2548
|
inner.change_log.acknowledge_versions(peer_id, versions);
|
|
2113
2549
|
persist_sync_metadata(&inner)
|
|
2114
2550
|
}
|
|
2115
2551
|
|
|
2116
2552
|
pub fn acknowledged_peer_versions(&self, peer_id: &str) -> Result<HashMap<String, u64>> {
|
|
2117
|
-
let inner = self.
|
|
2553
|
+
let inner = self.state();
|
|
2118
2554
|
Ok(inner.change_log.acknowledged_versions(peer_id))
|
|
2119
2555
|
}
|
|
2120
2556
|
|
|
2557
|
+
/// Earliest operation still available for incremental replay, by origin.
|
|
2558
|
+
pub fn retained_operation_floors(&self) -> Result<HashMap<String, u64>> {
|
|
2559
|
+
let inner = self.state();
|
|
2560
|
+
Ok(inner.change_log.retained_from())
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2121
2563
|
pub fn acknowledged_operation_count(&self, active_peers: &[String]) -> Result<usize> {
|
|
2122
|
-
let inner = self.
|
|
2564
|
+
let inner = self.state();
|
|
2123
2565
|
Ok(inner.change_log.acknowledged_operation_count(active_peers))
|
|
2124
2566
|
}
|
|
2125
2567
|
|
|
2126
2568
|
/// Atomically replace acknowledged history with a state snapshot and any
|
|
2127
2569
|
/// operations still needed by at least one configured peer.
|
|
2128
2570
|
pub fn compact_operation_log(&self, active_peers: &[String]) -> Result<usize> {
|
|
2129
|
-
let mut inner = self.
|
|
2571
|
+
let mut inner = self.state();
|
|
2130
2572
|
let removed = inner.change_log.prune_acknowledged(active_peers);
|
|
2131
2573
|
if removed == 0 {
|
|
2132
2574
|
return Ok(0);
|
|
@@ -2166,7 +2608,7 @@ impl FeltDb {
|
|
|
2166
2608
|
|
|
2167
2609
|
/// Register a peer in the distributed fabric
|
|
2168
2610
|
pub fn register_peer(&self, advertisement: PeerAdvertisement) -> Result<()> {
|
|
2169
|
-
let inner = self.
|
|
2611
|
+
let inner = self.state();
|
|
2170
2612
|
inner
|
|
2171
2613
|
.peer_registry
|
|
2172
2614
|
.register(advertisement)
|
|
@@ -2175,7 +2617,7 @@ impl FeltDb {
|
|
|
2175
2617
|
|
|
2176
2618
|
/// Get a peer's advertisement
|
|
2177
2619
|
pub fn get_peer(&self, peer_id: &PeerId) -> Result<Option<PeerAdvertisement>> {
|
|
2178
|
-
let inner = self.
|
|
2620
|
+
let inner = self.state();
|
|
2179
2621
|
inner
|
|
2180
2622
|
.peer_registry
|
|
2181
2623
|
.get(peer_id)
|
|
@@ -2184,7 +2626,7 @@ impl FeltDb {
|
|
|
2184
2626
|
|
|
2185
2627
|
/// Find all peers providing a namespace
|
|
2186
2628
|
pub fn peers_for_namespace(&self, namespace: &str) -> Result<Vec<PeerAdvertisement>> {
|
|
2187
|
-
let inner = self.
|
|
2629
|
+
let inner = self.state();
|
|
2188
2630
|
inner
|
|
2189
2631
|
.peer_registry
|
|
2190
2632
|
.peers_for_namespace(namespace)
|
|
@@ -2193,7 +2635,7 @@ impl FeltDb {
|
|
|
2193
2635
|
|
|
2194
2636
|
/// Find all peers providing a capability
|
|
2195
2637
|
pub fn peers_for_capability(&self, capability: &str) -> Result<Vec<PeerAdvertisement>> {
|
|
2196
|
-
let inner = self.
|
|
2638
|
+
let inner = self.state();
|
|
2197
2639
|
inner
|
|
2198
2640
|
.peer_registry
|
|
2199
2641
|
.peers_for_capability(capability)
|
|
@@ -2202,7 +2644,7 @@ impl FeltDb {
|
|
|
2202
2644
|
|
|
2203
2645
|
/// Get all registered peers
|
|
2204
2646
|
pub fn all_peers(&self) -> Result<Vec<PeerAdvertisement>> {
|
|
2205
|
-
let inner = self.
|
|
2647
|
+
let inner = self.state();
|
|
2206
2648
|
inner
|
|
2207
2649
|
.peer_registry
|
|
2208
2650
|
.all_peers()
|
|
@@ -2218,14 +2660,14 @@ impl FeltDb {
|
|
|
2218
2660
|
|
|
2219
2661
|
/// Get current sync state
|
|
2220
2662
|
pub fn sync_state(&self) -> Result<SyncState> {
|
|
2221
|
-
let inner = self.
|
|
2663
|
+
let inner = self.state();
|
|
2222
2664
|
Ok(inner.sync_state.clone())
|
|
2223
2665
|
}
|
|
2224
2666
|
|
|
2225
2667
|
/// Apply an operation from a remote peer
|
|
2226
2668
|
/// This merges the vector clock, resolves conflicts, and applies the mutation
|
|
2227
2669
|
pub fn apply_remote_operation(&self, op: Operation) -> Result<()> {
|
|
2228
|
-
let mut inner = self.
|
|
2670
|
+
let mut inner = self.state();
|
|
2229
2671
|
|
|
2230
2672
|
if op.content_hash != op.compute_content_hash() {
|
|
2231
2673
|
return Err(FlowError::CapabilityError(
|
|
@@ -2324,18 +2766,12 @@ impl FeltDb {
|
|
|
2324
2766
|
OperationType::Insert | OperationType::Update => {
|
|
2325
2767
|
append_event(&inner.path, &row)?;
|
|
2326
2768
|
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);
|
|
2769
|
+
inner.put_row(row);
|
|
2332
2770
|
}
|
|
2333
2771
|
OperationType::Delete => {
|
|
2334
2772
|
append_event(&inner.path, &row)?;
|
|
2335
2773
|
inner.authority_revision += 1;
|
|
2336
|
-
|
|
2337
|
-
bucket.remove(&op_to_apply.key);
|
|
2338
|
-
}
|
|
2774
|
+
inner.remove_row(&op_to_apply.capability, &op_to_apply.key);
|
|
2339
2775
|
}
|
|
2340
2776
|
}
|
|
2341
2777
|
if let Some(id) = op_to_apply
|
|
@@ -2366,7 +2802,7 @@ impl FeltDb {
|
|
|
2366
2802
|
|
|
2367
2803
|
/// Get the current vector clock state
|
|
2368
2804
|
pub fn get_vector_clock(&self) -> Result<Option<std::collections::HashMap<String, u64>>> {
|
|
2369
|
-
let inner = self.
|
|
2805
|
+
let inner = self.state();
|
|
2370
2806
|
Ok(inner.sync_state.vector_clock.clone())
|
|
2371
2807
|
}
|
|
2372
2808
|
}
|
|
@@ -2380,7 +2816,7 @@ pub struct Capability {
|
|
|
2380
2816
|
impl Capability {
|
|
2381
2817
|
pub fn insert<T: Serialize>(&self, value: T) -> Result<String> {
|
|
2382
2818
|
let key = {
|
|
2383
|
-
let mut inner = self.db.
|
|
2819
|
+
let mut inner = self.db.state();
|
|
2384
2820
|
let next = inner.key_counters.entry(self.name.clone()).or_default();
|
|
2385
2821
|
let key = format!("{}:{}", self.name, *next);
|
|
2386
2822
|
*next += 1;
|
|
@@ -2433,7 +2869,7 @@ impl Capability {
|
|
|
2433
2869
|
F: Fn(&T) -> bool,
|
|
2434
2870
|
{
|
|
2435
2871
|
let rust_type = type_name::<T>().to_string();
|
|
2436
|
-
let mut inner = self.db.
|
|
2872
|
+
let mut inner = self.db.state();
|
|
2437
2873
|
*inner
|
|
2438
2874
|
.query_count_by_type
|
|
2439
2875
|
.entry(rust_type.clone())
|
|
@@ -2487,7 +2923,7 @@ impl Capability {
|
|
|
2487
2923
|
let capability_name = self.name.clone();
|
|
2488
2924
|
|
|
2489
2925
|
let initial_results = {
|
|
2490
|
-
let mut inner = self.db.
|
|
2926
|
+
let mut inner = self.db.state();
|
|
2491
2927
|
*inner
|
|
2492
2928
|
.query_count_by_type
|
|
2493
2929
|
.entry(rust_type.clone())
|
|
@@ -2568,7 +3004,7 @@ where
|
|
|
2568
3004
|
}
|
|
2569
3005
|
|
|
2570
3006
|
let results = {
|
|
2571
|
-
let inner = self.db.
|
|
3007
|
+
let inner = self.db.state();
|
|
2572
3008
|
let mut out = Vec::new();
|
|
2573
3009
|
for bucket in inner.rows.values() {
|
|
2574
3010
|
for row in bucket.values() {
|
|
@@ -2639,7 +3075,7 @@ where
|
|
|
2639
3075
|
}
|
|
2640
3076
|
|
|
2641
3077
|
let results = {
|
|
2642
|
-
let inner = self.db.
|
|
3078
|
+
let inner = self.db.state();
|
|
2643
3079
|
let mut out = Vec::new();
|
|
2644
3080
|
if let Some(bucket) = inner.rows.get(&self.capability_name) {
|
|
2645
3081
|
for row in bucket.values() {
|
|
@@ -2772,15 +3208,9 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
2772
3208
|
inner.change_log.add_operation(operation);
|
|
2773
3209
|
}
|
|
2774
3210
|
if row.deleted {
|
|
2775
|
-
|
|
2776
|
-
bucket.remove(&row.key);
|
|
2777
|
-
}
|
|
3211
|
+
inner.remove_row(&row.capability, &row.key);
|
|
2778
3212
|
} else {
|
|
2779
|
-
inner
|
|
2780
|
-
.rows
|
|
2781
|
-
.entry(row.capability.clone())
|
|
2782
|
-
.or_default()
|
|
2783
|
-
.insert(row.key.clone(), row);
|
|
3213
|
+
inner.put_row(row);
|
|
2784
3214
|
}
|
|
2785
3215
|
}
|
|
2786
3216
|
}
|
|
@@ -2817,15 +3247,9 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
2817
3247
|
*counter = (*counter).max(id + 1);
|
|
2818
3248
|
}
|
|
2819
3249
|
if row.deleted {
|
|
2820
|
-
|
|
2821
|
-
bucket.remove(&row.key);
|
|
2822
|
-
}
|
|
3250
|
+
inner.remove_row(&row.capability, &row.key);
|
|
2823
3251
|
} else {
|
|
2824
|
-
inner
|
|
2825
|
-
.rows
|
|
2826
|
-
.entry(row.capability.clone())
|
|
2827
|
-
.or_default()
|
|
2828
|
-
.insert(row.key.clone(), row);
|
|
3252
|
+
inner.put_row(row);
|
|
2829
3253
|
}
|
|
2830
3254
|
}
|
|
2831
3255
|
|
|
@@ -2904,6 +3328,10 @@ fn persist_sync_metadata(inner: &Inner) -> Result<()> {
|
|
|
2904
3328
|
}
|
|
2905
3329
|
|
|
2906
3330
|
fn append_event(path: &Path, event: &StoredRow) -> Result<()> {
|
|
3331
|
+
// Single-record durability: the line is written and flushed to the OS, but
|
|
3332
|
+
// not fsynced. `append_transaction` below does fsync. The asymmetry is
|
|
3333
|
+
// pre-existing and PR36 measures it rather than changing it.
|
|
3334
|
+
let _span = workload_diagnostics::span(workload_diagnostics::Phase::Persistence);
|
|
2907
3335
|
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
|
|
2908
3336
|
let mut line = serde_json::to_string(event)?;
|
|
2909
3337
|
line.push('\n');
|
|
@@ -2913,6 +3341,10 @@ fn append_event(path: &Path, event: &StoredRow) -> Result<()> {
|
|
|
2913
3341
|
}
|
|
2914
3342
|
|
|
2915
3343
|
fn append_transaction(path: &Path, transaction: &TransactionLogRecord) -> Result<()> {
|
|
3344
|
+
// A transaction commit fsyncs. This span therefore includes the fsync, which
|
|
3345
|
+
// is the point: it is the durability cost of a commit, measured rather than
|
|
3346
|
+
// assumed.
|
|
3347
|
+
let _span = workload_diagnostics::span(workload_diagnostics::Phase::Persistence);
|
|
2916
3348
|
let mut file = OpenOptions::new().create(true).append(true).open(path)?;
|
|
2917
3349
|
write_json_line(&mut file, transaction)?;
|
|
2918
3350
|
file.sync_data()?;
|