create-feltdb 0.6.9 → 0.6.10
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/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
- package/dist/server-source/crates/feltdb/src/lib.rs +49 -5
- package/dist/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
- package/dist/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
- package/dist/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
- package/dist/server-source/crates/feltdb-server/src/main.rs +141 -0
- package/package.json +1 -1
- package/dist/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
|
@@ -41,6 +41,7 @@ use feltdb::{
|
|
|
41
41
|
Grant, GrantSigner, GrantStore, Subject as GrantSubject,
|
|
42
42
|
},
|
|
43
43
|
cardinality_endpoint::{CardinalityContext, CardinalityDiagnosticResponse},
|
|
44
|
+
AtomicMutation, AtomicPrecondition,
|
|
44
45
|
policy_evaluation::{Actor, PolicyContext, PolicyEvaluator, PolicySubject},
|
|
45
46
|
state_contract::{
|
|
46
47
|
begin_read, compare_schemas, execute_query as execute_state_query, execute_transaction,
|
|
@@ -6298,6 +6299,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
6298
6299
|
"/api/connections/{connection_id}",
|
|
6299
6300
|
axum::routing::delete(delete_connection_control),
|
|
6300
6301
|
)
|
|
6302
|
+
.route("/transactions", axum::routing::post(commit_transaction))
|
|
6303
|
+
.route("/revision", get(get_revision))
|
|
6301
6304
|
.route(
|
|
6302
6305
|
"/collections/{collection}",
|
|
6303
6306
|
get(list_records).post(create_record),
|
|
@@ -8974,6 +8977,30 @@ fn put_canonical(
|
|
|
8974
8977
|
}))
|
|
8975
8978
|
}
|
|
8976
8979
|
|
|
8980
|
+
/// The authority's current committed-state revision.
|
|
8981
|
+
///
|
|
8982
|
+
/// This is the value `apply_atomic_transaction` advances and writes into each
|
|
8983
|
+
/// durable transaction record as `state_after`. It already survived restart and
|
|
8984
|
+
/// already refused to move backwards; the only thing missing was a way to read
|
|
8985
|
+
/// it without performing a write, which is what a cache needs.
|
|
8986
|
+
///
|
|
8987
|
+
/// `scope` is the store's instance id. A revision is comparable only against
|
|
8988
|
+
/// another revision carrying the same scope, so a client that fails over to a
|
|
8989
|
+
/// different replica sees a scope change rather than a silently wrong
|
|
8990
|
+
/// comparison against a counter that means something else there.
|
|
8991
|
+
#[derive(Serialize)]
|
|
8992
|
+
struct RevisionResponse {
|
|
8993
|
+
revision: u64,
|
|
8994
|
+
scope: String,
|
|
8995
|
+
}
|
|
8996
|
+
|
|
8997
|
+
async fn get_revision(State(state): State<AppState>) -> Result<Json<RevisionResponse>, ApiError> {
|
|
8998
|
+
Ok(Json(RevisionResponse {
|
|
8999
|
+
revision: state.db.sequence()?,
|
|
9000
|
+
scope: state.db.instance_id()?,
|
|
9001
|
+
}))
|
|
9002
|
+
}
|
|
9003
|
+
|
|
8977
9004
|
async fn list_records(
|
|
8978
9005
|
State(state): State<AppState>,
|
|
8979
9006
|
Path(collection): Path<String>,
|
|
@@ -9000,6 +9027,120 @@ async fn get_record(
|
|
|
9000
9027
|
}))
|
|
9001
9028
|
}
|
|
9002
9029
|
|
|
9030
|
+
/// One operation inside an application transaction.
|
|
9031
|
+
#[derive(Deserialize)]
|
|
9032
|
+
#[serde(rename_all = "camelCase")]
|
|
9033
|
+
struct AtomicTxOperationRequest {
|
|
9034
|
+
collection: String,
|
|
9035
|
+
id: String,
|
|
9036
|
+
/// Absent value means delete.
|
|
9037
|
+
#[serde(default)]
|
|
9038
|
+
value: Option<Map<String, Value>>,
|
|
9039
|
+
/// When set, the record must not already exist for the transaction to commit.
|
|
9040
|
+
#[serde(default)]
|
|
9041
|
+
require_absent: bool,
|
|
9042
|
+
}
|
|
9043
|
+
|
|
9044
|
+
#[derive(Deserialize)]
|
|
9045
|
+
#[serde(rename_all = "camelCase")]
|
|
9046
|
+
struct AtomicTxRequest {
|
|
9047
|
+
/// Caller-supplied identity. Reusing one makes the commit idempotent.
|
|
9048
|
+
transaction_id: String,
|
|
9049
|
+
operations: Vec<AtomicTxOperationRequest>,
|
|
9050
|
+
}
|
|
9051
|
+
|
|
9052
|
+
#[derive(Serialize)]
|
|
9053
|
+
#[serde(rename_all = "camelCase")]
|
|
9054
|
+
struct AtomicTxResponse {
|
|
9055
|
+
transaction_id: String,
|
|
9056
|
+
/// True when this id had already been committed; nothing was reapplied.
|
|
9057
|
+
duplicate: bool,
|
|
9058
|
+
operations: usize,
|
|
9059
|
+
state_before: u64,
|
|
9060
|
+
state_after: u64,
|
|
9061
|
+
}
|
|
9062
|
+
|
|
9063
|
+
/// Commit several operations as one atomic, durable transaction.
|
|
9064
|
+
///
|
|
9065
|
+
/// This is the application-facing entry point to the same atomic commit path
|
|
9066
|
+
/// the substrate uses: preconditions are evaluated first, then one transaction
|
|
9067
|
+
/// record is written and fsynced, and only then is derived state updated.
|
|
9068
|
+
/// There is no second transaction mechanism — the request is translated into
|
|
9069
|
+
/// the store's own atomic transaction and nothing else.
|
|
9070
|
+
async fn commit_transaction(
|
|
9071
|
+
State(state): State<AppState>,
|
|
9072
|
+
Json(request): Json<AtomicTxRequest>,
|
|
9073
|
+
) -> Result<(StatusCode, Json<AtomicTxResponse>), ApiError> {
|
|
9074
|
+
if request.transaction_id.trim().is_empty() {
|
|
9075
|
+
return Err(ApiError(
|
|
9076
|
+
StatusCode::BAD_REQUEST,
|
|
9077
|
+
"transactionId is required".to_string(),
|
|
9078
|
+
));
|
|
9079
|
+
}
|
|
9080
|
+
if request.operations.is_empty() {
|
|
9081
|
+
return Err(ApiError(
|
|
9082
|
+
StatusCode::BAD_REQUEST,
|
|
9083
|
+
"a transaction requires at least one operation".to_string(),
|
|
9084
|
+
));
|
|
9085
|
+
}
|
|
9086
|
+
|
|
9087
|
+
// Validate every operation before any of them is turned into a mutation, so
|
|
9088
|
+
// a malformed request cannot commit a prefix of the transaction.
|
|
9089
|
+
let mut mutations = Vec::with_capacity(request.operations.len());
|
|
9090
|
+
let mut preconditions = Vec::new();
|
|
9091
|
+
let mut seen = std::collections::HashSet::new();
|
|
9092
|
+
for operation in request.operations {
|
|
9093
|
+
let key = record_key(&operation.collection, &operation.id)?;
|
|
9094
|
+
if !seen.insert(key.clone()) {
|
|
9095
|
+
return Err(ApiError(
|
|
9096
|
+
StatusCode::BAD_REQUEST,
|
|
9097
|
+
format!("transaction writes {key} more than once"),
|
|
9098
|
+
));
|
|
9099
|
+
}
|
|
9100
|
+
if operation.require_absent {
|
|
9101
|
+
// The store reads a precondition with no expected version as
|
|
9102
|
+
// "this record must not exist", and evaluates it inside the same
|
|
9103
|
+
// lock as the commit, so there is no window between the check and
|
|
9104
|
+
// the write.
|
|
9105
|
+
preconditions.push(AtomicPrecondition {
|
|
9106
|
+
capability: operation.collection.clone(),
|
|
9107
|
+
key: key.clone(),
|
|
9108
|
+
expected_version: None,
|
|
9109
|
+
});
|
|
9110
|
+
}
|
|
9111
|
+
let value = operation.value.map(|mut fields| {
|
|
9112
|
+
fields.insert("id".to_string(), Value::String(operation.id.clone()));
|
|
9113
|
+
Value::Object(fields)
|
|
9114
|
+
});
|
|
9115
|
+
mutations.push(AtomicMutation {
|
|
9116
|
+
capability: operation.collection,
|
|
9117
|
+
key,
|
|
9118
|
+
value,
|
|
9119
|
+
});
|
|
9120
|
+
}
|
|
9121
|
+
|
|
9122
|
+
let operations = mutations.len();
|
|
9123
|
+
let commit = state
|
|
9124
|
+
.db
|
|
9125
|
+
.apply_atomic_transaction(&request.transaction_id, None, &preconditions, &mutations, None)
|
|
9126
|
+
.map_err(|error| {
|
|
9127
|
+
// A refused transaction applied nothing, so this is a conflict, not
|
|
9128
|
+
// a partially completed request.
|
|
9129
|
+
ApiError(StatusCode::CONFLICT, error.to_string())
|
|
9130
|
+
})?;
|
|
9131
|
+
|
|
9132
|
+
Ok((
|
|
9133
|
+
if commit.duplicate { StatusCode::OK } else { StatusCode::CREATED },
|
|
9134
|
+
Json(AtomicTxResponse {
|
|
9135
|
+
transaction_id: commit.transaction_id,
|
|
9136
|
+
duplicate: commit.duplicate,
|
|
9137
|
+
operations,
|
|
9138
|
+
state_before: commit.state_before,
|
|
9139
|
+
state_after: commit.state_after,
|
|
9140
|
+
}),
|
|
9141
|
+
))
|
|
9142
|
+
}
|
|
9143
|
+
|
|
9003
9144
|
async fn create_record(
|
|
9004
9145
|
State(state): State<AppState>,
|
|
9005
9146
|
Path(collection): Path<String>,
|
package/package.json
CHANGED
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
/// Phase 1c: Three-Node TCP Scaling
|
|
2
|
-
///
|
|
3
|
-
/// Validates that FeltDB replication converges correctly with 3 nodes over real TCP.
|
|
4
|
-
///
|
|
5
|
-
/// Success Criteria:
|
|
6
|
-
/// - 3-node cluster established over TCP (1 leader + 2 followers)
|
|
7
|
-
/// - 1000 operations replicated to all 3 nodes
|
|
8
|
-
/// - All 3 nodes converge to identical state_hash
|
|
9
|
-
/// - Replication latency <1s (p99) on LAN
|
|
10
|
-
/// - Zero message loss or corruption
|
|
11
|
-
/// - Test passes 10 consecutive runs
|
|
12
|
-
/// - No deadlocks or stalls
|
|
13
|
-
|
|
14
|
-
#[cfg(test)]
|
|
15
|
-
mod tests {
|
|
16
|
-
use crate::convergence::VectorClock;
|
|
17
|
-
use crate::distributed_transactions::{
|
|
18
|
-
DistributedTransactionExecutor, ReplicationMessage, TransactionEnvelope,
|
|
19
|
-
};
|
|
20
|
-
use crate::in_process_transport::InProcessTransport;
|
|
21
|
-
use crate::replication_protocol::ProtocolTransport;
|
|
22
|
-
use crate::state_hash::StateHash;
|
|
23
|
-
use crate::tcp_transport::TcpTransport;
|
|
24
|
-
use crate::transactions::{ConsistencyContract, Operation, OperationCommand, OperationId, StateVersion};
|
|
25
|
-
use serde_json::json;
|
|
26
|
-
use std::collections::HashMap;
|
|
27
|
-
use std::sync::Arc;
|
|
28
|
-
use std::time::{Duration, Instant};
|
|
29
|
-
use tokio::sync::Mutex;
|
|
30
|
-
use tokio::task::{self, JoinSet};
|
|
31
|
-
|
|
32
|
-
/// Test node that combines FeltDb state with TCP transport
|
|
33
|
-
struct TestNode {
|
|
34
|
-
node_id: String,
|
|
35
|
-
port: u16,
|
|
36
|
-
executor: Arc<Mutex<DistributedTransactionExecutor>>,
|
|
37
|
-
transport: Arc<Mutex<TcpTransport>>,
|
|
38
|
-
sequence: Arc<Mutex<u64>>,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
impl TestNode {
|
|
42
|
-
fn new(node_id: String, port: u16) -> Self {
|
|
43
|
-
let initial_hash = StateHash::from_hex("0".repeat(64));
|
|
44
|
-
let executor = DistributedTransactionExecutor::new(node_id.clone(), initial_hash);
|
|
45
|
-
|
|
46
|
-
let addr = format!("127.0.0.1:{}", port);
|
|
47
|
-
let transport = TcpTransport::new(addr);
|
|
48
|
-
|
|
49
|
-
Self {
|
|
50
|
-
node_id,
|
|
51
|
-
port,
|
|
52
|
-
executor: Arc::new(Mutex::new(executor)),
|
|
53
|
-
transport: Arc::new(Mutex::new(transport)),
|
|
54
|
-
sequence: Arc::new(Mutex::new(0)),
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/// Get the bind address for this node
|
|
59
|
-
fn addr(&self) -> String {
|
|
60
|
-
format!("127.0.0.1:{}", self.port)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/// Get current state hash
|
|
64
|
-
async fn state_hash(&self) -> StateHash {
|
|
65
|
-
let executor = self.executor.lock().await;
|
|
66
|
-
executor
|
|
67
|
-
.get_replica_state(&self.node_id)
|
|
68
|
-
.map(|s| s.state_hash)
|
|
69
|
-
.unwrap_or_else(|| StateHash::from_hex("0".repeat(64)))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/// Check if this node has received all expected envelopes
|
|
73
|
-
async fn operations_applied(&self) -> u64 {
|
|
74
|
-
let executor = self.executor.lock().await;
|
|
75
|
-
executor
|
|
76
|
-
.get_replica_state(&self.node_id)
|
|
77
|
-
.map(|s| s.operations_applied)
|
|
78
|
-
.unwrap_or(0)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/// Submit a single operation (leader only)
|
|
82
|
-
async fn submit_operation(&self, op_idx: u64) -> Result<(), String> {
|
|
83
|
-
let mut executor = self.executor.lock().await;
|
|
84
|
-
let mut seq = self.sequence.lock().await;
|
|
85
|
-
*seq += 1;
|
|
86
|
-
|
|
87
|
-
let op_id = OperationId::new(self.node_id.clone(), *seq);
|
|
88
|
-
let parent_vc = VectorClock::new();
|
|
89
|
-
let parent_version = StateVersion::new(parent_vc, "0".repeat(64));
|
|
90
|
-
|
|
91
|
-
let mut fields = HashMap::new();
|
|
92
|
-
fields.insert("value".to_string(), json!(op_idx));
|
|
93
|
-
|
|
94
|
-
let operation = Operation::new(
|
|
95
|
-
op_id,
|
|
96
|
-
parent_version.clone(),
|
|
97
|
-
format!("op_{}", op_idx),
|
|
98
|
-
OperationCommand {
|
|
99
|
-
op_type: "set".to_string(),
|
|
100
|
-
collection: "items".to_string(),
|
|
101
|
-
record_id: format!("item_{}", op_idx),
|
|
102
|
-
fields,
|
|
103
|
-
},
|
|
104
|
-
self.node_id.clone(),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
let envelope = executor.execute_local_transaction(
|
|
108
|
-
*seq,
|
|
109
|
-
format!("txn_{}", op_idx),
|
|
110
|
-
parent_version,
|
|
111
|
-
vec![operation],
|
|
112
|
-
ConsistencyContract::causal(),
|
|
113
|
-
)?;
|
|
114
|
-
|
|
115
|
-
Ok(())
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/// Test coordinator for managing 3-node cluster
|
|
120
|
-
struct TestCluster {
|
|
121
|
-
nodes: Vec<TestNode>,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
impl TestCluster {
|
|
125
|
-
fn new(node_count: usize) -> Self {
|
|
126
|
-
let mut nodes = Vec::new();
|
|
127
|
-
for i in 0..node_count {
|
|
128
|
-
let node_id = format!("node-{}", i);
|
|
129
|
-
let port = 10000 + i as u16;
|
|
130
|
-
nodes.push(TestNode::new(node_id, port));
|
|
131
|
-
}
|
|
132
|
-
Self { nodes }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/// Get node by index
|
|
136
|
-
fn node(&self, idx: usize) -> &TestNode {
|
|
137
|
-
&self.nodes[idx]
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/// Start listening on all nodes
|
|
141
|
-
async fn start_listeners(&self) {
|
|
142
|
-
for node in &self.nodes {
|
|
143
|
-
let transport = node.transport.clone();
|
|
144
|
-
let _handle = task::spawn(async move {
|
|
145
|
-
let mut t = transport.lock().await;
|
|
146
|
-
let _ = t.listen().await;
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
// Give listeners time to start
|
|
150
|
-
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/// Connect followers to leader
|
|
154
|
-
async fn establish_replication_topology(&self) {
|
|
155
|
-
let leader_addr = self.nodes[0].addr();
|
|
156
|
-
|
|
157
|
-
for node in self.nodes.iter().skip(1) {
|
|
158
|
-
let mut t = node.transport.lock().await;
|
|
159
|
-
let _ = (*t).connect(&leader_addr).await;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/// Simulate replication: broadcast leader messages to all followers
|
|
164
|
-
async fn replicate_from_leader(&self, operations_count: u64) {
|
|
165
|
-
let leader = &self.nodes[0];
|
|
166
|
-
let followers: Vec<_> = (1..self.nodes.len()).collect();
|
|
167
|
-
|
|
168
|
-
// Wait for operations to be applied on followers
|
|
169
|
-
for op_idx in 0..operations_count {
|
|
170
|
-
// Small delay between operations to allow replication
|
|
171
|
-
tokio::time::sleep(Duration::from_millis(1)).await;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/// Verify all nodes have converged to same state
|
|
176
|
-
async fn verify_convergence(&self) -> Result<(), String> {
|
|
177
|
-
let mut hashes = Vec::new();
|
|
178
|
-
|
|
179
|
-
for node in &self.nodes {
|
|
180
|
-
let executor = node.executor.lock().await;
|
|
181
|
-
let state = executor.get_replica_state(&node.node_id);
|
|
182
|
-
if let Some(s) = state {
|
|
183
|
-
hashes.push(s.state_hash);
|
|
184
|
-
} else {
|
|
185
|
-
return Err(format!("Node {} has no state", node.node_id));
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if hashes.is_empty() {
|
|
190
|
-
return Err("No nodes".to_string());
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let first = &hashes[0];
|
|
194
|
-
for (idx, hash) in hashes.iter().enumerate().skip(1) {
|
|
195
|
-
if hash != first {
|
|
196
|
-
return Err(format!(
|
|
197
|
-
"Convergence failed: node-0 has {}, node-{} has {}",
|
|
198
|
-
first, idx, hash
|
|
199
|
-
));
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
Ok(())
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/// Get statistics about replication
|
|
207
|
-
async fn get_stats(&self) -> ClusterStats {
|
|
208
|
-
let mut total_ops_applied = 0;
|
|
209
|
-
let mut node_states = Vec::new();
|
|
210
|
-
|
|
211
|
-
for node in &self.nodes {
|
|
212
|
-
let ops = node.operations_applied().await;
|
|
213
|
-
let hash = node.state_hash().await;
|
|
214
|
-
total_ops_applied += ops;
|
|
215
|
-
node_states.push((node.node_id.clone(), ops, hash));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
ClusterStats {
|
|
219
|
-
node_states,
|
|
220
|
-
total_ops_applied,
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
struct ClusterStats {
|
|
226
|
-
node_states: Vec<(String, u64, StateHash)>,
|
|
227
|
-
total_ops_applied: u64,
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/// Phase 1c Test 1: Cluster initialization (no networking yet)
|
|
231
|
-
#[tokio::test]
|
|
232
|
-
async fn three_node_cluster_initializes() {
|
|
233
|
-
let cluster = TestCluster::new(3);
|
|
234
|
-
|
|
235
|
-
// Verify all nodes are created with initialized state
|
|
236
|
-
for node in &cluster.nodes {
|
|
237
|
-
let hash = node.state_hash().await;
|
|
238
|
-
// Hash should be deterministic initial value
|
|
239
|
-
assert_ne!(hash, StateHash::from_hex("".to_string()), "Node {} should have state", node.node_id);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/// Phase 1c Test 2: Submit single operation to leader
|
|
244
|
-
#[tokio::test]
|
|
245
|
-
async fn leader_submit_single_operation() {
|
|
246
|
-
let cluster = TestCluster::new(3);
|
|
247
|
-
let leader = cluster.node(0);
|
|
248
|
-
|
|
249
|
-
// Submit first operation
|
|
250
|
-
leader
|
|
251
|
-
.submit_operation(0)
|
|
252
|
-
.await
|
|
253
|
-
.expect("submit should succeed");
|
|
254
|
-
|
|
255
|
-
// Verify leader state changed
|
|
256
|
-
let ops_applied = leader.operations_applied().await;
|
|
257
|
-
assert_eq!(ops_applied, 1, "Leader should have 1 operation applied");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/// Phase 1c Test 3: Submit 100 operations to leader
|
|
261
|
-
#[tokio::test]
|
|
262
|
-
async fn leader_submit_100_operations() {
|
|
263
|
-
let cluster = TestCluster::new(3);
|
|
264
|
-
let leader = cluster.node(0);
|
|
265
|
-
let start = Instant::now();
|
|
266
|
-
|
|
267
|
-
// Submit 100 operations through leader
|
|
268
|
-
for i in 0..100 {
|
|
269
|
-
leader
|
|
270
|
-
.submit_operation(i)
|
|
271
|
-
.await
|
|
272
|
-
.expect("submit should succeed");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Check leader has all operations
|
|
276
|
-
let leader_ops = leader.operations_applied().await;
|
|
277
|
-
assert_eq!(leader_ops, 100, "Leader should have 100 operations applied");
|
|
278
|
-
|
|
279
|
-
println!(
|
|
280
|
-
"Submitted 100 operations in {:?}",
|
|
281
|
-
start.elapsed()
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/// Phase 1c Test 4: Leader can track multiple operations
|
|
286
|
-
#[tokio::test]
|
|
287
|
-
async fn leader_tracks_operation_sequence() {
|
|
288
|
-
let cluster = TestCluster::new(3);
|
|
289
|
-
let leader = cluster.node(0);
|
|
290
|
-
|
|
291
|
-
// Submit multiple operations
|
|
292
|
-
for i in 0..50 {
|
|
293
|
-
leader
|
|
294
|
-
.submit_operation(i)
|
|
295
|
-
.await
|
|
296
|
-
.expect("submit should succeed");
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
let ops_applied = leader.operations_applied().await;
|
|
300
|
-
assert_eq!(ops_applied, 50, "Leader should track all 50 operations");
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/// Phase 1c Test 5: Submit 1000 operations to leader
|
|
304
|
-
#[tokio::test]
|
|
305
|
-
async fn leader_submit_1000_operations() {
|
|
306
|
-
let cluster = TestCluster::new(3);
|
|
307
|
-
let leader = cluster.node(0);
|
|
308
|
-
let start = Instant::now();
|
|
309
|
-
|
|
310
|
-
// Submit 1000 operations
|
|
311
|
-
for i in 0..1000 {
|
|
312
|
-
leader
|
|
313
|
-
.submit_operation(i)
|
|
314
|
-
.await
|
|
315
|
-
.expect("submit should succeed");
|
|
316
|
-
|
|
317
|
-
// Add small delay to avoid overwhelming the system
|
|
318
|
-
if i % 100 == 0 {
|
|
319
|
-
tokio::time::sleep(Duration::from_millis(1)).await;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
let elapsed = start.elapsed();
|
|
324
|
-
let leader_ops = leader.operations_applied().await;
|
|
325
|
-
|
|
326
|
-
println!("Submitted 1000 operations in {:?}", elapsed);
|
|
327
|
-
println!("Leader has {} operations applied", leader_ops);
|
|
328
|
-
|
|
329
|
-
assert_eq!(leader_ops, 1000, "Leader should have all 1000 operations applied");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/// Phase 1c Test 6: Multiple nodes can submit operations independently
|
|
333
|
-
#[tokio::test]
|
|
334
|
-
async fn multiple_nodes_submit_independently() {
|
|
335
|
-
let cluster = TestCluster::new(3);
|
|
336
|
-
let node0 = cluster.node(0);
|
|
337
|
-
let node1 = cluster.node(1);
|
|
338
|
-
let node2 = cluster.node(2);
|
|
339
|
-
|
|
340
|
-
// Each node submits operations independently
|
|
341
|
-
for i in 0..10 {
|
|
342
|
-
node0.submit_operation(i).await.expect("node0 submit");
|
|
343
|
-
node1.submit_operation(i + 1000).await.expect("node1 submit");
|
|
344
|
-
node2.submit_operation(i + 2000).await.expect("node2 submit");
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Verify each node tracks its operations
|
|
348
|
-
assert_eq!(node0.operations_applied().await, 10, "Node0 ops");
|
|
349
|
-
assert_eq!(node1.operations_applied().await, 10, "Node1 ops");
|
|
350
|
-
assert_eq!(node2.operations_applied().await, 10, "Node2 ops");
|
|
351
|
-
|
|
352
|
-
println!("All 3 nodes submitted 10 operations each independently");
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/// Replication Test 1: Single operation replication between two nodes
|
|
356
|
-
#[tokio::test]
|
|
357
|
-
async fn replicate_single_operation_to_follower() {
|
|
358
|
-
let leader_id = "leader".to_string();
|
|
359
|
-
let follower_id = "follower".to_string();
|
|
360
|
-
|
|
361
|
-
let initial_hash = StateHash::from_hex("0".repeat(64));
|
|
362
|
-
let mut leader_executor = DistributedTransactionExecutor::new(leader_id.clone(), initial_hash.clone());
|
|
363
|
-
let mut follower_executor = DistributedTransactionExecutor::new(follower_id.clone(), initial_hash.clone());
|
|
364
|
-
|
|
365
|
-
// Register replicas
|
|
366
|
-
leader_executor.register_replica(follower_id.clone(), initial_hash.clone());
|
|
367
|
-
follower_executor.register_replica(leader_id.clone(), initial_hash.clone());
|
|
368
|
-
|
|
369
|
-
// Leader submits operation
|
|
370
|
-
let op_id = OperationId::new(leader_id.clone(), 1);
|
|
371
|
-
let parent_vc = VectorClock::new();
|
|
372
|
-
let parent_version = StateVersion::new(parent_vc, "0".repeat(64));
|
|
373
|
-
|
|
374
|
-
let mut fields = HashMap::new();
|
|
375
|
-
fields.insert("value".to_string(), json!(42));
|
|
376
|
-
|
|
377
|
-
let operation = Operation::new(
|
|
378
|
-
op_id,
|
|
379
|
-
parent_version.clone(),
|
|
380
|
-
"op_1".to_string(),
|
|
381
|
-
OperationCommand {
|
|
382
|
-
op_type: "set".to_string(),
|
|
383
|
-
collection: "items".to_string(),
|
|
384
|
-
record_id: "item_1".to_string(),
|
|
385
|
-
fields,
|
|
386
|
-
},
|
|
387
|
-
leader_id.clone(),
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
let envelope = leader_executor
|
|
391
|
-
.execute_local_transaction(
|
|
392
|
-
1,
|
|
393
|
-
"txn_1".to_string(),
|
|
394
|
-
parent_version.clone(),
|
|
395
|
-
vec![operation],
|
|
396
|
-
ConsistencyContract::causal(),
|
|
397
|
-
)
|
|
398
|
-
.expect("leader should execute");
|
|
399
|
-
|
|
400
|
-
// Create replication message
|
|
401
|
-
let message = ReplicationMessage::new(
|
|
402
|
-
envelope,
|
|
403
|
-
leader_id.clone(),
|
|
404
|
-
follower_id.clone(),
|
|
405
|
-
1,
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
// Follower applies replicated operation
|
|
409
|
-
let follower_result = follower_executor.receive_replicated_transaction(message, parent_version);
|
|
410
|
-
assert!(follower_result.is_ok(), "Follower should apply operation");
|
|
411
|
-
|
|
412
|
-
// Verify leader and follower have same state
|
|
413
|
-
let leader_state = leader_executor.get_replica_state(&leader_id).unwrap();
|
|
414
|
-
let follower_state = follower_executor.get_replica_state(&follower_id).unwrap();
|
|
415
|
-
|
|
416
|
-
println!(
|
|
417
|
-
"Leader hash: {}, Follower hash: {}",
|
|
418
|
-
leader_state.state_hash, follower_state.state_hash
|
|
419
|
-
);
|
|
420
|
-
println!(
|
|
421
|
-
"Leader ops: {}, Follower ops: {}",
|
|
422
|
-
leader_state.operations_applied, follower_state.operations_applied
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/// Replication Test 2: Multiple operations converge across 3 nodes
|
|
427
|
-
#[tokio::test]
|
|
428
|
-
async fn three_node_convergence_with_replication() {
|
|
429
|
-
let node_ids = ["leader", "follower1", "follower2"];
|
|
430
|
-
let mut executors: Vec<_> = node_ids
|
|
431
|
-
.iter()
|
|
432
|
-
.map(|id| {
|
|
433
|
-
let initial_hash = StateHash::from_hex("0".repeat(64));
|
|
434
|
-
DistributedTransactionExecutor::new(id.to_string(), initial_hash)
|
|
435
|
-
})
|
|
436
|
-
.collect();
|
|
437
|
-
|
|
438
|
-
let initial_hash = StateHash::from_hex("0".repeat(64));
|
|
439
|
-
|
|
440
|
-
// Register replicas on all executors
|
|
441
|
-
for executor in &mut executors {
|
|
442
|
-
for node_id in &node_ids {
|
|
443
|
-
if node_id != &executor.local_node_id.as_str() {
|
|
444
|
-
executor.register_replica(node_id.to_string(), initial_hash.clone());
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Leader submits 10 operations
|
|
450
|
-
let mut envelopes = Vec::new();
|
|
451
|
-
for i in 0..10 {
|
|
452
|
-
let op_id = OperationId::new("leader".to_string(), i + 1);
|
|
453
|
-
let parent_vc = VectorClock::new();
|
|
454
|
-
let parent_version = StateVersion::new(parent_vc, "0".repeat(64));
|
|
455
|
-
|
|
456
|
-
let mut fields = HashMap::new();
|
|
457
|
-
fields.insert("value".to_string(), json!(i));
|
|
458
|
-
|
|
459
|
-
let operation = Operation::new(
|
|
460
|
-
op_id,
|
|
461
|
-
parent_version.clone(),
|
|
462
|
-
format!("op_{}", i),
|
|
463
|
-
OperationCommand {
|
|
464
|
-
op_type: "set".to_string(),
|
|
465
|
-
collection: "items".to_string(),
|
|
466
|
-
record_id: format!("item_{}", i),
|
|
467
|
-
fields,
|
|
468
|
-
},
|
|
469
|
-
"leader".to_string(),
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
let envelope = executors[0]
|
|
473
|
-
.execute_local_transaction(
|
|
474
|
-
i + 1,
|
|
475
|
-
format!("txn_{}", i),
|
|
476
|
-
parent_version,
|
|
477
|
-
vec![operation],
|
|
478
|
-
ConsistencyContract::causal(),
|
|
479
|
-
)
|
|
480
|
-
.expect("leader execute");
|
|
481
|
-
|
|
482
|
-
envelopes.push(envelope);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Followers apply operations
|
|
486
|
-
for (follower_idx, follower_id) in node_ids.iter().enumerate().skip(1) {
|
|
487
|
-
for envelope in &envelopes {
|
|
488
|
-
let message = ReplicationMessage::new(
|
|
489
|
-
envelope.clone(),
|
|
490
|
-
"leader".to_string(),
|
|
491
|
-
follower_id.to_string(),
|
|
492
|
-
0,
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
let parent_version = StateVersion::new(VectorClock::new(), "0".repeat(64));
|
|
496
|
-
let _ = executors[follower_idx].receive_replicated_transaction(message, parent_version);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Verify convergence
|
|
501
|
-
let leader_state = executors[0].get_replica_state("leader").unwrap();
|
|
502
|
-
let follower1_state = executors[1].get_replica_state("follower1").unwrap();
|
|
503
|
-
let follower2_state = executors[2].get_replica_state("follower2").unwrap();
|
|
504
|
-
|
|
505
|
-
println!(
|
|
506
|
-
"Leader: {} ops, hash {}",
|
|
507
|
-
leader_state.operations_applied, leader_state.state_hash
|
|
508
|
-
);
|
|
509
|
-
println!(
|
|
510
|
-
"Follower1: {} ops, hash {}",
|
|
511
|
-
follower1_state.operations_applied, follower1_state.state_hash
|
|
512
|
-
);
|
|
513
|
-
println!(
|
|
514
|
-
"Follower2: {} ops, hash {}",
|
|
515
|
-
follower2_state.operations_applied, follower2_state.state_hash
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
// All nodes should have applied the same number of operations on their respective views
|
|
519
|
-
assert_eq!(leader_state.operations_applied, 10, "Leader ops");
|
|
520
|
-
assert_eq!(follower1_state.operations_applied, 10, "Follower1 ops");
|
|
521
|
-
assert_eq!(follower2_state.operations_applied, 10, "Follower2 ops");
|
|
522
|
-
}
|
|
523
|
-
}
|