@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,536 @@
|
|
|
1
|
+
/// Distributed Transactions & Replication
|
|
2
|
+
///
|
|
3
|
+
/// Coordinates transaction execution across multiple nodes:
|
|
4
|
+
/// - TransactionEnvelope: Atomic unit for multi-replica delivery
|
|
5
|
+
/// - ReplicationMessage: Operation streaming between replicas
|
|
6
|
+
/// - ReplicaState: Per-node state tracking with vector clocks
|
|
7
|
+
/// - DistributedExecutor: Orchestrates consistency semantics
|
|
8
|
+
///
|
|
9
|
+
/// Invariant: All replicas converge to identical state_hash for same operations
|
|
10
|
+
|
|
11
|
+
use crate::convergence::VectorClock;
|
|
12
|
+
use crate::durable_operation_log::OperationLog;
|
|
13
|
+
use crate::state_hash::StateHash;
|
|
14
|
+
use crate::transactions::*;
|
|
15
|
+
use std::collections::HashMap;
|
|
16
|
+
use std::path::PathBuf;
|
|
17
|
+
|
|
18
|
+
/// Uniquely identifies a transaction envelope
|
|
19
|
+
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
|
20
|
+
pub struct EnvelopeId {
|
|
21
|
+
pub originating_node: String,
|
|
22
|
+
pub sequence: u64,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl EnvelopeId {
|
|
26
|
+
pub fn new(originating_node: String, sequence: u64) -> Self {
|
|
27
|
+
Self {
|
|
28
|
+
originating_node,
|
|
29
|
+
sequence,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Atomic unit for multi-replica delivery
|
|
35
|
+
/// Prevents partial delivery of multi-operation transactions
|
|
36
|
+
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
|
37
|
+
pub struct TransactionEnvelope {
|
|
38
|
+
pub envelope_id: EnvelopeId,
|
|
39
|
+
pub transaction_id: String,
|
|
40
|
+
pub operations: Vec<Operation>,
|
|
41
|
+
pub consistency_contract: ConsistencyContract,
|
|
42
|
+
pub originating_node: String,
|
|
43
|
+
pub vector_clock: VectorClock,
|
|
44
|
+
pub timestamp_ms: u64,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl TransactionEnvelope {
|
|
48
|
+
pub fn new(
|
|
49
|
+
originating_node: String,
|
|
50
|
+
sequence: u64,
|
|
51
|
+
transaction_id: String,
|
|
52
|
+
operations: Vec<Operation>,
|
|
53
|
+
consistency_contract: ConsistencyContract,
|
|
54
|
+
) -> Self {
|
|
55
|
+
let mut vc = VectorClock::new();
|
|
56
|
+
vc.increment(&originating_node);
|
|
57
|
+
|
|
58
|
+
let timestamp_ms = std::time::SystemTime::now()
|
|
59
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
60
|
+
.unwrap()
|
|
61
|
+
.as_millis() as u64;
|
|
62
|
+
|
|
63
|
+
Self {
|
|
64
|
+
envelope_id: EnvelopeId::new(originating_node.clone(), sequence),
|
|
65
|
+
transaction_id,
|
|
66
|
+
operations,
|
|
67
|
+
consistency_contract,
|
|
68
|
+
originating_node,
|
|
69
|
+
vector_clock: vc,
|
|
70
|
+
timestamp_ms,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Advance vector clock to account for replication delay
|
|
75
|
+
pub fn advance_clock(&mut self, replica_clock: &VectorClock) {
|
|
76
|
+
self.vector_clock.merge(replica_clock);
|
|
77
|
+
self.vector_clock.increment(&self.originating_node);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Verify envelope integrity (all operations present)
|
|
81
|
+
pub fn verify_integrity(&self) -> bool {
|
|
82
|
+
!self.operations.is_empty()
|
|
83
|
+
&& self.transaction_id.len() > 0
|
|
84
|
+
&& self.originating_node.len() > 0
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Message format for distributed operation streaming
|
|
89
|
+
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
|
90
|
+
pub struct ReplicationMessage {
|
|
91
|
+
pub envelope: TransactionEnvelope,
|
|
92
|
+
pub from_node: String,
|
|
93
|
+
pub to_node: String,
|
|
94
|
+
pub sequence_number: u64,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl ReplicationMessage {
|
|
98
|
+
pub fn new(
|
|
99
|
+
envelope: TransactionEnvelope,
|
|
100
|
+
from_node: String,
|
|
101
|
+
to_node: String,
|
|
102
|
+
sequence_number: u64,
|
|
103
|
+
) -> Self {
|
|
104
|
+
Self {
|
|
105
|
+
envelope,
|
|
106
|
+
from_node,
|
|
107
|
+
to_node,
|
|
108
|
+
sequence_number,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// State of a replica
|
|
114
|
+
#[derive(Clone, Debug)]
|
|
115
|
+
pub struct ReplicaState {
|
|
116
|
+
pub node_id: String,
|
|
117
|
+
pub vector_clock: VectorClock,
|
|
118
|
+
pub state_hash: StateHash,
|
|
119
|
+
pub operations_applied: u64,
|
|
120
|
+
pub last_update_ms: u64,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
impl ReplicaState {
|
|
124
|
+
pub fn new(node_id: String, initial_hash: StateHash) -> Self {
|
|
125
|
+
Self {
|
|
126
|
+
node_id,
|
|
127
|
+
vector_clock: VectorClock::new(),
|
|
128
|
+
state_hash: initial_hash,
|
|
129
|
+
operations_applied: 0,
|
|
130
|
+
last_update_ms: std::time::SystemTime::now()
|
|
131
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
132
|
+
.unwrap()
|
|
133
|
+
.as_millis() as u64,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Update replica state after transaction
|
|
138
|
+
pub fn apply_transaction(
|
|
139
|
+
&mut self,
|
|
140
|
+
envelope: &TransactionEnvelope,
|
|
141
|
+
new_state_hash: StateHash,
|
|
142
|
+
) {
|
|
143
|
+
self.vector_clock.merge(&envelope.vector_clock);
|
|
144
|
+
self.vector_clock.increment(&self.node_id);
|
|
145
|
+
self.state_hash = new_state_hash;
|
|
146
|
+
self.operations_applied += envelope.operations.len() as u64;
|
|
147
|
+
self.last_update_ms = std::time::SystemTime::now()
|
|
148
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
149
|
+
.unwrap()
|
|
150
|
+
.as_millis() as u64;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Causal read: wait for vector clock to advance
|
|
154
|
+
pub fn wait_for_clock_advance(&self, required_clock: &VectorClock) -> bool {
|
|
155
|
+
!self.vector_clock.happens_before(required_clock)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Distributed transaction executor with durable operation log support
|
|
160
|
+
///
|
|
161
|
+
/// Durability architecture:
|
|
162
|
+
/// - Operation log is the ONLY durable source of truth
|
|
163
|
+
/// - Dedup set is derived (reconstructed from operation log on startup)
|
|
164
|
+
/// - No separate dedup file (eliminates two-file atomicity problem)
|
|
165
|
+
pub struct DistributedTransactionExecutor {
|
|
166
|
+
pub local_node_id: String,
|
|
167
|
+
pub replicas: HashMap<String, ReplicaState>,
|
|
168
|
+
pub envelopes_seen: std::collections::HashSet<EnvelopeId>,
|
|
169
|
+
pub transaction_executor: TransactionExecutor,
|
|
170
|
+
pub operation_log: Option<OperationLog>,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
impl DistributedTransactionExecutor {
|
|
174
|
+
/// Create a new distributed executor without durable persistence
|
|
175
|
+
pub fn new(node_id: String, initial_state_hash: StateHash) -> Self {
|
|
176
|
+
let mut replicas = HashMap::new();
|
|
177
|
+
replicas.insert(
|
|
178
|
+
node_id.clone(),
|
|
179
|
+
ReplicaState::new(node_id.clone(), initial_state_hash),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
Self {
|
|
183
|
+
local_node_id: node_id.clone(),
|
|
184
|
+
replicas,
|
|
185
|
+
envelopes_seen: std::collections::HashSet::new(),
|
|
186
|
+
transaction_executor: TransactionExecutor::new(node_id),
|
|
187
|
+
operation_log: None,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Create a new distributed executor with durable operation log
|
|
192
|
+
///
|
|
193
|
+
/// Note: dedup set is NOT persisted separately. It is reconstructed deterministically
|
|
194
|
+
/// from the operation log on startup, eliminating atomicity issues.
|
|
195
|
+
pub fn with_log(
|
|
196
|
+
node_id: String,
|
|
197
|
+
initial_state_hash: StateHash,
|
|
198
|
+
log_path: Option<PathBuf>,
|
|
199
|
+
) -> Result<Self, std::io::Error> {
|
|
200
|
+
let mut replicas = HashMap::new();
|
|
201
|
+
replicas.insert(
|
|
202
|
+
node_id.clone(),
|
|
203
|
+
ReplicaState::new(node_id.clone(), initial_state_hash),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
let operation_log = if let Some(path) = &log_path {
|
|
207
|
+
Some(OperationLog::new(path)?)
|
|
208
|
+
} else {
|
|
209
|
+
None
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
Ok(Self {
|
|
213
|
+
local_node_id: node_id.clone(),
|
|
214
|
+
replicas,
|
|
215
|
+
envelopes_seen: std::collections::HashSet::new(),
|
|
216
|
+
transaction_executor: TransactionExecutor::new(node_id),
|
|
217
|
+
operation_log,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Register a replica (peer node)
|
|
222
|
+
pub fn register_replica(&mut self, node_id: String, initial_state_hash: StateHash) {
|
|
223
|
+
self.replicas.insert(
|
|
224
|
+
node_id.clone(),
|
|
225
|
+
ReplicaState::new(node_id, initial_state_hash),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Execute transaction locally and prepare for replication
|
|
230
|
+
/// Ordering: execute → persist → update state → record dedup
|
|
231
|
+
pub fn execute_local_transaction(
|
|
232
|
+
&mut self,
|
|
233
|
+
sequence: u64,
|
|
234
|
+
transaction_id: String,
|
|
235
|
+
parent_version: StateVersion,
|
|
236
|
+
operations: Vec<Operation>,
|
|
237
|
+
consistency_contract: ConsistencyContract,
|
|
238
|
+
) -> Result<TransactionEnvelope, String> {
|
|
239
|
+
// Step 1: Execute transaction through 14C
|
|
240
|
+
let transition = self.transaction_executor.execute_transaction(
|
|
241
|
+
transaction_id.clone(),
|
|
242
|
+
parent_version,
|
|
243
|
+
operations.clone(),
|
|
244
|
+
consistency_contract.clone(),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if !transition.is_successful() {
|
|
248
|
+
return Err("Transaction execution failed".to_string());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Step 2: Create envelope for replication
|
|
252
|
+
let envelope = TransactionEnvelope::new(
|
|
253
|
+
self.local_node_id.clone(),
|
|
254
|
+
sequence,
|
|
255
|
+
transaction_id,
|
|
256
|
+
operations,
|
|
257
|
+
consistency_contract,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Step 3: Durably persist operation to log (with fsync)
|
|
261
|
+
// This must succeed before we update in-memory state
|
|
262
|
+
self.persist_envelope(&envelope)?;
|
|
263
|
+
|
|
264
|
+
// Step 4: Update local replica state (in-memory)
|
|
265
|
+
if let Some(local_replica) = self.replicas.get_mut(&self.local_node_id) {
|
|
266
|
+
let state_hash = StateHash::from_hex(transition.to_version.state_hash.clone());
|
|
267
|
+
local_replica.apply_transaction(&envelope, state_hash);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Step 5: Record envelope to prevent duplicates (in-memory, derived from log)
|
|
271
|
+
self.envelopes_seen.insert(envelope.envelope_id.clone());
|
|
272
|
+
|
|
273
|
+
Ok(envelope)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Receive replicated transaction from peer
|
|
277
|
+
/// Ordering: dedup check → execute → persist → update state
|
|
278
|
+
pub fn receive_replicated_transaction(
|
|
279
|
+
&mut self,
|
|
280
|
+
message: ReplicationMessage,
|
|
281
|
+
parent_version: StateVersion,
|
|
282
|
+
) -> Result<StateHash, String> {
|
|
283
|
+
// Step 1: Check for duplicate (prevent re-application)
|
|
284
|
+
if self.envelopes_seen.contains(&message.envelope.envelope_id) {
|
|
285
|
+
return Err("Duplicate envelope".to_string());
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Step 2: Execute transaction through 14C
|
|
289
|
+
let transition = self.transaction_executor.execute_transaction(
|
|
290
|
+
message.envelope.transaction_id.clone(),
|
|
291
|
+
parent_version,
|
|
292
|
+
message.envelope.operations.clone(),
|
|
293
|
+
message.envelope.consistency_contract.clone(),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
if !transition.is_successful() {
|
|
297
|
+
return Err("Transaction execution failed".to_string());
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Step 3: Durably persist operation to log (with fsync)
|
|
301
|
+
self.persist_envelope(&message.envelope)?;
|
|
302
|
+
|
|
303
|
+
// Step 4: Record envelope to prevent duplicates (in-memory)
|
|
304
|
+
self.envelopes_seen.insert(message.envelope.envelope_id.clone());
|
|
305
|
+
|
|
306
|
+
// Step 5: Update this replica's state (receiver updates itself)
|
|
307
|
+
if let Some(replica) = self.replicas.get_mut(&self.local_node_id) {
|
|
308
|
+
let state_hash = StateHash::from_hex(transition.to_version.state_hash.clone());
|
|
309
|
+
replica.apply_transaction(&message.envelope, state_hash);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Return the state hash
|
|
313
|
+
Ok(StateHash::from_hex(transition.to_version.state_hash))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// Get current state of a replica
|
|
317
|
+
pub fn get_replica_state(&self, node_id: &str) -> Option<ReplicaState> {
|
|
318
|
+
self.replicas.get(node_id).cloned()
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Check if replicas have converged to same state
|
|
322
|
+
pub fn check_convergence(&self) -> bool {
|
|
323
|
+
if self.replicas.is_empty() {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let first_hash = self
|
|
328
|
+
.replicas
|
|
329
|
+
.values()
|
|
330
|
+
.next()
|
|
331
|
+
.map(|r| r.state_hash.clone());
|
|
332
|
+
|
|
333
|
+
if let Some(first) = first_hash {
|
|
334
|
+
self.replicas.values().all(|r| r.state_hash == first)
|
|
335
|
+
} else {
|
|
336
|
+
false
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Get all replica states
|
|
341
|
+
pub fn get_all_replicas(&self) -> Vec<ReplicaState> {
|
|
342
|
+
self.replicas.values().cloned().collect()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// Causal read: wait for specific version
|
|
346
|
+
pub fn causal_read(&self, required_clock: &VectorClock) -> bool {
|
|
347
|
+
if let Some(local) = self.replicas.get(&self.local_node_id) {
|
|
348
|
+
local.wait_for_clock_advance(required_clock)
|
|
349
|
+
} else {
|
|
350
|
+
false
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// Get the local node ID
|
|
355
|
+
pub fn get_local_node_id(&self) -> String {
|
|
356
|
+
self.local_node_id.clone()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Load persisted operations from disk and rebuild internal state
|
|
360
|
+
///
|
|
361
|
+
/// Atomicity guarantee:
|
|
362
|
+
/// - Load operation log (single durable source of truth)
|
|
363
|
+
/// - Reconstruct dedup set deterministically from envelopes
|
|
364
|
+
/// - No separate dedup file (no two-file atomicity problem)
|
|
365
|
+
pub fn load_from_disk(&mut self, log_path: PathBuf) -> Result<usize, std::io::Error> {
|
|
366
|
+
let log = OperationLog::new(&log_path)?;
|
|
367
|
+
let envelopes = log.load_all()?;
|
|
368
|
+
|
|
369
|
+
// Reconstruct dedup set from loaded envelopes (deterministic, atomic with log)
|
|
370
|
+
for envelope in &envelopes {
|
|
371
|
+
self.envelopes_seen.insert(envelope.envelope_id.clone());
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Note: Replica state is not rebuilt from log in this initial version
|
|
375
|
+
// It will be recovered through normal replication when this node rejoins the cluster
|
|
376
|
+
self.operation_log = Some(log);
|
|
377
|
+
|
|
378
|
+
Ok(envelopes.len())
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// Persist an envelope to the durable operation log
|
|
382
|
+
/// Must be called after successful state application but before returning to caller
|
|
383
|
+
///
|
|
384
|
+
/// Note: dedup set is NOT persisted (it's derived from the operation log).
|
|
385
|
+
fn persist_envelope(&mut self, envelope: &TransactionEnvelope) -> Result<(), String> {
|
|
386
|
+
if let Some(ref mut log) = self.operation_log {
|
|
387
|
+
log.append(envelope)
|
|
388
|
+
.map_err(|e| format!("Failed to persist operation: {}", e))?;
|
|
389
|
+
}
|
|
390
|
+
Ok(())
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#[cfg(test)]
|
|
395
|
+
mod tests {
|
|
396
|
+
use super::*;
|
|
397
|
+
|
|
398
|
+
#[test]
|
|
399
|
+
fn test_envelope_creation() {
|
|
400
|
+
let envelope = TransactionEnvelope::new(
|
|
401
|
+
"node1".to_string(),
|
|
402
|
+
1,
|
|
403
|
+
"txn1".to_string(),
|
|
404
|
+
Vec::new(),
|
|
405
|
+
ConsistencyContract::causal(),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
assert_eq!(envelope.originating_node, "node1");
|
|
409
|
+
assert_eq!(envelope.transaction_id, "txn1");
|
|
410
|
+
assert_eq!(envelope.envelope_id.sequence, 1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#[test]
|
|
414
|
+
fn test_envelope_integrity() {
|
|
415
|
+
let envelope = TransactionEnvelope::new(
|
|
416
|
+
"node1".to_string(),
|
|
417
|
+
1,
|
|
418
|
+
"txn1".to_string(),
|
|
419
|
+
vec![], // Empty operations
|
|
420
|
+
ConsistencyContract::causal(),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
assert!(!envelope.verify_integrity());
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#[test]
|
|
427
|
+
fn test_replication_message_creation() {
|
|
428
|
+
let envelope = TransactionEnvelope::new(
|
|
429
|
+
"node1".to_string(),
|
|
430
|
+
1,
|
|
431
|
+
"txn1".to_string(),
|
|
432
|
+
Vec::new(),
|
|
433
|
+
ConsistencyContract::causal(),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
let msg = ReplicationMessage::new(envelope, "node1".to_string(), "node2".to_string(), 1);
|
|
437
|
+
|
|
438
|
+
assert_eq!(msg.from_node, "node1");
|
|
439
|
+
assert_eq!(msg.to_node, "node2");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#[test]
|
|
443
|
+
fn test_replica_state_creation() {
|
|
444
|
+
let state = ReplicaState::new("node1".to_string(), StateHash::from_hex("hash0".to_string()));
|
|
445
|
+
|
|
446
|
+
assert_eq!(state.node_id, "node1");
|
|
447
|
+
assert_eq!(state.operations_applied, 0);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#[test]
|
|
451
|
+
fn test_distributed_executor_creation() {
|
|
452
|
+
let executor = DistributedTransactionExecutor::new(
|
|
453
|
+
"node1".to_string(),
|
|
454
|
+
StateHash::from_hex("hash0".to_string()),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
assert!(executor.replicas.contains_key("node1"));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
#[test]
|
|
461
|
+
fn test_register_replica() {
|
|
462
|
+
let mut executor = DistributedTransactionExecutor::new(
|
|
463
|
+
"node1".to_string(),
|
|
464
|
+
StateHash::from_hex("hash0".to_string()),
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
executor.register_replica("node2".to_string(), StateHash::from_hex("hash0".to_string()));
|
|
468
|
+
|
|
469
|
+
assert!(executor.replicas.contains_key("node2"));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
#[test]
|
|
473
|
+
fn test_convergence_check_empty() {
|
|
474
|
+
let executor = DistributedTransactionExecutor::new(
|
|
475
|
+
"node1".to_string(),
|
|
476
|
+
StateHash::from_hex("hash0".to_string()),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Single replica should be converged
|
|
480
|
+
assert!(executor.check_convergence());
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
#[test]
|
|
484
|
+
fn test_convergence_check_multiple_replicas() {
|
|
485
|
+
let mut executor = DistributedTransactionExecutor::new(
|
|
486
|
+
"node1".to_string(),
|
|
487
|
+
StateHash::from_hex("hash0".to_string()),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
executor.register_replica("node2".to_string(), StateHash::from_hex("hash0".to_string()));
|
|
491
|
+
executor.register_replica("node3".to_string(), StateHash::from_hex("hash0".to_string()));
|
|
492
|
+
|
|
493
|
+
assert!(executor.check_convergence());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
#[test]
|
|
497
|
+
fn test_convergence_check_diverged() {
|
|
498
|
+
let mut executor = DistributedTransactionExecutor::new(
|
|
499
|
+
"node1".to_string(),
|
|
500
|
+
StateHash::from_hex("hash0".to_string()),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
executor.register_replica("node2".to_string(), StateHash::from_hex("hash1".to_string()));
|
|
504
|
+
|
|
505
|
+
assert!(!executor.check_convergence());
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
#[test]
|
|
509
|
+
fn test_causal_read_initial_state() {
|
|
510
|
+
let executor = DistributedTransactionExecutor::new(
|
|
511
|
+
"node1".to_string(),
|
|
512
|
+
StateHash::from_hex("hash0".to_string()),
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
let vc = VectorClock::new();
|
|
516
|
+
assert!(executor.causal_read(&vc));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#[test]
|
|
520
|
+
fn test_envelope_clock_advance() {
|
|
521
|
+
let mut envelope = TransactionEnvelope::new(
|
|
522
|
+
"node1".to_string(),
|
|
523
|
+
1,
|
|
524
|
+
"txn1".to_string(),
|
|
525
|
+
Vec::new(),
|
|
526
|
+
ConsistencyContract::causal(),
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
let mut other_clock = VectorClock::new();
|
|
530
|
+
other_clock.increment("node2");
|
|
531
|
+
|
|
532
|
+
envelope.advance_clock(&other_clock);
|
|
533
|
+
|
|
534
|
+
assert!(envelope.vector_clock.happens_before(&other_clock) == false);
|
|
535
|
+
}
|
|
536
|
+
}
|