@feltdb/core 0.12.0 → 0.12.1
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/agent-state.d.ts +489 -0
- package/dist/agent-state.d.ts.map +1 -0
- package/dist/agent-state.js +993 -0
- package/dist/build-identity.json +19 -0
- package/dist/create/build-identity.json +19 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/lib.rs +11 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +854 -155
- package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +31 -13
- package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +918 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +15 -2
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +4 -0
- package/dist/create/server-source/crates/feltdb-server/benches/query_shape_observer.rs +136 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +3 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +74 -5
- package/dist/create/server-source/crates/feltdb-server/src/query_shape.rs +1137 -0
- package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.expected.json +300 -0
- package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.json +25 -0
- package/dist/create/server-source/crates/feltdb-server/tests/query_shape_trace.rs +128 -0
- package/dist/index-core.d.ts +1 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +1 -0
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/GlobalSearch.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-BHTKlUgT.js → components-Bivm2lX6.js} +2 -1
- package/dist/studio/control-plane/Agents.d.ts.map +1 -1
- package/dist/studio/control-plane/Data.d.ts.map +1 -1
- package/dist/studio/control-plane/Operations.d.ts.map +1 -1
- package/dist/studio/control-plane/Overview.d.ts.map +1 -1
- package/dist/studio/control-plane/Schema.d.ts.map +1 -1
- package/dist/studio/control-plane/State.d.ts +11 -0
- package/dist/studio/control-plane/State.d.ts.map +1 -1
- package/dist/studio/control-plane/index.d.ts +1 -1
- package/dist/studio/control-plane/index.d.ts.map +1 -1
- package/dist/studio/control-plane/index.js +2 -2
- package/dist/studio/control-plane/primitives.d.ts +14 -2
- package/dist/studio/control-plane/primitives.d.ts.map +1 -1
- package/dist/studio/control-plane-CLm69XwX.js +4121 -0
- package/dist/studio/index.js +266 -235
- package/dist/studio/studio.css +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-DYfWAV0E.js → feltdb_wasm-DACcD-zs.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BejThWID.wasm +0 -0
- package/dist/studio-app/assets/index-CiC8VY-a.css +1 -0
- package/dist/studio-app/assets/index-CoZaxnlm.js +30 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +2 -2
- package/dist/studio/control-plane-DP8ljTDO.js +0 -3207
- package/dist/studio-app/assets/feltdb_wasm_bg-DHiOUhoQ.wasm +0 -0
- package/dist/studio-app/assets/index-6f6KVx5R.js +0 -30
- package/dist/studio-app/assets/index-BrsF-kWL.css +0 -1
|
@@ -1371,6 +1371,553 @@ fn current_time_ms() -> u64 {
|
|
|
1371
1371
|
.as_millis() as u64
|
|
1372
1372
|
}
|
|
1373
1373
|
|
|
1374
|
+
/// How long a revision is protected from garbage collection after it is
|
|
1375
|
+
/// minted, measured on its own `timestamp_ms`. A candidate an agent has just
|
|
1376
|
+
/// committed but not yet promoted is a root for this long, so a collection that
|
|
1377
|
+
/// races the promotion cannot reclaim it out from under the caller.
|
|
1378
|
+
pub(crate) const GC_GRACE_MS: u64 = 15 * 60 * 1000;
|
|
1379
|
+
|
|
1380
|
+
/// Durable state-model writes as FeltDB transactions.
|
|
1381
|
+
///
|
|
1382
|
+
/// # Ordering
|
|
1383
|
+
///
|
|
1384
|
+
/// Every operation here follows the same three steps:
|
|
1385
|
+
///
|
|
1386
|
+
/// 1. **Read** the `state` rows the decision depends on.
|
|
1387
|
+
/// 2. **Decide** in Rust: versions, presence, integrity.
|
|
1388
|
+
/// 3. **Commit** with `FeltDb::apply_atomic_transaction_guarded`, carrying a
|
|
1389
|
+
/// record precondition for *every row read in step 1*. FeltDB evaluates the
|
|
1390
|
+
/// preconditions inside its commit lock, before staging anything, and
|
|
1391
|
+
/// applies the mutations — one log record, `sync_data` before returning —
|
|
1392
|
+
/// only if every row is still as it was read. Otherwise nothing is applied
|
|
1393
|
+
/// and the operation starts again from step 1.
|
|
1394
|
+
///
|
|
1395
|
+
/// So a decision is only ever applied to the state it was made against. The
|
|
1396
|
+
/// mutual exclusion is FeltDB's `Inner` mutex, shared by every `StateStore`,
|
|
1397
|
+
/// every `Arc<FeltDb>` clone and every `FeltDb::open` of the same path in one
|
|
1398
|
+
/// process; the per-instance `mutation_lock` plays no part on this backing.
|
|
1399
|
+
///
|
|
1400
|
+
/// # How a row is pinned
|
|
1401
|
+
///
|
|
1402
|
+
/// FeltDB's record precondition compares `__version`, reading a row without one
|
|
1403
|
+
/// as version 1, and stamps `N + 1` on a row written under `expected_version =
|
|
1404
|
+
/// N` (`require_absent` writes 1). That gives three kinds of pin:
|
|
1405
|
+
///
|
|
1406
|
+
/// - **Versioned rows** (ref, ref incarnation) are only ever written under a
|
|
1407
|
+
/// precondition, so their `__version` advances on every write and
|
|
1408
|
+
/// `expected_version = <as read>` pins them; an absent one is pinned by
|
|
1409
|
+
/// `require_absent`.
|
|
1410
|
+
/// - **Immutable rows** (revision, content object) are never versioned, so
|
|
1411
|
+
/// `expected_version = 1` means exactly "still present" and writes nothing.
|
|
1412
|
+
/// - **The generation row** (`state:_gcgen`) is rewritten by every ref change
|
|
1413
|
+
/// and every committed revision *without* a precondition, carrying a
|
|
1414
|
+
/// `__version` unique to that transaction (an unconditional write is stored
|
|
1415
|
+
/// verbatim). Writers therefore never conflict with each other through it,
|
|
1416
|
+
/// while a collection that pins the value it read is refused if any of them
|
|
1417
|
+
/// committed in between.
|
|
1418
|
+
///
|
|
1419
|
+
/// | operation | pinned | writes |
|
|
1420
|
+
/// | --- | --- | --- |
|
|
1421
|
+
/// | create ref | ref (absent), incarnation, target | ref, incarnation, generation |
|
|
1422
|
+
/// | CAS ref | ref, incarnation, target | ref, generation |
|
|
1423
|
+
/// | delete ref | ref, incarnation | (ref removed), incarnation, generation |
|
|
1424
|
+
/// | commit revision | revision (absent), content object, parent | object (if new), revision, generation |
|
|
1425
|
+
/// | collect | generation | (unreachable revisions and objects removed) |
|
|
1426
|
+
///
|
|
1427
|
+
/// The incarnation row is pinned alongside the ref because a delete followed by
|
|
1428
|
+
/// a create returns the ref row to `__version` 1; the incarnation row is never
|
|
1429
|
+
/// deleted, so its version only grows and the pair cannot repeat.
|
|
1430
|
+
///
|
|
1431
|
+
/// # The races this closes
|
|
1432
|
+
///
|
|
1433
|
+
/// - *Concurrent CAS.* Both read the ref at `__version` N; the first commit
|
|
1434
|
+
/// stamps N + 1; the second's precondition fails, it re-reads, and reports
|
|
1435
|
+
/// `VersionConflict`.
|
|
1436
|
+
/// - *GC vs promotion.* If the promotion commits first, the generation moved
|
|
1437
|
+
/// and GC re-plans with the target as a ref root. If GC commits first, the
|
|
1438
|
+
/// promotion's target pin fails, and on re-read it reports `UnknownTarget`.
|
|
1439
|
+
/// - *GC vs commit of a child.* If the commit lands first, the generation
|
|
1440
|
+
/// moved. If GC lands first, the commit's parent pin fails and it reports
|
|
1441
|
+
/// `UnknownParent`.
|
|
1442
|
+
///
|
|
1443
|
+
/// Revisions minted by the record write path are not committed here; each is a
|
|
1444
|
+
/// child of that path's current head, which collection always treats as a
|
|
1445
|
+
/// root, so they cannot gain a collected parent.
|
|
1446
|
+
///
|
|
1447
|
+
/// # Idempotent ref requests
|
|
1448
|
+
///
|
|
1449
|
+
/// Ref transactions carry deterministic ids derived from the ref, its
|
|
1450
|
+
/// *incarnation* and the version they act on, and a payload hash of the rest
|
|
1451
|
+
/// of the request. FeltDB binds an id to its payload durably, across restart
|
|
1452
|
+
/// and compaction. A repeat of a committed request finds its own payload under
|
|
1453
|
+
/// its id and returns `Ok` without writing; any other request for the same slot
|
|
1454
|
+
/// finds a different payload and is a conflict. The incarnation
|
|
1455
|
+
/// (`state:refinc:…`, advanced by every create) keeps a deleted-and-recreated
|
|
1456
|
+
/// ref, back at version 1, from matching a transaction of its predecessor. Rows
|
|
1457
|
+
/// written before this scheme have no incarnation and are incarnation 0.
|
|
1458
|
+
pub(crate) mod durable {
|
|
1459
|
+
use super::*;
|
|
1460
|
+
use crate::{AtomicMutation, FeltDb, FlowError, PreconditionFailure, RecordPrecondition};
|
|
1461
|
+
use serde_json::json;
|
|
1462
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
1463
|
+
|
|
1464
|
+
/// Fresh reads and a new attempt after a refused precondition. Each refusal
|
|
1465
|
+
/// means a competing write committed, so the system as a whole makes
|
|
1466
|
+
/// progress; the bound only turns pathological contention into an error.
|
|
1467
|
+
const ATTEMPTS: usize = 64;
|
|
1468
|
+
|
|
1469
|
+
pub(super) const GENERATION_KEY: &str = "state:_gcgen";
|
|
1470
|
+
|
|
1471
|
+
fn incarnation_key(namespace: &str, name: &str) -> String {
|
|
1472
|
+
format!(
|
|
1473
|
+
"state:refinc:{}:{}",
|
|
1474
|
+
encode_ref_part(namespace),
|
|
1475
|
+
encode_ref_part(name)
|
|
1476
|
+
)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
fn put(key: &str, value: Value) -> AtomicMutation {
|
|
1480
|
+
AtomicMutation {
|
|
1481
|
+
capability: "state".into(),
|
|
1482
|
+
key: key.into(),
|
|
1483
|
+
value: Some(value),
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
fn remove(key: &str) -> AtomicMutation {
|
|
1488
|
+
AtomicMutation {
|
|
1489
|
+
capability: "state".into(),
|
|
1490
|
+
key: key.into(),
|
|
1491
|
+
value: None,
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/// The version FeltDB's record precondition reads for a row.
|
|
1496
|
+
fn row_version(row: &Value) -> u64 {
|
|
1497
|
+
row.get("__version").and_then(Value::as_u64).unwrap_or(1)
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
/// Pin a row to exactly what was read: absent, or this version.
|
|
1501
|
+
fn unchanged(key: &str, read: Option<&Value>) -> RecordPrecondition {
|
|
1502
|
+
RecordPrecondition {
|
|
1503
|
+
capability: "state".into(),
|
|
1504
|
+
key: key.into(),
|
|
1505
|
+
require_absent: read.is_none(),
|
|
1506
|
+
expected_version: read.map(row_version),
|
|
1507
|
+
..RecordPrecondition::default()
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/// Pin an immutable (never versioned) row as present.
|
|
1512
|
+
fn present(key: &str) -> RecordPrecondition {
|
|
1513
|
+
RecordPrecondition {
|
|
1514
|
+
capability: "state".into(),
|
|
1515
|
+
key: key.into(),
|
|
1516
|
+
expected_version: Some(1),
|
|
1517
|
+
..RecordPrecondition::default()
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/// The generation row's new value: a `__version` unique to this transaction.
|
|
1522
|
+
/// Transaction ids are never applied twice, so every rewrite changes it.
|
|
1523
|
+
fn advance_generation(transaction_id: &str) -> AtomicMutation {
|
|
1524
|
+
let digest = Sha256::digest(transaction_id.as_bytes());
|
|
1525
|
+
let mut bytes = [0u8; 8];
|
|
1526
|
+
bytes.copy_from_slice(&digest[..8]);
|
|
1527
|
+
put(
|
|
1528
|
+
GENERATION_KEY,
|
|
1529
|
+
json!({ "__version": u64::from_be_bytes(bytes), "transaction": transaction_id }),
|
|
1530
|
+
)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
fn unique_suffix() -> String {
|
|
1534
|
+
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
1535
|
+
let nanos = std::time::SystemTime::now()
|
|
1536
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
1537
|
+
.unwrap_or_default()
|
|
1538
|
+
.as_nanos();
|
|
1539
|
+
format!(
|
|
1540
|
+
"{}-{nanos}-{}",
|
|
1541
|
+
std::process::id(),
|
|
1542
|
+
COUNTER.fetch_add(1, Ordering::Relaxed)
|
|
1543
|
+
)
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
fn ref_transaction_id(
|
|
1547
|
+
kind: &str,
|
|
1548
|
+
namespace: &str,
|
|
1549
|
+
name: &str,
|
|
1550
|
+
incarnation: u64,
|
|
1551
|
+
version: Option<u64>,
|
|
1552
|
+
) -> String {
|
|
1553
|
+
let mut hasher = Sha256::new();
|
|
1554
|
+
for part in [
|
|
1555
|
+
"feltdb.state.ref.v1",
|
|
1556
|
+
kind,
|
|
1557
|
+
namespace,
|
|
1558
|
+
name,
|
|
1559
|
+
&incarnation.to_string(),
|
|
1560
|
+
&version.map(|v| v.to_string()).unwrap_or_default(),
|
|
1561
|
+
] {
|
|
1562
|
+
hasher.update(part.as_bytes());
|
|
1563
|
+
hasher.update([0u8]);
|
|
1564
|
+
}
|
|
1565
|
+
format!("state-ref:{:x}", hasher.finalize())
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/// serde_json maps are ordered (no `preserve_order`), so this is canonical.
|
|
1569
|
+
fn payload_hash(payload: &Value) -> String {
|
|
1570
|
+
format!("sha256:{:x}", Sha256::digest(payload.to_string().as_bytes()))
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
fn incarnation_of(row: &Value) -> u64 {
|
|
1574
|
+
row.get("__incarnation").and_then(Value::as_u64).unwrap_or(0)
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
fn incarnation_in(row: Option<&Value>) -> u64 {
|
|
1578
|
+
row.and_then(|row| row.get("incarnation").and_then(Value::as_u64))
|
|
1579
|
+
.unwrap_or(0)
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
fn ref_row(reference: &StateRef, incarnation: u64) -> Value {
|
|
1583
|
+
let mut row = serde_json::to_value(reference).unwrap_or(Value::Null);
|
|
1584
|
+
if let Some(object) = row.as_object_mut() {
|
|
1585
|
+
object.insert("__incarnation".into(), Value::from(incarnation));
|
|
1586
|
+
}
|
|
1587
|
+
row
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
enum Refusal {
|
|
1591
|
+
/// A pinned row changed: re-read and decide again.
|
|
1592
|
+
Changed,
|
|
1593
|
+
/// The id is already bound to another payload: re-read and decide again.
|
|
1594
|
+
OtherRequest,
|
|
1595
|
+
Failed(String),
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
fn refusal(error: FlowError) -> Refusal {
|
|
1599
|
+
match error {
|
|
1600
|
+
FlowError::PreconditionFailed(failure) => match *failure {
|
|
1601
|
+
PreconditionFailure::Present { .. }
|
|
1602
|
+
| PreconditionFailure::Missing { .. }
|
|
1603
|
+
| PreconditionFailure::Version { .. } => Refusal::Changed,
|
|
1604
|
+
other => Refusal::Failed(format!("{other:?}")),
|
|
1605
|
+
},
|
|
1606
|
+
FlowError::CapabilityError(message)
|
|
1607
|
+
if message.starts_with("TRANSACTION_ID_PAYLOAD_MISMATCH") =>
|
|
1608
|
+
{
|
|
1609
|
+
Refusal::OtherRequest
|
|
1610
|
+
}
|
|
1611
|
+
other => Refusal::Failed(other.to_string()),
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
fn storage(error: impl std::fmt::Display) -> RefError {
|
|
1616
|
+
RefError::Storage(error.to_string())
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
fn contended(what: &str) -> String {
|
|
1620
|
+
format!(
|
|
1621
|
+
"{what} was refused by concurrent changes on {ATTEMPTS} consecutive attempts; nothing was applied"
|
|
1622
|
+
)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
fn row(db: &FeltDb, key: &str) -> Result<Option<Value>, RefError> {
|
|
1626
|
+
db.get::<Value>(key).map_err(storage)
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
fn committed_payload(db: &FeltDb, id: &str) -> Result<Option<String>, RefError> {
|
|
1630
|
+
db.applied_transaction_payload_hash(id).map_err(storage)
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
fn decode(row: Value) -> Result<StateRef, RefError> {
|
|
1634
|
+
serde_json::from_value(row).map_err(storage)
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
fn commit(
|
|
1638
|
+
db: &FeltDb,
|
|
1639
|
+
id: &str,
|
|
1640
|
+
payload: Option<&str>,
|
|
1641
|
+
pins: &[RecordPrecondition],
|
|
1642
|
+
mutations: &[AtomicMutation],
|
|
1643
|
+
) -> Result<(), Refusal> {
|
|
1644
|
+
db.apply_atomic_transaction_guarded(id, payload, None, &[], pins, mutations, None)
|
|
1645
|
+
.map(|_| ())
|
|
1646
|
+
.map_err(refusal)
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
pub(super) fn create_ref(
|
|
1650
|
+
db: &FeltDb,
|
|
1651
|
+
namespace: &str,
|
|
1652
|
+
name: &str,
|
|
1653
|
+
target: StateId,
|
|
1654
|
+
authority: String,
|
|
1655
|
+
metadata: BTreeMap<String, Value>,
|
|
1656
|
+
) -> Result<StateRef, RefError> {
|
|
1657
|
+
let ref_k = ref_key(namespace, name);
|
|
1658
|
+
let inc_k = incarnation_key(namespace, name);
|
|
1659
|
+
let target_k = revision_key(&target);
|
|
1660
|
+
let payload = payload_hash(&json!({
|
|
1661
|
+
"target": target.as_hex(),
|
|
1662
|
+
"authority": authority,
|
|
1663
|
+
"metadata": metadata,
|
|
1664
|
+
}));
|
|
1665
|
+
for _ in 0..ATTEMPTS {
|
|
1666
|
+
let incarnation_row = row(db, &inc_k)?;
|
|
1667
|
+
if let Some(existing) = row(db, &ref_k)? {
|
|
1668
|
+
// The create that made this incarnation, repeated, is that create.
|
|
1669
|
+
let id = ref_transaction_id("create", namespace, name, incarnation_of(&existing), None);
|
|
1670
|
+
if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
|
|
1671
|
+
return decode(existing);
|
|
1672
|
+
}
|
|
1673
|
+
return Err(RefError::AlreadyExists);
|
|
1674
|
+
}
|
|
1675
|
+
if row(db, &target_k)?.is_none() {
|
|
1676
|
+
return Err(RefError::UnknownTarget(target));
|
|
1677
|
+
}
|
|
1678
|
+
let incarnation = incarnation_in(incarnation_row.as_ref()) + 1;
|
|
1679
|
+
let id = ref_transaction_id("create", namespace, name, incarnation, None);
|
|
1680
|
+
let reference = StateRef {
|
|
1681
|
+
namespace: namespace.to_string(),
|
|
1682
|
+
name: name.to_string(),
|
|
1683
|
+
target: target.clone(),
|
|
1684
|
+
version: 1,
|
|
1685
|
+
authority: authority.clone(),
|
|
1686
|
+
updated_at_ms: current_time_ms(),
|
|
1687
|
+
metadata: metadata.clone(),
|
|
1688
|
+
};
|
|
1689
|
+
let pins = [
|
|
1690
|
+
unchanged(&ref_k, None),
|
|
1691
|
+
unchanged(&inc_k, incarnation_row.as_ref()),
|
|
1692
|
+
present(&target_k),
|
|
1693
|
+
];
|
|
1694
|
+
let mutations = [
|
|
1695
|
+
put(&ref_k, ref_row(&reference, incarnation)),
|
|
1696
|
+
put(&inc_k, json!({ "incarnation": incarnation })),
|
|
1697
|
+
advance_generation(&id),
|
|
1698
|
+
];
|
|
1699
|
+
match commit(db, &id, Some(&payload), &pins, &mutations) {
|
|
1700
|
+
Ok(()) => return Ok(reference),
|
|
1701
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1702
|
+
Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
Err(RefError::Storage(contended("ref create")))
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
pub(super) fn compare_and_swap_ref(
|
|
1709
|
+
db: &FeltDb,
|
|
1710
|
+
namespace: &str,
|
|
1711
|
+
name: &str,
|
|
1712
|
+
expected_version: u64,
|
|
1713
|
+
target: StateId,
|
|
1714
|
+
authority: String,
|
|
1715
|
+
metadata: Option<BTreeMap<String, Value>>,
|
|
1716
|
+
) -> Result<StateRef, RefError> {
|
|
1717
|
+
let ref_k = ref_key(namespace, name);
|
|
1718
|
+
let inc_k = incarnation_key(namespace, name);
|
|
1719
|
+
let target_k = revision_key(&target);
|
|
1720
|
+
let payload = payload_hash(&json!({
|
|
1721
|
+
"target": target.as_hex(),
|
|
1722
|
+
"authority": authority,
|
|
1723
|
+
"metadata": metadata,
|
|
1724
|
+
}));
|
|
1725
|
+
for _ in 0..ATTEMPTS {
|
|
1726
|
+
let incarnation_row = row(db, &inc_k)?;
|
|
1727
|
+
let Some(ref_read) = row(db, &ref_k)? else {
|
|
1728
|
+
return Err(RefError::NotFound);
|
|
1729
|
+
};
|
|
1730
|
+
let incarnation = incarnation_of(&ref_read);
|
|
1731
|
+
let current = decode(ref_read.clone())?;
|
|
1732
|
+
let id = ref_transaction_id("cas", namespace, name, incarnation, Some(expected_version));
|
|
1733
|
+
if current.version != expected_version {
|
|
1734
|
+
// Either this exact request already committed (a retry), or
|
|
1735
|
+
// another one did: the payload bound to the id says which.
|
|
1736
|
+
if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
|
|
1737
|
+
if current.version == expected_version + 1 && current.target == target {
|
|
1738
|
+
return Ok(current);
|
|
1739
|
+
}
|
|
1740
|
+
// Committed, and the ref has moved on since. Describe the
|
|
1741
|
+
// transition this request made; `updated_at_ms` is now.
|
|
1742
|
+
return Ok(StateRef {
|
|
1743
|
+
version: expected_version + 1,
|
|
1744
|
+
target,
|
|
1745
|
+
authority,
|
|
1746
|
+
updated_at_ms: current_time_ms(),
|
|
1747
|
+
metadata: metadata.unwrap_or(current.metadata),
|
|
1748
|
+
..current
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
return Err(RefError::VersionConflict {
|
|
1752
|
+
expected: expected_version,
|
|
1753
|
+
current: current.version,
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
if row(db, &target_k)?.is_none() {
|
|
1757
|
+
return Err(RefError::UnknownTarget(target));
|
|
1758
|
+
}
|
|
1759
|
+
let next = StateRef {
|
|
1760
|
+
version: expected_version + 1,
|
|
1761
|
+
target: target.clone(),
|
|
1762
|
+
authority: authority.clone(),
|
|
1763
|
+
updated_at_ms: current_time_ms(),
|
|
1764
|
+
metadata: metadata.clone().unwrap_or_else(|| current.metadata.clone()),
|
|
1765
|
+
..current
|
|
1766
|
+
};
|
|
1767
|
+
let pins = [
|
|
1768
|
+
unchanged(&ref_k, Some(&ref_read)),
|
|
1769
|
+
unchanged(&inc_k, incarnation_row.as_ref()),
|
|
1770
|
+
present(&target_k),
|
|
1771
|
+
];
|
|
1772
|
+
let mutations = [put(&ref_k, ref_row(&next, incarnation)), advance_generation(&id)];
|
|
1773
|
+
match commit(db, &id, Some(&payload), &pins, &mutations) {
|
|
1774
|
+
Ok(()) => return Ok(next),
|
|
1775
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1776
|
+
Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
Err(RefError::Storage(contended("ref compare-and-swap")))
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
pub(super) fn delete_ref(
|
|
1783
|
+
db: &FeltDb,
|
|
1784
|
+
namespace: &str,
|
|
1785
|
+
name: &str,
|
|
1786
|
+
expected_version: Option<u64>,
|
|
1787
|
+
) -> Result<(), RefError> {
|
|
1788
|
+
let ref_k = ref_key(namespace, name);
|
|
1789
|
+
let inc_k = incarnation_key(namespace, name);
|
|
1790
|
+
let payload = payload_hash(&json!({ "expected_version": expected_version }));
|
|
1791
|
+
for _ in 0..ATTEMPTS {
|
|
1792
|
+
let incarnation_row = row(db, &inc_k)?;
|
|
1793
|
+
let Some(ref_read) = row(db, &ref_k)? else {
|
|
1794
|
+
// A repeat of a delete that already removed this incarnation.
|
|
1795
|
+
let id = ref_transaction_id(
|
|
1796
|
+
"delete",
|
|
1797
|
+
namespace,
|
|
1798
|
+
name,
|
|
1799
|
+
incarnation_in(incarnation_row.as_ref()),
|
|
1800
|
+
None,
|
|
1801
|
+
);
|
|
1802
|
+
if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
|
|
1803
|
+
return Ok(());
|
|
1804
|
+
}
|
|
1805
|
+
return Err(RefError::NotFound);
|
|
1806
|
+
};
|
|
1807
|
+
let incarnation = incarnation_of(&ref_read);
|
|
1808
|
+
let current = decode(ref_read.clone())?;
|
|
1809
|
+
if let Some(expected) = expected_version {
|
|
1810
|
+
if current.version != expected {
|
|
1811
|
+
return Err(RefError::VersionConflict {
|
|
1812
|
+
expected,
|
|
1813
|
+
current: current.version,
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
let id = ref_transaction_id("delete", namespace, name, incarnation, None);
|
|
1818
|
+
let pins = [
|
|
1819
|
+
unchanged(&ref_k, Some(&ref_read)),
|
|
1820
|
+
unchanged(&inc_k, incarnation_row.as_ref()),
|
|
1821
|
+
];
|
|
1822
|
+
let mutations = [
|
|
1823
|
+
remove(&ref_k),
|
|
1824
|
+
put(&inc_k, json!({ "incarnation": incarnation })),
|
|
1825
|
+
advance_generation(&id),
|
|
1826
|
+
];
|
|
1827
|
+
match commit(db, &id, Some(&payload), &pins, &mutations) {
|
|
1828
|
+
Ok(()) => return Ok(()),
|
|
1829
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1830
|
+
Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
Err(RefError::Storage(contended("ref delete")))
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
pub(super) fn commit_revision(
|
|
1837
|
+
db: &FeltDb,
|
|
1838
|
+
revision: StateRevision,
|
|
1839
|
+
) -> Result<StateRevision, RevisionError> {
|
|
1840
|
+
let fail = |error: &dyn std::fmt::Display| RevisionError::Storage(error.to_string());
|
|
1841
|
+
let object = StateObject::new(revision.content.clone());
|
|
1842
|
+
let rev_k = revision_key(&revision.id);
|
|
1843
|
+
let obj_k = object_key(&object.id);
|
|
1844
|
+
let parent_k = revision.parent_id.as_ref().map(revision_key);
|
|
1845
|
+
for _ in 0..ATTEMPTS {
|
|
1846
|
+
if let Some(existing) = db.get::<StateRevision>(&rev_k).map_err(|e| fail(&e))? {
|
|
1847
|
+
let same = existing.resource == revision.resource
|
|
1848
|
+
&& existing.content_id == revision.content_id
|
|
1849
|
+
&& existing.parent_id == revision.parent_id
|
|
1850
|
+
&& existing.sequence == revision.sequence;
|
|
1851
|
+
return if same {
|
|
1852
|
+
Ok(existing)
|
|
1853
|
+
} else {
|
|
1854
|
+
Err(RevisionError::AlreadyCommitted(revision.id.clone()))
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
let mut pins = vec![unchanged(&rev_k, None)];
|
|
1858
|
+
if let (Some(parent), Some(parent_k)) = (&revision.parent_id, &parent_k) {
|
|
1859
|
+
if db.get::<Value>(parent_k).map_err(|e| fail(&e))?.is_none() {
|
|
1860
|
+
return Err(RevisionError::UnknownParent(parent.clone()));
|
|
1861
|
+
}
|
|
1862
|
+
pins.push(present(parent_k));
|
|
1863
|
+
}
|
|
1864
|
+
let id = format!("state-revision:{}:{}", revision.id.as_hex(), unique_suffix());
|
|
1865
|
+
let mut mutations = Vec::with_capacity(3);
|
|
1866
|
+
match db.get::<StateObject>(&obj_k).map_err(|e| fail(&e))? {
|
|
1867
|
+
Some(existing) if !existing.verify_integrity() => {
|
|
1868
|
+
return Err(RevisionError::Storage(
|
|
1869
|
+
"Stored state object failed integrity verification".to_string(),
|
|
1870
|
+
));
|
|
1871
|
+
}
|
|
1872
|
+
Some(_) => pins.push(present(&obj_k)),
|
|
1873
|
+
None => {
|
|
1874
|
+
pins.push(unchanged(&obj_k, None));
|
|
1875
|
+
mutations.push(put(&obj_k, serde_json::to_value(&object).map_err(|e| fail(&e))?));
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
mutations.push(put(&rev_k, serde_json::to_value(&revision).map_err(|e| fail(&e))?));
|
|
1879
|
+
mutations.push(advance_generation(&id));
|
|
1880
|
+
match commit(db, &id, None, &pins, &mutations) {
|
|
1881
|
+
Ok(()) => return Ok(revision),
|
|
1882
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1883
|
+
Err(Refusal::Failed(message)) => return Err(RevisionError::Storage(message)),
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
Err(RevisionError::Storage(contended("revision commit")))
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
pub(super) fn collect(
|
|
1890
|
+
store: &StateStore,
|
|
1891
|
+
db: &FeltDb,
|
|
1892
|
+
now_ms: u64,
|
|
1893
|
+
) -> Result<GarbageCollectionReport, GarbageCollectionError> {
|
|
1894
|
+
let fail = |error: &dyn std::fmt::Display| GarbageCollectionError::Storage(error.to_string());
|
|
1895
|
+
for _ in 0..ATTEMPTS {
|
|
1896
|
+
// The generation first: everything planned below was read after it,
|
|
1897
|
+
// so a write that lands after this read moves it.
|
|
1898
|
+
let generation = db.get::<Value>(GENERATION_KEY).map_err(|e| fail(&e))?;
|
|
1899
|
+
let report = store.plan_collection(now_ms, &db.revision_head_ids());
|
|
1900
|
+
if report.collected_revisions.is_empty() && report.collected_objects.is_empty() {
|
|
1901
|
+
return Ok(report);
|
|
1902
|
+
}
|
|
1903
|
+
let pins = [unchanged(GENERATION_KEY, generation.as_ref())];
|
|
1904
|
+
let mutations: Vec<AtomicMutation> = report
|
|
1905
|
+
.collected_revisions
|
|
1906
|
+
.iter()
|
|
1907
|
+
.map(|id| remove(&revision_key(id)))
|
|
1908
|
+
.chain(report.collected_objects.iter().map(|id| remove(&object_key(id))))
|
|
1909
|
+
.collect();
|
|
1910
|
+
let id = format!("state-gc:{}", unique_suffix());
|
|
1911
|
+
match commit(db, &id, None, &pins, &mutations) {
|
|
1912
|
+
Ok(()) => return Ok(report),
|
|
1913
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1914
|
+
Err(Refusal::Failed(message)) => return Err(GarbageCollectionError::Storage(message)),
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
Err(GarbageCollectionError::Storage(contended("garbage collection")))
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1374
1921
|
fn collect_referenced_objects(revision: &StateRevision) -> BTreeSet<StateId> {
|
|
1375
1922
|
let mut ids = BTreeSet::from([revision.content_id.clone()]);
|
|
1376
1923
|
if let Some(values) = revision
|
|
@@ -1692,34 +2239,7 @@ impl StateStore {
|
|
|
1692
2239
|
state.revisions.insert(revision.id.clone(), revision.clone());
|
|
1693
2240
|
}
|
|
1694
2241
|
Backing::Durable(feltdb) => {
|
|
1695
|
-
|
|
1696
|
-
if let Some(existing) = feltdb
|
|
1697
|
-
.get::<StateObject>(&object_key(&object.id))
|
|
1698
|
-
.map_err(|error| {
|
|
1699
|
-
RevisionError::Storage(format!("Failed to read state object: {error}"))
|
|
1700
|
-
})?
|
|
1701
|
-
{
|
|
1702
|
-
if !existing.verify_integrity() {
|
|
1703
|
-
return Err(RevisionError::Storage(
|
|
1704
|
-
"Stored state object failed integrity verification".to_string(),
|
|
1705
|
-
));
|
|
1706
|
-
}
|
|
1707
|
-
} else {
|
|
1708
|
-
feltdb
|
|
1709
|
-
.insert(&object_key(&object.id), object)
|
|
1710
|
-
.map_err(|error| {
|
|
1711
|
-
RevisionError::Storage(format!(
|
|
1712
|
-
"Failed to persist state object to FeltDB: {error}"
|
|
1713
|
-
))
|
|
1714
|
-
})?;
|
|
1715
|
-
}
|
|
1716
|
-
feltdb
|
|
1717
|
-
.insert(&revision_key(&revision.id), revision.clone())
|
|
1718
|
-
.map_err(|error| {
|
|
1719
|
-
RevisionError::Storage(format!(
|
|
1720
|
-
"Failed to persist revision to FeltDB: {error}"
|
|
1721
|
-
))
|
|
1722
|
-
})?;
|
|
2242
|
+
return durable::commit_revision(feltdb, revision);
|
|
1723
2243
|
}
|
|
1724
2244
|
}
|
|
1725
2245
|
Ok(revision)
|
|
@@ -1816,6 +2336,12 @@ impl StateStore {
|
|
|
1816
2336
|
}
|
|
1817
2337
|
|
|
1818
2338
|
/// Create a mutable reference in a namespace.
|
|
2339
|
+
///
|
|
2340
|
+
/// On the durable backing this is one fenced FeltDB transaction (see
|
|
2341
|
+
/// [`durable`]): the ref is created at version 1 only if, at the moment of
|
|
2342
|
+
/// commit, it is still absent and its target revision is still present.
|
|
2343
|
+
/// Repeating an identical create that already succeeded returns the ref it
|
|
2344
|
+
/// created; a different create of an existing ref is `AlreadyExists`.
|
|
1819
2345
|
pub fn create_ref(
|
|
1820
2346
|
&self,
|
|
1821
2347
|
namespace: &str,
|
|
@@ -1827,24 +2353,31 @@ impl StateStore {
|
|
|
1827
2353
|
if !valid_ref_component(namespace) || !valid_ref_component(name) {
|
|
1828
2354
|
return Err(RefError::InvalidIdentifier);
|
|
1829
2355
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2356
|
+
match &self.backing {
|
|
2357
|
+
Backing::Durable(feltdb) => {
|
|
2358
|
+
durable::create_ref(feltdb, namespace, name, target, authority, metadata)
|
|
2359
|
+
}
|
|
2360
|
+
Backing::Volatile(_) => {
|
|
2361
|
+
let _guard = self.mutation_lock.lock().unwrap();
|
|
2362
|
+
if !self.exists(&target) {
|
|
2363
|
+
return Err(RefError::UnknownTarget(target));
|
|
2364
|
+
}
|
|
2365
|
+
if self.get_ref(namespace, name).is_some() {
|
|
2366
|
+
return Err(RefError::AlreadyExists);
|
|
2367
|
+
}
|
|
2368
|
+
let reference = StateRef {
|
|
2369
|
+
namespace: namespace.to_string(),
|
|
2370
|
+
name: name.to_string(),
|
|
2371
|
+
target,
|
|
2372
|
+
version: 1,
|
|
2373
|
+
authority,
|
|
2374
|
+
updated_at_ms: current_time_ms(),
|
|
2375
|
+
metadata,
|
|
2376
|
+
};
|
|
2377
|
+
self.write_volatile_ref(&reference);
|
|
2378
|
+
Ok(reference)
|
|
2379
|
+
}
|
|
1836
2380
|
}
|
|
1837
|
-
let reference = StateRef {
|
|
1838
|
-
namespace: namespace.to_string(),
|
|
1839
|
-
name: name.to_string(),
|
|
1840
|
-
target,
|
|
1841
|
-
version: 1,
|
|
1842
|
-
authority,
|
|
1843
|
-
updated_at_ms: current_time_ms(),
|
|
1844
|
-
metadata,
|
|
1845
|
-
};
|
|
1846
|
-
self.write_ref(&reference)?;
|
|
1847
|
-
Ok(reference)
|
|
1848
2381
|
}
|
|
1849
2382
|
|
|
1850
2383
|
/// Read a reference.
|
|
@@ -1879,6 +2412,17 @@ impl StateStore {
|
|
|
1879
2412
|
}
|
|
1880
2413
|
|
|
1881
2414
|
/// Compare-and-swap move for a reference.
|
|
2415
|
+
///
|
|
2416
|
+
/// On the durable backing this is one fenced FeltDB transaction: it commits
|
|
2417
|
+
/// only if, at the moment of commit, the ref is still at
|
|
2418
|
+
/// `expected_version` and the target revision is still present, whichever
|
|
2419
|
+
/// `StateStore` or `FeltDb` handle the competing writes came through.
|
|
2420
|
+
///
|
|
2421
|
+
/// The request `(ref incarnation, expected_version, target, authority,
|
|
2422
|
+
/// metadata)` is idempotent: repeating one that already committed — after a
|
|
2423
|
+
/// lost response, a restart or a compaction — returns `Ok` and does not
|
|
2424
|
+
/// advance the ref again. Any other request for the same expected version
|
|
2425
|
+
/// is a `VersionConflict`.
|
|
1882
2426
|
pub fn compare_and_swap_ref(
|
|
1883
2427
|
&self,
|
|
1884
2428
|
namespace: &str,
|
|
@@ -1891,29 +2435,45 @@ impl StateStore {
|
|
|
1891
2435
|
if !valid_ref_component(namespace) || !valid_ref_component(name) {
|
|
1892
2436
|
return Err(RefError::InvalidIdentifier);
|
|
1893
2437
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2438
|
+
match &self.backing {
|
|
2439
|
+
Backing::Durable(feltdb) => durable::compare_and_swap_ref(
|
|
2440
|
+
feltdb,
|
|
2441
|
+
namespace,
|
|
2442
|
+
name,
|
|
2443
|
+
expected_version,
|
|
2444
|
+
target,
|
|
2445
|
+
authority,
|
|
2446
|
+
metadata,
|
|
2447
|
+
),
|
|
2448
|
+
Backing::Volatile(_) => {
|
|
2449
|
+
let _guard = self.mutation_lock.lock().unwrap();
|
|
2450
|
+
let mut current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
|
|
2451
|
+
if current.version != expected_version {
|
|
2452
|
+
return Err(RefError::VersionConflict {
|
|
2453
|
+
expected: expected_version,
|
|
2454
|
+
current: current.version,
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
if !self.exists(&target) {
|
|
2458
|
+
return Err(RefError::UnknownTarget(target));
|
|
2459
|
+
}
|
|
2460
|
+
current.target = target;
|
|
2461
|
+
current.version += 1;
|
|
2462
|
+
current.authority = authority;
|
|
2463
|
+
current.updated_at_ms = current_time_ms();
|
|
2464
|
+
if let Some(metadata) = metadata {
|
|
2465
|
+
current.metadata = metadata;
|
|
2466
|
+
}
|
|
2467
|
+
self.write_volatile_ref(¤t);
|
|
2468
|
+
Ok(current)
|
|
2469
|
+
}
|
|
1911
2470
|
}
|
|
1912
|
-
self.write_ref(¤t)?;
|
|
1913
|
-
Ok(current)
|
|
1914
2471
|
}
|
|
1915
2472
|
|
|
1916
2473
|
/// Delete a reference, optionally enforcing the current version.
|
|
2474
|
+
///
|
|
2475
|
+
/// On the durable backing this is one fenced FeltDB transaction, and a
|
|
2476
|
+
/// repeat of an identical delete that already committed returns `Ok`.
|
|
1917
2477
|
pub fn delete_ref(
|
|
1918
2478
|
&self,
|
|
1919
2479
|
namespace: &str,
|
|
@@ -1923,17 +2483,29 @@ impl StateStore {
|
|
|
1923
2483
|
if !valid_ref_component(namespace) || !valid_ref_component(name) {
|
|
1924
2484
|
return Err(RefError::InvalidIdentifier);
|
|
1925
2485
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2486
|
+
match &self.backing {
|
|
2487
|
+
Backing::Durable(feltdb) => {
|
|
2488
|
+
durable::delete_ref(feltdb, namespace, name, expected_version)
|
|
2489
|
+
}
|
|
2490
|
+
Backing::Volatile(state) => {
|
|
2491
|
+
let _guard = self.mutation_lock.lock().unwrap();
|
|
2492
|
+
let current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
|
|
2493
|
+
if let Some(expected) = expected_version {
|
|
2494
|
+
if current.version != expected {
|
|
2495
|
+
return Err(RefError::VersionConflict {
|
|
2496
|
+
expected,
|
|
2497
|
+
current: current.version,
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
state
|
|
2502
|
+
.lock()
|
|
2503
|
+
.unwrap()
|
|
2504
|
+
.refs
|
|
2505
|
+
.remove(&(namespace.to_string(), name.to_string()));
|
|
2506
|
+
Ok(())
|
|
1934
2507
|
}
|
|
1935
2508
|
}
|
|
1936
|
-
self.remove_ref(namespace, name)
|
|
1937
2509
|
}
|
|
1938
2510
|
|
|
1939
2511
|
/// Enumerate refs, optionally scoped to one namespace.
|
|
@@ -1991,95 +2563,122 @@ impl StateStore {
|
|
|
1991
2563
|
Ok(projected)
|
|
1992
2564
|
}
|
|
1993
2565
|
|
|
1994
|
-
/// Collect revisions and objects
|
|
2566
|
+
/// Collect revisions and objects that no root can reach.
|
|
2567
|
+
///
|
|
2568
|
+
/// **Roots** are: every ref target; every resource's latest revision (each
|
|
2569
|
+
/// revision at the resource's highest sequence, and the head the record
|
|
2570
|
+
/// write path will mint its next revision from); and every revision
|
|
2571
|
+
/// younger than 15 minutes (`GC_GRACE_MS`). **Reachable** is the roots and all their
|
|
2572
|
+
/// ancestors by `parent_id`, because history, diff, rollback and replicated
|
|
2573
|
+
/// reconstruction all walk that chain. Only a revision that is old, named
|
|
2574
|
+
/// by no ref, not latest, and an ancestor of none of those is collected —
|
|
2575
|
+
/// in practice, an abandoned fork. Retention (`keep_last`) is unaffected:
|
|
2576
|
+
/// it is positional and runs at the mint, not here.
|
|
2577
|
+
///
|
|
2578
|
+
/// On the durable backing every deletion commits in one FeltDB transaction
|
|
2579
|
+
/// fenced on the state generation, which every ref change and every
|
|
2580
|
+
/// committed revision advances. A concurrent change aborts the collection
|
|
2581
|
+
/// and it is re-planned from fresh reads; see [`durable`].
|
|
1995
2582
|
pub fn collect_unreachable(&self) -> Result<GarbageCollectionReport, GarbageCollectionError> {
|
|
1996
|
-
|
|
2583
|
+
self.collect_unreachable_at(current_time_ms())
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/// [`collect_unreachable`](Self::collect_unreachable) with an explicit
|
|
2587
|
+
/// clock, so the grace period can be tested without waiting for it.
|
|
2588
|
+
pub(crate) fn collect_unreachable_at(
|
|
2589
|
+
&self,
|
|
2590
|
+
now_ms: u64,
|
|
2591
|
+
) -> Result<GarbageCollectionReport, GarbageCollectionError> {
|
|
2592
|
+
match &self.backing {
|
|
2593
|
+
Backing::Volatile(state) => {
|
|
2594
|
+
let _guard = self.mutation_lock.lock().unwrap();
|
|
2595
|
+
let report = self.plan_collection(now_ms, &[]);
|
|
2596
|
+
let mut state = state.lock().unwrap();
|
|
2597
|
+
for id in &report.collected_revisions {
|
|
2598
|
+
state.revisions.remove(id);
|
|
2599
|
+
}
|
|
2600
|
+
for id in &report.collected_objects {
|
|
2601
|
+
state.objects.remove(id);
|
|
2602
|
+
}
|
|
2603
|
+
Ok(report)
|
|
2604
|
+
}
|
|
2605
|
+
Backing::Durable(feltdb) => durable::collect(self, feltdb, now_ms),
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
/// What a collection at `now_ms` would keep and delete, from one read of
|
|
2610
|
+
/// refs and revisions. `extra_roots` are heads known outside `StateStore`.
|
|
2611
|
+
fn plan_collection(&self, now_ms: u64, extra_roots: &[StateId]) -> GarbageCollectionReport {
|
|
1997
2612
|
let roots = self.list_refs(None);
|
|
2613
|
+
let revisions = self.all_revisions();
|
|
2614
|
+
let by_id: HashMap<StateId, &StateRevision> =
|
|
2615
|
+
revisions.iter().map(|revision| (revision.id.clone(), revision)).collect();
|
|
2616
|
+
|
|
2617
|
+
let mut latest: HashMap<&str, u64> = HashMap::new();
|
|
2618
|
+
for revision in &revisions {
|
|
2619
|
+
let entry = latest.entry(revision.resource.as_str()).or_insert(revision.sequence);
|
|
2620
|
+
if revision.sequence > *entry {
|
|
2621
|
+
*entry = revision.sequence;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
let mut pending: Vec<StateId> = roots.iter().map(|reference| reference.target.clone()).collect();
|
|
2626
|
+
pending.extend(extra_roots.iter().cloned());
|
|
2627
|
+
for revision in &revisions {
|
|
2628
|
+
let is_latest = latest.get(revision.resource.as_str()) == Some(&revision.sequence);
|
|
2629
|
+
let is_young = now_ms.saturating_sub(revision.timestamp_ms) < GC_GRACE_MS;
|
|
2630
|
+
if is_latest || is_young {
|
|
2631
|
+
pending.push(revision.id.clone());
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
1998
2635
|
let mut reachable_revisions = BTreeSet::new();
|
|
1999
2636
|
let mut reachable_objects = BTreeSet::new();
|
|
2000
|
-
let mut pending: Vec<StateId> = roots.iter().map(|reference| reference.target.clone()).collect();
|
|
2001
2637
|
while let Some(id) = pending.pop() {
|
|
2638
|
+
let Some(revision) = by_id.get(&id) else {
|
|
2639
|
+
// A root or parent that is not held (expired by retention, or
|
|
2640
|
+
// never committed) ends the walk; it is not an error.
|
|
2641
|
+
continue;
|
|
2642
|
+
};
|
|
2002
2643
|
if !reachable_revisions.insert(id.clone()) {
|
|
2003
2644
|
continue;
|
|
2004
2645
|
}
|
|
2005
|
-
let Some(revision) = self.get(&id) else {
|
|
2006
|
-
continue;
|
|
2007
|
-
};
|
|
2008
2646
|
if let Some(parent_id) = revision.parent_id.clone() {
|
|
2009
2647
|
pending.push(parent_id);
|
|
2010
2648
|
}
|
|
2011
|
-
|
|
2012
|
-
reachable_objects.insert(object_id);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
let mut all_revisions: Vec<StateId> = self.all_revisions().into_iter().map(|r| r.id).collect();
|
|
2017
|
-
all_revisions.sort();
|
|
2018
|
-
let mut collected_revisions = Vec::new();
|
|
2019
|
-
for revision_id in all_revisions {
|
|
2020
|
-
if !reachable_revisions.contains(&revision_id) {
|
|
2021
|
-
self.remove(&revision_id)
|
|
2022
|
-
.map_err(|error| GarbageCollectionError::Storage(error.to_string()))?;
|
|
2023
|
-
collected_revisions.push(revision_id);
|
|
2024
|
-
}
|
|
2649
|
+
reachable_objects.extend(collect_referenced_objects(revision));
|
|
2025
2650
|
}
|
|
2026
2651
|
|
|
2027
|
-
let mut
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2652
|
+
let mut collected_revisions: Vec<StateId> = revisions
|
|
2653
|
+
.iter()
|
|
2654
|
+
.map(|revision| revision.id.clone())
|
|
2655
|
+
.filter(|id| !reachable_revisions.contains(id))
|
|
2656
|
+
.collect();
|
|
2657
|
+
collected_revisions.sort();
|
|
2658
|
+
let mut collected_objects: Vec<StateId> = self
|
|
2659
|
+
.all_objects()
|
|
2660
|
+
.into_iter()
|
|
2661
|
+
.map(|object| object.id)
|
|
2662
|
+
.filter(|id| !reachable_objects.contains(id))
|
|
2663
|
+
.collect();
|
|
2664
|
+
collected_objects.sort();
|
|
2036
2665
|
|
|
2037
|
-
|
|
2038
|
-
reachable_revisions_out.sort();
|
|
2039
|
-
let mut reachable_objects_out: Vec<_> = reachable_objects.into_iter().collect();
|
|
2040
|
-
reachable_objects_out.sort();
|
|
2041
|
-
Ok(GarbageCollectionReport {
|
|
2666
|
+
GarbageCollectionReport {
|
|
2042
2667
|
roots,
|
|
2043
|
-
reachable_revisions:
|
|
2044
|
-
reachable_objects:
|
|
2668
|
+
reachable_revisions: reachable_revisions.into_iter().collect(),
|
|
2669
|
+
reachable_objects: reachable_objects.into_iter().collect(),
|
|
2045
2670
|
collected_revisions,
|
|
2046
2671
|
collected_objects,
|
|
2047
|
-
})
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
fn write_ref(&self, reference: &StateRef) -> Result<(), RefError> {
|
|
2051
|
-
match &self.backing {
|
|
2052
|
-
Backing::Volatile(state) => {
|
|
2053
|
-
state.lock().unwrap().refs.insert(
|
|
2054
|
-
(reference.namespace.clone(), reference.name.clone()),
|
|
2055
|
-
reference.clone(),
|
|
2056
|
-
);
|
|
2057
|
-
}
|
|
2058
|
-
Backing::Durable(feltdb) => {
|
|
2059
|
-
feltdb
|
|
2060
|
-
.insert(&ref_key(&reference.namespace, &reference.name), reference.clone())
|
|
2061
|
-
.map_err(|error| RefError::Storage(error.to_string()))?;
|
|
2062
|
-
}
|
|
2063
2672
|
}
|
|
2064
|
-
Ok(())
|
|
2065
2673
|
}
|
|
2066
2674
|
|
|
2067
|
-
fn
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
.refs
|
|
2074
|
-
.remove(&(namespace.to_string(), name.to_string()));
|
|
2075
|
-
}
|
|
2076
|
-
Backing::Durable(feltdb) => {
|
|
2077
|
-
feltdb
|
|
2078
|
-
.delete(&ref_key(namespace, name))
|
|
2079
|
-
.map_err(|error| RefError::Storage(error.to_string()))?;
|
|
2080
|
-
}
|
|
2675
|
+
fn write_volatile_ref(&self, reference: &StateRef) {
|
|
2676
|
+
if let Backing::Volatile(state) = &self.backing {
|
|
2677
|
+
state.lock().unwrap().refs.insert(
|
|
2678
|
+
(reference.namespace.clone(), reference.name.clone()),
|
|
2679
|
+
reference.clone(),
|
|
2680
|
+
);
|
|
2081
2681
|
}
|
|
2082
|
-
Ok(())
|
|
2083
2682
|
}
|
|
2084
2683
|
|
|
2085
2684
|
fn all_revisions(&self) -> Vec<StateRevision> {
|
|
@@ -2234,20 +2833,6 @@ impl StateStore {
|
|
|
2234
2833
|
Ok(())
|
|
2235
2834
|
}
|
|
2236
2835
|
|
|
2237
|
-
fn remove_object(&self, id: &StateId) -> Result<(), GarbageCollectionError> {
|
|
2238
|
-
match &self.backing {
|
|
2239
|
-
Backing::Volatile(state) => {
|
|
2240
|
-
state.lock().unwrap().objects.remove(id);
|
|
2241
|
-
}
|
|
2242
|
-
Backing::Durable(feltdb) => {
|
|
2243
|
-
feltdb
|
|
2244
|
-
.delete(&object_key(id))
|
|
2245
|
-
.map_err(|error| GarbageCollectionError::Storage(error.to_string()))?;
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
Ok(())
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
2836
|
fn retention_state(&self, resource: &str) -> RetentionState {
|
|
2252
2837
|
match &self.backing {
|
|
2253
2838
|
Backing::Volatile(state) => state
|
|
@@ -2316,6 +2901,120 @@ mod tests {
|
|
|
2316
2901
|
use super::*;
|
|
2317
2902
|
use serde_json::json;
|
|
2318
2903
|
|
|
2904
|
+
// -- garbage collection eligibility, with an explicit clock -------------
|
|
2905
|
+
|
|
2906
|
+
/// `A(0) → B(1) → C(2)` on one resource, plus a fork `F(1)` off `A`.
|
|
2907
|
+
fn forked(store: &StateStore) -> (StateRevision, StateRevision, StateRevision, StateRevision) {
|
|
2908
|
+
let a = store.create("doc:gc", json!({"n": "a"}).to_string(), "t".into()).unwrap();
|
|
2909
|
+
let b = store.commit(json!({"n": "b"}).to_string(), &a, "t".into()).unwrap();
|
|
2910
|
+
let c = store.commit(json!({"n": "c"}).to_string(), &b, "t".into()).unwrap();
|
|
2911
|
+
let f = store.commit(json!({"n": "f"}).to_string(), &a, "t".into()).unwrap();
|
|
2912
|
+
(a, b, c, f)
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
#[test]
|
|
2916
|
+
fn gc_grace_boundary_is_exclusive() {
|
|
2917
|
+
let store = StateStore::new_volatile();
|
|
2918
|
+
let (_, _, _, f) = forked(&store);
|
|
2919
|
+
let inside = store.collect_unreachable_at(f.timestamp_ms + GC_GRACE_MS - 1).unwrap();
|
|
2920
|
+
assert!(inside.collected_revisions.is_empty(), "younger than the grace: a root");
|
|
2921
|
+
let outside = store.collect_unreachable_at(f.timestamp_ms + GC_GRACE_MS).unwrap();
|
|
2922
|
+
assert_eq!(outside.collected_revisions, vec![f.id.clone()]);
|
|
2923
|
+
assert!(!store.exists(&f.id));
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
#[test]
|
|
2927
|
+
fn gc_keeps_latest_revisions_refs_and_all_their_ancestors() {
|
|
2928
|
+
for store in [StateStore::new_volatile(), durable_store().1] {
|
|
2929
|
+
let (a, b, c, f) = forked(&store);
|
|
2930
|
+
let g = store.commit(json!({"n": "g"}).to_string(), &f, "t".into()).unwrap();
|
|
2931
|
+
// g ties c at the highest sequence (2): both are "latest".
|
|
2932
|
+
let far = c.timestamp_ms + 365 * 24 * 60 * 60 * 1000;
|
|
2933
|
+
let report = store.collect_unreachable_at(far).unwrap();
|
|
2934
|
+
assert!(report.collected_revisions.is_empty(), "every revision is a root or an ancestor");
|
|
2935
|
+
|
|
2936
|
+
// A later revision on the main line makes f and g a stale fork …
|
|
2937
|
+
let d = store.commit(json!({"n": "d"}).to_string(), &c, "t".into()).unwrap();
|
|
2938
|
+
// … but a ref on g keeps g and its ancestor f.
|
|
2939
|
+
store
|
|
2940
|
+
.create_ref("candidates", "g", g.id.clone(), "t".into(), BTreeMap::new())
|
|
2941
|
+
.unwrap();
|
|
2942
|
+
assert!(store.collect_unreachable_at(far).unwrap().collected_revisions.is_empty());
|
|
2943
|
+
|
|
2944
|
+
store.delete_ref("candidates", "g", None).unwrap();
|
|
2945
|
+
let mut collected = store.collect_unreachable_at(far).unwrap().collected_revisions;
|
|
2946
|
+
collected.sort();
|
|
2947
|
+
let mut expected = vec![f.id.clone(), g.id.clone()];
|
|
2948
|
+
expected.sort();
|
|
2949
|
+
assert_eq!(collected, expected, "only the unreferenced stale fork");
|
|
2950
|
+
for kept in [&a, &b, &c, &d] {
|
|
2951
|
+
assert!(store.exists(&kept.id));
|
|
2952
|
+
}
|
|
2953
|
+
// The fork's content objects went with it; the kept ones stayed.
|
|
2954
|
+
let objects: BTreeSet<StateId> = store.all_objects().into_iter().map(|o| o.id).collect();
|
|
2955
|
+
for kept in [&a, &b, &c, &d] {
|
|
2956
|
+
assert!(objects.contains(&kept.content_id), "object of a kept revision");
|
|
2957
|
+
}
|
|
2958
|
+
for gone in [&f, &g] {
|
|
2959
|
+
assert!(!objects.contains(&gone.content_id), "object of a collected revision");
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
fn durable_store() -> (tempfile::TempDir, StateStore, Arc<crate::FeltDb>) {
|
|
2965
|
+
let dir = tempfile::TempDir::new().unwrap();
|
|
2966
|
+
let db = Arc::new(crate::FeltDb::open(dir.path().join("gc.log")).unwrap());
|
|
2967
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
2968
|
+
(dir, store, db)
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
/// The record write path mints from its own head, which an explicit
|
|
2972
|
+
/// `StateStore::commit` fork can overtake by sequence. That head must stay
|
|
2973
|
+
/// a root, or the next record write would mint a child of a deleted parent.
|
|
2974
|
+
#[test]
|
|
2975
|
+
fn gc_keeps_the_write_path_head_even_when_a_fork_is_longer() {
|
|
2976
|
+
let (_dir, store, db) = durable_store();
|
|
2977
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
2978
|
+
db.update("tasks:1", json!({"n": 1})).unwrap();
|
|
2979
|
+
let resource = store.resources().into_iter().find(|r| r.contains("tasks")).unwrap();
|
|
2980
|
+
let history = store.history_of(&resource);
|
|
2981
|
+
assert_eq!(history.len(), 2, "the write path minted two revisions");
|
|
2982
|
+
let (w0, w1) = (history[0].clone(), history[1].clone());
|
|
2983
|
+
|
|
2984
|
+
// A longer explicit fork off w0: x(1) → x2(2) overtakes w1(1).
|
|
2985
|
+
let x = store.commit(json!({"x": 1}).to_string(), &w0, "t".into()).unwrap();
|
|
2986
|
+
let x2 = store.commit(json!({"x": 2}).to_string(), &x, "t".into()).unwrap();
|
|
2987
|
+
assert_eq!(store.head_of(&resource).unwrap().id, x2.id);
|
|
2988
|
+
assert!(db.revision_head_ids().contains(&w1.id), "the write path still mints from w1");
|
|
2989
|
+
|
|
2990
|
+
let far = x2.timestamp_ms + 365 * 24 * 60 * 60 * 1000;
|
|
2991
|
+
let report = store.collect_unreachable_at(far).unwrap();
|
|
2992
|
+
assert!(!report.collected_revisions.contains(&w1.id), "w1 is the write path's head");
|
|
2993
|
+
assert!(store.exists(&w1.id));
|
|
2994
|
+
|
|
2995
|
+
db.update("tasks:1", json!({"n": 2})).unwrap();
|
|
2996
|
+
let w2 = store
|
|
2997
|
+
.history_of(&resource)
|
|
2998
|
+
.into_iter()
|
|
2999
|
+
.find(|r| r.parent_id.as_ref() == Some(&w1.id))
|
|
3000
|
+
.expect("the next write minted a child of w1");
|
|
3001
|
+
assert_eq!(store.parent_of(&w2.id), Some(ParentLookup::Revision(Box::new(w1))));
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
/// The generation row, the incarnation rows and the ref rows the durable
|
|
3005
|
+
/// path writes are invisible to the history and ref listings.
|
|
3006
|
+
#[test]
|
|
3007
|
+
fn internal_rows_do_not_appear_as_history_or_refs() {
|
|
3008
|
+
let (_dir, store, _db) = durable_store();
|
|
3009
|
+
let (a, _, _, _) = forked(&store);
|
|
3010
|
+
store.create_ref("heads", "main", a.id.clone(), "t".into(), BTreeMap::new()).unwrap();
|
|
3011
|
+
store.delete_ref("heads", "main", Some(1)).unwrap();
|
|
3012
|
+
store.create_ref("heads", "main", a.id.clone(), "t".into(), BTreeMap::new()).unwrap();
|
|
3013
|
+
assert_eq!(store.list_refs(None).len(), 1);
|
|
3014
|
+
assert_eq!(store.resources(), vec!["doc:gc".to_string()]);
|
|
3015
|
+
assert_eq!(store.history_of("doc:gc").len(), 4);
|
|
3016
|
+
}
|
|
3017
|
+
|
|
2319
3018
|
#[test]
|
|
2320
3019
|
fn test_state_id_deterministic() {
|
|
2321
3020
|
let json = r#"{"name":"Alice","age":30}"#;
|