@feltdb/core 0.8.4 → 0.8.5
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 +507 -6
- 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_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 +1514 -537
- 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/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/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -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/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 +1032 -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/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-server/src/app_state.rs +2 -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 +326 -6
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-DaNwCLRX.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-j8IlhNqJ.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
|
@@ -173,7 +173,10 @@ fn lease_is_checked_with_the_server_clock_inside_cas() {
|
|
|
173
173
|
current_epoch: 3,
|
|
174
174
|
}
|
|
175
175
|
);
|
|
176
|
-
assert_eq!(
|
|
176
|
+
assert_eq!(
|
|
177
|
+
db.get_value(expired_key).unwrap().unwrap()["value"],
|
|
178
|
+
"before"
|
|
179
|
+
);
|
|
177
180
|
|
|
178
181
|
drop(db);
|
|
179
182
|
let _ = fs::remove_file(path);
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
/// - Node health (operations applied, crash count)
|
|
8
8
|
/// - Recovery metrics (duration, type)
|
|
9
9
|
/// - Operation status (acknowledged, committed, diverged, lost)
|
|
10
|
-
|
|
11
10
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
12
11
|
use std::sync::Arc;
|
|
13
12
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
///
|
|
6
6
|
/// This is the core Gate 1 test: does operation-specific merge
|
|
7
7
|
/// semantics guarantee convergence despite arbitrary delivery order?
|
|
8
|
-
|
|
9
8
|
use crate::operation_log::MemoryOperationLog;
|
|
10
9
|
use crate::permutation_scheduler::{OperationSchedule, PermutationScheduler};
|
|
11
10
|
use crate::replay_fuzzing::ReplayFuzzer;
|
|
@@ -201,7 +200,7 @@ impl ConvergenceAggregation {
|
|
|
201
200
|
mod tests {
|
|
202
201
|
use super::*;
|
|
203
202
|
use crate::convergence::VectorClock;
|
|
204
|
-
use crate::transactions::{
|
|
203
|
+
use crate::transactions::{OperationCommand, OperationId, StateVersion};
|
|
205
204
|
use std::collections::HashMap;
|
|
206
205
|
|
|
207
206
|
fn create_test_operation(seq: u64, collection: &str, record_id: &str, value: i32) -> Operation {
|
|
@@ -169,7 +169,10 @@ impl TransactionRecord {
|
|
|
169
169
|
|
|
170
170
|
/// Records touched by this transaction, for conflict detection.
|
|
171
171
|
pub fn touched_keys(&self) -> HashSet<(String, String)> {
|
|
172
|
-
self.operations
|
|
172
|
+
self.operations
|
|
173
|
+
.iter()
|
|
174
|
+
.map(|operation| operation.key())
|
|
175
|
+
.collect()
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
|
|
@@ -177,12 +180,18 @@ impl TransactionRecord {
|
|
|
177
180
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
178
181
|
pub enum RejectionReason {
|
|
179
182
|
/// A precondition did not hold. Carries the operation index and a reason.
|
|
180
|
-
PreconditionFailed {
|
|
183
|
+
PreconditionFailed {
|
|
184
|
+
operation_index: usize,
|
|
185
|
+
detail: String,
|
|
186
|
+
},
|
|
181
187
|
/// The transaction contained no operations.
|
|
182
188
|
Empty,
|
|
183
189
|
/// The transaction writes the same record twice, so its outcome would
|
|
184
190
|
/// depend on operation order rather than on the transaction as a whole.
|
|
185
|
-
ConflictingWritesWithinTransaction {
|
|
191
|
+
ConflictingWritesWithinTransaction {
|
|
192
|
+
collection: String,
|
|
193
|
+
record_id: String,
|
|
194
|
+
},
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
/// The outcome of applying a record to derived state.
|
|
@@ -265,7 +274,10 @@ impl AtomicTransactionLog {
|
|
|
265
274
|
/// Read the log without modifying it.
|
|
266
275
|
pub fn read_log(path: &Path) -> Result<RecoveryReport, std::io::Error> {
|
|
267
276
|
if !path.exists() {
|
|
268
|
-
return Ok(RecoveryReport {
|
|
277
|
+
return Ok(RecoveryReport {
|
|
278
|
+
committed: Vec::new(),
|
|
279
|
+
discarded_tail: None,
|
|
280
|
+
});
|
|
269
281
|
}
|
|
270
282
|
|
|
271
283
|
let reader = BufReader::new(File::open(path)?);
|
|
@@ -286,7 +298,9 @@ impl AtomicTransactionLog {
|
|
|
286
298
|
}
|
|
287
299
|
line_number += 1;
|
|
288
300
|
let terminated = buffer.ends_with(b"\n");
|
|
289
|
-
let text = String::from_utf8_lossy(&buffer)
|
|
301
|
+
let text = String::from_utf8_lossy(&buffer)
|
|
302
|
+
.trim_end_matches('\n')
|
|
303
|
+
.to_string();
|
|
290
304
|
entries.push((line_number, text, terminated));
|
|
291
305
|
}
|
|
292
306
|
|
|
@@ -311,21 +325,28 @@ impl AtomicTransactionLog {
|
|
|
311
325
|
// An unterminated, unparseable, or checksum-failing final entry
|
|
312
326
|
// is a transaction that was still being written when the
|
|
313
327
|
// process died. It never committed.
|
|
314
|
-
defect = Some(LogDefect::TornTail {
|
|
328
|
+
defect = Some(LogDefect::TornTail {
|
|
329
|
+
bytes: text.len() + usize::from(terminated),
|
|
330
|
+
});
|
|
315
331
|
} else {
|
|
316
332
|
return Err(std::io::Error::new(
|
|
317
333
|
std::io::ErrorKind::InvalidData,
|
|
318
334
|
format!(
|
|
319
335
|
"transaction log corrupted at line {}: {}",
|
|
320
336
|
number,
|
|
321
|
-
parsed
|
|
337
|
+
parsed
|
|
338
|
+
.err()
|
|
339
|
+
.map(|error| error.to_string())
|
|
322
340
|
.unwrap_or_else(|| "checksum mismatch".to_string())
|
|
323
341
|
),
|
|
324
342
|
));
|
|
325
343
|
}
|
|
326
344
|
}
|
|
327
345
|
|
|
328
|
-
Ok(RecoveryReport {
|
|
346
|
+
Ok(RecoveryReport {
|
|
347
|
+
committed,
|
|
348
|
+
discarded_tail: defect,
|
|
349
|
+
})
|
|
329
350
|
}
|
|
330
351
|
|
|
331
352
|
/// Commit a record: one write, one fsync. This is the atomic commit point.
|
|
@@ -420,7 +441,11 @@ pub struct TransactionExecutor {
|
|
|
420
441
|
|
|
421
442
|
impl TransactionExecutor {
|
|
422
443
|
pub fn new() -> Self {
|
|
423
|
-
Self {
|
|
444
|
+
Self {
|
|
445
|
+
state: CanonicalState::new(),
|
|
446
|
+
applied: HashSet::new(),
|
|
447
|
+
applied_order: Vec::new(),
|
|
448
|
+
}
|
|
424
449
|
}
|
|
425
450
|
|
|
426
451
|
pub fn state(&self) -> &CanonicalState {
|
|
@@ -455,8 +480,12 @@ impl TransactionExecutor {
|
|
|
455
480
|
}
|
|
456
481
|
|
|
457
482
|
for (index, operation) in record.operations.iter().enumerate() {
|
|
458
|
-
let Some(precondition) = &operation.precondition else {
|
|
459
|
-
|
|
483
|
+
let Some(precondition) = &operation.precondition else {
|
|
484
|
+
continue;
|
|
485
|
+
};
|
|
486
|
+
let current = self
|
|
487
|
+
.state
|
|
488
|
+
.get_record(&operation.collection, &operation.record_id);
|
|
460
489
|
let failure = match (precondition, current) {
|
|
461
490
|
(Precondition::MustExist, None) => Some("record does not exist".to_string()),
|
|
462
491
|
(Precondition::MustNotExist, Some(_)) => Some("record already exists".to_string()),
|
|
@@ -467,7 +496,10 @@ impl TransactionExecutor {
|
|
|
467
496
|
_ => None,
|
|
468
497
|
};
|
|
469
498
|
if let Some(detail) = failure {
|
|
470
|
-
return Err(RejectionReason::PreconditionFailed {
|
|
499
|
+
return Err(RejectionReason::PreconditionFailed {
|
|
500
|
+
operation_index: index,
|
|
501
|
+
detail,
|
|
502
|
+
});
|
|
471
503
|
}
|
|
472
504
|
}
|
|
473
505
|
|
|
@@ -486,8 +518,11 @@ impl TransactionExecutor {
|
|
|
486
518
|
// Preflight succeeded, so every mutation below is known to be legal.
|
|
487
519
|
// Staging them first keeps the mutation phase free of any decision that
|
|
488
520
|
// could fail half-way through.
|
|
489
|
-
let staged: Vec<(&TransactionOperation, ())> =
|
|
490
|
-
|
|
521
|
+
let staged: Vec<(&TransactionOperation, ())> = record
|
|
522
|
+
.operations
|
|
523
|
+
.iter()
|
|
524
|
+
.map(|operation| (operation, ()))
|
|
525
|
+
.collect();
|
|
491
526
|
for (operation, ()) in staged {
|
|
492
527
|
match &operation.op {
|
|
493
528
|
TransactionOp::Set(value) => self.state.set_record(
|
|
@@ -526,11 +561,14 @@ impl TransactionExecutor {
|
|
|
526
561
|
.iter()
|
|
527
562
|
.filter(|operation| match &operation.op {
|
|
528
563
|
TransactionOp::Set(value) => {
|
|
529
|
-
self.state
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
self.state.get_record(&operation.collection, &operation.record_id).is_none()
|
|
564
|
+
self.state
|
|
565
|
+
.get_record(&operation.collection, &operation.record_id)
|
|
566
|
+
== Some(value)
|
|
533
567
|
}
|
|
568
|
+
TransactionOp::Delete => self
|
|
569
|
+
.state
|
|
570
|
+
.get_record(&operation.collection, &operation.record_id)
|
|
571
|
+
.is_none(),
|
|
534
572
|
})
|
|
535
573
|
.count()
|
|
536
574
|
}
|
|
@@ -554,12 +592,29 @@ pub struct TransactionalNode {
|
|
|
554
592
|
|
|
555
593
|
impl TransactionalNode {
|
|
556
594
|
/// Start a node, recovering derived state from the durable log.
|
|
557
|
-
pub fn open<P: AsRef<Path>>(
|
|
595
|
+
pub fn open<P: AsRef<Path>>(
|
|
596
|
+
path: P,
|
|
597
|
+
node_id: &str,
|
|
598
|
+
) -> Result<(Self, RecoveryReport), std::io::Error> {
|
|
558
599
|
let (log, report) = AtomicTransactionLog::open(path)?;
|
|
559
600
|
let mut executor = TransactionExecutor::new();
|
|
560
601
|
executor.recover(&report.committed);
|
|
561
|
-
let next_sequence = report
|
|
562
|
-
|
|
602
|
+
let next_sequence = report
|
|
603
|
+
.committed
|
|
604
|
+
.iter()
|
|
605
|
+
.map(|record| record.sequence)
|
|
606
|
+
.max()
|
|
607
|
+
.unwrap_or(0)
|
|
608
|
+
+ 1;
|
|
609
|
+
Ok((
|
|
610
|
+
Self {
|
|
611
|
+
log,
|
|
612
|
+
executor,
|
|
613
|
+
node_id: node_id.to_string(),
|
|
614
|
+
next_sequence,
|
|
615
|
+
},
|
|
616
|
+
report,
|
|
617
|
+
))
|
|
563
618
|
}
|
|
564
619
|
|
|
565
620
|
pub fn state(&self) -> &CanonicalState {
|
|
@@ -574,8 +629,17 @@ impl TransactionalNode {
|
|
|
574
629
|
&mut self.log
|
|
575
630
|
}
|
|
576
631
|
|
|
577
|
-
pub fn build(
|
|
578
|
-
|
|
632
|
+
pub fn build(
|
|
633
|
+
&mut self,
|
|
634
|
+
transaction_id: &str,
|
|
635
|
+
operations: Vec<TransactionOperation>,
|
|
636
|
+
) -> TransactionRecord {
|
|
637
|
+
let record = TransactionRecord::new(
|
|
638
|
+
transaction_id,
|
|
639
|
+
&self.node_id,
|
|
640
|
+
self.next_sequence,
|
|
641
|
+
operations,
|
|
642
|
+
);
|
|
579
643
|
self.next_sequence += 1;
|
|
580
644
|
record
|
|
581
645
|
}
|
|
@@ -585,7 +649,11 @@ impl TransactionalNode {
|
|
|
585
649
|
/// A rejected transaction is never written to the log, so a failed
|
|
586
650
|
/// precondition leaves no trace in durable state.
|
|
587
651
|
pub fn commit(&mut self, record: &TransactionRecord) -> Result<ApplyOutcome, std::io::Error> {
|
|
588
|
-
if self
|
|
652
|
+
if self
|
|
653
|
+
.executor
|
|
654
|
+
.applied_transactions()
|
|
655
|
+
.contains(&record.transaction_id)
|
|
656
|
+
{
|
|
589
657
|
return Ok(ApplyOutcome::AlreadyApplied);
|
|
590
658
|
}
|
|
591
659
|
if let Err(reason) = self.executor.preflight(record) {
|
|
@@ -640,14 +708,23 @@ impl TransactionalNode {
|
|
|
640
708
|
/// Applying a replicated transaction is the same all-or-nothing path, and
|
|
641
709
|
/// a transaction already applied here is ignored, so duplicate delivery is
|
|
642
710
|
/// safe.
|
|
643
|
-
pub fn accept_replicated(
|
|
711
|
+
pub fn accept_replicated(
|
|
712
|
+
&mut self,
|
|
713
|
+
record: &TransactionRecord,
|
|
714
|
+
) -> Result<ApplyOutcome, std::io::Error> {
|
|
644
715
|
if !record.verify_checksum() {
|
|
645
|
-
return Ok(ApplyOutcome::Rejected(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
716
|
+
return Ok(ApplyOutcome::Rejected(
|
|
717
|
+
RejectionReason::PreconditionFailed {
|
|
718
|
+
operation_index: 0,
|
|
719
|
+
detail: "replicated record failed its integrity check".to_string(),
|
|
720
|
+
},
|
|
721
|
+
));
|
|
649
722
|
}
|
|
650
|
-
if self
|
|
723
|
+
if self
|
|
724
|
+
.executor
|
|
725
|
+
.applied_transactions()
|
|
726
|
+
.contains(&record.transaction_id)
|
|
727
|
+
{
|
|
651
728
|
return Ok(ApplyOutcome::AlreadyApplied);
|
|
652
729
|
}
|
|
653
730
|
if let Err(reason) = self.executor.preflight(record) {
|
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
/// - Rejected operations tracking
|
|
8
8
|
/// - Index health metrics
|
|
9
9
|
/// - Recovery state visibility
|
|
10
|
-
|
|
11
10
|
use serde::{Deserialize, Serialize};
|
|
12
11
|
use std::collections::HashMap;
|
|
13
|
-
use std::sync::Arc;
|
|
14
12
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
13
|
+
use std::sync::Arc;
|
|
15
14
|
|
|
16
15
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
17
16
|
pub struct ReplicationLagMetric {
|
|
@@ -102,7 +101,13 @@ impl ObservabilityManager {
|
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
|
|
105
|
-
pub fn record_replication_lag(
|
|
104
|
+
pub fn record_replication_lag(
|
|
105
|
+
&mut self,
|
|
106
|
+
source: String,
|
|
107
|
+
target: String,
|
|
108
|
+
lag_ms: u64,
|
|
109
|
+
pending: u64,
|
|
110
|
+
) {
|
|
106
111
|
let now = std::time::SystemTime::now()
|
|
107
112
|
.duration_since(std::time::UNIX_EPOCH)
|
|
108
113
|
.unwrap_or_default()
|
|
@@ -250,7 +255,8 @@ impl ObservabilityManager {
|
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
pub fn record_operation_throughput(&self, ops_per_sec: u64) {
|
|
253
|
-
self.operation_throughput
|
|
258
|
+
self.operation_throughput
|
|
259
|
+
.store(ops_per_sec, Ordering::Relaxed);
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
pub fn get_health_report(&self) -> HealthReport {
|
|
@@ -259,7 +265,8 @@ impl ObservabilityManager {
|
|
|
259
265
|
.unwrap_or_default()
|
|
260
266
|
.as_secs();
|
|
261
267
|
|
|
262
|
-
let overall_status = if self
|
|
268
|
+
let overall_status = if self
|
|
269
|
+
.convergence_status
|
|
263
270
|
.values()
|
|
264
271
|
.all(|c| c.diverged_peers.is_empty())
|
|
265
272
|
{
|
|
@@ -273,7 +280,13 @@ impl ObservabilityManager {
|
|
|
273
280
|
overall_status: overall_status.to_string(),
|
|
274
281
|
replication_lag: self.replication_lags.values().cloned().collect(),
|
|
275
282
|
convergence: self.convergence_status.values().cloned().collect(),
|
|
276
|
-
rejected_operations: self
|
|
283
|
+
rejected_operations: self
|
|
284
|
+
.rejected_operations
|
|
285
|
+
.iter()
|
|
286
|
+
.rev()
|
|
287
|
+
.take(100)
|
|
288
|
+
.cloned()
|
|
289
|
+
.collect(),
|
|
277
290
|
index_health: self.index_health.values().cloned().collect(),
|
|
278
291
|
recovery_states: self.recovery_states.values().cloned().collect(),
|
|
279
292
|
query_latency_p99: self.query_latency_p99.load(Ordering::Relaxed),
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
/// to declare how it converges under concurrent application.
|
|
6
6
|
///
|
|
7
7
|
/// This registry is the foundation of operation-specific CRDT semantics.
|
|
8
|
-
|
|
9
8
|
use std::collections::HashMap;
|
|
10
9
|
|
|
11
10
|
/// Conflict classification for an operation
|
|
@@ -40,13 +39,13 @@ pub enum ConvergenceSemantic {
|
|
|
40
39
|
#[derive(Clone, Debug)]
|
|
41
40
|
pub struct OperationAlgebra {
|
|
42
41
|
pub operation_type: String,
|
|
43
|
-
pub read_set_bound: String,
|
|
44
|
-
pub write_set_bound: String,
|
|
42
|
+
pub read_set_bound: String, // "single_record", "multiple_records", etc
|
|
43
|
+
pub write_set_bound: String, // "single_record", "multiple_records", etc
|
|
45
44
|
pub conflict_type: ConflictType,
|
|
46
45
|
pub convergence_semantic: ConvergenceSemantic,
|
|
47
46
|
pub merge_function_description: String,
|
|
48
|
-
pub commutativity_proof: Option<String>,
|
|
49
|
-
pub determinism_proof: Option<String>,
|
|
47
|
+
pub commutativity_proof: Option<String>, // Reference to commutative algebra proof
|
|
48
|
+
pub determinism_proof: Option<String>, // Reference to determinism proof
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
impl OperationAlgebra {
|
|
@@ -70,7 +69,11 @@ impl OperationAlgebra {
|
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
pub fn with_proofs(
|
|
72
|
+
pub fn with_proofs(
|
|
73
|
+
mut self,
|
|
74
|
+
commutativity: Option<String>,
|
|
75
|
+
determinism: Option<String>,
|
|
76
|
+
) -> Self {
|
|
74
77
|
self.commutativity_proof = commutativity;
|
|
75
78
|
self.determinism_proof = determinism;
|
|
76
79
|
self
|
|
@@ -249,7 +252,8 @@ impl OperationAlgebraRegistry {
|
|
|
249
252
|
|
|
250
253
|
/// Register an operation algebra
|
|
251
254
|
pub fn register(&mut self, algebra: OperationAlgebra) {
|
|
252
|
-
self.algebras
|
|
255
|
+
self.algebras
|
|
256
|
+
.insert(algebra.operation_type.clone(), algebra);
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
/// Look up algebra for operation type
|
|
@@ -390,10 +394,7 @@ mod tests {
|
|
|
390
394
|
ConvergenceSemantic::StrictlyCommutative,
|
|
391
395
|
"test merge",
|
|
392
396
|
)
|
|
393
|
-
.with_proofs(
|
|
394
|
-
Some("proof1".to_string()),
|
|
395
|
-
Some("proof2".to_string()),
|
|
396
|
-
);
|
|
397
|
+
.with_proofs(Some("proof1".to_string()), Some("proof2".to_string()));
|
|
397
398
|
|
|
398
399
|
assert!(algebra.commutativity_proof.is_some());
|
|
399
400
|
assert!(algebra.determinism_proof.is_some());
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
/// Append-only log storing all executed operations for durability, replay, and recovery.
|
|
4
4
|
/// Operations are immutable once appended; the log is the authoritative source for
|
|
5
5
|
/// what has been executed in the system.
|
|
6
|
-
|
|
7
|
-
use crate::transactions::{OperationId, Operation, StateVersion};
|
|
6
|
+
use crate::transactions::{Operation, OperationId, StateVersion};
|
|
8
7
|
use std::collections::HashMap;
|
|
9
8
|
use std::sync::{Arc, Mutex};
|
|
10
9
|
|
|
@@ -39,7 +38,10 @@ impl MemoryOperationLog {
|
|
|
39
38
|
let mut entries = self.entries.lock().unwrap();
|
|
40
39
|
let mut by_id = self.by_id.lock().unwrap();
|
|
41
40
|
|
|
42
|
-
let op_id_key = format!(
|
|
41
|
+
let op_id_key = format!(
|
|
42
|
+
"{}:{}",
|
|
43
|
+
operation.operation_id.node_id, operation.operation_id.sequence
|
|
44
|
+
);
|
|
43
45
|
|
|
44
46
|
if by_id.contains_key(&op_id_key) {
|
|
45
47
|
return Err(format!("Operation already logged: {}", op_id_key));
|
|
@@ -125,7 +127,10 @@ impl MemoryOperationLog {
|
|
|
125
127
|
|
|
126
128
|
let removed: Vec<_> = entries.drain(truncate_at as usize..).collect();
|
|
127
129
|
for entry in removed {
|
|
128
|
-
let op_id_key = format!(
|
|
130
|
+
let op_id_key = format!(
|
|
131
|
+
"{}:{}",
|
|
132
|
+
entry.operation_id.node_id, entry.operation_id.sequence
|
|
133
|
+
);
|
|
129
134
|
by_id.remove(&op_id_key);
|
|
130
135
|
}
|
|
131
136
|
|
|
@@ -25,8 +25,16 @@ mod tests {
|
|
|
25
25
|
fn user_order_audit() -> Vec<AtomicMutation> {
|
|
26
26
|
vec![
|
|
27
27
|
mutation("users", "users:u1", json!({ "id": "u1", "name": "Ada" })),
|
|
28
|
-
mutation(
|
|
29
|
-
|
|
28
|
+
mutation(
|
|
29
|
+
"orders",
|
|
30
|
+
"orders:o1",
|
|
31
|
+
json!({ "id": "o1", "user": "u1", "total": 42 }),
|
|
32
|
+
),
|
|
33
|
+
mutation(
|
|
34
|
+
"audit",
|
|
35
|
+
"audit:a1",
|
|
36
|
+
json!({ "id": "a1", "event": "order.created" }),
|
|
37
|
+
),
|
|
30
38
|
]
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -42,7 +50,8 @@ mod tests {
|
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
#[test]
|
|
45
|
-
fn a_cross_collection_transaction_commits_every_operation(
|
|
53
|
+
fn a_cross_collection_transaction_commits_every_operation(
|
|
54
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
46
55
|
let dir = TempDir::new()?;
|
|
47
56
|
let path = dir.path().join("app.log");
|
|
48
57
|
let db = FeltDb::open(&path)?;
|
|
@@ -65,12 +74,17 @@ mod tests {
|
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
let reopened = FeltDb::open(&path)?;
|
|
68
|
-
assert_eq!(
|
|
77
|
+
assert_eq!(
|
|
78
|
+
visible_count(&reopened),
|
|
79
|
+
3,
|
|
80
|
+
"every row must recover together"
|
|
81
|
+
);
|
|
69
82
|
Ok(())
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
#[test]
|
|
73
|
-
fn a_failed_precondition_leaves_no_operation_applied() -> Result<(), Box<dyn std::error::Error>>
|
|
86
|
+
fn a_failed_precondition_leaves_no_operation_applied() -> Result<(), Box<dyn std::error::Error>>
|
|
87
|
+
{
|
|
74
88
|
let dir = TempDir::new()?;
|
|
75
89
|
let path = dir.path().join("app.log");
|
|
76
90
|
let db = FeltDb::open(&path)?;
|
|
@@ -82,8 +96,12 @@ mod tests {
|
|
|
82
96
|
expected_version: Some(7),
|
|
83
97
|
}];
|
|
84
98
|
|
|
85
|
-
let outcome =
|
|
86
|
-
|
|
99
|
+
let outcome =
|
|
100
|
+
db.apply_atomic_transaction("tx-bad", None, &preconditions, &user_order_audit(), None);
|
|
101
|
+
assert!(
|
|
102
|
+
outcome.is_err(),
|
|
103
|
+
"a failed precondition must reject the transaction"
|
|
104
|
+
);
|
|
87
105
|
|
|
88
106
|
assert_eq!(visible_count(&db), 0, "no operation may be applied");
|
|
89
107
|
drop(db);
|
|
@@ -102,7 +120,10 @@ mod tests {
|
|
|
102
120
|
let first = db.apply_atomic_transaction("tx-1", None, &[], &user_order_audit(), None)?;
|
|
103
121
|
assert!(!first.duplicate);
|
|
104
122
|
let second = db.apply_atomic_transaction("tx-1", None, &[], &user_order_audit(), None)?;
|
|
105
|
-
assert!(
|
|
123
|
+
assert!(
|
|
124
|
+
second.duplicate,
|
|
125
|
+
"the same transaction id must not apply twice"
|
|
126
|
+
);
|
|
106
127
|
assert_eq!(visible_count(&db), 3);
|
|
107
128
|
Ok(())
|
|
108
129
|
}
|
|
@@ -118,8 +139,12 @@ mod tests {
|
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
let reopened = FeltDb::open(&path)?;
|
|
121
|
-
let replayed =
|
|
122
|
-
|
|
142
|
+
let replayed =
|
|
143
|
+
reopened.apply_atomic_transaction("tx-stable", None, &[], &user_order_audit(), None)?;
|
|
144
|
+
assert!(
|
|
145
|
+
replayed.duplicate,
|
|
146
|
+
"identity must be restored so redelivery stays idempotent"
|
|
147
|
+
);
|
|
123
148
|
assert_eq!(visible_count(&reopened), 3);
|
|
124
149
|
Ok(())
|
|
125
150
|
}
|
|
@@ -132,8 +157,8 @@ mod tests {
|
|
|
132
157
|
/// intact. Refusing to open at all would turn a normal crash into an
|
|
133
158
|
/// outage.
|
|
134
159
|
#[test]
|
|
135
|
-
fn a_torn_transaction_record_is_discarded_and_the_store_still_opens(
|
|
136
|
-
-> Result<(), Box<dyn std::error::Error>> {
|
|
160
|
+
fn a_torn_transaction_record_is_discarded_and_the_store_still_opens(
|
|
161
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
137
162
|
let dir = TempDir::new()?;
|
|
138
163
|
let path = dir.path().join("app.log");
|
|
139
164
|
|
|
@@ -154,7 +179,11 @@ mod tests {
|
|
|
154
179
|
let recovered = FeltDb::open(&path).map_err(|error| {
|
|
155
180
|
format!("a torn tail must not stop the store from opening: {error}")
|
|
156
181
|
})?;
|
|
157
|
-
assert_eq!(
|
|
182
|
+
assert_eq!(
|
|
183
|
+
visible_count(&recovered),
|
|
184
|
+
3,
|
|
185
|
+
"committed history must survive"
|
|
186
|
+
);
|
|
158
187
|
Ok(())
|
|
159
188
|
}
|
|
160
189
|
|
|
@@ -178,7 +207,11 @@ mod tests {
|
|
|
178
207
|
"tx-2",
|
|
179
208
|
None,
|
|
180
209
|
&[],
|
|
181
|
-
&[mutation(
|
|
210
|
+
&[mutation(
|
|
211
|
+
"users",
|
|
212
|
+
"users:u2",
|
|
213
|
+
json!({ "id": "u2", "name": "Grace" }),
|
|
214
|
+
)],
|
|
182
215
|
None,
|
|
183
216
|
)?;
|
|
184
217
|
}
|
|
@@ -193,8 +226,15 @@ mod tests {
|
|
|
193
226
|
drop(file);
|
|
194
227
|
|
|
195
228
|
let recovered = FeltDb::open(&path)?;
|
|
196
|
-
assert_eq!(
|
|
197
|
-
|
|
229
|
+
assert_eq!(
|
|
230
|
+
visible_count(&recovered),
|
|
231
|
+
3,
|
|
232
|
+
"the committed transaction survives"
|
|
233
|
+
);
|
|
234
|
+
assert!(
|
|
235
|
+
!present(&recovered, "users:u2"),
|
|
236
|
+
"a torn transaction must apply nothing"
|
|
237
|
+
);
|
|
198
238
|
Ok(())
|
|
199
239
|
}
|
|
200
240
|
|
|
@@ -245,9 +285,16 @@ mod tests {
|
|
|
245
285
|
}
|
|
246
286
|
|
|
247
287
|
let reopened = FeltDb::open(&path)?;
|
|
248
|
-
assert!(
|
|
288
|
+
assert!(
|
|
289
|
+
present(&reopened, "users:solo"),
|
|
290
|
+
"single writes must still recover"
|
|
291
|
+
);
|
|
249
292
|
assert!(present(&reopened, "users:solo2"));
|
|
250
|
-
assert_eq!(
|
|
293
|
+
assert_eq!(
|
|
294
|
+
visible_count(&reopened),
|
|
295
|
+
3,
|
|
296
|
+
"transaction rows recover alongside them"
|
|
297
|
+
);
|
|
251
298
|
Ok(())
|
|
252
299
|
}
|
|
253
300
|
|