@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.
Files changed (59) hide show
  1. package/dist/cli/commands.js +4 -1
  2. package/dist/cli/provisioning-neutrality.js +79 -0
  3. package/dist/collection.d.ts +43 -1
  4. package/dist/collection.d.ts.map +1 -1
  5. package/dist/collection.js +192 -22
  6. package/dist/create/create.js +25 -21
  7. package/dist/create/managed-account.js +11 -0
  8. package/dist/create/package-versions.js +1 -1
  9. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +595 -0
  10. package/dist/create/server-source/crates/feltdb/src/lib.rs +547 -115
  11. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +11 -2
  12. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +126 -0
  13. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +292 -2
  14. package/dist/create/server-source/crates/feltdb/src/sync.rs +12 -0
  15. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +443 -0
  16. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +233 -0
  17. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +892 -0
  18. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +1137 -29
  19. package/dist/create/server-source/crates/feltdb-server/src/main.rs +474 -28
  20. package/dist/db.d.ts +33 -34
  21. package/dist/db.d.ts.map +1 -1
  22. package/dist/db.js +74 -20
  23. package/dist/deployment.d.ts +30 -0
  24. package/dist/deployment.d.ts.map +1 -0
  25. package/dist/deployment.js +130 -0
  26. package/dist/embedded-transaction.d.ts +22 -4
  27. package/dist/embedded-transaction.d.ts.map +1 -1
  28. package/dist/embedded-transaction.js +51 -5
  29. package/dist/feltdb.d.ts +14 -2
  30. package/dist/feltdb.d.ts.map +1 -1
  31. package/dist/file-db.js +1 -1
  32. package/dist/http-client.d.ts +14 -0
  33. package/dist/http-client.d.ts.map +1 -1
  34. package/dist/http-client.js +23 -5
  35. package/dist/http-db.d.ts +119 -1
  36. package/dist/http-db.d.ts.map +1 -1
  37. package/dist/http-db.js +346 -31
  38. package/dist/index-core.d.ts +2 -0
  39. package/dist/index-core.d.ts.map +1 -1
  40. package/dist/index-core.js +2 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +9 -0
  43. package/dist/indexeddb-db.d.ts.map +1 -1
  44. package/dist/indexeddb-db.js +35 -21
  45. package/dist/managed-recovery.d.ts +192 -0
  46. package/dist/managed-recovery.d.ts.map +1 -0
  47. package/dist/managed-recovery.js +242 -0
  48. package/dist/memory-db.js +1 -1
  49. package/dist/studio-app/assets/{feltdb_wasm-DB8cX151.js → feltdb_wasm-CVQWgXO-.js} +1 -1
  50. package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
  51. package/dist/studio-app/assets/index-DwgNAIIX.js +29 -0
  52. package/dist/studio-app/index.html +1 -1
  53. package/dist/transaction.d.ts +30 -0
  54. package/dist/transaction.d.ts.map +1 -1
  55. package/dist/transaction.js +41 -0
  56. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  57. package/package.json +1 -1
  58. package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
  59. 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, &current.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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned").authority_revision)
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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
- if let Some(bucket) = inner.rows.get_mut(&row.capability) {
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
- // Track whether this is an insert (new row) or update (existing row)
1502
- let is_insert = !inner
1503
- .rows
1504
- .get(&row.capability)
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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
- if let Some(bucket) = inner.rows.get_mut(&capability) {
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
1885
- Ok(inner
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.rows.clear();
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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
- if let Some(bucket) = inner.rows.get_mut(&op_to_apply.capability) {
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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.inner.lock().expect("lock poisoned");
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
- if let Some(bucket) = inner.rows.get_mut(&row.capability) {
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
- if let Some(bucket) = inner.rows.get_mut(&row.capability) {
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()?;