@feltdb/core 0.12.0 → 0.12.2
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 +28 -0
- package/dist/cli/commands.js +88 -43
- package/dist/collection.d.ts +41 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +116 -47
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +11 -1
- package/dist/create/build-identity.json +28 -0
- package/dist/create/cli.js +3 -3
- package/dist/create/create.js +17 -19
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +18 -3
- package/dist/create/server-source/Cargo.toml +4 -2
- package/dist/create/server-source/crates/feltdb/Cargo.toml +20 -0
- package/dist/create/server-source/crates/feltdb/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb/README.md +75 -0
- package/dist/create/server-source/crates/feltdb/src/agent_state.rs +320 -0
- package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +1545 -0
- package/dist/create/server-source/crates/feltdb/src/authority.rs +145 -15
- package/dist/create/server-source/crates/feltdb/src/lib.rs +238 -23
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +850 -155
- package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +1188 -0
- package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +2884 -0
- package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +534 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +23340 -0
- 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-client/Cargo.toml +29 -0
- package/dist/create/server-source/crates/feltdb-client/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb-client/README.md +70 -0
- package/dist/create/server-source/crates/feltdb-client/src/application.rs +527 -0
- package/dist/create/server-source/crates/feltdb-client/src/authority.rs +679 -0
- package/dist/create/server-source/crates/feltdb-client/src/control.rs +292 -0
- package/dist/create/server-source/crates/feltdb-client/src/error.rs +227 -0
- package/dist/create/server-source/crates/feltdb-client/src/events.rs +146 -0
- package/dist/create/server-source/crates/feltdb-client/src/lib.rs +466 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/README.md +4 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/private-ca.pem +20 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server-key.pem +28 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server.pem +20 -0
- package/dist/create/server-source/crates/feltdb-client/tests/private_ca.rs +95 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +24 -1
- package/dist/create/server-source/crates/feltdb-server/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +12 -1
- 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/backup.rs +6 -2
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +21 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +75 -8
- 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/create/server-source/crates/feltdb-server/tests/rust_client.rs +683 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +3 -1
- package/dist/create/template/default-project/README.md +12 -7
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +1 -1
- package/dist/create/template/dot-feltdb-README.md +3 -3
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +43 -17
- package/dist/development-runtime-bridge.d.ts.map +1 -1
- package/dist/development-runtime-bridge.js +22 -1
- package/dist/embedded-transaction.d.ts +75 -8
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +124 -18
- package/dist/error-codes.d.ts +2 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +2 -0
- package/dist/feltdb.d.ts +1 -11
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +6 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +43 -11
- 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/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +65 -41
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +29 -8
- package/dist/studio/{KeyManagementPanel-DZuSeWBK.js → KeyManagementPanel-D5FMvvU9.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/KeyManagementPanel.d.ts.map +1 -1
- package/dist/studio/components/KeyManagementPanel.js +1 -1
- package/dist/studio/components/RouteErrorBoundary.d.ts +5 -0
- package/dist/studio/components/RouteErrorBoundary.d.ts.map +1 -0
- package/dist/studio/components/index.js +2 -2
- package/dist/studio/{components-BHTKlUgT.js → components-B4eKIUN5.js} +3 -2
- 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/Instance.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/context.d.ts +4 -1
- package/dist/studio/control-plane/context.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 +22 -2
- package/dist/studio/control-plane/primitives.d.ts.map +1 -1
- package/dist/studio/control-plane-fxMcJn74.js +4173 -0
- package/dist/studio/index.js +307 -235
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
- package/dist/studio/utils/control-plane-api.js +4 -3
- package/dist/studio/utils/service-api.d.ts.map +1 -1
- package/dist/studio/utils/service-api.js +5 -4
- package/dist/studio-app/assets/{feltdb_wasm-DYfWAV0E.js → feltdb_wasm-BatwwogY.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BX0newOr.wasm +0 -0
- package/dist/studio-app/assets/index-CiC8VY-a.css +1 -0
- package/dist/studio-app/assets/index-DoVvbIay.js +30 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +4 -3
- package/dist/transaction-identity.d.ts +62 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +138 -0
- package/dist/transaction.d.ts +50 -5
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +28 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/wasm/package.json +1 -1
- package/package.json +2 -2
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- 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,549 @@ 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::TransactionIdPayloadMismatch { .. } => Refusal::OtherRequest,
|
|
1607
|
+
other => Refusal::Failed(other.to_string()),
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
fn storage(error: impl std::fmt::Display) -> RefError {
|
|
1612
|
+
RefError::Storage(error.to_string())
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
fn contended(what: &str) -> String {
|
|
1616
|
+
format!(
|
|
1617
|
+
"{what} was refused by concurrent changes on {ATTEMPTS} consecutive attempts; nothing was applied"
|
|
1618
|
+
)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
fn row(db: &FeltDb, key: &str) -> Result<Option<Value>, RefError> {
|
|
1622
|
+
db.get::<Value>(key).map_err(storage)
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
fn committed_payload(db: &FeltDb, id: &str) -> Result<Option<String>, RefError> {
|
|
1626
|
+
db.applied_transaction_payload_hash(id).map_err(storage)
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
fn decode(row: Value) -> Result<StateRef, RefError> {
|
|
1630
|
+
serde_json::from_value(row).map_err(storage)
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
fn commit(
|
|
1634
|
+
db: &FeltDb,
|
|
1635
|
+
id: &str,
|
|
1636
|
+
payload: Option<&str>,
|
|
1637
|
+
pins: &[RecordPrecondition],
|
|
1638
|
+
mutations: &[AtomicMutation],
|
|
1639
|
+
) -> Result<(), Refusal> {
|
|
1640
|
+
db.apply_atomic_transaction_guarded(id, payload, None, &[], pins, mutations, None)
|
|
1641
|
+
.map(|_| ())
|
|
1642
|
+
.map_err(refusal)
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
pub(super) fn create_ref(
|
|
1646
|
+
db: &FeltDb,
|
|
1647
|
+
namespace: &str,
|
|
1648
|
+
name: &str,
|
|
1649
|
+
target: StateId,
|
|
1650
|
+
authority: String,
|
|
1651
|
+
metadata: BTreeMap<String, Value>,
|
|
1652
|
+
) -> Result<StateRef, RefError> {
|
|
1653
|
+
let ref_k = ref_key(namespace, name);
|
|
1654
|
+
let inc_k = incarnation_key(namespace, name);
|
|
1655
|
+
let target_k = revision_key(&target);
|
|
1656
|
+
let payload = payload_hash(&json!({
|
|
1657
|
+
"target": target.as_hex(),
|
|
1658
|
+
"authority": authority,
|
|
1659
|
+
"metadata": metadata,
|
|
1660
|
+
}));
|
|
1661
|
+
for _ in 0..ATTEMPTS {
|
|
1662
|
+
let incarnation_row = row(db, &inc_k)?;
|
|
1663
|
+
if let Some(existing) = row(db, &ref_k)? {
|
|
1664
|
+
// The create that made this incarnation, repeated, is that create.
|
|
1665
|
+
let id = ref_transaction_id("create", namespace, name, incarnation_of(&existing), None);
|
|
1666
|
+
if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
|
|
1667
|
+
return decode(existing);
|
|
1668
|
+
}
|
|
1669
|
+
return Err(RefError::AlreadyExists);
|
|
1670
|
+
}
|
|
1671
|
+
if row(db, &target_k)?.is_none() {
|
|
1672
|
+
return Err(RefError::UnknownTarget(target));
|
|
1673
|
+
}
|
|
1674
|
+
let incarnation = incarnation_in(incarnation_row.as_ref()) + 1;
|
|
1675
|
+
let id = ref_transaction_id("create", namespace, name, incarnation, None);
|
|
1676
|
+
let reference = StateRef {
|
|
1677
|
+
namespace: namespace.to_string(),
|
|
1678
|
+
name: name.to_string(),
|
|
1679
|
+
target: target.clone(),
|
|
1680
|
+
version: 1,
|
|
1681
|
+
authority: authority.clone(),
|
|
1682
|
+
updated_at_ms: current_time_ms(),
|
|
1683
|
+
metadata: metadata.clone(),
|
|
1684
|
+
};
|
|
1685
|
+
let pins = [
|
|
1686
|
+
unchanged(&ref_k, None),
|
|
1687
|
+
unchanged(&inc_k, incarnation_row.as_ref()),
|
|
1688
|
+
present(&target_k),
|
|
1689
|
+
];
|
|
1690
|
+
let mutations = [
|
|
1691
|
+
put(&ref_k, ref_row(&reference, incarnation)),
|
|
1692
|
+
put(&inc_k, json!({ "incarnation": incarnation })),
|
|
1693
|
+
advance_generation(&id),
|
|
1694
|
+
];
|
|
1695
|
+
match commit(db, &id, Some(&payload), &pins, &mutations) {
|
|
1696
|
+
Ok(()) => return Ok(reference),
|
|
1697
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1698
|
+
Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
Err(RefError::Storage(contended("ref create")))
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
pub(super) fn compare_and_swap_ref(
|
|
1705
|
+
db: &FeltDb,
|
|
1706
|
+
namespace: &str,
|
|
1707
|
+
name: &str,
|
|
1708
|
+
expected_version: u64,
|
|
1709
|
+
target: StateId,
|
|
1710
|
+
authority: String,
|
|
1711
|
+
metadata: Option<BTreeMap<String, Value>>,
|
|
1712
|
+
) -> Result<StateRef, RefError> {
|
|
1713
|
+
let ref_k = ref_key(namespace, name);
|
|
1714
|
+
let inc_k = incarnation_key(namespace, name);
|
|
1715
|
+
let target_k = revision_key(&target);
|
|
1716
|
+
let payload = payload_hash(&json!({
|
|
1717
|
+
"target": target.as_hex(),
|
|
1718
|
+
"authority": authority,
|
|
1719
|
+
"metadata": metadata,
|
|
1720
|
+
}));
|
|
1721
|
+
for _ in 0..ATTEMPTS {
|
|
1722
|
+
let incarnation_row = row(db, &inc_k)?;
|
|
1723
|
+
let Some(ref_read) = row(db, &ref_k)? else {
|
|
1724
|
+
return Err(RefError::NotFound);
|
|
1725
|
+
};
|
|
1726
|
+
let incarnation = incarnation_of(&ref_read);
|
|
1727
|
+
let current = decode(ref_read.clone())?;
|
|
1728
|
+
let id = ref_transaction_id("cas", namespace, name, incarnation, Some(expected_version));
|
|
1729
|
+
if current.version != expected_version {
|
|
1730
|
+
// Either this exact request already committed (a retry), or
|
|
1731
|
+
// another one did: the payload bound to the id says which.
|
|
1732
|
+
if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
|
|
1733
|
+
if current.version == expected_version + 1 && current.target == target {
|
|
1734
|
+
return Ok(current);
|
|
1735
|
+
}
|
|
1736
|
+
// Committed, and the ref has moved on since. Describe the
|
|
1737
|
+
// transition this request made; `updated_at_ms` is now.
|
|
1738
|
+
return Ok(StateRef {
|
|
1739
|
+
version: expected_version + 1,
|
|
1740
|
+
target,
|
|
1741
|
+
authority,
|
|
1742
|
+
updated_at_ms: current_time_ms(),
|
|
1743
|
+
metadata: metadata.unwrap_or(current.metadata),
|
|
1744
|
+
..current
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
return Err(RefError::VersionConflict {
|
|
1748
|
+
expected: expected_version,
|
|
1749
|
+
current: current.version,
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
if row(db, &target_k)?.is_none() {
|
|
1753
|
+
return Err(RefError::UnknownTarget(target));
|
|
1754
|
+
}
|
|
1755
|
+
let next = StateRef {
|
|
1756
|
+
version: expected_version + 1,
|
|
1757
|
+
target: target.clone(),
|
|
1758
|
+
authority: authority.clone(),
|
|
1759
|
+
updated_at_ms: current_time_ms(),
|
|
1760
|
+
metadata: metadata.clone().unwrap_or_else(|| current.metadata.clone()),
|
|
1761
|
+
..current
|
|
1762
|
+
};
|
|
1763
|
+
let pins = [
|
|
1764
|
+
unchanged(&ref_k, Some(&ref_read)),
|
|
1765
|
+
unchanged(&inc_k, incarnation_row.as_ref()),
|
|
1766
|
+
present(&target_k),
|
|
1767
|
+
];
|
|
1768
|
+
let mutations = [put(&ref_k, ref_row(&next, incarnation)), advance_generation(&id)];
|
|
1769
|
+
match commit(db, &id, Some(&payload), &pins, &mutations) {
|
|
1770
|
+
Ok(()) => return Ok(next),
|
|
1771
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1772
|
+
Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
Err(RefError::Storage(contended("ref compare-and-swap")))
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
pub(super) fn delete_ref(
|
|
1779
|
+
db: &FeltDb,
|
|
1780
|
+
namespace: &str,
|
|
1781
|
+
name: &str,
|
|
1782
|
+
expected_version: Option<u64>,
|
|
1783
|
+
) -> Result<(), RefError> {
|
|
1784
|
+
let ref_k = ref_key(namespace, name);
|
|
1785
|
+
let inc_k = incarnation_key(namespace, name);
|
|
1786
|
+
let payload = payload_hash(&json!({ "expected_version": expected_version }));
|
|
1787
|
+
for _ in 0..ATTEMPTS {
|
|
1788
|
+
let incarnation_row = row(db, &inc_k)?;
|
|
1789
|
+
let Some(ref_read) = row(db, &ref_k)? else {
|
|
1790
|
+
// A repeat of a delete that already removed this incarnation.
|
|
1791
|
+
let id = ref_transaction_id(
|
|
1792
|
+
"delete",
|
|
1793
|
+
namespace,
|
|
1794
|
+
name,
|
|
1795
|
+
incarnation_in(incarnation_row.as_ref()),
|
|
1796
|
+
None,
|
|
1797
|
+
);
|
|
1798
|
+
if committed_payload(db, &id)?.as_deref() == Some(payload.as_str()) {
|
|
1799
|
+
return Ok(());
|
|
1800
|
+
}
|
|
1801
|
+
return Err(RefError::NotFound);
|
|
1802
|
+
};
|
|
1803
|
+
let incarnation = incarnation_of(&ref_read);
|
|
1804
|
+
let current = decode(ref_read.clone())?;
|
|
1805
|
+
if let Some(expected) = expected_version {
|
|
1806
|
+
if current.version != expected {
|
|
1807
|
+
return Err(RefError::VersionConflict {
|
|
1808
|
+
expected,
|
|
1809
|
+
current: current.version,
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
let id = ref_transaction_id("delete", namespace, name, incarnation, None);
|
|
1814
|
+
let pins = [
|
|
1815
|
+
unchanged(&ref_k, Some(&ref_read)),
|
|
1816
|
+
unchanged(&inc_k, incarnation_row.as_ref()),
|
|
1817
|
+
];
|
|
1818
|
+
let mutations = [
|
|
1819
|
+
remove(&ref_k),
|
|
1820
|
+
put(&inc_k, json!({ "incarnation": incarnation })),
|
|
1821
|
+
advance_generation(&id),
|
|
1822
|
+
];
|
|
1823
|
+
match commit(db, &id, Some(&payload), &pins, &mutations) {
|
|
1824
|
+
Ok(()) => return Ok(()),
|
|
1825
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1826
|
+
Err(Refusal::Failed(message)) => return Err(RefError::Storage(message)),
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
Err(RefError::Storage(contended("ref delete")))
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
pub(super) fn commit_revision(
|
|
1833
|
+
db: &FeltDb,
|
|
1834
|
+
revision: StateRevision,
|
|
1835
|
+
) -> Result<StateRevision, RevisionError> {
|
|
1836
|
+
let fail = |error: &dyn std::fmt::Display| RevisionError::Storage(error.to_string());
|
|
1837
|
+
let object = StateObject::new(revision.content.clone());
|
|
1838
|
+
let rev_k = revision_key(&revision.id);
|
|
1839
|
+
let obj_k = object_key(&object.id);
|
|
1840
|
+
let parent_k = revision.parent_id.as_ref().map(revision_key);
|
|
1841
|
+
for _ in 0..ATTEMPTS {
|
|
1842
|
+
if let Some(existing) = db.get::<StateRevision>(&rev_k).map_err(|e| fail(&e))? {
|
|
1843
|
+
let same = existing.resource == revision.resource
|
|
1844
|
+
&& existing.content_id == revision.content_id
|
|
1845
|
+
&& existing.parent_id == revision.parent_id
|
|
1846
|
+
&& existing.sequence == revision.sequence;
|
|
1847
|
+
return if same {
|
|
1848
|
+
Ok(existing)
|
|
1849
|
+
} else {
|
|
1850
|
+
Err(RevisionError::AlreadyCommitted(revision.id.clone()))
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
let mut pins = vec![unchanged(&rev_k, None)];
|
|
1854
|
+
if let (Some(parent), Some(parent_k)) = (&revision.parent_id, &parent_k) {
|
|
1855
|
+
if db.get::<Value>(parent_k).map_err(|e| fail(&e))?.is_none() {
|
|
1856
|
+
return Err(RevisionError::UnknownParent(parent.clone()));
|
|
1857
|
+
}
|
|
1858
|
+
pins.push(present(parent_k));
|
|
1859
|
+
}
|
|
1860
|
+
let id = format!("state-revision:{}:{}", revision.id.as_hex(), unique_suffix());
|
|
1861
|
+
let mut mutations = Vec::with_capacity(3);
|
|
1862
|
+
match db.get::<StateObject>(&obj_k).map_err(|e| fail(&e))? {
|
|
1863
|
+
Some(existing) if !existing.verify_integrity() => {
|
|
1864
|
+
return Err(RevisionError::Storage(
|
|
1865
|
+
"Stored state object failed integrity verification".to_string(),
|
|
1866
|
+
));
|
|
1867
|
+
}
|
|
1868
|
+
Some(_) => pins.push(present(&obj_k)),
|
|
1869
|
+
None => {
|
|
1870
|
+
pins.push(unchanged(&obj_k, None));
|
|
1871
|
+
mutations.push(put(&obj_k, serde_json::to_value(&object).map_err(|e| fail(&e))?));
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
mutations.push(put(&rev_k, serde_json::to_value(&revision).map_err(|e| fail(&e))?));
|
|
1875
|
+
mutations.push(advance_generation(&id));
|
|
1876
|
+
match commit(db, &id, None, &pins, &mutations) {
|
|
1877
|
+
Ok(()) => return Ok(revision),
|
|
1878
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1879
|
+
Err(Refusal::Failed(message)) => return Err(RevisionError::Storage(message)),
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
Err(RevisionError::Storage(contended("revision commit")))
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
pub(super) fn collect(
|
|
1886
|
+
store: &StateStore,
|
|
1887
|
+
db: &FeltDb,
|
|
1888
|
+
now_ms: u64,
|
|
1889
|
+
) -> Result<GarbageCollectionReport, GarbageCollectionError> {
|
|
1890
|
+
let fail = |error: &dyn std::fmt::Display| GarbageCollectionError::Storage(error.to_string());
|
|
1891
|
+
for _ in 0..ATTEMPTS {
|
|
1892
|
+
// The generation first: everything planned below was read after it,
|
|
1893
|
+
// so a write that lands after this read moves it.
|
|
1894
|
+
let generation = db.get::<Value>(GENERATION_KEY).map_err(|e| fail(&e))?;
|
|
1895
|
+
let report = store.plan_collection(now_ms, &db.revision_head_ids());
|
|
1896
|
+
if report.collected_revisions.is_empty() && report.collected_objects.is_empty() {
|
|
1897
|
+
return Ok(report);
|
|
1898
|
+
}
|
|
1899
|
+
let pins = [unchanged(GENERATION_KEY, generation.as_ref())];
|
|
1900
|
+
let mutations: Vec<AtomicMutation> = report
|
|
1901
|
+
.collected_revisions
|
|
1902
|
+
.iter()
|
|
1903
|
+
.map(|id| remove(&revision_key(id)))
|
|
1904
|
+
.chain(report.collected_objects.iter().map(|id| remove(&object_key(id))))
|
|
1905
|
+
.collect();
|
|
1906
|
+
let id = format!("state-gc:{}", unique_suffix());
|
|
1907
|
+
match commit(db, &id, None, &pins, &mutations) {
|
|
1908
|
+
Ok(()) => return Ok(report),
|
|
1909
|
+
Err(Refusal::Changed | Refusal::OtherRequest) => continue,
|
|
1910
|
+
Err(Refusal::Failed(message)) => return Err(GarbageCollectionError::Storage(message)),
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
Err(GarbageCollectionError::Storage(contended("garbage collection")))
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1374
1917
|
fn collect_referenced_objects(revision: &StateRevision) -> BTreeSet<StateId> {
|
|
1375
1918
|
let mut ids = BTreeSet::from([revision.content_id.clone()]);
|
|
1376
1919
|
if let Some(values) = revision
|
|
@@ -1692,34 +2235,7 @@ impl StateStore {
|
|
|
1692
2235
|
state.revisions.insert(revision.id.clone(), revision.clone());
|
|
1693
2236
|
}
|
|
1694
2237
|
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
|
-
})?;
|
|
2238
|
+
return durable::commit_revision(feltdb, revision);
|
|
1723
2239
|
}
|
|
1724
2240
|
}
|
|
1725
2241
|
Ok(revision)
|
|
@@ -1816,6 +2332,12 @@ impl StateStore {
|
|
|
1816
2332
|
}
|
|
1817
2333
|
|
|
1818
2334
|
/// Create a mutable reference in a namespace.
|
|
2335
|
+
///
|
|
2336
|
+
/// On the durable backing this is one fenced FeltDB transaction (see
|
|
2337
|
+
/// [`durable`]): the ref is created at version 1 only if, at the moment of
|
|
2338
|
+
/// commit, it is still absent and its target revision is still present.
|
|
2339
|
+
/// Repeating an identical create that already succeeded returns the ref it
|
|
2340
|
+
/// created; a different create of an existing ref is `AlreadyExists`.
|
|
1819
2341
|
pub fn create_ref(
|
|
1820
2342
|
&self,
|
|
1821
2343
|
namespace: &str,
|
|
@@ -1827,24 +2349,31 @@ impl StateStore {
|
|
|
1827
2349
|
if !valid_ref_component(namespace) || !valid_ref_component(name) {
|
|
1828
2350
|
return Err(RefError::InvalidIdentifier);
|
|
1829
2351
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2352
|
+
match &self.backing {
|
|
2353
|
+
Backing::Durable(feltdb) => {
|
|
2354
|
+
durable::create_ref(feltdb, namespace, name, target, authority, metadata)
|
|
2355
|
+
}
|
|
2356
|
+
Backing::Volatile(_) => {
|
|
2357
|
+
let _guard = self.mutation_lock.lock().unwrap();
|
|
2358
|
+
if !self.exists(&target) {
|
|
2359
|
+
return Err(RefError::UnknownTarget(target));
|
|
2360
|
+
}
|
|
2361
|
+
if self.get_ref(namespace, name).is_some() {
|
|
2362
|
+
return Err(RefError::AlreadyExists);
|
|
2363
|
+
}
|
|
2364
|
+
let reference = StateRef {
|
|
2365
|
+
namespace: namespace.to_string(),
|
|
2366
|
+
name: name.to_string(),
|
|
2367
|
+
target,
|
|
2368
|
+
version: 1,
|
|
2369
|
+
authority,
|
|
2370
|
+
updated_at_ms: current_time_ms(),
|
|
2371
|
+
metadata,
|
|
2372
|
+
};
|
|
2373
|
+
self.write_volatile_ref(&reference);
|
|
2374
|
+
Ok(reference)
|
|
2375
|
+
}
|
|
1836
2376
|
}
|
|
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
2377
|
}
|
|
1849
2378
|
|
|
1850
2379
|
/// Read a reference.
|
|
@@ -1879,6 +2408,17 @@ impl StateStore {
|
|
|
1879
2408
|
}
|
|
1880
2409
|
|
|
1881
2410
|
/// Compare-and-swap move for a reference.
|
|
2411
|
+
///
|
|
2412
|
+
/// On the durable backing this is one fenced FeltDB transaction: it commits
|
|
2413
|
+
/// only if, at the moment of commit, the ref is still at
|
|
2414
|
+
/// `expected_version` and the target revision is still present, whichever
|
|
2415
|
+
/// `StateStore` or `FeltDb` handle the competing writes came through.
|
|
2416
|
+
///
|
|
2417
|
+
/// The request `(ref incarnation, expected_version, target, authority,
|
|
2418
|
+
/// metadata)` is idempotent: repeating one that already committed — after a
|
|
2419
|
+
/// lost response, a restart or a compaction — returns `Ok` and does not
|
|
2420
|
+
/// advance the ref again. Any other request for the same expected version
|
|
2421
|
+
/// is a `VersionConflict`.
|
|
1882
2422
|
pub fn compare_and_swap_ref(
|
|
1883
2423
|
&self,
|
|
1884
2424
|
namespace: &str,
|
|
@@ -1891,29 +2431,45 @@ impl StateStore {
|
|
|
1891
2431
|
if !valid_ref_component(namespace) || !valid_ref_component(name) {
|
|
1892
2432
|
return Err(RefError::InvalidIdentifier);
|
|
1893
2433
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2434
|
+
match &self.backing {
|
|
2435
|
+
Backing::Durable(feltdb) => durable::compare_and_swap_ref(
|
|
2436
|
+
feltdb,
|
|
2437
|
+
namespace,
|
|
2438
|
+
name,
|
|
2439
|
+
expected_version,
|
|
2440
|
+
target,
|
|
2441
|
+
authority,
|
|
2442
|
+
metadata,
|
|
2443
|
+
),
|
|
2444
|
+
Backing::Volatile(_) => {
|
|
2445
|
+
let _guard = self.mutation_lock.lock().unwrap();
|
|
2446
|
+
let mut current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
|
|
2447
|
+
if current.version != expected_version {
|
|
2448
|
+
return Err(RefError::VersionConflict {
|
|
2449
|
+
expected: expected_version,
|
|
2450
|
+
current: current.version,
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
if !self.exists(&target) {
|
|
2454
|
+
return Err(RefError::UnknownTarget(target));
|
|
2455
|
+
}
|
|
2456
|
+
current.target = target;
|
|
2457
|
+
current.version += 1;
|
|
2458
|
+
current.authority = authority;
|
|
2459
|
+
current.updated_at_ms = current_time_ms();
|
|
2460
|
+
if let Some(metadata) = metadata {
|
|
2461
|
+
current.metadata = metadata;
|
|
2462
|
+
}
|
|
2463
|
+
self.write_volatile_ref(¤t);
|
|
2464
|
+
Ok(current)
|
|
2465
|
+
}
|
|
1911
2466
|
}
|
|
1912
|
-
self.write_ref(¤t)?;
|
|
1913
|
-
Ok(current)
|
|
1914
2467
|
}
|
|
1915
2468
|
|
|
1916
2469
|
/// Delete a reference, optionally enforcing the current version.
|
|
2470
|
+
///
|
|
2471
|
+
/// On the durable backing this is one fenced FeltDB transaction, and a
|
|
2472
|
+
/// repeat of an identical delete that already committed returns `Ok`.
|
|
1917
2473
|
pub fn delete_ref(
|
|
1918
2474
|
&self,
|
|
1919
2475
|
namespace: &str,
|
|
@@ -1923,17 +2479,29 @@ impl StateStore {
|
|
|
1923
2479
|
if !valid_ref_component(namespace) || !valid_ref_component(name) {
|
|
1924
2480
|
return Err(RefError::InvalidIdentifier);
|
|
1925
2481
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2482
|
+
match &self.backing {
|
|
2483
|
+
Backing::Durable(feltdb) => {
|
|
2484
|
+
durable::delete_ref(feltdb, namespace, name, expected_version)
|
|
2485
|
+
}
|
|
2486
|
+
Backing::Volatile(state) => {
|
|
2487
|
+
let _guard = self.mutation_lock.lock().unwrap();
|
|
2488
|
+
let current = self.get_ref(namespace, name).ok_or(RefError::NotFound)?;
|
|
2489
|
+
if let Some(expected) = expected_version {
|
|
2490
|
+
if current.version != expected {
|
|
2491
|
+
return Err(RefError::VersionConflict {
|
|
2492
|
+
expected,
|
|
2493
|
+
current: current.version,
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
state
|
|
2498
|
+
.lock()
|
|
2499
|
+
.unwrap()
|
|
2500
|
+
.refs
|
|
2501
|
+
.remove(&(namespace.to_string(), name.to_string()));
|
|
2502
|
+
Ok(())
|
|
1934
2503
|
}
|
|
1935
2504
|
}
|
|
1936
|
-
self.remove_ref(namespace, name)
|
|
1937
2505
|
}
|
|
1938
2506
|
|
|
1939
2507
|
/// Enumerate refs, optionally scoped to one namespace.
|
|
@@ -1991,95 +2559,122 @@ impl StateStore {
|
|
|
1991
2559
|
Ok(projected)
|
|
1992
2560
|
}
|
|
1993
2561
|
|
|
1994
|
-
/// Collect revisions and objects
|
|
2562
|
+
/// Collect revisions and objects that no root can reach.
|
|
2563
|
+
///
|
|
2564
|
+
/// **Roots** are: every ref target; every resource's latest revision (each
|
|
2565
|
+
/// revision at the resource's highest sequence, and the head the record
|
|
2566
|
+
/// write path will mint its next revision from); and every revision
|
|
2567
|
+
/// younger than 15 minutes (`GC_GRACE_MS`). **Reachable** is the roots and all their
|
|
2568
|
+
/// ancestors by `parent_id`, because history, diff, rollback and replicated
|
|
2569
|
+
/// reconstruction all walk that chain. Only a revision that is old, named
|
|
2570
|
+
/// by no ref, not latest, and an ancestor of none of those is collected —
|
|
2571
|
+
/// in practice, an abandoned fork. Retention (`keep_last`) is unaffected:
|
|
2572
|
+
/// it is positional and runs at the mint, not here.
|
|
2573
|
+
///
|
|
2574
|
+
/// On the durable backing every deletion commits in one FeltDB transaction
|
|
2575
|
+
/// fenced on the state generation, which every ref change and every
|
|
2576
|
+
/// committed revision advances. A concurrent change aborts the collection
|
|
2577
|
+
/// and it is re-planned from fresh reads; see [`durable`].
|
|
1995
2578
|
pub fn collect_unreachable(&self) -> Result<GarbageCollectionReport, GarbageCollectionError> {
|
|
1996
|
-
|
|
2579
|
+
self.collect_unreachable_at(current_time_ms())
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
/// [`collect_unreachable`](Self::collect_unreachable) with an explicit
|
|
2583
|
+
/// clock, so the grace period can be tested without waiting for it.
|
|
2584
|
+
pub(crate) fn collect_unreachable_at(
|
|
2585
|
+
&self,
|
|
2586
|
+
now_ms: u64,
|
|
2587
|
+
) -> Result<GarbageCollectionReport, GarbageCollectionError> {
|
|
2588
|
+
match &self.backing {
|
|
2589
|
+
Backing::Volatile(state) => {
|
|
2590
|
+
let _guard = self.mutation_lock.lock().unwrap();
|
|
2591
|
+
let report = self.plan_collection(now_ms, &[]);
|
|
2592
|
+
let mut state = state.lock().unwrap();
|
|
2593
|
+
for id in &report.collected_revisions {
|
|
2594
|
+
state.revisions.remove(id);
|
|
2595
|
+
}
|
|
2596
|
+
for id in &report.collected_objects {
|
|
2597
|
+
state.objects.remove(id);
|
|
2598
|
+
}
|
|
2599
|
+
Ok(report)
|
|
2600
|
+
}
|
|
2601
|
+
Backing::Durable(feltdb) => durable::collect(self, feltdb, now_ms),
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/// What a collection at `now_ms` would keep and delete, from one read of
|
|
2606
|
+
/// refs and revisions. `extra_roots` are heads known outside `StateStore`.
|
|
2607
|
+
fn plan_collection(&self, now_ms: u64, extra_roots: &[StateId]) -> GarbageCollectionReport {
|
|
1997
2608
|
let roots = self.list_refs(None);
|
|
2609
|
+
let revisions = self.all_revisions();
|
|
2610
|
+
let by_id: HashMap<StateId, &StateRevision> =
|
|
2611
|
+
revisions.iter().map(|revision| (revision.id.clone(), revision)).collect();
|
|
2612
|
+
|
|
2613
|
+
let mut latest: HashMap<&str, u64> = HashMap::new();
|
|
2614
|
+
for revision in &revisions {
|
|
2615
|
+
let entry = latest.entry(revision.resource.as_str()).or_insert(revision.sequence);
|
|
2616
|
+
if revision.sequence > *entry {
|
|
2617
|
+
*entry = revision.sequence;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
let mut pending: Vec<StateId> = roots.iter().map(|reference| reference.target.clone()).collect();
|
|
2622
|
+
pending.extend(extra_roots.iter().cloned());
|
|
2623
|
+
for revision in &revisions {
|
|
2624
|
+
let is_latest = latest.get(revision.resource.as_str()) == Some(&revision.sequence);
|
|
2625
|
+
let is_young = now_ms.saturating_sub(revision.timestamp_ms) < GC_GRACE_MS;
|
|
2626
|
+
if is_latest || is_young {
|
|
2627
|
+
pending.push(revision.id.clone());
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
1998
2631
|
let mut reachable_revisions = BTreeSet::new();
|
|
1999
2632
|
let mut reachable_objects = BTreeSet::new();
|
|
2000
|
-
let mut pending: Vec<StateId> = roots.iter().map(|reference| reference.target.clone()).collect();
|
|
2001
2633
|
while let Some(id) = pending.pop() {
|
|
2634
|
+
let Some(revision) = by_id.get(&id) else {
|
|
2635
|
+
// A root or parent that is not held (expired by retention, or
|
|
2636
|
+
// never committed) ends the walk; it is not an error.
|
|
2637
|
+
continue;
|
|
2638
|
+
};
|
|
2002
2639
|
if !reachable_revisions.insert(id.clone()) {
|
|
2003
2640
|
continue;
|
|
2004
2641
|
}
|
|
2005
|
-
let Some(revision) = self.get(&id) else {
|
|
2006
|
-
continue;
|
|
2007
|
-
};
|
|
2008
2642
|
if let Some(parent_id) = revision.parent_id.clone() {
|
|
2009
2643
|
pending.push(parent_id);
|
|
2010
2644
|
}
|
|
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
|
-
}
|
|
2645
|
+
reachable_objects.extend(collect_referenced_objects(revision));
|
|
2025
2646
|
}
|
|
2026
2647
|
|
|
2027
|
-
let mut
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2648
|
+
let mut collected_revisions: Vec<StateId> = revisions
|
|
2649
|
+
.iter()
|
|
2650
|
+
.map(|revision| revision.id.clone())
|
|
2651
|
+
.filter(|id| !reachable_revisions.contains(id))
|
|
2652
|
+
.collect();
|
|
2653
|
+
collected_revisions.sort();
|
|
2654
|
+
let mut collected_objects: Vec<StateId> = self
|
|
2655
|
+
.all_objects()
|
|
2656
|
+
.into_iter()
|
|
2657
|
+
.map(|object| object.id)
|
|
2658
|
+
.filter(|id| !reachable_objects.contains(id))
|
|
2659
|
+
.collect();
|
|
2660
|
+
collected_objects.sort();
|
|
2036
2661
|
|
|
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 {
|
|
2662
|
+
GarbageCollectionReport {
|
|
2042
2663
|
roots,
|
|
2043
|
-
reachable_revisions:
|
|
2044
|
-
reachable_objects:
|
|
2664
|
+
reachable_revisions: reachable_revisions.into_iter().collect(),
|
|
2665
|
+
reachable_objects: reachable_objects.into_iter().collect(),
|
|
2045
2666
|
collected_revisions,
|
|
2046
2667
|
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
2668
|
}
|
|
2064
|
-
Ok(())
|
|
2065
2669
|
}
|
|
2066
2670
|
|
|
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
|
-
}
|
|
2671
|
+
fn write_volatile_ref(&self, reference: &StateRef) {
|
|
2672
|
+
if let Backing::Volatile(state) = &self.backing {
|
|
2673
|
+
state.lock().unwrap().refs.insert(
|
|
2674
|
+
(reference.namespace.clone(), reference.name.clone()),
|
|
2675
|
+
reference.clone(),
|
|
2676
|
+
);
|
|
2081
2677
|
}
|
|
2082
|
-
Ok(())
|
|
2083
2678
|
}
|
|
2084
2679
|
|
|
2085
2680
|
fn all_revisions(&self) -> Vec<StateRevision> {
|
|
@@ -2234,20 +2829,6 @@ impl StateStore {
|
|
|
2234
2829
|
Ok(())
|
|
2235
2830
|
}
|
|
2236
2831
|
|
|
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
2832
|
fn retention_state(&self, resource: &str) -> RetentionState {
|
|
2252
2833
|
match &self.backing {
|
|
2253
2834
|
Backing::Volatile(state) => state
|
|
@@ -2316,6 +2897,120 @@ mod tests {
|
|
|
2316
2897
|
use super::*;
|
|
2317
2898
|
use serde_json::json;
|
|
2318
2899
|
|
|
2900
|
+
// -- garbage collection eligibility, with an explicit clock -------------
|
|
2901
|
+
|
|
2902
|
+
/// `A(0) → B(1) → C(2)` on one resource, plus a fork `F(1)` off `A`.
|
|
2903
|
+
fn forked(store: &StateStore) -> (StateRevision, StateRevision, StateRevision, StateRevision) {
|
|
2904
|
+
let a = store.create("doc:gc", json!({"n": "a"}).to_string(), "t".into()).unwrap();
|
|
2905
|
+
let b = store.commit(json!({"n": "b"}).to_string(), &a, "t".into()).unwrap();
|
|
2906
|
+
let c = store.commit(json!({"n": "c"}).to_string(), &b, "t".into()).unwrap();
|
|
2907
|
+
let f = store.commit(json!({"n": "f"}).to_string(), &a, "t".into()).unwrap();
|
|
2908
|
+
(a, b, c, f)
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
#[test]
|
|
2912
|
+
fn gc_grace_boundary_is_exclusive() {
|
|
2913
|
+
let store = StateStore::new_volatile();
|
|
2914
|
+
let (_, _, _, f) = forked(&store);
|
|
2915
|
+
let inside = store.collect_unreachable_at(f.timestamp_ms + GC_GRACE_MS - 1).unwrap();
|
|
2916
|
+
assert!(inside.collected_revisions.is_empty(), "younger than the grace: a root");
|
|
2917
|
+
let outside = store.collect_unreachable_at(f.timestamp_ms + GC_GRACE_MS).unwrap();
|
|
2918
|
+
assert_eq!(outside.collected_revisions, vec![f.id.clone()]);
|
|
2919
|
+
assert!(!store.exists(&f.id));
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
#[test]
|
|
2923
|
+
fn gc_keeps_latest_revisions_refs_and_all_their_ancestors() {
|
|
2924
|
+
for store in [StateStore::new_volatile(), durable_store().1] {
|
|
2925
|
+
let (a, b, c, f) = forked(&store);
|
|
2926
|
+
let g = store.commit(json!({"n": "g"}).to_string(), &f, "t".into()).unwrap();
|
|
2927
|
+
// g ties c at the highest sequence (2): both are "latest".
|
|
2928
|
+
let far = c.timestamp_ms + 365 * 24 * 60 * 60 * 1000;
|
|
2929
|
+
let report = store.collect_unreachable_at(far).unwrap();
|
|
2930
|
+
assert!(report.collected_revisions.is_empty(), "every revision is a root or an ancestor");
|
|
2931
|
+
|
|
2932
|
+
// A later revision on the main line makes f and g a stale fork …
|
|
2933
|
+
let d = store.commit(json!({"n": "d"}).to_string(), &c, "t".into()).unwrap();
|
|
2934
|
+
// … but a ref on g keeps g and its ancestor f.
|
|
2935
|
+
store
|
|
2936
|
+
.create_ref("candidates", "g", g.id.clone(), "t".into(), BTreeMap::new())
|
|
2937
|
+
.unwrap();
|
|
2938
|
+
assert!(store.collect_unreachable_at(far).unwrap().collected_revisions.is_empty());
|
|
2939
|
+
|
|
2940
|
+
store.delete_ref("candidates", "g", None).unwrap();
|
|
2941
|
+
let mut collected = store.collect_unreachable_at(far).unwrap().collected_revisions;
|
|
2942
|
+
collected.sort();
|
|
2943
|
+
let mut expected = vec![f.id.clone(), g.id.clone()];
|
|
2944
|
+
expected.sort();
|
|
2945
|
+
assert_eq!(collected, expected, "only the unreferenced stale fork");
|
|
2946
|
+
for kept in [&a, &b, &c, &d] {
|
|
2947
|
+
assert!(store.exists(&kept.id));
|
|
2948
|
+
}
|
|
2949
|
+
// The fork's content objects went with it; the kept ones stayed.
|
|
2950
|
+
let objects: BTreeSet<StateId> = store.all_objects().into_iter().map(|o| o.id).collect();
|
|
2951
|
+
for kept in [&a, &b, &c, &d] {
|
|
2952
|
+
assert!(objects.contains(&kept.content_id), "object of a kept revision");
|
|
2953
|
+
}
|
|
2954
|
+
for gone in [&f, &g] {
|
|
2955
|
+
assert!(!objects.contains(&gone.content_id), "object of a collected revision");
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
fn durable_store() -> (tempfile::TempDir, StateStore, Arc<crate::FeltDb>) {
|
|
2961
|
+
let dir = tempfile::TempDir::new().unwrap();
|
|
2962
|
+
let db = Arc::new(crate::FeltDb::open(dir.path().join("gc.log")).unwrap());
|
|
2963
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
2964
|
+
(dir, store, db)
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
/// The record write path mints from its own head, which an explicit
|
|
2968
|
+
/// `StateStore::commit` fork can overtake by sequence. That head must stay
|
|
2969
|
+
/// a root, or the next record write would mint a child of a deleted parent.
|
|
2970
|
+
#[test]
|
|
2971
|
+
fn gc_keeps_the_write_path_head_even_when_a_fork_is_longer() {
|
|
2972
|
+
let (_dir, store, db) = durable_store();
|
|
2973
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
2974
|
+
db.update("tasks:1", json!({"n": 1})).unwrap();
|
|
2975
|
+
let resource = store.resources().into_iter().find(|r| r.contains("tasks")).unwrap();
|
|
2976
|
+
let history = store.history_of(&resource);
|
|
2977
|
+
assert_eq!(history.len(), 2, "the write path minted two revisions");
|
|
2978
|
+
let (w0, w1) = (history[0].clone(), history[1].clone());
|
|
2979
|
+
|
|
2980
|
+
// A longer explicit fork off w0: x(1) → x2(2) overtakes w1(1).
|
|
2981
|
+
let x = store.commit(json!({"x": 1}).to_string(), &w0, "t".into()).unwrap();
|
|
2982
|
+
let x2 = store.commit(json!({"x": 2}).to_string(), &x, "t".into()).unwrap();
|
|
2983
|
+
assert_eq!(store.head_of(&resource).unwrap().id, x2.id);
|
|
2984
|
+
assert!(db.revision_head_ids().contains(&w1.id), "the write path still mints from w1");
|
|
2985
|
+
|
|
2986
|
+
let far = x2.timestamp_ms + 365 * 24 * 60 * 60 * 1000;
|
|
2987
|
+
let report = store.collect_unreachable_at(far).unwrap();
|
|
2988
|
+
assert!(!report.collected_revisions.contains(&w1.id), "w1 is the write path's head");
|
|
2989
|
+
assert!(store.exists(&w1.id));
|
|
2990
|
+
|
|
2991
|
+
db.update("tasks:1", json!({"n": 2})).unwrap();
|
|
2992
|
+
let w2 = store
|
|
2993
|
+
.history_of(&resource)
|
|
2994
|
+
.into_iter()
|
|
2995
|
+
.find(|r| r.parent_id.as_ref() == Some(&w1.id))
|
|
2996
|
+
.expect("the next write minted a child of w1");
|
|
2997
|
+
assert_eq!(store.parent_of(&w2.id), Some(ParentLookup::Revision(Box::new(w1))));
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
/// The generation row, the incarnation rows and the ref rows the durable
|
|
3001
|
+
/// path writes are invisible to the history and ref listings.
|
|
3002
|
+
#[test]
|
|
3003
|
+
fn internal_rows_do_not_appear_as_history_or_refs() {
|
|
3004
|
+
let (_dir, store, _db) = durable_store();
|
|
3005
|
+
let (a, _, _, _) = forked(&store);
|
|
3006
|
+
store.create_ref("heads", "main", a.id.clone(), "t".into(), BTreeMap::new()).unwrap();
|
|
3007
|
+
store.delete_ref("heads", "main", Some(1)).unwrap();
|
|
3008
|
+
store.create_ref("heads", "main", a.id.clone(), "t".into(), BTreeMap::new()).unwrap();
|
|
3009
|
+
assert_eq!(store.list_refs(None).len(), 1);
|
|
3010
|
+
assert_eq!(store.resources(), vec!["doc:gc".to_string()]);
|
|
3011
|
+
assert_eq!(store.history_of("doc:gc").len(), 4);
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2319
3014
|
#[test]
|
|
2320
3015
|
fn test_state_id_deterministic() {
|
|
2321
3016
|
let json = r#"{"name":"Alice","age":30}"#;
|