@feltdb/core 0.8.4 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +507 -6
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1514 -537
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +288 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1032 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +2 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +326 -6
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-DaNwCLRX.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DnsHNv6g.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-j8IlhNqJ.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
|
@@ -47,32 +47,22 @@ mod tests {
|
|
|
47
47
|
let initial_state_hash = StateHash::from_hex("hash_initial".to_string());
|
|
48
48
|
|
|
49
49
|
// Node A (Server)
|
|
50
|
-
let mut node_a_executor =
|
|
51
|
-
"node_a".to_string(),
|
|
52
|
-
initial_state_hash.clone(),
|
|
53
|
-
);
|
|
50
|
+
let mut node_a_executor =
|
|
51
|
+
DistributedTransactionExecutor::new("node_a".to_string(), initial_state_hash.clone());
|
|
54
52
|
|
|
55
53
|
let mut node_a_transport = TcpTransport::new("127.0.0.1:19020".to_string());
|
|
56
54
|
|
|
57
55
|
// Register the peer with same initial state
|
|
58
|
-
node_a_executor.register_replica(
|
|
59
|
-
"node_b".to_string(),
|
|
60
|
-
initial_state_hash.clone(),
|
|
61
|
-
);
|
|
56
|
+
node_a_executor.register_replica("node_b".to_string(), initial_state_hash.clone());
|
|
62
57
|
|
|
63
58
|
// Node B (Client)
|
|
64
|
-
let mut node_b_executor =
|
|
65
|
-
"node_b".to_string(),
|
|
66
|
-
initial_state_hash.clone(),
|
|
67
|
-
);
|
|
59
|
+
let mut node_b_executor =
|
|
60
|
+
DistributedTransactionExecutor::new("node_b".to_string(), initial_state_hash.clone());
|
|
68
61
|
|
|
69
62
|
let mut node_b_transport = TcpTransport::new("127.0.0.1:19021".to_string());
|
|
70
63
|
|
|
71
64
|
// Register the peer with same initial state
|
|
72
|
-
node_b_executor.register_replica(
|
|
73
|
-
"node_a".to_string(),
|
|
74
|
-
initial_state_hash.clone(),
|
|
75
|
-
);
|
|
65
|
+
node_b_executor.register_replica("node_a".to_string(), initial_state_hash.clone());
|
|
76
66
|
|
|
77
67
|
// ===== Start server listening in a separate task =====
|
|
78
68
|
let server_transport = node_a_transport.clone();
|
|
@@ -131,21 +121,17 @@ mod tests {
|
|
|
131
121
|
|
|
132
122
|
let envelope = node_a_executor
|
|
133
123
|
.execute_local_transaction(
|
|
134
|
-
1,
|
|
135
|
-
"tx_1".to_string(),
|
|
136
|
-
parent_version.clone(),
|
|
137
|
-
vec![op],
|
|
124
|
+
1, // sequence
|
|
125
|
+
"tx_1".to_string(), // transaction_id
|
|
126
|
+
parent_version.clone(), // parent_version
|
|
127
|
+
vec![op], // operations
|
|
138
128
|
ConsistencyContract::local(),
|
|
139
129
|
)
|
|
140
130
|
.expect("execute_local_transaction on node_a");
|
|
141
131
|
|
|
142
132
|
// ===== Node A sends ReplicationMessage to Node B =====
|
|
143
|
-
let replication_msg =
|
|
144
|
-
envelope,
|
|
145
|
-
"node_a".to_string(),
|
|
146
|
-
"node_b".to_string(),
|
|
147
|
-
1,
|
|
148
|
-
);
|
|
133
|
+
let replication_msg =
|
|
134
|
+
ReplicationMessage::new(envelope, "node_a".to_string(), "node_b".to_string(), 1);
|
|
149
135
|
|
|
150
136
|
node_a_transport
|
|
151
137
|
.send(replication_msg)
|
|
@@ -156,10 +142,7 @@ mod tests {
|
|
|
156
142
|
sleep(Duration::from_millis(200)).await;
|
|
157
143
|
|
|
158
144
|
// ===== Node B receives ReplicationMessage =====
|
|
159
|
-
let received_messages = node_b_transport
|
|
160
|
-
.receive()
|
|
161
|
-
.await
|
|
162
|
-
.expect("receive on node_b");
|
|
145
|
+
let received_messages = node_b_transport.receive().await.expect("receive on node_b");
|
|
163
146
|
|
|
164
147
|
assert_eq!(
|
|
165
148
|
received_messages.len(),
|
|
@@ -247,9 +230,7 @@ mod tests {
|
|
|
247
230
|
if start.elapsed() > timeout {
|
|
248
231
|
panic!(
|
|
249
232
|
"{} startup read timeout after {:?}: got {:?}",
|
|
250
|
-
node_name,
|
|
251
|
-
timeout,
|
|
252
|
-
line
|
|
233
|
+
node_name, timeout, line
|
|
253
234
|
);
|
|
254
235
|
}
|
|
255
236
|
thread::sleep(Duration::from_millis(50));
|
|
@@ -387,19 +368,15 @@ mod tests {
|
|
|
387
368
|
let initial_state_hash = StateHash::from_hex("hash_initial".to_string());
|
|
388
369
|
|
|
389
370
|
// Node A (Server)
|
|
390
|
-
let mut node_a_executor =
|
|
391
|
-
"node_a".to_string(),
|
|
392
|
-
initial_state_hash.clone(),
|
|
393
|
-
);
|
|
371
|
+
let mut node_a_executor =
|
|
372
|
+
DistributedTransactionExecutor::new("node_a".to_string(), initial_state_hash.clone());
|
|
394
373
|
node_a_executor.register_replica("node_b".to_string(), initial_state_hash.clone());
|
|
395
374
|
|
|
396
375
|
let mut node_a_transport = TcpTransport::new("127.0.0.1:19050".to_string());
|
|
397
376
|
|
|
398
377
|
// Node B (Client)
|
|
399
|
-
let mut node_b_executor =
|
|
400
|
-
"node_b".to_string(),
|
|
401
|
-
initial_state_hash.clone(),
|
|
402
|
-
);
|
|
378
|
+
let mut node_b_executor =
|
|
379
|
+
DistributedTransactionExecutor::new("node_b".to_string(), initial_state_hash.clone());
|
|
403
380
|
node_b_executor.register_replica("node_a".to_string(), initial_state_hash.clone());
|
|
404
381
|
|
|
405
382
|
let mut node_b_transport = TcpTransport::new("127.0.0.1:19051".to_string());
|
|
@@ -431,7 +408,10 @@ mod tests {
|
|
|
431
408
|
sleep(Duration::from_millis(50)).await;
|
|
432
409
|
attempts += 1;
|
|
433
410
|
}
|
|
434
|
-
assert!(
|
|
411
|
+
assert!(
|
|
412
|
+
node_a_transport.is_connected(),
|
|
413
|
+
"Server should be connected"
|
|
414
|
+
);
|
|
435
415
|
println!("✓ Connection established\n");
|
|
436
416
|
|
|
437
417
|
// === STEP 2-4: Execute and replicate transaction 1 ===
|
|
@@ -468,18 +448,13 @@ mod tests {
|
|
|
468
448
|
println!("✓ Transaction 1 executed on Node A\n");
|
|
469
449
|
|
|
470
450
|
println!("STEP 3: Replicate transaction 1 to Node B...");
|
|
471
|
-
let msg1 =
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
.await
|
|
475
|
-
.expect("send tx1");
|
|
451
|
+
let msg1 =
|
|
452
|
+
ReplicationMessage::new(envelope1, "node_a".to_string(), "node_b".to_string(), 1);
|
|
453
|
+
node_a_transport.send(msg1).await.expect("send tx1");
|
|
476
454
|
|
|
477
455
|
sleep(Duration::from_millis(200)).await;
|
|
478
456
|
|
|
479
|
-
let received1 = node_b_transport
|
|
480
|
-
.receive()
|
|
481
|
-
.await
|
|
482
|
-
.expect("receive tx1");
|
|
457
|
+
let received1 = node_b_transport.receive().await.expect("receive tx1");
|
|
483
458
|
assert_eq!(received1.len(), 1);
|
|
484
459
|
|
|
485
460
|
let mut parent_vc_b = VectorClock::new();
|
|
@@ -503,9 +478,10 @@ mod tests {
|
|
|
503
478
|
state_a_after_tx1.state_hash, state_b_after_tx1.state_hash,
|
|
504
479
|
"State hashes should match after tx1"
|
|
505
480
|
);
|
|
506
|
-
println!(
|
|
507
|
-
|
|
508
|
-
state_b_after_tx1.state_hash
|
|
481
|
+
println!(
|
|
482
|
+
"✓ State converged: {} == {}\n",
|
|
483
|
+
state_a_after_tx1.state_hash, state_b_after_tx1.state_hash
|
|
484
|
+
);
|
|
509
485
|
|
|
510
486
|
// === STEP 5-6: Close connection deliberately ===
|
|
511
487
|
println!("STEP 5: Deliberately closing TCP connection...");
|
|
@@ -520,8 +496,14 @@ mod tests {
|
|
|
520
496
|
println!("✓ Connection closed\n");
|
|
521
497
|
|
|
522
498
|
println!("STEP 6: Verify both nodes detect disconnect...");
|
|
523
|
-
assert!(
|
|
524
|
-
|
|
499
|
+
assert!(
|
|
500
|
+
!node_a_transport.is_connected(),
|
|
501
|
+
"Node A should be disconnected"
|
|
502
|
+
);
|
|
503
|
+
assert!(
|
|
504
|
+
!node_b_transport.is_connected(),
|
|
505
|
+
"Node B should be disconnected"
|
|
506
|
+
);
|
|
525
507
|
println!("✓ Both nodes detected disconnect\n");
|
|
526
508
|
|
|
527
509
|
// === STEP 7: Re-establish connection ===
|
|
@@ -563,7 +545,8 @@ mod tests {
|
|
|
563
545
|
let mut parent_vc_tx2 = VectorClock::new();
|
|
564
546
|
parent_vc_tx2.increment("node_a");
|
|
565
547
|
parent_vc_tx2.increment("node_b"); // Include B's history
|
|
566
|
-
let parent_version_tx2 =
|
|
548
|
+
let parent_version_tx2 =
|
|
549
|
+
StateVersion::new(parent_vc_tx2, state_a_after_tx1.state_hash.to_string());
|
|
567
550
|
|
|
568
551
|
let mut fields2 = HashMap::new();
|
|
569
552
|
fields2.insert("tx".to_string(), json!("2"));
|
|
@@ -593,24 +576,20 @@ mod tests {
|
|
|
593
576
|
println!("✓ Transaction 2 executed on Node A\n");
|
|
594
577
|
|
|
595
578
|
println!("STEP 9: Replicate transaction 2 to Node B...");
|
|
596
|
-
let msg2 =
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
.await
|
|
600
|
-
.expect("send tx2");
|
|
579
|
+
let msg2 =
|
|
580
|
+
ReplicationMessage::new(envelope2, "node_a".to_string(), "node_b".to_string(), 2);
|
|
581
|
+
node_a_transport_v2.send(msg2).await.expect("send tx2");
|
|
601
582
|
|
|
602
583
|
sleep(Duration::from_millis(200)).await;
|
|
603
584
|
|
|
604
|
-
let received2 = node_b_transport
|
|
605
|
-
.receive()
|
|
606
|
-
.await
|
|
607
|
-
.expect("receive tx2");
|
|
585
|
+
let received2 = node_b_transport.receive().await.expect("receive tx2");
|
|
608
586
|
assert_eq!(received2.len(), 1);
|
|
609
587
|
|
|
610
588
|
let mut parent_vc_b_tx2 = VectorClock::new();
|
|
611
589
|
parent_vc_b_tx2.increment("node_a");
|
|
612
590
|
parent_vc_b_tx2.increment("node_b");
|
|
613
|
-
let parent_version_b_tx2 =
|
|
591
|
+
let parent_version_b_tx2 =
|
|
592
|
+
StateVersion::new(parent_vc_b_tx2, state_b_after_tx1.state_hash.to_string());
|
|
614
593
|
|
|
615
594
|
node_b_executor
|
|
616
595
|
.receive_replicated_transaction(received2[0].clone(), parent_version_b_tx2)
|
|
@@ -637,10 +616,14 @@ mod tests {
|
|
|
637
616
|
state_a_final.operations_applied >= 2,
|
|
638
617
|
"Should have applied at least 2 transactions"
|
|
639
618
|
);
|
|
640
|
-
println!(
|
|
641
|
-
|
|
642
|
-
state_b_final.state_hash
|
|
643
|
-
|
|
619
|
+
println!(
|
|
620
|
+
"✓ Final convergence verified: {} == {}",
|
|
621
|
+
state_a_final.state_hash, state_b_final.state_hash
|
|
622
|
+
);
|
|
623
|
+
println!(
|
|
624
|
+
"✓ Operations applied: {}\n",
|
|
625
|
+
state_a_final.operations_applied
|
|
626
|
+
);
|
|
644
627
|
|
|
645
628
|
// Verify metrics
|
|
646
629
|
assert!(
|
|
@@ -677,18 +660,14 @@ mod tests {
|
|
|
677
660
|
println!("PHASE 1: Initial setup and first convergence\n");
|
|
678
661
|
|
|
679
662
|
// Node A (Server)
|
|
680
|
-
let mut node_a_executor =
|
|
681
|
-
"node_a".to_string(),
|
|
682
|
-
initial_state_hash.clone(),
|
|
683
|
-
);
|
|
663
|
+
let mut node_a_executor =
|
|
664
|
+
DistributedTransactionExecutor::new("node_a".to_string(), initial_state_hash.clone());
|
|
684
665
|
node_a_executor.register_replica("node_b".to_string(), initial_state_hash.clone());
|
|
685
666
|
let mut node_a_transport = TcpTransport::new("127.0.0.1:19060".to_string());
|
|
686
667
|
|
|
687
668
|
// Node B (Client) - will be restarted later
|
|
688
|
-
let mut node_b_executor =
|
|
689
|
-
"node_b".to_string(),
|
|
690
|
-
initial_state_hash.clone(),
|
|
691
|
-
);
|
|
669
|
+
let mut node_b_executor =
|
|
670
|
+
DistributedTransactionExecutor::new("node_b".to_string(), initial_state_hash.clone());
|
|
692
671
|
node_b_executor.register_replica("node_a".to_string(), initial_state_hash.clone());
|
|
693
672
|
let mut node_b_transport = TcpTransport::new("127.0.0.1:19061".to_string());
|
|
694
673
|
|
|
@@ -716,7 +695,10 @@ mod tests {
|
|
|
716
695
|
sleep(Duration::from_millis(50)).await;
|
|
717
696
|
attempts += 1;
|
|
718
697
|
}
|
|
719
|
-
assert!(
|
|
698
|
+
assert!(
|
|
699
|
+
node_a_transport.is_connected(),
|
|
700
|
+
"Server should be connected"
|
|
701
|
+
);
|
|
720
702
|
println!("✓ Connection established\n");
|
|
721
703
|
|
|
722
704
|
// Execute and replicate tx1
|
|
@@ -753,19 +735,14 @@ mod tests {
|
|
|
753
735
|
println!("✓ Transaction 1 executed\n");
|
|
754
736
|
|
|
755
737
|
println!("Step 3: Replicate transaction 1 to Node B...");
|
|
756
|
-
let msg1 =
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
.await
|
|
760
|
-
.expect("send tx1");
|
|
738
|
+
let msg1 =
|
|
739
|
+
ReplicationMessage::new(envelope1, "node_a".to_string(), "node_b".to_string(), 1);
|
|
740
|
+
node_a_transport.send(msg1).await.expect("send tx1");
|
|
761
741
|
|
|
762
742
|
sleep(Duration::from_millis(100)).await;
|
|
763
743
|
|
|
764
744
|
// Receive and apply on B
|
|
765
|
-
let received = node_b_transport
|
|
766
|
-
.receive()
|
|
767
|
-
.await
|
|
768
|
-
.expect("receive tx1");
|
|
745
|
+
let received = node_b_transport.receive().await.expect("receive tx1");
|
|
769
746
|
assert_eq!(received.len(), 1, "should receive 1 message");
|
|
770
747
|
|
|
771
748
|
let parent_vc = VectorClock::new();
|
|
@@ -804,7 +781,10 @@ mod tests {
|
|
|
804
781
|
state_b_before.operations_applied, 1,
|
|
805
782
|
"node_b should have applied 1 operation"
|
|
806
783
|
);
|
|
807
|
-
println!(
|
|
784
|
+
println!(
|
|
785
|
+
"✓ State converged: {} == {}\n",
|
|
786
|
+
state_a_before.state_hash, state_b_before.state_hash
|
|
787
|
+
);
|
|
808
788
|
|
|
809
789
|
// ===== PHASE 2: Node B Offline (Restart Simulation) =====
|
|
810
790
|
println!("PHASE 2: Node B goes offline, A continues\n");
|
|
@@ -816,8 +796,10 @@ mod tests {
|
|
|
816
796
|
state_b_before.operations_applied,
|
|
817
797
|
state_b_before.vector_clock.clone(),
|
|
818
798
|
);
|
|
819
|
-
println!(
|
|
820
|
-
|
|
799
|
+
println!(
|
|
800
|
+
"✓ Persisted Node B state: ops={}, hash={}\n",
|
|
801
|
+
persisted_state_b.1, persisted_state_b.0
|
|
802
|
+
);
|
|
821
803
|
|
|
822
804
|
// Drop node_b transport/executor to simulate shutdown
|
|
823
805
|
println!("Step 6: STOPPING Node B (complete offline)...");
|
|
@@ -861,27 +843,29 @@ mod tests {
|
|
|
861
843
|
let state_a_before_restart = node_a_executor
|
|
862
844
|
.get_replica_state("node_a")
|
|
863
845
|
.expect("get node_a state");
|
|
864
|
-
println!(
|
|
865
|
-
|
|
866
|
-
state_a_before_restart.state_hash
|
|
846
|
+
println!(
|
|
847
|
+
"\n✓ Node A advanced to: ops={}, hash={}\n",
|
|
848
|
+
state_a_before_restart.operations_applied, state_a_before_restart.state_hash
|
|
849
|
+
);
|
|
867
850
|
|
|
868
851
|
// ===== PHASE 3: Node B Restart & Reconnection =====
|
|
869
852
|
println!("PHASE 3: Restart Node B from persisted state\n");
|
|
870
853
|
|
|
871
854
|
println!("Step 8: RESTARTING Node B from persisted state...");
|
|
872
855
|
// Recreate node_b_executor with persisted state
|
|
873
|
-
let mut node_b_executor_restarted =
|
|
874
|
-
"node_b".to_string(),
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
node_b_executor_restarted.register_replica("node_a".to_string(), persisted_state_b.0.clone());
|
|
856
|
+
let mut node_b_executor_restarted =
|
|
857
|
+
DistributedTransactionExecutor::new("node_b".to_string(), persisted_state_b.0.clone());
|
|
858
|
+
node_b_executor_restarted
|
|
859
|
+
.register_replica("node_a".to_string(), persisted_state_b.0.clone());
|
|
878
860
|
let mut node_b_transport_restarted = TcpTransport::new("127.0.0.1:0".to_string());
|
|
879
861
|
|
|
880
862
|
// Recreate Node A transport v2 to accept new connection (fresh listen)
|
|
881
863
|
let mut node_a_transport_v2 = TcpTransport::new("127.0.0.1:19063".to_string());
|
|
882
864
|
|
|
883
|
-
println!(
|
|
884
|
-
|
|
865
|
+
println!(
|
|
866
|
+
"✓ Node B restarted with persisted state: ops={}, hash={}\n",
|
|
867
|
+
persisted_state_b.1, persisted_state_b.0
|
|
868
|
+
);
|
|
885
869
|
|
|
886
870
|
println!("Step 9: Attempting reconnect to Node A (new port 19063)...");
|
|
887
871
|
|
|
@@ -897,8 +881,9 @@ mod tests {
|
|
|
897
881
|
|
|
898
882
|
let reconnect_result = tokio::time::timeout(
|
|
899
883
|
Duration::from_millis(500),
|
|
900
|
-
node_b_transport_restarted.connect("127.0.0.1:19063")
|
|
901
|
-
)
|
|
884
|
+
node_b_transport_restarted.connect("127.0.0.1:19063"),
|
|
885
|
+
)
|
|
886
|
+
.await;
|
|
902
887
|
|
|
903
888
|
match reconnect_result {
|
|
904
889
|
Ok(Ok(())) => {
|
|
@@ -920,11 +905,8 @@ mod tests {
|
|
|
920
905
|
|
|
921
906
|
// Try to receive any messages from Node A
|
|
922
907
|
let timeout = Duration::from_millis(500);
|
|
923
|
-
let received_after_restart =
|
|
924
|
-
timeout,
|
|
925
|
-
node_b_transport_restarted.receive()
|
|
926
|
-
)
|
|
927
|
-
.await;
|
|
908
|
+
let received_after_restart =
|
|
909
|
+
tokio::time::timeout(timeout, node_b_transport_restarted.receive()).await;
|
|
928
910
|
|
|
929
911
|
let recovery_result = match received_after_restart {
|
|
930
912
|
Ok(Ok(msgs)) => {
|
|
@@ -934,8 +916,10 @@ mod tests {
|
|
|
934
916
|
} else {
|
|
935
917
|
println!(" → Received {} message(s) from Node A", msgs.len());
|
|
936
918
|
for msg in &msgs {
|
|
937
|
-
println!(
|
|
938
|
-
|
|
919
|
+
println!(
|
|
920
|
+
" Message from {} to {} (seq {})",
|
|
921
|
+
msg.from_node, msg.to_node, msg.sequence_number
|
|
922
|
+
);
|
|
939
923
|
}
|
|
940
924
|
"received_messages"
|
|
941
925
|
}
|
|
@@ -945,7 +929,10 @@ mod tests {
|
|
|
945
929
|
"transport_error"
|
|
946
930
|
}
|
|
947
931
|
Err(_) => {
|
|
948
|
-
println!(
|
|
932
|
+
println!(
|
|
933
|
+
" → Timeout: No messages received within {} ms",
|
|
934
|
+
timeout.as_millis()
|
|
935
|
+
);
|
|
949
936
|
"timeout"
|
|
950
937
|
}
|
|
951
938
|
};
|
|
@@ -978,7 +965,10 @@ mod tests {
|
|
|
978
965
|
if converged && state_a_final.operations_applied == state_b_final.operations_applied {
|
|
979
966
|
println!("✅ OUTCOME A: Protocol has built-in recovery semantics");
|
|
980
967
|
println!(" - Node B recovered from offline state");
|
|
981
|
-
println!(
|
|
968
|
+
println!(
|
|
969
|
+
" - All {} transactions replicated and applied",
|
|
970
|
+
state_a_final.operations_applied
|
|
971
|
+
);
|
|
982
972
|
println!(" - Final state converged");
|
|
983
973
|
println!(" - No duplicates detected");
|
|
984
974
|
println!(" FINDING: 14C handles crash recovery internally");
|
|
@@ -987,22 +977,35 @@ mod tests {
|
|
|
987
977
|
println!(" - Node B re-applied tx1 after restart");
|
|
988
978
|
println!(" - Missing tx2, tx3, tx4");
|
|
989
979
|
println!(" - State diverged");
|
|
990
|
-
println!(
|
|
980
|
+
println!(
|
|
981
|
+
" FINDING: Protocol lacks durability across restart; dedup is in-memory only"
|
|
982
|
+
);
|
|
991
983
|
} else if behind {
|
|
992
984
|
println!("⚠️ OUTCOME B: Protocol lacks recovery mechanism");
|
|
993
985
|
println!(" - Node B restarted with 1 operation");
|
|
994
|
-
println!(
|
|
986
|
+
println!(
|
|
987
|
+
" - Node A has {} operations",
|
|
988
|
+
state_a_final.operations_applied
|
|
989
|
+
);
|
|
995
990
|
println!(" - No recovery messages received");
|
|
996
|
-
println!(
|
|
997
|
-
|
|
991
|
+
println!(
|
|
992
|
+
" - State diverged: B={} vs A={}",
|
|
993
|
+
state_b_final.state_hash, state_a_final.state_hash
|
|
994
|
+
);
|
|
998
995
|
println!(" FINDING: Recovery mechanism not built into 14C protocol");
|
|
999
|
-
println!(
|
|
996
|
+
println!(
|
|
997
|
+
" Must be added in application layer or as separate protocol extension"
|
|
998
|
+
);
|
|
1000
999
|
} else {
|
|
1001
1000
|
println!("🔍 OUTCOME D: Unexpected state");
|
|
1002
|
-
println!(
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1001
|
+
println!(
|
|
1002
|
+
" B ops: {} | A ops: {}",
|
|
1003
|
+
state_b_final.operations_applied, state_a_final.operations_applied
|
|
1004
|
+
);
|
|
1005
|
+
println!(
|
|
1006
|
+
" B hash: {} | A hash: {}",
|
|
1007
|
+
state_b_final.state_hash, state_a_final.state_hash
|
|
1008
|
+
);
|
|
1006
1009
|
}
|
|
1007
1010
|
|
|
1008
1011
|
println!("\n📋 Summary:");
|
|
@@ -1024,16 +1027,12 @@ mod tests {
|
|
|
1024
1027
|
let initial_state_hash = StateHash::from_hex("hash_initial".to_string());
|
|
1025
1028
|
|
|
1026
1029
|
// Setup two nodes and converge
|
|
1027
|
-
let mut executor_a =
|
|
1028
|
-
"node_a".to_string(),
|
|
1029
|
-
initial_state_hash.clone(),
|
|
1030
|
-
);
|
|
1030
|
+
let mut executor_a =
|
|
1031
|
+
DistributedTransactionExecutor::new("node_a".to_string(), initial_state_hash.clone());
|
|
1031
1032
|
executor_a.register_replica("node_b".to_string(), initial_state_hash.clone());
|
|
1032
1033
|
|
|
1033
|
-
let mut executor_b =
|
|
1034
|
-
"node_b".to_string(),
|
|
1035
|
-
initial_state_hash.clone(),
|
|
1036
|
-
);
|
|
1034
|
+
let mut executor_b =
|
|
1035
|
+
DistributedTransactionExecutor::new("node_b".to_string(), initial_state_hash.clone());
|
|
1037
1036
|
executor_b.register_replica("node_a".to_string(), initial_state_hash.clone());
|
|
1038
1037
|
|
|
1039
1038
|
// Execute and replicate multiple transactions
|
|
@@ -1088,16 +1087,28 @@ mod tests {
|
|
|
1088
1087
|
let state_b = executor_b.get_replica_state("node_b").unwrap();
|
|
1089
1088
|
|
|
1090
1089
|
println!("After 3 transactions:");
|
|
1091
|
-
println!(
|
|
1092
|
-
|
|
1090
|
+
println!(
|
|
1091
|
+
" Node A: {} ops, hash={}",
|
|
1092
|
+
state_a.operations_applied, state_a.state_hash
|
|
1093
|
+
);
|
|
1094
|
+
println!(
|
|
1095
|
+
" Node B: {} ops, hash={}",
|
|
1096
|
+
state_b.operations_applied, state_b.state_hash
|
|
1097
|
+
);
|
|
1093
1098
|
|
|
1094
1099
|
// Analyze what would need to be persisted
|
|
1095
1100
|
println!("\nCandidate durable state:");
|
|
1096
1101
|
println!(" ✓ Operations (records of what was executed)");
|
|
1097
1102
|
println!(" - Sequence numbers (1, 2, 3)");
|
|
1098
1103
|
println!(" - Transaction IDs (tx_1, tx_2, tx_3)");
|
|
1099
|
-
println!(
|
|
1100
|
-
|
|
1104
|
+
println!(
|
|
1105
|
+
" ✓ Last applied sequence number: {}",
|
|
1106
|
+
state_a.operations_applied
|
|
1107
|
+
);
|
|
1108
|
+
println!(
|
|
1109
|
+
" ? State hash: {} (derived from ops, or durable?)",
|
|
1110
|
+
state_a.state_hash
|
|
1111
|
+
);
|
|
1101
1112
|
println!(" ? Vector clocks: (needed for causal order?)");
|
|
1102
1113
|
println!(" ? EnvelopeId set (for dedup: which envelopes seen?)");
|
|
1103
1114
|
|
|
@@ -1126,17 +1137,15 @@ mod tests {
|
|
|
1126
1137
|
|
|
1127
1138
|
let initial_state_hash = StateHash::from_hex("hash_initial".to_string());
|
|
1128
1139
|
|
|
1129
|
-
let mut executor =
|
|
1130
|
-
"node_b".to_string(),
|
|
1131
|
-
initial_state_hash.clone(),
|
|
1132
|
-
);
|
|
1140
|
+
let mut executor =
|
|
1141
|
+
DistributedTransactionExecutor::new("node_b".to_string(), initial_state_hash.clone());
|
|
1133
1142
|
|
|
1134
1143
|
// Simulate: "Here's what we persisted before going offline"
|
|
1135
1144
|
let persisted_data = (
|
|
1136
|
-
"node_b",
|
|
1137
|
-
initial_state_hash.clone(),
|
|
1138
|
-
2u64,
|
|
1139
|
-
vec!["tx_1", "tx_2"],
|
|
1145
|
+
"node_b", // node_id
|
|
1146
|
+
initial_state_hash.clone(), // last_state_hash
|
|
1147
|
+
2u64, // last_sequence_applied
|
|
1148
|
+
vec!["tx_1", "tx_2"], // operation ids
|
|
1140
1149
|
);
|
|
1141
1150
|
|
|
1142
1151
|
println!("Persisted state:");
|
|
@@ -1341,7 +1350,9 @@ mod tests {
|
|
|
1341
1350
|
println!("Q5: Recovery + 14C interaction?");
|
|
1342
1351
|
println!(" → Must maintain causal order; sequence recovery before new replication\n");
|
|
1343
1352
|
|
|
1344
|
-
println!(
|
|
1353
|
+
println!(
|
|
1354
|
+
"Next: Phase 1b.5 will implement minimal recovery protocol based on these answers."
|
|
1355
|
+
);
|
|
1345
1356
|
println!("Not before: Must understand what recovery must guarantee.\n");
|
|
1346
1357
|
}
|
|
1347
1358
|
|
|
@@ -1402,7 +1413,10 @@ mod tests {
|
|
|
1402
1413
|
|
|
1403
1414
|
let (seq_a_initial, ops_a_initial) = recovery_a.recovery_state_snapshot();
|
|
1404
1415
|
let (seq_b_initial, ops_b_initial) = recovery_b.recovery_state_snapshot();
|
|
1405
|
-
assert_eq!(
|
|
1416
|
+
assert_eq!(
|
|
1417
|
+
seq_a_initial, seq_b_initial,
|
|
1418
|
+
"A and B should converge on tx1"
|
|
1419
|
+
);
|
|
1406
1420
|
assert_eq!(ops_a_initial, ops_b_initial, "Same op count");
|
|
1407
1421
|
|
|
1408
1422
|
// Simulate offline: B stops, A executes tx2
|
|
@@ -1436,11 +1450,15 @@ mod tests {
|
|
|
1436
1450
|
"Same operation count"
|
|
1437
1451
|
);
|
|
1438
1452
|
|
|
1453
|
+
println!("✓ Invariant proven: continuous ≡ offline + recovery");
|
|
1454
|
+
println!(
|
|
1455
|
+
"✓ A sequence: {}, B sequence after recovery: {}",
|
|
1456
|
+
seq_a_after_tx2, seq_b_after_recovery
|
|
1457
|
+
);
|
|
1439
1458
|
println!(
|
|
1440
|
-
"✓
|
|
1459
|
+
"✓ A operations: {}, B operations: {}",
|
|
1460
|
+
ops_a_after_tx2, ops_b_after_recovery
|
|
1441
1461
|
);
|
|
1442
|
-
println!("✓ A sequence: {}, B sequence after recovery: {}", seq_a_after_tx2, seq_b_after_recovery);
|
|
1443
|
-
println!("✓ A operations: {}, B operations: {}", ops_a_after_tx2, ops_b_after_recovery);
|
|
1444
1462
|
}
|
|
1445
1463
|
|
|
1446
1464
|
#[test]
|
|
@@ -1487,25 +1505,15 @@ mod tests {
|
|
|
1487
1505
|
seq_a_final, seq_b_final,
|
|
1488
1506
|
"Multiple recovered operations preserve convergence"
|
|
1489
1507
|
);
|
|
1490
|
-
assert_eq!(
|
|
1491
|
-
ops_a_final, ops_b_final,
|
|
1492
|
-
"Same operation count"
|
|
1493
|
-
);
|
|
1508
|
+
assert_eq!(ops_a_final, ops_b_final, "Same operation count");
|
|
1494
1509
|
|
|
1510
|
+
println!("✓ Invariant: continuous ≡ offline + recovery (4 operations)");
|
|
1495
1511
|
println!(
|
|
1496
|
-
"✓
|
|
1497
|
-
|
|
1498
|
-
println!("✓ A operations: {}, B recovered: {}", ops_a_final, ops_b_final);
|
|
1499
|
-
assert_eq!(
|
|
1500
|
-
ops_a_final,
|
|
1501
|
-
4,
|
|
1502
|
-
"A should have 4 operations"
|
|
1503
|
-
);
|
|
1504
|
-
assert_eq!(
|
|
1505
|
-
ops_b_final,
|
|
1506
|
-
4,
|
|
1507
|
-
"B should have 4 operations after recovery"
|
|
1512
|
+
"✓ A operations: {}, B recovered: {}",
|
|
1513
|
+
ops_a_final, ops_b_final
|
|
1508
1514
|
);
|
|
1515
|
+
assert_eq!(ops_a_final, 4, "A should have 4 operations");
|
|
1516
|
+
assert_eq!(ops_b_final, 4, "B should have 4 operations after recovery");
|
|
1509
1517
|
}
|
|
1510
1518
|
|
|
1511
1519
|
#[test]
|
|
@@ -1549,10 +1557,18 @@ mod tests {
|
|
|
1549
1557
|
}
|
|
1550
1558
|
|
|
1551
1559
|
// Verify: dedup prevented double-apply
|
|
1552
|
-
assert_eq!(
|
|
1560
|
+
assert_eq!(
|
|
1561
|
+
recovery_b.operation_log.len(),
|
|
1562
|
+
4,
|
|
1563
|
+
"B should have 4 ops, not 5"
|
|
1564
|
+
);
|
|
1553
1565
|
|
|
1554
1566
|
println!("✓ Recovery maintains sequence integrity");
|
|
1555
|
-
println!(
|
|
1567
|
+
println!(
|
|
1568
|
+
"✓ A operations: {}, B after recovery: {}",
|
|
1569
|
+
recovery_a.operation_log.len(),
|
|
1570
|
+
recovery_b.operation_log.len()
|
|
1571
|
+
);
|
|
1556
1572
|
}
|
|
1557
1573
|
|
|
1558
1574
|
#[test]
|
|
@@ -1583,21 +1599,35 @@ mod tests {
|
|
|
1583
1599
|
|
|
1584
1600
|
// Recovery begins: B requests tx3, tx4, tx5
|
|
1585
1601
|
let recovered_ops = recovery_a.get_operations_since(last_seq_b_start);
|
|
1586
|
-
assert_eq!(
|
|
1602
|
+
assert_eq!(
|
|
1603
|
+
recovered_ops.len(),
|
|
1604
|
+
3,
|
|
1605
|
+
"Should need to recover 3 operations"
|
|
1606
|
+
);
|
|
1587
1607
|
|
|
1588
1608
|
// Simulate interruption: apply tx3 only, then B crashes
|
|
1589
1609
|
recovery_b.log_operation("tx3".to_string(), 3);
|
|
1590
1610
|
let (last_seq_b_after_partial, _) = recovery_b.recovery_state_snapshot();
|
|
1591
|
-
assert_eq!(
|
|
1611
|
+
assert_eq!(
|
|
1612
|
+
last_seq_b_after_partial, 3,
|
|
1613
|
+
"B should have seq 3 after partial recovery"
|
|
1614
|
+
);
|
|
1592
1615
|
|
|
1593
1616
|
// B RESTARTS from persisted state
|
|
1594
1617
|
recovery_b.restore_from_log();
|
|
1595
1618
|
let (last_seq_b_after_crash, _) = recovery_b.recovery_state_snapshot();
|
|
1596
|
-
assert_eq!(
|
|
1619
|
+
assert_eq!(
|
|
1620
|
+
last_seq_b_after_crash, 3,
|
|
1621
|
+
"B should restore to seq 3 after crash"
|
|
1622
|
+
);
|
|
1597
1623
|
|
|
1598
1624
|
// Recovery resumes: B asks for operations after sequence 3
|
|
1599
1625
|
let remaining_ops = recovery_a.get_operations_since(last_seq_b_after_crash);
|
|
1600
|
-
assert_eq!(
|
|
1626
|
+
assert_eq!(
|
|
1627
|
+
remaining_ops.len(),
|
|
1628
|
+
2,
|
|
1629
|
+
"Should need to recover 2 remaining ops"
|
|
1630
|
+
);
|
|
1601
1631
|
|
|
1602
1632
|
for seq in remaining_ops {
|
|
1603
1633
|
recovery_b.log_operation(format!("tx{}", seq), seq);
|
|
@@ -1645,7 +1675,10 @@ mod tests {
|
|
|
1645
1675
|
let (seq_a_diverged, ops_a_diverged) = recovery_a.recovery_state_snapshot();
|
|
1646
1676
|
let (seq_b_offline, ops_b_offline) = recovery_b.recovery_state_snapshot();
|
|
1647
1677
|
|
|
1648
|
-
println!(
|
|
1678
|
+
println!(
|
|
1679
|
+
" A after divergence: seq={}, ops={}",
|
|
1680
|
+
seq_a_diverged, ops_a_diverged
|
|
1681
|
+
);
|
|
1649
1682
|
println!(" B offline: seq={}, ops={}", seq_b_offline, ops_b_offline);
|
|
1650
1683
|
|
|
1651
1684
|
// B restarts and recovers
|
|
@@ -1664,8 +1697,7 @@ mod tests {
|
|
|
1664
1697
|
// INDEPENDENT VERIFICATION: Both nodes have same operations
|
|
1665
1698
|
// Verify by independent state calculation
|
|
1666
1699
|
assert_eq!(
|
|
1667
|
-
ops_a_final,
|
|
1668
|
-
ops_b_final,
|
|
1700
|
+
ops_a_final, ops_b_final,
|
|
1669
1701
|
"Both should have same operation count"
|
|
1670
1702
|
);
|
|
1671
1703
|
|
|
@@ -1679,7 +1711,8 @@ mod tests {
|
|
|
1679
1711
|
|
|
1680
1712
|
// SEMANTIC EQUIVALENCE: Both should have identical state snapshot
|
|
1681
1713
|
assert_eq!(
|
|
1682
|
-
(seq_a_final, ops_a_final),
|
|
1714
|
+
(seq_a_final, ops_a_final),
|
|
1715
|
+
(seq_b_final, ops_b_final),
|
|
1683
1716
|
"Independent recovery produces identical state"
|
|
1684
1717
|
);
|
|
1685
1718
|
|
|
@@ -1687,8 +1720,10 @@ mod tests {
|
|
|
1687
1720
|
"✓ State verification passed: A=(seq={}, ops={}), B=(seq={}, ops={})",
|
|
1688
1721
|
seq_a_final, ops_a_final, seq_b_final, ops_b_final
|
|
1689
1722
|
);
|
|
1690
|
-
println!(
|
|
1691
|
-
|
|
1723
|
+
println!(
|
|
1724
|
+
"✓ Operation sequences identical: {} operations on both",
|
|
1725
|
+
ops_a_final
|
|
1726
|
+
);
|
|
1692
1727
|
println!("✓ Semantic equivalence proven: both nodes identical");
|
|
1693
1728
|
}
|
|
1694
1729
|
|
|
@@ -1787,7 +1822,11 @@ mod tests {
|
|
|
1787
1822
|
}
|
|
1788
1823
|
|
|
1789
1824
|
fn get_recovery_state(&self) -> (u64, usize, usize) {
|
|
1790
|
-
(
|
|
1825
|
+
(
|
|
1826
|
+
self.last_sequence,
|
|
1827
|
+
self.operation_log.len(),
|
|
1828
|
+
self.dedup_set.len(),
|
|
1829
|
+
)
|
|
1791
1830
|
}
|
|
1792
1831
|
|
|
1793
1832
|
fn simulate_crash_and_restart(&mut self) {
|
|
@@ -1799,7 +1838,11 @@ mod tests {
|
|
|
1799
1838
|
self.dedup_set.clear();
|
|
1800
1839
|
}
|
|
1801
1840
|
|
|
1802
|
-
fn restore_from_persisted(
|
|
1841
|
+
fn restore_from_persisted(
|
|
1842
|
+
&mut self,
|
|
1843
|
+
ops: Vec<(String, u64)>,
|
|
1844
|
+
dedup: std::collections::HashSet<String>,
|
|
1845
|
+
) {
|
|
1803
1846
|
self.operation_log = ops;
|
|
1804
1847
|
self.dedup_set = dedup;
|
|
1805
1848
|
if let Some((_, seq)) = self.operation_log.last() {
|
|
@@ -1840,7 +1883,10 @@ mod tests {
|
|
|
1840
1883
|
// Attack: Kill process B (SIGKILL, no graceful shutdown)
|
|
1841
1884
|
recovery_b.simulate_crash_and_restart();
|
|
1842
1885
|
let (seq_b_after_crash, _, _) = recovery_b.get_recovery_state();
|
|
1843
|
-
assert_eq!(
|
|
1886
|
+
assert_eq!(
|
|
1887
|
+
seq_b_after_crash, 0,
|
|
1888
|
+
"B's in-memory state should be cleared after crash"
|
|
1889
|
+
);
|
|
1844
1890
|
|
|
1845
1891
|
// Recover: New process B reads from disk
|
|
1846
1892
|
recovery_b.restore_from_persisted(persisted_ops, persisted_dedup);
|
|
@@ -1850,11 +1896,20 @@ mod tests {
|
|
|
1850
1896
|
// Verify: B recovered from disk, not from memory
|
|
1851
1897
|
assert_eq!(seq_b_restored, 2, "B should restore sequence 2 from disk");
|
|
1852
1898
|
assert_eq!(ops_b_restored, 2, "B should have 2 operations from disk");
|
|
1853
|
-
assert_eq!(
|
|
1854
|
-
|
|
1899
|
+
assert_eq!(
|
|
1900
|
+
dedup_b_restored, 2,
|
|
1901
|
+
"B should have 2 dedup entries from disk"
|
|
1902
|
+
);
|
|
1903
|
+
assert_eq!(
|
|
1904
|
+
seq_b_before_crash, seq_b_restored,
|
|
1905
|
+
"State should survive disk read"
|
|
1906
|
+
);
|
|
1855
1907
|
|
|
1856
1908
|
println!("✓ Process B crashed and restarted from disk");
|
|
1857
|
-
println!(
|
|
1909
|
+
println!(
|
|
1910
|
+
"✓ Recovered state: seq={}, ops={}, dedup={}",
|
|
1911
|
+
seq_b_restored, ops_b_restored, dedup_b_restored
|
|
1912
|
+
);
|
|
1858
1913
|
println!("✓ Invariant: Process crash does not lose persisted state");
|
|
1859
1914
|
}
|
|
1860
1915
|
|
|
@@ -1895,11 +1950,18 @@ mod tests {
|
|
|
1895
1950
|
let (seq_b_restarted, _, _) = recovery_b.get_recovery_state();
|
|
1896
1951
|
|
|
1897
1952
|
// Verify: B is back to its pre-offline state (sequence 2)
|
|
1898
|
-
assert_eq!(
|
|
1953
|
+
assert_eq!(
|
|
1954
|
+
seq_b_restarted, 2,
|
|
1955
|
+
"B should restart with sequence 2 (pre-offline state)"
|
|
1956
|
+
);
|
|
1899
1957
|
|
|
1900
1958
|
// B requests recovery from A for sequence > 2
|
|
1901
1959
|
let missed = recovery_a.get_operations_since(seq_b_restarted);
|
|
1902
|
-
assert_eq!(
|
|
1960
|
+
assert_eq!(
|
|
1961
|
+
missed.len(),
|
|
1962
|
+
2,
|
|
1963
|
+
"B should need to recover 2 operations (tx3, tx4)"
|
|
1964
|
+
);
|
|
1903
1965
|
|
|
1904
1966
|
println!("✓ Process B restarted and recovered from disk (old state cleared)");
|
|
1905
1967
|
println!("✓ B's sequence after restart: {}", seq_b_restarted);
|
|
@@ -2004,11 +2066,17 @@ mod tests {
|
|
|
2004
2066
|
|
|
2005
2067
|
// B restarts (crashes)
|
|
2006
2068
|
recovery_b.simulate_crash_and_restart();
|
|
2007
|
-
assert!(
|
|
2069
|
+
assert!(
|
|
2070
|
+
!recovery_b.is_seen("A:2"),
|
|
2071
|
+
"Dedup set should be cleared after crash"
|
|
2072
|
+
);
|
|
2008
2073
|
|
|
2009
2074
|
// B restarts from disk
|
|
2010
2075
|
recovery_b.restore_from_persisted(persisted_ops, persisted_dedup);
|
|
2011
|
-
assert!(
|
|
2076
|
+
assert!(
|
|
2077
|
+
recovery_b.is_seen("A:2"),
|
|
2078
|
+
"Dedup set should be restored from disk"
|
|
2079
|
+
);
|
|
2012
2080
|
|
|
2013
2081
|
// Attack: A delivers tx2 again (network retry, didn't know B already had it)
|
|
2014
2082
|
let tx2_envelope_id = "A:2";
|
|
@@ -2056,7 +2124,10 @@ mod tests {
|
|
|
2056
2124
|
|
|
2057
2125
|
// Attack: Same recovery request again (network glitch, retry)
|
|
2058
2126
|
let missed_second = recovery_a.get_operations_since(seq_b);
|
|
2059
|
-
assert_eq!(
|
|
2127
|
+
assert_eq!(
|
|
2128
|
+
missed_first, missed_second,
|
|
2129
|
+
"Same recovery request should return same ops"
|
|
2130
|
+
);
|
|
2060
2131
|
|
|
2061
2132
|
// Apply same recovery again (idempotence)
|
|
2062
2133
|
for seq in missed_second.iter() {
|
|
@@ -2073,10 +2144,19 @@ mod tests {
|
|
|
2073
2144
|
assert_eq!(ops_b_1, ops_b_2, "Operation count unchanged");
|
|
2074
2145
|
assert_eq!(dedup_b_1, dedup_b_2, "Dedup count unchanged");
|
|
2075
2146
|
|
|
2076
|
-
println!(
|
|
2077
|
-
|
|
2147
|
+
println!(
|
|
2148
|
+
"✓ First recovery request: {} operations",
|
|
2149
|
+
missed_first.len()
|
|
2150
|
+
);
|
|
2151
|
+
println!(
|
|
2152
|
+
"✓ Second recovery request: {} operations",
|
|
2153
|
+
missed_second.len()
|
|
2154
|
+
);
|
|
2078
2155
|
println!("✓ Dedup prevented re-application");
|
|
2079
|
-
println!(
|
|
2156
|
+
println!(
|
|
2157
|
+
"✓ Final state: seq={}, ops={}, dedup={}",
|
|
2158
|
+
seq_b_idempotent, ops_b_2, dedup_b_2
|
|
2159
|
+
);
|
|
2080
2160
|
println!("✓ Invariant: Recovery is idempotent (same request twice = same result)");
|
|
2081
2161
|
}
|
|
2082
2162
|
|
|
@@ -2125,7 +2205,10 @@ mod tests {
|
|
|
2125
2205
|
recovery_b.restore_from_persisted(persisted_ops, persisted_dedup);
|
|
2126
2206
|
|
|
2127
2207
|
let (seq_b_restored, _, _) = recovery_b.get_recovery_state();
|
|
2128
|
-
assert_eq!(
|
|
2208
|
+
assert_eq!(
|
|
2209
|
+
seq_b_restored, 4,
|
|
2210
|
+
"B should restore recovery position at tx4"
|
|
2211
|
+
);
|
|
2129
2212
|
|
|
2130
2213
|
// B requests recovery from sequence 4 (not 2, not 3)
|
|
2131
2214
|
let remaining = recovery_a.get_operations_since(seq_b_restored);
|
|
@@ -2289,7 +2372,10 @@ mod tests {
|
|
|
2289
2372
|
println!(" Success rate: {:.1}%", success_rate);
|
|
2290
2373
|
println!(" Cycles completed: {}", self.cycles_completed);
|
|
2291
2374
|
println!(" Convergence failures: {}", self.convergence_failures);
|
|
2292
|
-
println!(
|
|
2375
|
+
println!(
|
|
2376
|
+
" Crash recovery failures: {}",
|
|
2377
|
+
self.crash_recovery_failures
|
|
2378
|
+
);
|
|
2293
2379
|
}
|
|
2294
2380
|
|
|
2295
2381
|
println!("\nRECOVERY LATENCY (measured, not inferred)");
|
|
@@ -2301,7 +2387,10 @@ mod tests {
|
|
|
2301
2387
|
println!(" Min: {}ms", min);
|
|
2302
2388
|
println!(" Avg: {}ms", avg);
|
|
2303
2389
|
println!(" Max: {}ms", max);
|
|
2304
|
-
println!(
|
|
2390
|
+
println!(
|
|
2391
|
+
" Samples: {}",
|
|
2392
|
+
self.recovery_latencies_ms.len()
|
|
2393
|
+
);
|
|
2305
2394
|
}
|
|
2306
2395
|
|
|
2307
2396
|
println!("\nREPLAY THROUGHPUT (measured)");
|
|
@@ -2328,14 +2417,17 @@ mod tests {
|
|
|
2328
2417
|
|
|
2329
2418
|
println!("\nDEDUPLICATION STATE (measured)");
|
|
2330
2419
|
if !self.dedup_set_sizes.is_empty() {
|
|
2331
|
-
let avg: usize =
|
|
2332
|
-
/ self.dedup_set_sizes.len();
|
|
2420
|
+
let avg: usize =
|
|
2421
|
+
self.dedup_set_sizes.iter().sum::<usize>() / self.dedup_set_sizes.len();
|
|
2333
2422
|
let min = self.dedup_set_sizes.iter().min().unwrap_or(&0);
|
|
2334
2423
|
let max = self.dedup_set_sizes.iter().max().unwrap_or(&0);
|
|
2335
2424
|
println!(" Min entries: {}", min);
|
|
2336
2425
|
println!(" Avg entries: {}", avg);
|
|
2337
2426
|
println!(" Max entries: {}", max);
|
|
2338
|
-
println!(
|
|
2427
|
+
println!(
|
|
2428
|
+
" Growth characteristic: {:?}",
|
|
2429
|
+
if max > &min { "unbounded" } else { "stable" }
|
|
2430
|
+
);
|
|
2339
2431
|
}
|
|
2340
2432
|
|
|
2341
2433
|
println!("\nOPERATION LOG (measured)");
|
|
@@ -2361,7 +2453,14 @@ mod tests {
|
|
|
2361
2453
|
|
|
2362
2454
|
println!("\nOVERALL STATISTICS");
|
|
2363
2455
|
println!(" Total operations: {}", self.total_operations);
|
|
2364
|
-
println!(
|
|
2456
|
+
println!(
|
|
2457
|
+
" Architectural failures: {}",
|
|
2458
|
+
if self.convergence_failures == 0 && self.crash_recovery_failures == 0 {
|
|
2459
|
+
0
|
|
2460
|
+
} else {
|
|
2461
|
+
1
|
|
2462
|
+
}
|
|
2463
|
+
);
|
|
2365
2464
|
println!("\n=== END SOAK VALIDATION ===\n");
|
|
2366
2465
|
}
|
|
2367
2466
|
}
|
|
@@ -2416,7 +2515,10 @@ mod tests {
|
|
|
2416
2515
|
if final_seq_b == 105 && final_ops_b == 105 {
|
|
2417
2516
|
metrics.record_recovery(recovery_time, throughput, 100);
|
|
2418
2517
|
metrics.record_dedup(final_dedup);
|
|
2419
|
-
println!(
|
|
2518
|
+
println!(
|
|
2519
|
+
" Cycle {}: ✓ converged (seq={}, latency={}ms)",
|
|
2520
|
+
cycle, final_seq_b, recovery_time
|
|
2521
|
+
);
|
|
2420
2522
|
} else {
|
|
2421
2523
|
metrics.record_convergence_failure();
|
|
2422
2524
|
println!(" Cycle {}: ✗ FAILED to converge", cycle);
|
|
@@ -2551,7 +2653,10 @@ mod tests {
|
|
|
2551
2653
|
if final_seq_b == 50 {
|
|
2552
2654
|
metrics.record_recovery(recovery_time, 0, 45);
|
|
2553
2655
|
metrics.record_dedup(dedup_size);
|
|
2554
|
-
println!(
|
|
2656
|
+
println!(
|
|
2657
|
+
" Cycle {}: ✓ recovered after crash (seq={})",
|
|
2658
|
+
cycle, final_seq_b
|
|
2659
|
+
);
|
|
2555
2660
|
} else {
|
|
2556
2661
|
metrics.record_crash_recovery_failure();
|
|
2557
2662
|
println!(" Cycle {}: ✗ FAILED to recover after crash", cycle);
|
|
@@ -2627,11 +2732,7 @@ mod tests {
|
|
|
2627
2732
|
|
|
2628
2733
|
// Execute operations
|
|
2629
2734
|
for i in 1..=*op_count {
|
|
2630
|
-
recovery.persist_operation(
|
|
2631
|
-
format!("tx{}", i),
|
|
2632
|
-
i as u64,
|
|
2633
|
-
format!("A:{}", i),
|
|
2634
|
-
);
|
|
2735
|
+
recovery.persist_operation(format!("tx{}", i), i as u64, format!("A:{}", i));
|
|
2635
2736
|
}
|
|
2636
2737
|
|
|
2637
2738
|
let startup_time = start.elapsed().as_millis();
|
|
@@ -2754,15 +2855,36 @@ mod tests {
|
|
|
2754
2855
|
// Analyze backlog scaling
|
|
2755
2856
|
println!("RECOVERY LATENCY SCALING");
|
|
2756
2857
|
if !self.backlog_10_latency.is_empty() {
|
|
2757
|
-
let avg_10: u128 = self.backlog_10_latency.iter().sum::<u128>()
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2858
|
+
let avg_10: u128 = self.backlog_10_latency.iter().sum::<u128>()
|
|
2859
|
+
/ self.backlog_10_latency.len() as u128;
|
|
2860
|
+
let avg_100: u128 = self.backlog_100_latency.iter().sum::<u128>()
|
|
2861
|
+
/ if self.backlog_100_latency.is_empty() {
|
|
2862
|
+
1
|
|
2863
|
+
} else {
|
|
2864
|
+
self.backlog_100_latency.len() as u128
|
|
2865
|
+
};
|
|
2866
|
+
let avg_1k: u128 = self.backlog_1k_latency.iter().sum::<u128>()
|
|
2867
|
+
/ if self.backlog_1k_latency.is_empty() {
|
|
2868
|
+
1
|
|
2869
|
+
} else {
|
|
2870
|
+
self.backlog_1k_latency.len() as u128
|
|
2871
|
+
};
|
|
2762
2872
|
|
|
2763
|
-
println!(
|
|
2764
|
-
|
|
2765
|
-
|
|
2873
|
+
println!(
|
|
2874
|
+
" 10 ops: {}ms (avg, {} samples)",
|
|
2875
|
+
avg_10,
|
|
2876
|
+
self.backlog_10_latency.len()
|
|
2877
|
+
);
|
|
2878
|
+
println!(
|
|
2879
|
+
" 100 ops: {}ms (avg, {} samples)",
|
|
2880
|
+
avg_100,
|
|
2881
|
+
self.backlog_100_latency.len()
|
|
2882
|
+
);
|
|
2883
|
+
println!(
|
|
2884
|
+
" 1,000 ops: {}ms (avg, {} samples)",
|
|
2885
|
+
avg_1k,
|
|
2886
|
+
self.backlog_1k_latency.len()
|
|
2887
|
+
);
|
|
2766
2888
|
|
|
2767
2889
|
// Detect scaling characteristic
|
|
2768
2890
|
if avg_1k > 0 && avg_10 > 0 {
|
|
@@ -2785,24 +2907,40 @@ mod tests {
|
|
|
2785
2907
|
// Analyze throughput
|
|
2786
2908
|
println!("\nRECOVERY REPLAY THROUGHPUT");
|
|
2787
2909
|
if !self.backlog_10_throughput.is_empty() {
|
|
2788
|
-
let avg_10: u64 = self.backlog_10_throughput.iter().sum::<u64>()
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2910
|
+
let avg_10: u64 = self.backlog_10_throughput.iter().sum::<u64>()
|
|
2911
|
+
/ self.backlog_10_throughput.len() as u64;
|
|
2912
|
+
let avg_100: u64 = self.backlog_100_throughput.iter().sum::<u64>()
|
|
2913
|
+
/ if self.backlog_100_throughput.is_empty() {
|
|
2914
|
+
1
|
|
2915
|
+
} else {
|
|
2916
|
+
self.backlog_100_throughput.len() as u64
|
|
2917
|
+
};
|
|
2918
|
+
let avg_1k: u64 = self.backlog_1k_throughput.iter().sum::<u64>()
|
|
2919
|
+
/ if self.backlog_1k_throughput.is_empty() {
|
|
2920
|
+
1
|
|
2921
|
+
} else {
|
|
2922
|
+
self.backlog_1k_throughput.len() as u64
|
|
2923
|
+
};
|
|
2793
2924
|
|
|
2794
2925
|
println!(" 10 ops: {} ops/sec", avg_10);
|
|
2795
2926
|
println!(" 100 ops: {} ops/sec", avg_100);
|
|
2796
2927
|
println!(" 1,000 ops: {} ops/sec", avg_1k);
|
|
2797
|
-
println!(
|
|
2798
|
-
|
|
2928
|
+
println!(
|
|
2929
|
+
" Throughput trend: {}",
|
|
2930
|
+
if avg_1k >= avg_100 {
|
|
2931
|
+
"stable"
|
|
2932
|
+
} else {
|
|
2933
|
+
"degrading"
|
|
2934
|
+
}
|
|
2935
|
+
);
|
|
2799
2936
|
}
|
|
2800
2937
|
|
|
2801
2938
|
// Analyze startup/restore
|
|
2802
2939
|
println!("\nSTARTUP / RESTORE SCALING");
|
|
2803
2940
|
if !self.startup_times.is_empty() {
|
|
2804
2941
|
let min_startup: u128 = *self.startup_times.iter().min().unwrap_or(&0);
|
|
2805
|
-
let avg_startup: u128 =
|
|
2942
|
+
let avg_startup: u128 =
|
|
2943
|
+
self.startup_times.iter().sum::<u128>() / self.startup_times.len() as u128;
|
|
2806
2944
|
let max_startup: u128 = *self.startup_times.iter().max().unwrap_or(&0);
|
|
2807
2945
|
|
|
2808
2946
|
println!(" Min: {}ms", min_startup);
|
|
@@ -2829,22 +2967,33 @@ mod tests {
|
|
|
2829
2967
|
|
|
2830
2968
|
println!(" Avg log size: {} bytes", avg_bytes);
|
|
2831
2969
|
println!(" Bytes per operation: ~{}", bytes_per_op);
|
|
2832
|
-
println!(
|
|
2833
|
-
|
|
2970
|
+
println!(
|
|
2971
|
+
" Linear growth: {}",
|
|
2972
|
+
if self.log_bytes.len() > 1 {
|
|
2973
|
+
"observed"
|
|
2974
|
+
} else {
|
|
2975
|
+
"cannot determine"
|
|
2976
|
+
}
|
|
2977
|
+
);
|
|
2834
2978
|
}
|
|
2835
2979
|
|
|
2836
2980
|
// Analyze memory
|
|
2837
2981
|
println!("\nMEMORY CHARACTERISTICS");
|
|
2838
2982
|
if !self.dedup_sizes.is_empty() {
|
|
2839
2983
|
let min_dedup: usize = *self.dedup_sizes.iter().min().unwrap_or(&0);
|
|
2840
|
-
let avg_dedup: usize =
|
|
2984
|
+
let avg_dedup: usize =
|
|
2985
|
+
self.dedup_sizes.iter().sum::<usize>() / self.dedup_sizes.len();
|
|
2841
2986
|
let max_dedup: usize = *self.dedup_sizes.iter().max().unwrap_or(&0);
|
|
2842
2987
|
|
|
2843
2988
|
println!(" Dedup entries (min): {}", min_dedup);
|
|
2844
2989
|
println!(" Dedup entries (avg): {}", avg_dedup);
|
|
2845
2990
|
println!(" Dedup entries (max): {}", max_dedup);
|
|
2846
2991
|
|
|
2847
|
-
let growth_ratio = if min_dedup > 0 {
|
|
2992
|
+
let growth_ratio = if min_dedup > 0 {
|
|
2993
|
+
max_dedup as f64 / min_dedup as f64
|
|
2994
|
+
} else {
|
|
2995
|
+
0.0
|
|
2996
|
+
};
|
|
2848
2997
|
println!(" Growth ratio: {:.2}x", growth_ratio);
|
|
2849
2998
|
|
|
2850
2999
|
if growth_ratio > 10.0 {
|
|
@@ -2867,7 +3016,9 @@ mod tests {
|
|
|
2867
3016
|
if self.convergence_failures == 0 {
|
|
2868
3017
|
println!(" Assessment: ✅ PASSES (100% convergence)");
|
|
2869
3018
|
} else {
|
|
2870
|
-
println!(
|
|
3019
|
+
println!(
|
|
3020
|
+
" Assessment: ⚠️ POTENTIAL ISSUE (not 100% convergence)"
|
|
3021
|
+
);
|
|
2871
3022
|
}
|
|
2872
3023
|
}
|
|
2873
3024
|
|
|
@@ -2968,7 +3119,11 @@ mod tests {
|
|
|
2968
3119
|
}
|
|
2969
3120
|
|
|
2970
3121
|
let latency = start.elapsed().as_millis();
|
|
2971
|
-
let throughput = if latency > 0 {
|
|
3122
|
+
let throughput = if latency > 0 {
|
|
3123
|
+
(*backlog_size as u128 / latency) as u64
|
|
3124
|
+
} else {
|
|
3125
|
+
0
|
|
3126
|
+
};
|
|
2972
3127
|
|
|
2973
3128
|
match backlog_size {
|
|
2974
3129
|
10 => {
|
|
@@ -2992,7 +3147,10 @@ mod tests {
|
|
|
2992
3147
|
evidence.convergence_failures += 1;
|
|
2993
3148
|
}
|
|
2994
3149
|
|
|
2995
|
-
println!(
|
|
3150
|
+
println!(
|
|
3151
|
+
" {} ops: {}ms latency, {} ops/sec",
|
|
3152
|
+
backlog_size, latency, throughput
|
|
3153
|
+
);
|
|
2996
3154
|
}
|
|
2997
3155
|
|
|
2998
3156
|
// Collect startup/resource data
|
|
@@ -3013,7 +3171,10 @@ mod tests {
|
|
|
3013
3171
|
evidence.dedup_sizes.push(dedup_size);
|
|
3014
3172
|
evidence.log_bytes.push(estimated_log as u64);
|
|
3015
3173
|
|
|
3016
|
-
println!(
|
|
3174
|
+
println!(
|
|
3175
|
+
" {} ops: {}ms startup, dedup={}, log≈{}B",
|
|
3176
|
+
op_count, startup_time, dedup_size, estimated_log
|
|
3177
|
+
);
|
|
3017
3178
|
}
|
|
3018
3179
|
|
|
3019
3180
|
// Produce analysis report
|
|
@@ -3039,7 +3200,9 @@ mod tests {
|
|
|
3039
3200
|
println!("PREAMBLE");
|
|
3040
3201
|
println!(" This contract is based on:");
|
|
3041
3202
|
println!(" • Phase 1b.5: Recovery protocol architectural proof");
|
|
3042
|
-
println!(
|
|
3203
|
+
println!(
|
|
3204
|
+
" • Phase 1b.6: Recovery durability adversarial validation (7 attack scenarios)"
|
|
3205
|
+
);
|
|
3043
3206
|
println!(" • Phase 1b.7: Sustained soak validation under stress");
|
|
3044
3207
|
println!(" • Phase 1b.8: Performance evidence analysis");
|
|
3045
3208
|
println!("");
|
|
@@ -3144,7 +3307,9 @@ mod tests {
|
|
|
3144
3307
|
println!(" NEXT DECISION:");
|
|
3145
3308
|
println!(" Is FeltDB intended as:");
|
|
3146
3309
|
println!(" A) Authoritative-primary with recoverable replicas (current 1b scope)");
|
|
3147
|
-
println!(
|
|
3310
|
+
println!(
|
|
3311
|
+
" B) Multi-master local-first distributed system (requires Phase 2 redesign)"
|
|
3312
|
+
);
|
|
3148
3313
|
println!("");
|
|
3149
3314
|
println!(" This contract is definitive FOR option A.");
|
|
3150
3315
|
println!(" Option B requires new work on conflict resolution and consensus.\n");
|