@feltdb/core 0.8.4 → 0.8.6
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/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1648 -46
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation.rs +39 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1565 -536
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +9 -3
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
- package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +132 -0
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +272 -0
- package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +467 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +309 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +445 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +518 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +278 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1365 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +417 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +442 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +16 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +695 -47
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +21 -0
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-C1VhI-U5.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8HXbAXb.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-Bbos1m2U.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
//! Production-facing evidence for the resource-scoped revision model.
|
|
2
|
+
//!
|
|
3
|
+
//! The [contract](../../../docs/architecture/revision-identity-and-ancestry.md)
|
|
4
|
+
//! proves the 21 clauses; this file proves the scenarios a database has to
|
|
5
|
+
//! survive rather than the clauses a model has to satisfy.
|
|
6
|
+
//!
|
|
7
|
+
//! Three groups: identity, ancestry, and bounded history.
|
|
8
|
+
|
|
9
|
+
use feltdb::state_model::{
|
|
10
|
+
ParentLookup, RetentionPolicy, RevisionError, StateRevision, StateStore,
|
|
11
|
+
};
|
|
12
|
+
use feltdb::{FeltDb, JsonCasResult};
|
|
13
|
+
use serde_json::json;
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
use tempfile::TempDir;
|
|
16
|
+
|
|
17
|
+
fn durable(name: &str) -> (TempDir, Arc<FeltDb>, StateStore) {
|
|
18
|
+
let directory = TempDir::new().unwrap();
|
|
19
|
+
let db = Arc::new(FeltDb::open(directory.path().join(name)).unwrap());
|
|
20
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
21
|
+
(directory, db, store)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn revision_lines(log: &str) -> usize {
|
|
25
|
+
log.lines()
|
|
26
|
+
.filter(|line| line.contains("state:revision:") && !line.contains(r#""deleted":true"#))
|
|
27
|
+
.count()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Identity
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/// Writing the same state twice to one resource records two revisions.
|
|
35
|
+
#[test]
|
|
36
|
+
fn same_content_twice_is_distinct() {
|
|
37
|
+
let (_directory, db, store) = durable("same-twice.log");
|
|
38
|
+
|
|
39
|
+
db.insert("tasks:1", json!({"title": "A"})).unwrap();
|
|
40
|
+
db.update("tasks:1", json!({"title": "A"})).unwrap();
|
|
41
|
+
|
|
42
|
+
let history = store.history_of("tasks:1");
|
|
43
|
+
assert_eq!(history.len(), 2, "two writes, two revisions");
|
|
44
|
+
assert_ne!(history[0].id, history[1].id);
|
|
45
|
+
assert_eq!(
|
|
46
|
+
history[0].content_id, history[1].content_id,
|
|
47
|
+
"and it was the same state both times"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// A revert is a new historical node, not a return to an old one.
|
|
52
|
+
#[test]
|
|
53
|
+
fn revert_creates_new_revision() {
|
|
54
|
+
let (_directory, db, store) = durable("revert.log");
|
|
55
|
+
|
|
56
|
+
db.insert("tasks:1", json!({"title": "A"})).unwrap();
|
|
57
|
+
db.update("tasks:1", json!({"title": "B"})).unwrap();
|
|
58
|
+
db.update("tasks:1", json!({"title": "A"})).unwrap();
|
|
59
|
+
|
|
60
|
+
let history = store.history_of("tasks:1");
|
|
61
|
+
assert_eq!(history.len(), 3);
|
|
62
|
+
assert_ne!(
|
|
63
|
+
history[0].id, history[2].id,
|
|
64
|
+
"the revert did not resurrect an identity"
|
|
65
|
+
);
|
|
66
|
+
assert_eq!(history[2].parent_id.as_ref(), Some(&history[1].id));
|
|
67
|
+
assert_eq!(history[1].parent_id.as_ref(), Some(&history[0].id));
|
|
68
|
+
assert_eq!(history[0].parent_id, None);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Identical content on two resources is two histories, never one object.
|
|
72
|
+
#[test]
|
|
73
|
+
fn same_content_across_resources_is_distinct() {
|
|
74
|
+
let (_directory, db, store) = durable("two-resources.log");
|
|
75
|
+
|
|
76
|
+
db.insert("tasks:1", json!({"title": "A"})).unwrap();
|
|
77
|
+
db.insert("tasks:2", json!({"title": "A"})).unwrap();
|
|
78
|
+
|
|
79
|
+
let one = store.head_of("tasks:1").unwrap();
|
|
80
|
+
let two = store.head_of("tasks:2").unwrap();
|
|
81
|
+
|
|
82
|
+
assert_ne!(one.id, two.id);
|
|
83
|
+
assert_eq!(one.content_id, two.content_id);
|
|
84
|
+
assert_eq!(one.resource, "tasks:1");
|
|
85
|
+
assert_eq!(two.resource, "tasks:2");
|
|
86
|
+
assert_eq!(store.history_of("tasks:1").len(), 1);
|
|
87
|
+
assert_eq!(store.history_of("tasks:2").len(), 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Ancestry
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/// **T6, closed.** No later mint rewrites an existing revision's ancestry.
|
|
95
|
+
#[test]
|
|
96
|
+
fn committed_parent_is_immutable() {
|
|
97
|
+
let (_directory, db, store) = durable("immutable.log");
|
|
98
|
+
|
|
99
|
+
db.insert("tasks:1", json!({"title": "A"})).unwrap();
|
|
100
|
+
db.update("tasks:1", json!({"title": "B"})).unwrap();
|
|
101
|
+
let second = store.head_of("tasks:1").unwrap();
|
|
102
|
+
let recorded_parent = second.parent_id.clone();
|
|
103
|
+
assert!(recorded_parent.is_some());
|
|
104
|
+
|
|
105
|
+
// Everything that rewrote it before: the same content again, the same
|
|
106
|
+
// content as a root, and the same content on another resource.
|
|
107
|
+
db.update("tasks:1", json!({"title": "B"})).unwrap();
|
|
108
|
+
db.insert("tasks:2", json!({"title": "B"})).unwrap();
|
|
109
|
+
assert!(store
|
|
110
|
+
.create("tasks:1", r#"{"title":"B"}"#.into(), "x".into())
|
|
111
|
+
.is_err());
|
|
112
|
+
|
|
113
|
+
assert_eq!(
|
|
114
|
+
store.get(&second.id).unwrap().parent_id,
|
|
115
|
+
recorded_parent,
|
|
116
|
+
"the committed revision kept the parent it was committed with"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// **T5, closed.** A parent must be an authoritative stored revision.
|
|
121
|
+
#[test]
|
|
122
|
+
fn foreign_parent_is_rejected() {
|
|
123
|
+
let (_directory, _db, store) = durable("foreign.log");
|
|
124
|
+
|
|
125
|
+
let real = store
|
|
126
|
+
.create("tasks:1", r#"{"title":"A"}"#.into(), "x".into())
|
|
127
|
+
.unwrap();
|
|
128
|
+
|
|
129
|
+
// A revision the caller built but never committed.
|
|
130
|
+
let fabricated = StateRevision::at(
|
|
131
|
+
"tasks:1".into(),
|
|
132
|
+
r#"{"never":"stored"}"#.into(),
|
|
133
|
+
None,
|
|
134
|
+
0,
|
|
135
|
+
"x".into(),
|
|
136
|
+
);
|
|
137
|
+
assert!(matches!(
|
|
138
|
+
store.commit(r#"{"title":"B"}"#.into(), &fabricated, "x".into()),
|
|
139
|
+
Err(RevisionError::UnknownParent(_))
|
|
140
|
+
));
|
|
141
|
+
|
|
142
|
+
// And a real revision, but of another resource.
|
|
143
|
+
assert!(matches!(
|
|
144
|
+
store.mint(
|
|
145
|
+
"tasks:2",
|
|
146
|
+
r#"{"title":"B"}"#.into(),
|
|
147
|
+
Some(&real),
|
|
148
|
+
"x".into()
|
|
149
|
+
),
|
|
150
|
+
Err(RevisionError::ForeignParent { .. })
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// The fenced path mints from the revision the fence itself committed against,
|
|
155
|
+
/// so no caller-supplied historical object can choose the ancestry.
|
|
156
|
+
#[test]
|
|
157
|
+
fn the_fenced_path_mints_from_the_current_revision() {
|
|
158
|
+
let (_directory, db, store) = durable("fenced.log");
|
|
159
|
+
|
|
160
|
+
db.insert("tasks:1", json!({"title": "A", "__version": 1}))
|
|
161
|
+
.unwrap();
|
|
162
|
+
let before = store.head_of("tasks:1").unwrap();
|
|
163
|
+
|
|
164
|
+
// A refused write mints nothing.
|
|
165
|
+
let refused = db
|
|
166
|
+
.compare_and_set_json("tasks:1", 99, None, None, false, json!({"title": "Z"}))
|
|
167
|
+
.unwrap();
|
|
168
|
+
assert!(matches!(refused, JsonCasResult::VersionConflict { .. }));
|
|
169
|
+
assert_eq!(store.history_of("tasks:1").len(), 1, "no revision minted");
|
|
170
|
+
|
|
171
|
+
// An accepted one descends from what was current.
|
|
172
|
+
db.compare_and_set_json("tasks:1", 1, None, None, false, json!({"title": "B"}))
|
|
173
|
+
.unwrap();
|
|
174
|
+
let after = store.head_of("tasks:1").unwrap();
|
|
175
|
+
assert_eq!(after.parent_id.as_ref(), Some(&before.id));
|
|
176
|
+
assert_eq!(after.sequence, before.sequence + 1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Bounded history
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/// The whole point of the policy: writing forever does not store forever.
|
|
184
|
+
#[test]
|
|
185
|
+
fn repeated_mutation_has_bounded_history() {
|
|
186
|
+
let (_directory, db, store) = durable("bounded.log");
|
|
187
|
+
store
|
|
188
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(5))
|
|
189
|
+
.unwrap();
|
|
190
|
+
|
|
191
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
192
|
+
for n in 1..200 {
|
|
193
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
assert_eq!(
|
|
197
|
+
store.history_of("tasks:1").len(),
|
|
198
|
+
5,
|
|
199
|
+
"two hundred writes, five retained revisions"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Retention never takes the state the application is using.
|
|
204
|
+
#[test]
|
|
205
|
+
fn current_revision_survives_retention() {
|
|
206
|
+
let (_directory, db, store) = durable("current.log");
|
|
207
|
+
// Even asked for zero, the current revision survives.
|
|
208
|
+
store
|
|
209
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(0))
|
|
210
|
+
.unwrap();
|
|
211
|
+
|
|
212
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
213
|
+
for n in 1..20 {
|
|
214
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let history = store.history_of("tasks:1");
|
|
218
|
+
assert_eq!(history.len(), 1, "keep_last is clamped to at least one");
|
|
219
|
+
assert!(history[0].content.contains(r#""n":19"#));
|
|
220
|
+
|
|
221
|
+
let current: serde_json::Value = db.get("tasks:1").unwrap().unwrap();
|
|
222
|
+
assert_eq!(current["n"], json!(19), "and the row itself is untouched");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// A retained revision's ancestry stays valid, and the cut is legible.
|
|
226
|
+
///
|
|
227
|
+
/// This is the hazard the retention rule was designed around: expiring an
|
|
228
|
+
/// ancestor must not silently leave a retained child pointing at nothing. The
|
|
229
|
+
/// child's `parent_id` is never rewritten — that would break immutability — so
|
|
230
|
+
/// the resource records a horizon, and the oldest survivor reports its parent
|
|
231
|
+
/// as **expired** rather than missing.
|
|
232
|
+
#[test]
|
|
233
|
+
fn parent_survives_required_retention() {
|
|
234
|
+
let (_directory, db, store) = durable("ancestry.log");
|
|
235
|
+
store
|
|
236
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(3))
|
|
237
|
+
.unwrap();
|
|
238
|
+
|
|
239
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
240
|
+
for n in 1..10 {
|
|
241
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let history = store.history_of("tasks:1");
|
|
245
|
+
assert_eq!(history.len(), 3);
|
|
246
|
+
|
|
247
|
+
// Every retained revision but the oldest resolves its parent.
|
|
248
|
+
for revision in &history[1..] {
|
|
249
|
+
assert!(
|
|
250
|
+
matches!(
|
|
251
|
+
store.parent_of(&revision.id),
|
|
252
|
+
Some(ParentLookup::Revision(_))
|
|
253
|
+
),
|
|
254
|
+
"a retained child's parent is retained"
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// The oldest names a parent that was expired on purpose, and says so.
|
|
259
|
+
assert!(matches!(
|
|
260
|
+
store.parent_of(&history[0].id),
|
|
261
|
+
Some(ParentLookup::Expired(_))
|
|
262
|
+
));
|
|
263
|
+
assert_eq!(
|
|
264
|
+
store.retention_horizon("tasks:1"),
|
|
265
|
+
history[0].sequence,
|
|
266
|
+
"the horizon is exactly where history was cut"
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Expired revisions are reclaimed, not merely hidden.
|
|
271
|
+
///
|
|
272
|
+
/// Retention removes them from state immediately. The bytes go when the log is
|
|
273
|
+
/// compacted, which is the operation that reclaims space — and this is the one
|
|
274
|
+
/// place the two mechanisms meet: **retention decides what history exists;
|
|
275
|
+
/// compaction reclaims what retention removed.** Neither does the other's job.
|
|
276
|
+
#[test]
|
|
277
|
+
fn expired_history_is_reclaimable() {
|
|
278
|
+
let directory = TempDir::new().unwrap();
|
|
279
|
+
let path = directory.path().join("reclaim.log");
|
|
280
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
281
|
+
let store = StateStore::with_feltdb(db.clone()).unwrap();
|
|
282
|
+
|
|
283
|
+
store
|
|
284
|
+
.set_retention_policy("tasks:1", RetentionPolicy::keep_last(2))
|
|
285
|
+
.unwrap();
|
|
286
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
287
|
+
for n in 1..50 {
|
|
288
|
+
db.update("tasks:1", json!({ "n": n })).unwrap();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
assert_eq!(store.history_of("tasks:1").len(), 2, "history is bounded");
|
|
292
|
+
|
|
293
|
+
let versions = db.operation_versions().unwrap();
|
|
294
|
+
db.acknowledge_peer_versions("peer-1".to_string(), versions)
|
|
295
|
+
.unwrap();
|
|
296
|
+
db.compact_operation_log(&["peer-1".to_string()]).unwrap();
|
|
297
|
+
|
|
298
|
+
let log = std::fs::read_to_string(&path).unwrap();
|
|
299
|
+
assert_eq!(
|
|
300
|
+
revision_lines(&log),
|
|
301
|
+
2,
|
|
302
|
+
"compaction wrote out only the revisions retention kept"
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// And the bound holds across a restart.
|
|
306
|
+
drop(store);
|
|
307
|
+
drop(db);
|
|
308
|
+
let reopened = Arc::new(FeltDb::open(&path).unwrap());
|
|
309
|
+
let store = StateStore::with_feltdb(reopened.clone()).unwrap();
|
|
310
|
+
assert_eq!(store.history_of("tasks:1").len(), 2);
|
|
311
|
+
let current: serde_json::Value = reopened.get("tasks:1").unwrap().unwrap();
|
|
312
|
+
assert_eq!(current["n"], json!(49), "current state survived all of it");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Minting survives a restart: the head index is derived, not durable, and a
|
|
316
|
+
/// reopened database continues the resource's history rather than restarting it.
|
|
317
|
+
#[test]
|
|
318
|
+
fn history_continues_across_a_restart() {
|
|
319
|
+
let directory = TempDir::new().unwrap();
|
|
320
|
+
let path = directory.path().join("restart.log");
|
|
321
|
+
let db = Arc::new(FeltDb::open(&path).unwrap());
|
|
322
|
+
|
|
323
|
+
db.insert("tasks:1", json!({"n": 0})).unwrap();
|
|
324
|
+
db.update("tasks:1", json!({"n": 1})).unwrap();
|
|
325
|
+
drop(db);
|
|
326
|
+
|
|
327
|
+
let reopened = Arc::new(FeltDb::open(&path).unwrap());
|
|
328
|
+
reopened.update("tasks:1", json!({"n": 2})).unwrap();
|
|
329
|
+
let store = StateStore::with_feltdb(reopened).unwrap();
|
|
330
|
+
|
|
331
|
+
let history = store.history_of("tasks:1");
|
|
332
|
+
assert_eq!(history.len(), 3, "the third write continued the history");
|
|
333
|
+
assert_eq!(
|
|
334
|
+
history[2].sequence, 2,
|
|
335
|
+
"sequence continued, did not restart"
|
|
336
|
+
);
|
|
337
|
+
assert_eq!(history[2].parent_id.as_ref(), Some(&history[1].id));
|
|
338
|
+
}
|