@feltdb/core 0.4.11 → 0.4.13
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/cli/commands.js +5 -2
- package/dist/cli/index.js +1 -1
- package/dist/create/cli.js +16 -1
- package/dist/create/create.js +30 -16
- package/dist/create/docker-compose-generator.js +48 -9
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +2238 -0
- package/dist/create/server-source/Cargo.toml +7 -0
- package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
- package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
- package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
- package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
- package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
- package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
- package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
- package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
- package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
- package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
- package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
- package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
- package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
- package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
- package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
- package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
- package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
- package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
- package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
- package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
- package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
- package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
- package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
- package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
- package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
- package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
- package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
- package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
- package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
- package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
- package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
- package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/flowspec.d.ts +2 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +28 -4
- package/dist/state-contract.d.ts +7 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-q43NSTTH.js → components-bHTARBin.js} +160 -89
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-DZpBoVMp.js → index-BqKquLuU.js} +8 -8
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/// Durable Operation Log
|
|
2
|
+
///
|
|
3
|
+
/// Provides persistent storage for operation envelopes with fsync durability.
|
|
4
|
+
/// The log is the source of truth for recovery; replica state and dedup sets are derived from it.
|
|
5
|
+
///
|
|
6
|
+
/// Invariant: Every successfully persisted operation can be recovered after process restart.
|
|
7
|
+
|
|
8
|
+
use crate::distributed_transactions::TransactionEnvelope;
|
|
9
|
+
use std::fs::{File, OpenOptions};
|
|
10
|
+
use std::io::{BufRead, BufReader, Write};
|
|
11
|
+
use std::path::{Path, PathBuf};
|
|
12
|
+
|
|
13
|
+
/// Durable operation log with fsync semantics
|
|
14
|
+
pub struct OperationLog {
|
|
15
|
+
path: PathBuf,
|
|
16
|
+
file: Option<File>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl OperationLog {
|
|
20
|
+
/// Create or open a durable log at the given path
|
|
21
|
+
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, std::io::Error> {
|
|
22
|
+
let path = path.as_ref().to_path_buf();
|
|
23
|
+
|
|
24
|
+
// Create parent directory if needed
|
|
25
|
+
if let Some(parent) = path.parent() {
|
|
26
|
+
std::fs::create_dir_all(parent)?;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Open log file in append mode
|
|
30
|
+
let file = OpenOptions::new()
|
|
31
|
+
.create(true)
|
|
32
|
+
.append(true)
|
|
33
|
+
.open(&path)?;
|
|
34
|
+
|
|
35
|
+
Ok(Self {
|
|
36
|
+
path,
|
|
37
|
+
file: Some(file),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Append and durably persist an operation envelope
|
|
42
|
+
/// Returns error if write or fsync fails
|
|
43
|
+
pub fn append(&mut self, envelope: &TransactionEnvelope) -> Result<(), std::io::Error> {
|
|
44
|
+
if let Some(ref mut file) = self.file {
|
|
45
|
+
// Serialize envelope to JSON line
|
|
46
|
+
let json = serde_json::to_string(envelope)
|
|
47
|
+
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
|
48
|
+
|
|
49
|
+
// Write JSON line (newline-delimited)
|
|
50
|
+
writeln!(file, "{}", json)?;
|
|
51
|
+
|
|
52
|
+
// Force durable persistence to disk
|
|
53
|
+
file.sync_all()?;
|
|
54
|
+
|
|
55
|
+
Ok(())
|
|
56
|
+
} else {
|
|
57
|
+
Err(std::io::Error::new(
|
|
58
|
+
std::io::ErrorKind::NotConnected,
|
|
59
|
+
"Operation log file not open",
|
|
60
|
+
))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Load all persisted envelopes from log
|
|
65
|
+
/// Returns envelopes in order they were persisted
|
|
66
|
+
pub fn load_all(&self) -> Result<Vec<TransactionEnvelope>, std::io::Error> {
|
|
67
|
+
if !self.path.exists() {
|
|
68
|
+
return Ok(Vec::new());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let file = File::open(&self.path)?;
|
|
72
|
+
let reader = BufReader::new(file);
|
|
73
|
+
let mut envelopes = Vec::new();
|
|
74
|
+
|
|
75
|
+
for line in reader.lines() {
|
|
76
|
+
let line = line?;
|
|
77
|
+
if line.trim().is_empty() {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
match serde_json::from_str::<TransactionEnvelope>(&line) {
|
|
82
|
+
Ok(envelope) => envelopes.push(envelope),
|
|
83
|
+
Err(e) => {
|
|
84
|
+
return Err(std::io::Error::new(
|
|
85
|
+
std::io::ErrorKind::InvalidData,
|
|
86
|
+
format!("Failed to deserialize envelope: {}", e),
|
|
87
|
+
))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Ok(envelopes)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Get the number of persisted operations
|
|
96
|
+
pub fn count(&self) -> Result<usize, std::io::Error> {
|
|
97
|
+
let envelopes = self.load_all()?;
|
|
98
|
+
Ok(envelopes.len())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Clear the log (used for testing)
|
|
102
|
+
pub fn clear(&mut self) -> Result<(), std::io::Error> {
|
|
103
|
+
if let Some(file) = self.file.take() {
|
|
104
|
+
drop(file);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
std::fs::remove_file(&self.path)?;
|
|
108
|
+
|
|
109
|
+
let file = OpenOptions::new()
|
|
110
|
+
.create(true)
|
|
111
|
+
.append(true)
|
|
112
|
+
.open(&self.path)?;
|
|
113
|
+
|
|
114
|
+
self.file = Some(file);
|
|
115
|
+
Ok(())
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[cfg(test)]
|
|
120
|
+
mod tests {
|
|
121
|
+
use super::*;
|
|
122
|
+
use crate::transactions::{ConsistencyLevel, ConsistencyContract};
|
|
123
|
+
use tempfile::TempDir;
|
|
124
|
+
|
|
125
|
+
fn create_test_envelope(sequence: u64) -> TransactionEnvelope {
|
|
126
|
+
TransactionEnvelope::new(
|
|
127
|
+
"node1".to_string(),
|
|
128
|
+
sequence,
|
|
129
|
+
format!("tx{}", sequence),
|
|
130
|
+
Vec::new(), // Empty operations for test
|
|
131
|
+
ConsistencyContract {
|
|
132
|
+
level: ConsistencyLevel::Causal,
|
|
133
|
+
timeout_ms: Some(1000),
|
|
134
|
+
required_acks: None,
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn test_operation_log_append() -> Result<(), Box<dyn std::error::Error>> {
|
|
141
|
+
let temp_dir = TempDir::new()?;
|
|
142
|
+
let log_path = temp_dir.path().join("test.log");
|
|
143
|
+
|
|
144
|
+
let mut log = OperationLog::new(&log_path)?;
|
|
145
|
+
let envelope = create_test_envelope(1);
|
|
146
|
+
|
|
147
|
+
log.append(&envelope)?;
|
|
148
|
+
|
|
149
|
+
assert!(log_path.exists());
|
|
150
|
+
Ok(())
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#[test]
|
|
154
|
+
fn test_operation_log_load() -> Result<(), Box<dyn std::error::Error>> {
|
|
155
|
+
let temp_dir = TempDir::new()?;
|
|
156
|
+
let log_path = temp_dir.path().join("test.log");
|
|
157
|
+
|
|
158
|
+
let mut log = OperationLog::new(&log_path)?;
|
|
159
|
+
let envelope1 = create_test_envelope(1);
|
|
160
|
+
let envelope2 = create_test_envelope(2);
|
|
161
|
+
|
|
162
|
+
log.append(&envelope1)?;
|
|
163
|
+
log.append(&envelope2)?;
|
|
164
|
+
|
|
165
|
+
let loaded = log.load_all()?;
|
|
166
|
+
assert_eq!(loaded.len(), 2);
|
|
167
|
+
assert_eq!(loaded[0].envelope_id.sequence, 1);
|
|
168
|
+
assert_eq!(loaded[1].envelope_id.sequence, 2);
|
|
169
|
+
|
|
170
|
+
Ok(())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn test_operation_log_persistence() -> Result<(), Box<dyn std::error::Error>> {
|
|
175
|
+
let temp_dir = TempDir::new()?;
|
|
176
|
+
let log_path = temp_dir.path().join("test.log");
|
|
177
|
+
|
|
178
|
+
// Write to log
|
|
179
|
+
{
|
|
180
|
+
let mut log = OperationLog::new(&log_path)?;
|
|
181
|
+
let envelope = create_test_envelope(1);
|
|
182
|
+
log.append(&envelope)?;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Reload from disk
|
|
186
|
+
{
|
|
187
|
+
let log = OperationLog::new(&log_path)?;
|
|
188
|
+
let loaded = log.load_all()?;
|
|
189
|
+
assert_eq!(loaded.len(), 1);
|
|
190
|
+
assert_eq!(loaded[0].envelope_id.sequence, 1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Ok(())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn test_operation_log_empty() -> Result<(), Box<dyn std::error::Error>> {
|
|
198
|
+
let temp_dir = TempDir::new()?;
|
|
199
|
+
let log_path = temp_dir.path().join("nonexistent.log");
|
|
200
|
+
|
|
201
|
+
let log = OperationLog::new(&log_path)?;
|
|
202
|
+
let loaded = log.load_all()?;
|
|
203
|
+
assert_eq!(loaded.len(), 0);
|
|
204
|
+
|
|
205
|
+
Ok(())
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/// Durable Synchronization Primitives
|
|
2
|
+
///
|
|
3
|
+
/// Core synchronization guarantees:
|
|
4
|
+
/// - Change/delta ingestion with sequence tracking
|
|
5
|
+
/// - Idempotent application of operations
|
|
6
|
+
/// - Ordering guarantees via sequence numbers
|
|
7
|
+
/// - Checkpoints and high-water marks for recovery
|
|
8
|
+
/// - Replay and recovery mechanisms
|
|
9
|
+
/// - Backpressure handling for flow control
|
|
10
|
+
|
|
11
|
+
use serde::{Deserialize, Serialize};
|
|
12
|
+
use std::collections::HashMap;
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
|
15
|
+
pub struct ChangeId {
|
|
16
|
+
pub node_id: String,
|
|
17
|
+
pub sequence: u64,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl ChangeId {
|
|
21
|
+
pub fn new(node_id: String, sequence: u64) -> Self {
|
|
22
|
+
ChangeId { node_id, sequence }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
27
|
+
pub enum ChangeType {
|
|
28
|
+
Insert,
|
|
29
|
+
Update,
|
|
30
|
+
Delete,
|
|
31
|
+
Upsert,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
35
|
+
pub struct DurableChange {
|
|
36
|
+
pub change_id: ChangeId,
|
|
37
|
+
pub change_type: ChangeType,
|
|
38
|
+
pub resource_id: String,
|
|
39
|
+
pub data: String,
|
|
40
|
+
pub timestamp: u64,
|
|
41
|
+
pub idempotency_key: String,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl DurableChange {
|
|
45
|
+
pub fn new(
|
|
46
|
+
node_id: String,
|
|
47
|
+
sequence: u64,
|
|
48
|
+
change_type: ChangeType,
|
|
49
|
+
resource_id: String,
|
|
50
|
+
data: String,
|
|
51
|
+
idempotency_key: String,
|
|
52
|
+
) -> Self {
|
|
53
|
+
DurableChange {
|
|
54
|
+
change_id: ChangeId::new(node_id, sequence),
|
|
55
|
+
change_type,
|
|
56
|
+
resource_id,
|
|
57
|
+
data,
|
|
58
|
+
timestamp: std::time::SystemTime::now()
|
|
59
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
60
|
+
.unwrap_or_default()
|
|
61
|
+
.as_secs(),
|
|
62
|
+
idempotency_key,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
68
|
+
pub struct Checkpoint {
|
|
69
|
+
pub checkpoint_id: String,
|
|
70
|
+
pub node_id: String,
|
|
71
|
+
pub high_water_mark: u64,
|
|
72
|
+
pub applied_changes: Vec<String>,
|
|
73
|
+
pub timestamp: u64,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
77
|
+
pub struct SyncState {
|
|
78
|
+
pub node_id: String,
|
|
79
|
+
pub last_applied_sequence: u64,
|
|
80
|
+
pub pending_changes: Vec<DurableChange>,
|
|
81
|
+
pub checkpoints: Vec<Checkpoint>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
85
|
+
pub enum BackpressureAction {
|
|
86
|
+
Accept,
|
|
87
|
+
Throttle { delay_ms: u64 },
|
|
88
|
+
Reject,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub struct DurableSyncManager {
|
|
92
|
+
node_id: String,
|
|
93
|
+
sync_state: SyncState,
|
|
94
|
+
applied_ids: std::collections::HashSet<String>,
|
|
95
|
+
backpressure_threshold: usize,
|
|
96
|
+
max_pending_changes: usize,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
impl DurableSyncManager {
|
|
100
|
+
pub fn new(node_id: String) -> Self {
|
|
101
|
+
DurableSyncManager {
|
|
102
|
+
node_id: node_id.clone(),
|
|
103
|
+
sync_state: SyncState {
|
|
104
|
+
node_id,
|
|
105
|
+
last_applied_sequence: 0,
|
|
106
|
+
pending_changes: Vec::new(),
|
|
107
|
+
checkpoints: Vec::new(),
|
|
108
|
+
},
|
|
109
|
+
applied_ids: std::collections::HashSet::new(),
|
|
110
|
+
backpressure_threshold: 10000,
|
|
111
|
+
max_pending_changes: 50000,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pub fn ingest_change(&mut self, change: DurableChange) -> Result<(), String> {
|
|
116
|
+
if self.sync_state.pending_changes.len() >= self.max_pending_changes {
|
|
117
|
+
return Err("Max pending changes exceeded".to_string());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
self.sync_state.pending_changes.push(change);
|
|
121
|
+
self.sync_state.pending_changes.sort_by_key(|c| c.change_id.sequence);
|
|
122
|
+
|
|
123
|
+
Ok(())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pub fn apply_changes_idempotent(&mut self) -> Result<Vec<DurableChange>, String> {
|
|
127
|
+
let mut applied = Vec::new();
|
|
128
|
+
|
|
129
|
+
while let Some(change) = self.sync_state.pending_changes.first() {
|
|
130
|
+
if change.change_id.sequence != self.sync_state.last_applied_sequence + 1 {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if self.applied_ids.contains(&change.idempotency_key) {
|
|
135
|
+
self.sync_state.pending_changes.remove(0);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
applied.push(change.clone());
|
|
140
|
+
self.applied_ids.insert(change.idempotency_key.clone());
|
|
141
|
+
self.sync_state.last_applied_sequence = change.change_id.sequence;
|
|
142
|
+
self.sync_state.pending_changes.remove(0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Ok(applied)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub fn create_checkpoint(&mut self) -> Result<Checkpoint, String> {
|
|
149
|
+
let checkpoint = Checkpoint {
|
|
150
|
+
checkpoint_id: format!("cp-{}-{}", self.node_id, self.sync_state.last_applied_sequence),
|
|
151
|
+
node_id: self.node_id.clone(),
|
|
152
|
+
high_water_mark: self.sync_state.last_applied_sequence,
|
|
153
|
+
applied_changes: self
|
|
154
|
+
.applied_ids
|
|
155
|
+
.iter()
|
|
156
|
+
.cloned()
|
|
157
|
+
.collect(),
|
|
158
|
+
timestamp: std::time::SystemTime::now()
|
|
159
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
160
|
+
.unwrap_or_default()
|
|
161
|
+
.as_secs(),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
self.sync_state.checkpoints.push(checkpoint.clone());
|
|
165
|
+
Ok(checkpoint)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
pub fn get_high_water_mark(&self) -> u64 {
|
|
169
|
+
self.sync_state.last_applied_sequence
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pub fn get_pending_count(&self) -> usize {
|
|
173
|
+
self.sync_state.pending_changes.len()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pub fn evaluate_backpressure(&self) -> BackpressureAction {
|
|
177
|
+
let pending = self.sync_state.pending_changes.len();
|
|
178
|
+
|
|
179
|
+
if pending >= self.max_pending_changes {
|
|
180
|
+
BackpressureAction::Reject
|
|
181
|
+
} else if pending >= self.backpressure_threshold {
|
|
182
|
+
BackpressureAction::Throttle {
|
|
183
|
+
delay_ms: ((pending - self.backpressure_threshold) / 100) as u64,
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
BackpressureAction::Accept
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pub fn replay_from_checkpoint(&mut self, checkpoint: &Checkpoint) -> Result<usize, String> {
|
|
191
|
+
let mut replayed = 0;
|
|
192
|
+
let applied_set: std::collections::HashSet<_> = checkpoint.applied_changes.iter().cloned().collect();
|
|
193
|
+
|
|
194
|
+
for change in self.sync_state.pending_changes.drain(..) {
|
|
195
|
+
if !applied_set.contains(&change.idempotency_key)
|
|
196
|
+
&& change.change_id.sequence <= checkpoint.high_water_mark
|
|
197
|
+
{
|
|
198
|
+
replayed += 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
self.sync_state.last_applied_sequence = checkpoint.high_water_mark;
|
|
203
|
+
Ok(replayed)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pub fn get_checkpoint_count(&self) -> usize {
|
|
207
|
+
self.sync_state.checkpoints.len()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
pub fn get_last_checkpoint(&self) -> Option<Checkpoint> {
|
|
211
|
+
self.sync_state.checkpoints.last().cloned()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[cfg(test)]
|
|
216
|
+
mod tests {
|
|
217
|
+
use super::*;
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn test_idempotent_application() {
|
|
221
|
+
let mut manager = DurableSyncManager::new("node-1".to_string());
|
|
222
|
+
|
|
223
|
+
let change = DurableChange::new(
|
|
224
|
+
"node-1".to_string(),
|
|
225
|
+
1,
|
|
226
|
+
ChangeType::Insert,
|
|
227
|
+
"resource-1".to_string(),
|
|
228
|
+
"data".to_string(),
|
|
229
|
+
"idem-1".to_string(),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
manager.ingest_change(change.clone()).unwrap();
|
|
233
|
+
let applied = manager.apply_changes_idempotent().unwrap();
|
|
234
|
+
|
|
235
|
+
assert_eq!(applied.len(), 1);
|
|
236
|
+
assert_eq!(manager.get_high_water_mark(), 1);
|
|
237
|
+
|
|
238
|
+
manager.ingest_change(change).unwrap();
|
|
239
|
+
let applied = manager.apply_changes_idempotent().unwrap();
|
|
240
|
+
assert_eq!(applied.len(), 0);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[test]
|
|
244
|
+
fn test_checkpoint_recovery() {
|
|
245
|
+
let mut manager = DurableSyncManager::new("node-1".to_string());
|
|
246
|
+
|
|
247
|
+
let change1 = DurableChange::new(
|
|
248
|
+
"node-1".to_string(),
|
|
249
|
+
1,
|
|
250
|
+
ChangeType::Insert,
|
|
251
|
+
"res-1".to_string(),
|
|
252
|
+
"data1".to_string(),
|
|
253
|
+
"idem-1".to_string(),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
manager.ingest_change(change1).unwrap();
|
|
257
|
+
manager.apply_changes_idempotent().unwrap();
|
|
258
|
+
let cp = manager.create_checkpoint().unwrap();
|
|
259
|
+
|
|
260
|
+
assert_eq!(cp.high_water_mark, 1);
|
|
261
|
+
assert_eq!(manager.get_checkpoint_count(), 1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[test]
|
|
265
|
+
fn test_backpressure() {
|
|
266
|
+
let mut manager = DurableSyncManager::new("node-1".to_string());
|
|
267
|
+
manager.backpressure_threshold = 100;
|
|
268
|
+
|
|
269
|
+
for i in 0..150 {
|
|
270
|
+
let change = DurableChange::new(
|
|
271
|
+
"node-1".to_string(),
|
|
272
|
+
(i + 1) as u64,
|
|
273
|
+
ChangeType::Insert,
|
|
274
|
+
format!("res-{}", i),
|
|
275
|
+
"data".to_string(),
|
|
276
|
+
format!("idem-{}", i),
|
|
277
|
+
);
|
|
278
|
+
manager.ingest_change(change).ok();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
match manager.evaluate_backpressure() {
|
|
282
|
+
BackpressureAction::Throttle { delay_ms } => assert!(delay_ms >= 0),
|
|
283
|
+
_ => panic!("Expected throttle"),
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[test]
|
|
288
|
+
fn test_ordering_guarantee() {
|
|
289
|
+
let mut manager = DurableSyncManager::new("node-1".to_string());
|
|
290
|
+
|
|
291
|
+
let change2 = DurableChange::new(
|
|
292
|
+
"node-1".to_string(),
|
|
293
|
+
2,
|
|
294
|
+
ChangeType::Insert,
|
|
295
|
+
"res-2".to_string(),
|
|
296
|
+
"data2".to_string(),
|
|
297
|
+
"idem-2".to_string(),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
let change1 = DurableChange::new(
|
|
301
|
+
"node-1".to_string(),
|
|
302
|
+
1,
|
|
303
|
+
ChangeType::Insert,
|
|
304
|
+
"res-1".to_string(),
|
|
305
|
+
"data1".to_string(),
|
|
306
|
+
"idem-1".to_string(),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
manager.ingest_change(change2).unwrap();
|
|
310
|
+
manager.ingest_change(change1).unwrap();
|
|
311
|
+
|
|
312
|
+
let applied = manager.apply_changes_idempotent().unwrap();
|
|
313
|
+
assert_eq!(applied[0].change_id.sequence, 1);
|
|
314
|
+
assert_eq!(applied[1].change_id.sequence, 2);
|
|
315
|
+
}
|
|
316
|
+
}
|