@feltdb/core 0.15.0 → 0.16.0

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.
Files changed (247) hide show
  1. package/dist/agent-state.d.ts +20 -0
  2. package/dist/agent-state.d.ts.map +1 -1
  3. package/dist/agent-state.js +36 -0
  4. package/dist/build-identity.json +12 -4
  5. package/dist/cli/commands.js +187 -4
  6. package/dist/cli/publish-engine.js +116 -16
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +3 -0
  9. package/dist/create/build-identity.json +12 -4
  10. package/dist/create/managed-account.js +93 -35
  11. package/dist/create/package-versions.js +1 -1
  12. package/dist/create/server-source/Cargo.lock +6 -509
  13. package/dist/create/server-source/Cargo.toml +2 -13
  14. package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
  15. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
  16. package/dist/create/server-source/crates/feltdb-server/src/main.rs +255 -12
  17. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  18. package/dist/db.d.ts +11 -1
  19. package/dist/db.d.ts.map +1 -1
  20. package/dist/db.js +15 -2
  21. package/dist/error-codes.d.ts +5 -0
  22. package/dist/error-codes.d.ts.map +1 -1
  23. package/dist/error-codes.js +9 -1
  24. package/dist/http-db.d.ts +16 -0
  25. package/dist/http-db.d.ts.map +1 -1
  26. package/dist/http-db.js +36 -6
  27. package/dist/managed-recovery.d.ts +6 -0
  28. package/dist/managed-recovery.d.ts.map +1 -1
  29. package/dist/managed-recovery.js +9 -0
  30. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-DtybUY0q.js} +1 -1
  31. package/dist/studio-app/assets/feltdb_wasm_bg-DsgIUwaB.wasm +0 -0
  32. package/dist/studio-app/assets/index-C4OffsZl.js +37 -0
  33. package/dist/studio-app/index.html +1 -1
  34. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  35. package/dist/wasm/package.json +1 -1
  36. package/dist/workload.d.ts +55 -3
  37. package/dist/workload.d.ts.map +1 -1
  38. package/dist/workload.js +23 -0
  39. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  40. package/dist/workspace/workspace-connection.js +14 -3
  41. package/package.json +1 -1
  42. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  43. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  44. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  45. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  46. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  47. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  49. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  51. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  61. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  63. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  64. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  65. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  66. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  67. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  68. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  69. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  70. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  71. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  72. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  73. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  74. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  75. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  76. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  77. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  80. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  83. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  84. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  86. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  87. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  89. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  90. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  91. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  92. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  93. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  94. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  95. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  96. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  97. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  98. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  99. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  100. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  101. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  102. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  103. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  105. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  106. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  107. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  108. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  109. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  110. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  111. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  112. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  113. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  114. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  115. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  116. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  117. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  118. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  119. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  120. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  121. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  122. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  123. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  124. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  125. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  126. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  127. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  128. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  129. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  130. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  131. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  132. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  133. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  134. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  135. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  136. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  137. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  138. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  139. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  140. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  141. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  142. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  143. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  144. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  145. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  146. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  147. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  148. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  149. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  150. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  151. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  152. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  153. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  154. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  155. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  156. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  157. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  158. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  159. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  160. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  161. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  162. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  163. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  164. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  165. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  166. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  167. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  168. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  169. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  170. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  171. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  172. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  173. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  174. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  175. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  176. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  177. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  178. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  179. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  180. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  181. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  182. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  183. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  184. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  185. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  186. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  187. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  188. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  189. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  190. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  191. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  192. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  193. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  194. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  195. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  196. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  197. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  198. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  199. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  200. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  204. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  206. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  207. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  208. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  209. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  210. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  211. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  212. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  213. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  220. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  221. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  222. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  223. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  225. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  226. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  227. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  228. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  229. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  230. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  231. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  232. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  235. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  236. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  237. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  238. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  239. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  240. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  241. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  242. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  243. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  244. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  245. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  246. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  247. package/dist/studio-app/assets/index-1wd0uKua.js +0 -30
@@ -1,1334 +0,0 @@
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
- use crate::causal_dependency_barrier::CausalDependencyBarrier;
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(&mut self, envelope: &TransactionEnvelope, new_state_hash: StateHash) {
139
- self.vector_clock.merge(&envelope.vector_clock);
140
- self.vector_clock.increment(&self.node_id);
141
- self.state_hash = new_state_hash;
142
- self.operations_applied += envelope.operations.len() as u64;
143
- self.last_update_ms = std::time::SystemTime::now()
144
- .duration_since(std::time::UNIX_EPOCH)
145
- .unwrap()
146
- .as_millis() as u64;
147
- }
148
-
149
- /// Causal read: wait for vector clock to advance
150
- pub fn wait_for_clock_advance(&self, required_clock: &VectorClock) -> bool {
151
- !self.vector_clock.happens_before(required_clock)
152
- }
153
- }
154
-
155
- /// Distributed transaction executor with durable operation log support
156
- ///
157
- /// Durability architecture:
158
- /// - Operation log is the ONLY durable source of truth
159
- /// - Dedup set is derived (reconstructed from operation log on startup)
160
- /// - No separate dedup file (eliminates two-file atomicity problem)
161
- pub struct DistributedTransactionExecutor {
162
- pub local_node_id: String,
163
- pub replicas: HashMap<String, ReplicaState>,
164
- pub envelopes_seen: std::collections::HashSet<EnvelopeId>,
165
- pub transaction_executor: TransactionExecutor,
166
- pub operation_log: Option<OperationLog>,
167
- /// Causal delivery gate for replicated envelopes.
168
- ///
169
- /// Holds identifiers and dependency clocks only. A blocked envelope's
170
- /// payload stays in the durable operation log and is replayed from there
171
- /// when its dependencies land.
172
- pub causal_barrier: CausalDependencyBarrier,
173
- /// The bound on the causal backlog.
174
- pub causal_capacity: CausalCapacity,
175
- /// The next identity this node will issue for its own operations.
176
- ///
177
- /// Recovered from the durable log rather than reset, because
178
- /// `(origin, sequence)` is an identity and receivers deduplicate on it. A
179
- /// counter that restarts re-issues identities that already exist, and the
180
- /// receiver discards the new operation as a duplicate.
181
- next_origin_sequence: u64,
182
- /// Envelopes that are durable in the log but not buffered, because the
183
- /// backlog was at its bound when they arrived.
184
- ///
185
- /// Derived state, like `envelopes_seen`: it is rebuilt from the log on
186
- /// recovery and exists so that a redelivery of a deferred envelope is
187
- /// recognised without a log scan.
188
- pub deferred: std::collections::HashSet<EnvelopeId>,
189
- }
190
-
191
- /// What the receiver did with a replicated envelope.
192
- ///
193
- /// This is a typed outcome rather than a string, because a caller has to be
194
- /// able to tell "applied" from "held back" from "already known" without
195
- /// matching on prose. The previous signature returned
196
- /// `Result<StateHash, String>` and signalled a duplicate as
197
- /// `Err("Duplicate envelope")`.
198
- #[derive(Clone, Debug, PartialEq)]
199
- pub enum ReceiveOutcome {
200
- /// Dependencies were satisfied; the envelope was applied.
201
- Applied { state_hash: StateHash },
202
- /// Durable, but not yet applied: some dependency has not arrived.
203
- /// The payload is in the operation log; the barrier holds its identity.
204
- PendingDependencies { missing: Vec<String> },
205
- /// This envelope has been seen before. Applying it again is a no-op.
206
- AlreadyKnown,
207
- /// Durable, but not buffered: the backlog is at its bound.
208
- ///
209
- /// This is *not* a refusal and nothing is lost. The envelope was fsynced to
210
- /// the operation log before this was decided, exactly as a buffered one is;
211
- /// what is withheld is the in-memory metadata entry. The caller's obligation
212
- /// is transport backpressure -- stop reading from that peer so TCP blocks
213
- /// the sender -- because returning a refusal to a peer means either silent
214
- /// loss or a retransmission protocol this repository does not have.
215
- ///
216
- /// The envelope is admitted from the log once the backlog drains.
217
- CapacityDeferred {
218
- missing: Vec<String>,
219
- pending_entries: usize,
220
- pending_bytes: usize,
221
- },
222
- }
223
-
224
- /// What the executor did with a locally submitted transaction.
225
- ///
226
- /// Capacity refusal is a distinct outcome rather than an `Err(String)`, because
227
- /// a caller has to tell "this replica is saturated, retry" from "this
228
- /// transaction is invalid". The two need opposite responses and the previous
229
- /// signature made them the same value.
230
- #[derive(Clone, Debug)]
231
- pub enum SubmitOutcome {
232
- Admitted(TransactionEnvelope),
233
- /// The causal backlog is at its bound, so no new local work is admitted.
234
- /// Nothing was executed, persisted, or applied: there is nothing to undo
235
- /// and nothing to recover.
236
- CapacityExceeded {
237
- pending_entries: usize,
238
- pending_bytes: usize,
239
- limit_entries: usize,
240
- limit_bytes: usize,
241
- },
242
- }
243
-
244
- /// The bound on the causal backlog, as decided in
245
- /// `docs/architecture/causal-backlog-bound.md`.
246
- ///
247
- /// Two limits, because they are driven by different variables. Bytes bound the
248
- /// outstanding payload volume; entries bound the metadata, whose per-entry cost
249
- /// scales with cluster size rather than with operation size, so a byte bound
250
- /// alone would score ten thousand pending one-byte operations as nearly free.
251
- ///
252
- /// Both are inclusive: the bound is `<=`, so filling exactly to it is accepted.
253
- #[derive(Clone, Copy, Debug, PartialEq)]
254
- pub struct CausalCapacity {
255
- pub max_pending_entries: usize,
256
- pub max_pending_bytes: usize,
257
- }
258
-
259
- impl CausalCapacity {
260
- /// The shipping default.
261
- ///
262
- /// These are policy, not measurements, and are deliberately not derived
263
- /// from a benchmark constant -- the figure that used to justify backlog
264
- /// sizing here was `backlog_depth * 1500`, which was the benchmark's own
265
- /// input rather than an observation. What the acceptance test establishes
266
- /// is that the bound is *enforced through the production path*, and it
267
- /// reports the occupancy it actually observed rather than asserting a
268
- /// predicted footprint.
269
- pub const DEFAULT_MAX_PENDING_ENTRIES: usize = 10_000;
270
- pub const DEFAULT_MAX_PENDING_BYTES: usize = 64 * 1024 * 1024;
271
-
272
- /// A bound small enough to be reached by a test through the real path.
273
- pub fn new(max_pending_entries: usize, max_pending_bytes: usize) -> Self {
274
- Self {
275
- max_pending_entries,
276
- max_pending_bytes,
277
- }
278
- }
279
- }
280
-
281
- impl Default for CausalCapacity {
282
- fn default() -> Self {
283
- Self {
284
- max_pending_entries: Self::DEFAULT_MAX_PENDING_ENTRIES,
285
- max_pending_bytes: Self::DEFAULT_MAX_PENDING_BYTES,
286
- }
287
- }
288
- }
289
-
290
- impl DistributedTransactionExecutor {
291
- /// Create a new distributed executor without durable persistence
292
- pub fn new(node_id: String, initial_state_hash: StateHash) -> Self {
293
- let mut replicas = HashMap::new();
294
- replicas.insert(
295
- node_id.clone(),
296
- ReplicaState::new(node_id.clone(), initial_state_hash),
297
- );
298
-
299
- Self {
300
- local_node_id: node_id.clone(),
301
- replicas,
302
- envelopes_seen: std::collections::HashSet::new(),
303
- transaction_executor: TransactionExecutor::new(node_id),
304
- operation_log: None,
305
- causal_barrier: CausalDependencyBarrier::new(),
306
- causal_capacity: CausalCapacity::default(),
307
- next_origin_sequence: 1,
308
- deferred: std::collections::HashSet::new(),
309
- }
310
- }
311
-
312
- /// Create a new distributed executor with durable operation log
313
- ///
314
- /// Note: dedup set is NOT persisted separately. It is reconstructed deterministically
315
- /// from the operation log on startup, eliminating atomicity issues.
316
- pub fn with_log(
317
- node_id: String,
318
- initial_state_hash: StateHash,
319
- log_path: Option<PathBuf>,
320
- ) -> Result<Self, std::io::Error> {
321
- let mut replicas = HashMap::new();
322
- replicas.insert(
323
- node_id.clone(),
324
- ReplicaState::new(node_id.clone(), initial_state_hash),
325
- );
326
-
327
- let operation_log = if let Some(path) = &log_path {
328
- Some(OperationLog::new(path)?)
329
- } else {
330
- None
331
- };
332
-
333
- // Identity is recovered here as well as in `load_from_disk`, because a
334
- // caller that opens an existing log and issues an operation without
335
- // replaying it must still not re-issue an identity that log already
336
- // contains.
337
- let next_origin_sequence = match &operation_log {
338
- Some(log) => Self::next_sequence_after(log, &node_id)?,
339
- None => 1,
340
- };
341
-
342
- Ok(Self {
343
- local_node_id: node_id.clone(),
344
- replicas,
345
- envelopes_seen: std::collections::HashSet::new(),
346
- transaction_executor: TransactionExecutor::new(node_id),
347
- operation_log,
348
- causal_barrier: CausalDependencyBarrier::new(),
349
- causal_capacity: CausalCapacity::default(),
350
- next_origin_sequence,
351
- deferred: std::collections::HashSet::new(),
352
- })
353
- }
354
-
355
- /// Register a replica (peer node)
356
- pub fn register_replica(&mut self, node_id: String, initial_state_hash: StateHash) {
357
- self.replicas.insert(
358
- node_id.clone(),
359
- ReplicaState::new(node_id, initial_state_hash),
360
- );
361
- }
362
-
363
- /// Execute transaction locally and prepare for replication
364
- /// Ordering: execute → persist → update state → record dedup
365
- // ---------------------------------------------------------------------
366
- // Durable operation identity
367
- //
368
- // `(origin, sequence)` is an identity, not a counter. Receivers
369
- // deduplicate on it, so two operations must never be assigned the same
370
- // one -- and "must never" has to hold across a process restart, which is
371
- // where it previously did not: `feltdb_node` kept the counter in memory
372
- // and started it at 1, so a restarted origin re-issued identities its own
373
- // earlier operations already held and its peers discarded the new writes
374
- // as duplicates. See `dedup_bound_investigation.rs`.
375
- //
376
- // The authority is the durable log, and specifically the log rather than
377
- // the causal clock. Those are different identity domains: the clock
378
- // component advances only on a successful apply, while the sequence is
379
- // burned by an attempt, so the two disagree by exactly the number of
380
- // refused transactions. `replication-convergence.md` fixes that the
381
- // sequence is sparse and that making it dense is a separate deferred
382
- // decision, so this derives identity from the sequence domain and leaves
383
- // that decision alone.
384
- // ---------------------------------------------------------------------
385
-
386
- /// The highest identity committed to the durable log, per origin.
387
- ///
388
- /// For this node it is its own identity high-water mark; for others it is
389
- /// how far this replica's log carries their identities. A checkpoint that
390
- /// discards log prefix must preserve these, because the log is also the
391
- /// source `next_origin_sequence` is recovered from.
392
- pub fn origin_sequence_high_water(&self) -> std::collections::BTreeMap<String, u64> {
393
- let mut marks = std::collections::BTreeMap::new();
394
- let Some(ref log) = self.operation_log else {
395
- return marks;
396
- };
397
- let Ok(envelopes) = log.load_all() else {
398
- return marks;
399
- };
400
- for envelope in envelopes {
401
- let entry = marks
402
- .entry(envelope.envelope_id.originating_node.clone())
403
- .or_insert(0u64);
404
- *entry = (*entry).max(envelope.envelope_id.sequence);
405
- }
406
- marks
407
- }
408
-
409
- /// The next identity this node would issue.
410
- pub fn next_origin_sequence(&self) -> u64 {
411
- self.next_origin_sequence
412
- }
413
-
414
- /// Claim the next identity for an operation this node originates.
415
- ///
416
- /// Advancing on the attempt rather than on success is deliberate: it
417
- /// preserves the sparse-sequence semantics the convergence contract froze,
418
- /// where a refused transaction burns a number. Making the sequence dense is
419
- /// a separate decision and is not made here as a side effect.
420
- ///
421
- /// A number burned and then lost to a crash is safe to re-issue, because
422
- /// nothing carrying it was ever persisted: recovery derives from what the
423
- /// log holds, so an identity is only reserved forever once an envelope
424
- /// bearing it is durable. That is the invariant -- no two *persisted*
425
- /// envelopes from one origin share a sequence -- and it is the one
426
- /// receivers depend on.
427
- pub fn allocate_origin_sequence(&mut self) -> u64 {
428
- let claimed = self.next_origin_sequence;
429
- self.next_origin_sequence = claimed.saturating_add(1);
430
- claimed
431
- }
432
-
433
- /// Derive the next identity from a durable log: one past the highest this
434
- /// node has already committed to it.
435
- ///
436
- /// Filtered to locally-originated envelopes, because the log also holds
437
- /// replicated ones and another node's sequence says nothing about this
438
- /// node's identity space.
439
- fn next_sequence_after(log: &OperationLog, node_id: &str) -> Result<u64, std::io::Error> {
440
- let highest = log
441
- .load_all()?
442
- .iter()
443
- .filter(|envelope| envelope.envelope_id.originating_node == node_id)
444
- .map(|envelope| envelope.envelope_id.sequence)
445
- .max()
446
- .unwrap_or(0);
447
- Ok(highest.saturating_add(1))
448
- }
449
-
450
- /// Submit local work, allocating its identity from durable state.
451
- ///
452
- /// This is the entry point a node should use: it owns the allocation, so
453
- /// the identity cannot be assigned by one caller and persisted by another,
454
- /// and there is no counter outside the executor to fall out of step with
455
- /// the log.
456
- pub fn submit_local_transaction(
457
- &mut self,
458
- transaction_id: String,
459
- parent_version: StateVersion,
460
- operations: Vec<Operation>,
461
- consistency_contract: ConsistencyContract,
462
- ) -> Result<SubmitOutcome, String> {
463
- // Allocation happens after the capacity check inside
464
- // `try_execute_local_transaction`, so a refusal does not burn an
465
- // identity: nothing was admitted, and there is nothing to account for.
466
- if self.would_breach_bound(Self::operations_bytes(&operations)) {
467
- return Ok(SubmitOutcome::CapacityExceeded {
468
- pending_entries: self.causal_barrier.pending_count(),
469
- pending_bytes: self.causal_barrier.pending_payload_bytes(),
470
- limit_entries: self.causal_capacity.max_pending_entries,
471
- limit_bytes: self.causal_capacity.max_pending_bytes,
472
- });
473
- }
474
- let sequence = self.allocate_origin_sequence();
475
- self.execute_local_transaction(
476
- sequence,
477
- transaction_id,
478
- parent_version,
479
- operations,
480
- consistency_contract,
481
- )
482
- .map(SubmitOutcome::Admitted)
483
- }
484
-
485
- /// Submit local work, subject to the causal backlog bound.
486
- ///
487
- /// The bound is checked *before* anything is executed, persisted or
488
- /// applied, so a refusal leaves nothing to undo and nothing to recover:
489
- /// the operation was never admitted. That is what makes refusal the right
490
- /// answer on this path and the wrong one for a replicated arrival, which
491
- /// was already admitted and made durable somewhere else.
492
- pub fn try_execute_local_transaction(
493
- &mut self,
494
- sequence: u64,
495
- transaction_id: String,
496
- parent_version: StateVersion,
497
- operations: Vec<Operation>,
498
- consistency_contract: ConsistencyContract,
499
- ) -> Result<SubmitOutcome, String> {
500
- let projected_bytes = Self::operations_bytes(&operations);
501
- if self.would_breach_bound(projected_bytes) {
502
- return Ok(SubmitOutcome::CapacityExceeded {
503
- pending_entries: self.causal_barrier.pending_count(),
504
- pending_bytes: self.causal_barrier.pending_payload_bytes(),
505
- limit_entries: self.causal_capacity.max_pending_entries,
506
- limit_bytes: self.causal_capacity.max_pending_bytes,
507
- });
508
- }
509
- self.execute_local_transaction(
510
- sequence,
511
- transaction_id,
512
- parent_version,
513
- operations,
514
- consistency_contract,
515
- )
516
- .map(SubmitOutcome::Admitted)
517
- }
518
-
519
- /// The size local work would contribute, measured before it is executed.
520
- fn operations_bytes(operations: &[Operation]) -> usize {
521
- serde_json::to_string(operations)
522
- .map(|json| json.len())
523
- .unwrap_or(0)
524
- }
525
-
526
- /// Execute local work unconditionally.
527
- ///
528
- /// This does not consult the backlog bound; `try_execute_local_transaction`
529
- /// is the admitting entry point and the one the node binary uses. This
530
- /// remains for callers that have already made the admission decision.
531
- pub fn execute_local_transaction(
532
- &mut self,
533
- sequence: u64,
534
- transaction_id: String,
535
- parent_version: StateVersion,
536
- operations: Vec<Operation>,
537
- consistency_contract: ConsistencyContract,
538
- ) -> Result<TransactionEnvelope, String> {
539
- // Step 1: Execute transaction through 14C
540
- let transition = self.transaction_executor.execute_transaction(
541
- transaction_id.clone(),
542
- parent_version,
543
- operations.clone(),
544
- consistency_contract.clone(),
545
- );
546
-
547
- if !transition.is_successful() {
548
- return Err("Transaction execution failed".to_string());
549
- }
550
-
551
- // Step 2: Create envelope for replication
552
- let mut envelope = TransactionEnvelope::new(
553
- self.local_node_id.clone(),
554
- sequence,
555
- transaction_id,
556
- operations,
557
- consistency_contract,
558
- );
559
-
560
- // Step 2a: Stamp the envelope with this node's causal knowledge.
561
- //
562
- // `TransactionEnvelope::new` starts from an empty clock and increments
563
- // only the origin, so every envelope it built carried {origin: 1}
564
- // whatever the node had already seen. That is a position, not a
565
- // history: a receiver given those clocks has no dependencies to
566
- // enforce, which is why causal ordering could not have worked on this
567
- // path even once the barrier was wired to it. The clock must carry the
568
- // applied frontier this operation was produced on top of.
569
- let mut stamped = self.causal_barrier.get_frontier().clone();
570
- stamped.increment(&self.local_node_id);
571
- envelope.vector_clock = stamped;
572
-
573
- // Step 3: Durably persist operation to log (with fsync)
574
- // This must succeed before we update in-memory state
575
- self.persist_envelope(&envelope)?;
576
-
577
- // Step 4: Update local replica state (in-memory)
578
- if let Some(local_replica) = self.replicas.get_mut(&self.local_node_id) {
579
- let state_hash = StateHash::from_hex(transition.to_version.state_hash.clone());
580
- local_replica.apply_transaction(&envelope, state_hash);
581
- }
582
-
583
- // Step 5: Record envelope to prevent duplicates (in-memory, derived from log)
584
- self.envelopes_seen.insert(envelope.envelope_id.clone());
585
-
586
- // Step 6: A locally executed operation is applied, so it advances the
587
- // frontier exactly as a replicated one does. Own writes are always
588
- // causally eligible: they are built on the frontier they advance.
589
- self.causal_barrier.record_applied(
590
- &Self::barrier_key(&envelope.envelope_id),
591
- &envelope.vector_clock,
592
- );
593
-
594
- Ok(envelope)
595
- }
596
-
597
- /// Receive replicated transaction from peer
598
- /// Ordering: dedup check → execute → persist → update state
599
- pub fn receive_replicated_transaction(
600
- &mut self,
601
- message: ReplicationMessage,
602
- parent_version: StateVersion,
603
- ) -> Result<StateHash, String> {
604
- match self.receive_replicated(message, parent_version)? {
605
- ReceiveOutcome::Applied { state_hash } => Ok(state_hash),
606
- ReceiveOutcome::PendingDependencies { missing } => Err(format!(
607
- "Envelope is durable but waiting on dependencies: {}",
608
- missing.join(", ")
609
- )),
610
- ReceiveOutcome::AlreadyKnown => Err("Duplicate envelope".to_string()),
611
- ReceiveOutcome::CapacityDeferred { missing, .. } => Err(format!(
612
- "CAPACITY_DEFERRED: envelope is durable but the causal backlog is at its bound, waiting on: {}",
613
- missing.join(", ")
614
- )),
615
- }
616
- }
617
-
618
- /// The causal receive path.
619
- ///
620
- /// Ordering is deliberate and is the substance of this change:
621
- ///
622
- /// dedup -> persist -> causal eligibility -> apply -> release
623
- ///
624
- /// Persisting *before* deciding eligibility is what makes "held back" a
625
- /// durable state rather than an in-memory one. An envelope that arrives
626
- /// ahead of its dependencies is fsynced to the operation log and then
627
- /// retained in the barrier by identity alone; if the process dies at that
628
- /// moment, recovery replays the log and rediscovers it as pending. This is
629
- /// also what catch-up will require, because a peer sending A3, A1, A2 in
630
- /// transport order needs the receiver to be able to say "I have A3
631
- /// durably, and I cannot apply it yet".
632
- pub fn receive_replicated(
633
- &mut self,
634
- message: ReplicationMessage,
635
- parent_version: StateVersion,
636
- ) -> Result<ReceiveOutcome, String> {
637
- let envelope_id = message.envelope.envelope_id.clone();
638
- let key = Self::barrier_key(&envelope_id);
639
-
640
- // Step 1: Duplicate delivery is a no-op, whether the original was
641
- // applied or is still waiting. Both states are recorded before this
642
- // point is reached again, so one logical operation stays one.
643
- if self.envelopes_seen.contains(&envelope_id)
644
- || self.causal_barrier.is_pending(&key)
645
- || self.deferred.contains(&envelope_id)
646
- {
647
- return Ok(ReceiveOutcome::AlreadyKnown);
648
- }
649
-
650
- // Step 2: Durability first. The log is the source of truth, so an
651
- // envelope becomes durable on receipt and not on application.
652
- self.persist_envelope(&message.envelope)?;
653
-
654
- // Step 3: Eligibility. The dependencies are everything the origin had
655
- // seen when it produced this operation, which is its clock with its own
656
- // entry stepped back by one; the operation itself is not its own
657
- // dependency.
658
- let dependencies = Self::dependencies_of(&message.envelope);
659
- if !self.causal_barrier.is_eligible(&dependencies) {
660
- let missing = self.missing_dependencies(&dependencies);
661
-
662
- // Step 3a: The bound. Buffering costs an entry and holds a payload
663
- // outstanding in the log; if either would breach its limit, the
664
- // metadata entry is withheld and the envelope stays durable and
665
- // deferred. It is never refused and never evicted -- refusing a
666
- // peer means silent loss or a retransmission protocol that does not
667
- // exist here, and eviction is silent loss under another name.
668
- let payload_bytes = Self::envelope_bytes(&message.envelope);
669
- if self.would_breach_bound(payload_bytes) {
670
- self.deferred.insert(envelope_id);
671
- return Ok(ReceiveOutcome::CapacityDeferred {
672
- missing,
673
- pending_entries: self.causal_barrier.pending_count(),
674
- pending_bytes: self.causal_barrier.pending_payload_bytes(),
675
- });
676
- }
677
-
678
- self.causal_barrier
679
- .retain_pending_sized(key, dependencies, payload_bytes);
680
- return Ok(ReceiveOutcome::PendingDependencies { missing });
681
- }
682
-
683
- // Step 4: Apply, which advances the frontier.
684
- let state_hash = self.apply_envelope(&message.envelope, parent_version.clone())?;
685
-
686
- // Step 5: This application may have unblocked others, and may have
687
- // freed the capacity a deferred envelope was waiting for. Drain both.
688
- self.drain(parent_version)?;
689
-
690
- Ok(ReceiveOutcome::Applied { state_hash })
691
- }
692
-
693
- /// Drive the backlog forward from outside the receive path.
694
- ///
695
- /// A deferred envelope is durable but unbuffered, and the event that frees
696
- /// capacity for it may arrive on a different connection than the one that
697
- /// was deferred. A caller applying transport backpressure needs a way to
698
- /// make progress without reading from the backpressured peer, and this is
699
- /// it.
700
- pub fn drain_backlog(&mut self, parent_version: StateVersion) -> Result<usize, String> {
701
- self.drain(parent_version)
702
- }
703
-
704
- /// Whether admitting one more entry of this size would breach either bound.
705
- ///
706
- /// Inclusive, as the contract specifies: filling exactly to the bound is
707
- /// accepted, and the breach is the step past it.
708
- fn would_breach_bound(&self, payload_bytes: usize) -> bool {
709
- let entries = self.causal_barrier.pending_count();
710
- let bytes = self.causal_barrier.pending_payload_bytes();
711
- entries + 1 > self.causal_capacity.max_pending_entries
712
- || bytes + payload_bytes > self.causal_capacity.max_pending_bytes
713
- }
714
-
715
- /// The envelope's size as the durable log stores it.
716
- ///
717
- /// Measured with the log's own serialisation rather than a nominal
718
- /// per-operation constant, so a large envelope consumes its actual byte
719
- /// count.
720
- fn envelope_bytes(envelope: &TransactionEnvelope) -> usize {
721
- serde_json::to_string(envelope)
722
- .map(|json| json.len())
723
- .unwrap_or(0)
724
- }
725
-
726
- /// Apply one envelope and advance the applied frontier.
727
- fn apply_envelope(
728
- &mut self,
729
- envelope: &TransactionEnvelope,
730
- parent_version: StateVersion,
731
- ) -> Result<StateHash, String> {
732
- let transition = self.transaction_executor.execute_transaction(
733
- envelope.transaction_id.clone(),
734
- parent_version,
735
- envelope.operations.clone(),
736
- envelope.consistency_contract.clone(),
737
- );
738
-
739
- if !transition.is_successful() {
740
- return Err("Transaction execution failed".to_string());
741
- }
742
-
743
- self.envelopes_seen.insert(envelope.envelope_id.clone());
744
-
745
- let state_hash = StateHash::from_hex(transition.to_version.state_hash.clone());
746
- if let Some(replica) = self.replicas.get_mut(&self.local_node_id) {
747
- replica.apply_transaction(envelope, state_hash.clone());
748
- }
749
-
750
- // The frontier advances here and only here, so it describes what has
751
- // been successfully applied rather than what was claimed or received.
752
- self.causal_barrier.record_applied(
753
- &Self::barrier_key(&envelope.envelope_id),
754
- &envelope.vector_clock,
755
- );
756
-
757
- Ok(state_hash)
758
- }
759
-
760
- /// Drive the backlog forward until it stops moving.
761
- ///
762
- /// Two things can make progress and each can enable the other: applying a
763
- /// pending envelope advances the frontier, which may make other pending
764
- /// envelopes eligible; and applying anything frees capacity, which may let
765
- /// a deferred envelope be admitted from the log. So this alternates until
766
- /// neither step changes anything, which terminates because every iteration
767
- /// that continues has strictly reduced the number of unapplied envelopes.
768
- fn drain(&mut self, parent_version: StateVersion) -> Result<usize, String> {
769
- let mut progressed_total = 0;
770
- loop {
771
- let released = self.release_eligible(parent_version.clone())?;
772
- let admitted = self.admit_deferred(parent_version.clone())?;
773
- progressed_total += released + admitted;
774
- if released == 0 && admitted == 0 {
775
- return Ok(progressed_total);
776
- }
777
- }
778
- }
779
-
780
- /// Apply every pending envelope the frontier now satisfies, repeatedly,
781
- /// because releasing one can make the next eligible.
782
- ///
783
- /// The log is read once per pass rather than once per key. It used to be
784
- /// once per key: `replay_from_log` called `load_all()` for every eligible
785
- /// entry, so a release cascade over a backlog of n entries against a log of
786
- /// m records did n full scans. That is the exact shape of load this change
787
- /// makes reachable -- a bounded backlog is one that is allowed to get
788
- /// large -- so the quadratic scan had to go with it.
789
- fn release_eligible(&mut self, parent_version: StateVersion) -> Result<usize, String> {
790
- let mut released = 0;
791
- loop {
792
- let ready = self.causal_barrier.eligible_pending();
793
- if ready.is_empty() {
794
- return Ok(released);
795
- }
796
- let by_key = self.log_by_key()?;
797
- let mut progressed = false;
798
- for key in ready {
799
- let Some(envelope) = by_key.get(&key) else {
800
- // The barrier holds an identity whose payload is not in the
801
- // log. Dropping the entry would be silent loss, so leave it
802
- // pending and surface it rather than pretend it applied.
803
- continue;
804
- };
805
- self.apply_envelope(&envelope.clone(), parent_version.clone())?;
806
- released += 1;
807
- progressed = true;
808
- }
809
- if !progressed {
810
- return Ok(released);
811
- }
812
- }
813
- }
814
-
815
- /// Move envelopes that are durable but unbuffered into the backlog, as far
816
- /// as the bound now allows.
817
- ///
818
- /// This is what makes `CapacityDeferred` safe: the envelope was fsynced
819
- /// before it was deferred, so admitting it later needs no cooperation from
820
- /// the sender and survives a restart in between. Entries are admitted in
821
- /// key order so that two replicas draining the same set drain it
822
- /// identically -- behaviour at capacity must not depend on iteration order
823
- /// or timing.
824
- fn admit_deferred(&mut self, parent_version: StateVersion) -> Result<usize, String> {
825
- if self.deferred.is_empty() {
826
- return Ok(0);
827
- }
828
- let by_key = self.log_by_key()?;
829
- let mut keys: Vec<String> = self.deferred.iter().map(Self::barrier_key).collect();
830
- keys.sort();
831
-
832
- let mut admitted = 0;
833
- for key in keys {
834
- let Some(envelope) = by_key.get(&key).cloned() else {
835
- continue;
836
- };
837
- let dependencies = Self::dependencies_of(&envelope);
838
- let payload_bytes = Self::envelope_bytes(&envelope);
839
-
840
- if self.causal_barrier.is_eligible(&dependencies) {
841
- self.deferred.remove(&envelope.envelope_id);
842
- self.apply_envelope(&envelope, parent_version.clone())?;
843
- admitted += 1;
844
- continue;
845
- }
846
- if self.would_breach_bound(payload_bytes) {
847
- // Still no room. Leave it deferred; it is durable and will be
848
- // reconsidered on the next drain or after a restart.
849
- continue;
850
- }
851
- self.deferred.remove(&envelope.envelope_id);
852
- self.causal_barrier
853
- .retain_pending_sized(key, dependencies, payload_bytes);
854
- admitted += 1;
855
- }
856
- Ok(admitted)
857
- }
858
-
859
- /// The durable log, indexed by barrier key.
860
- ///
861
- /// This is why the barrier never needs the payload: it holds the identity,
862
- /// and the bytes stay where they were already fsynced.
863
- fn log_by_key(&self) -> Result<HashMap<String, TransactionEnvelope>, String> {
864
- let Some(ref log) = self.operation_log else {
865
- return Ok(HashMap::new());
866
- };
867
- let envelopes = log
868
- .load_all()
869
- .map_err(|e| format!("Failed to replay operation log: {}", e))?;
870
- Ok(envelopes
871
- .into_iter()
872
- .map(|envelope| (Self::barrier_key(&envelope.envelope_id), envelope))
873
- .collect())
874
- }
875
-
876
- /// The stable string identity the barrier is keyed by.
877
- fn barrier_key(envelope_id: &EnvelopeId) -> String {
878
- format!("{}:{}", envelope_id.originating_node, envelope_id.sequence)
879
- }
880
-
881
- /// The causal dependencies of an envelope: what its origin had applied
882
- /// before producing it.
883
- fn dependencies_of(envelope: &TransactionEnvelope) -> VectorClock {
884
- let mut dependencies = envelope.vector_clock.clone();
885
- let own = dependencies.get(&envelope.originating_node);
886
- dependencies
887
- .clocks
888
- .insert(envelope.originating_node.clone(), own.saturating_sub(1));
889
- dependencies
890
- }
891
-
892
- /// Which dimensions of a dependency clock the frontier has not reached.
893
- fn missing_dependencies(&self, dependencies: &VectorClock) -> Vec<String> {
894
- let frontier = self.causal_barrier.get_frontier();
895
- let mut missing: Vec<String> = dependencies
896
- .clocks
897
- .iter()
898
- .filter(|(node, required)| frontier.get(node) < **required)
899
- .map(|(node, required)| format!("{}@{} (have {})", node, required, frontier.get(node)))
900
- .collect();
901
- missing.sort();
902
- missing
903
- }
904
-
905
- /// How many envelopes are durable but waiting on dependencies.
906
- ///
907
- /// Instrumentation only; no limit is enforced. See
908
- /// docs/architecture/causal-backlog-bound.md.
909
- pub fn pending_causal_count(&self) -> usize {
910
- self.causal_barrier.pending_count()
911
- }
912
-
913
- /// Approximate heap cost of the pending causal metadata, in bytes.
914
- pub fn pending_causal_metadata_bytes(&self) -> usize {
915
- self.causal_barrier.pending_metadata_bytes()
916
- }
917
-
918
- /// Durable payload bytes the backlog is waiting on. The byte bound is
919
- /// expressed in these.
920
- pub fn pending_causal_payload_bytes(&self) -> usize {
921
- self.causal_barrier.pending_payload_bytes()
922
- }
923
-
924
- /// Envelopes that are durable but not buffered, because the bound was
925
- /// reached when they arrived. They are not lost and not evicted.
926
- pub fn deferred_causal_count(&self) -> usize {
927
- self.deferred.len()
928
- }
929
-
930
- /// Keys of envelopes that are durable but not buffered, sorted.
931
- pub fn deferred_causal_keys(&self) -> Vec<String> {
932
- let mut keys: Vec<String> = self.deferred.iter().map(Self::barrier_key).collect();
933
- keys.sort();
934
- keys
935
- }
936
-
937
- /// Maximum occupancy ever reached, for entries and payload bytes.
938
- ///
939
- /// Reported alongside the current values because a backlog that filled and
940
- /// drained between two reads is invisible to a current-value gauge, and the
941
- /// question an operator has is whether the bound was ever approached.
942
- pub fn max_causal_occupancy(&self) -> (usize, usize) {
943
- (
944
- self.causal_barrier.max_pending_entries_seen(),
945
- self.causal_barrier.max_pending_payload_bytes_seen(),
946
- )
947
- }
948
-
949
- /// The bound in force on this executor.
950
- pub fn causal_capacity(&self) -> CausalCapacity {
951
- self.causal_capacity
952
- }
953
-
954
- /// Set the bound. Used by the node binary so an acceptance test can reach
955
- /// the bound through the production path rather than by constructing a
956
- /// barrier directly.
957
- pub fn set_causal_capacity(&mut self, capacity: CausalCapacity) {
958
- self.causal_capacity = capacity;
959
- }
960
-
961
- /// The applied causal frontier: what this replica has successfully applied.
962
- pub fn causal_frontier(&self) -> &VectorClock {
963
- self.causal_barrier.get_frontier()
964
- }
965
-
966
- /// Identities of envelopes durable but not yet applied.
967
- pub fn pending_causal_keys(&self) -> Vec<String> {
968
- self.causal_barrier.pending_keys()
969
- }
970
-
971
- /// Materialize the applied records, order-independently.
972
- ///
973
- /// This exists because the chained `H(parent, command)` state hash is a
974
- /// missing-operation detector for an ordered history, not a convergence
975
- /// identity: two replicas that applied the same set of concurrent
976
- /// operations in different orders hold the same data and report different
977
- /// chained hashes. `CanonicalState` is a BTreeMap keyed by
978
- /// (collection, record_id), so its hash depends on the resulting records
979
- /// and not on the path taken to them, which is what "the replicas agree"
980
- /// should mean.
981
- ///
982
- /// Built from the durable log, filtered to what the barrier records as
983
- /// applied and taken in application order, so a pending envelope's
984
- /// operations are excluded until it is actually released.
985
- pub fn canonical_state(&self) -> Result<crate::state_hash::CanonicalState, String> {
986
- let mut state = crate::state_hash::CanonicalState::new();
987
- let Some(ref log) = self.operation_log else {
988
- return Ok(state);
989
- };
990
- let envelopes = log
991
- .load_all()
992
- .map_err(|e| format!("Failed to read operation log: {}", e))?;
993
- let by_key: HashMap<String, &TransactionEnvelope> = envelopes
994
- .iter()
995
- .map(|envelope| (Self::barrier_key(&envelope.envelope_id), envelope))
996
- .collect();
997
-
998
- for key in self.causal_barrier.applied_keys() {
999
- let Some(envelope) = by_key.get(key) else {
1000
- continue;
1001
- };
1002
- for operation in &envelope.operations {
1003
- let command = &operation.command;
1004
- let fields: serde_json::Map<String, serde_json::Value> = command
1005
- .fields
1006
- .iter()
1007
- .map(|(name, value)| (name.clone(), value.clone()))
1008
- .collect();
1009
- state.set_record(
1010
- command.collection.clone(),
1011
- command.record_id.clone(),
1012
- serde_json::Value::Object(fields),
1013
- );
1014
- }
1015
- }
1016
- Ok(state)
1017
- }
1018
-
1019
- /// Get current state of a replica
1020
- pub fn get_replica_state(&self, node_id: &str) -> Option<ReplicaState> {
1021
- self.replicas.get(node_id).cloned()
1022
- }
1023
-
1024
- /// Check if replicas have converged to same state
1025
- pub fn check_convergence(&self) -> bool {
1026
- if self.replicas.is_empty() {
1027
- return true;
1028
- }
1029
-
1030
- let first_hash = self.replicas.values().next().map(|r| r.state_hash.clone());
1031
-
1032
- if let Some(first) = first_hash {
1033
- self.replicas.values().all(|r| r.state_hash == first)
1034
- } else {
1035
- false
1036
- }
1037
- }
1038
-
1039
- /// Get all replica states
1040
- pub fn get_all_replicas(&self) -> Vec<ReplicaState> {
1041
- self.replicas.values().cloned().collect()
1042
- }
1043
-
1044
- /// Causal read: wait for specific version
1045
- pub fn causal_read(&self, required_clock: &VectorClock) -> bool {
1046
- if let Some(local) = self.replicas.get(&self.local_node_id) {
1047
- local.wait_for_clock_advance(required_clock)
1048
- } else {
1049
- false
1050
- }
1051
- }
1052
-
1053
- /// Get the local node ID
1054
- pub fn get_local_node_id(&self) -> String {
1055
- self.local_node_id.clone()
1056
- }
1057
-
1058
- /// Load persisted operations from disk and rebuild internal state
1059
- ///
1060
- /// Atomicity guarantee:
1061
- /// - Load operation log (single durable source of truth)
1062
- /// - Reconstruct dedup set deterministically from envelopes
1063
- /// - No separate dedup file (no two-file atomicity problem)
1064
- pub fn load_from_disk(&mut self, log_path: PathBuf) -> Result<usize, std::io::Error> {
1065
- let log = OperationLog::new(&log_path)?;
1066
- let envelopes = log.load_all()?;
1067
-
1068
- self.operation_log = Some(log);
1069
-
1070
- // Rebuild the causal state, not just the dedup set.
1071
- //
1072
- // The log records what was *received*, in arrival order, and arrival
1073
- // order is not causal order -- that is the whole reason the barrier
1074
- // exists. So recovery cannot simply mark everything seen: it has to
1075
- // replay eligibility, exactly as the live path does, and come out with
1076
- // the same split between applied and still-pending. This is what makes
1077
- // "deliver B, restart, deliver A" work: B is durable across the
1078
- // restart, is rediscovered as pending rather than as applied, and is
1079
- // released when A finally arrives.
1080
- let mut undecided: Vec<TransactionEnvelope> = envelopes.clone();
1081
- loop {
1082
- let mut progressed = false;
1083
- let mut still_undecided = Vec::new();
1084
- for envelope in undecided.into_iter() {
1085
- let dependencies = Self::dependencies_of(&envelope);
1086
- if self.causal_barrier.is_eligible(&dependencies) {
1087
- self.envelopes_seen.insert(envelope.envelope_id.clone());
1088
- self.causal_barrier.record_applied(
1089
- &Self::barrier_key(&envelope.envelope_id),
1090
- &envelope.vector_clock,
1091
- );
1092
- progressed = true;
1093
- } else {
1094
- still_undecided.push(envelope);
1095
- }
1096
- }
1097
- undecided = still_undecided;
1098
- if !progressed || undecided.is_empty() {
1099
- break;
1100
- }
1101
- }
1102
-
1103
- // Whatever could not be made eligible is durable and waiting, and is
1104
- // restored to the barrier in that state -- as far as the bound allows.
1105
- //
1106
- // Recovery has to respect the same bound the live path does, or a node
1107
- // could restart into an over-full backlog that it would never have
1108
- // accepted while running, and the bound would hold only until the first
1109
- // crash. Entries are restored in key order so that recovery is
1110
- // deterministic: the same log and the same bound produce the same split
1111
- // between pending and deferred every time.
1112
- undecided.sort_by_key(|envelope| Self::barrier_key(&envelope.envelope_id));
1113
- for envelope in undecided {
1114
- let dependencies = Self::dependencies_of(&envelope);
1115
- let payload_bytes = Self::envelope_bytes(&envelope);
1116
- if self.would_breach_bound(payload_bytes) {
1117
- self.deferred.insert(envelope.envelope_id.clone());
1118
- continue;
1119
- }
1120
- self.causal_barrier.retain_pending_sized(
1121
- Self::barrier_key(&envelope.envelope_id),
1122
- dependencies,
1123
- payload_bytes,
1124
- );
1125
- }
1126
-
1127
- // Restore the observable local replica state from the same applied
1128
- // subset. Recovery previously rebuilt the barrier and canonical data
1129
- // but left this counter and clock at their constructor values, making
1130
- // a fully recovered node report that it had applied zero operations.
1131
- let applied_keys = self.causal_barrier.applied_keys();
1132
- let operations_applied = envelopes
1133
- .iter()
1134
- .filter(|envelope| applied_keys.contains(&Self::barrier_key(&envelope.envelope_id)))
1135
- .map(|envelope| envelope.operations.len() as u64)
1136
- .sum();
1137
- let recovered_clock = self.causal_barrier.get_frontier().clone();
1138
- let recovered_hash = self
1139
- .canonical_state()
1140
- .map(|state| state.hash())
1141
- .unwrap_or_else(|_| StateHash::from_hex("0".repeat(64)));
1142
- if let Some(local) = self.replicas.get_mut(&self.local_node_id) {
1143
- local.vector_clock = recovered_clock;
1144
- local.operations_applied = operations_applied;
1145
- local.state_hash = recovered_hash;
1146
- }
1147
-
1148
- // Identity is recovered from the same log, and this is the half that
1149
- // was missing: the causal frontier was already rebuilt here, so a
1150
- // restarted origin came back knowing its causal position and not its
1151
- // identity position, and issued colliding identities on top of a
1152
- // correct clock.
1153
- self.next_origin_sequence = envelopes
1154
- .iter()
1155
- .filter(|envelope| envelope.envelope_id.originating_node == self.local_node_id)
1156
- .map(|envelope| envelope.envelope_id.sequence)
1157
- .max()
1158
- .unwrap_or(0)
1159
- .saturating_add(1);
1160
-
1161
- Ok(envelopes.len())
1162
- }
1163
-
1164
- /// Persist an envelope to the durable operation log
1165
- /// Must be called after successful state application but before returning to caller
1166
- ///
1167
- /// Note: dedup set is NOT persisted (it's derived from the operation log).
1168
- fn persist_envelope(&mut self, envelope: &TransactionEnvelope) -> Result<(), String> {
1169
- if let Some(ref mut log) = self.operation_log {
1170
- log.append(envelope)
1171
- .map_err(|e| format!("Failed to persist operation: {}", e))?;
1172
- }
1173
- Ok(())
1174
- }
1175
- }
1176
-
1177
- #[cfg(test)]
1178
- mod tests {
1179
- use super::*;
1180
-
1181
- #[test]
1182
- fn test_envelope_creation() {
1183
- let envelope = TransactionEnvelope::new(
1184
- "node1".to_string(),
1185
- 1,
1186
- "txn1".to_string(),
1187
- Vec::new(),
1188
- ConsistencyContract::causal(),
1189
- );
1190
-
1191
- assert_eq!(envelope.originating_node, "node1");
1192
- assert_eq!(envelope.transaction_id, "txn1");
1193
- assert_eq!(envelope.envelope_id.sequence, 1);
1194
- }
1195
-
1196
- #[test]
1197
- fn test_envelope_integrity() {
1198
- let envelope = TransactionEnvelope::new(
1199
- "node1".to_string(),
1200
- 1,
1201
- "txn1".to_string(),
1202
- vec![], // Empty operations
1203
- ConsistencyContract::causal(),
1204
- );
1205
-
1206
- assert!(!envelope.verify_integrity());
1207
- }
1208
-
1209
- #[test]
1210
- fn test_replication_message_creation() {
1211
- let envelope = TransactionEnvelope::new(
1212
- "node1".to_string(),
1213
- 1,
1214
- "txn1".to_string(),
1215
- Vec::new(),
1216
- ConsistencyContract::causal(),
1217
- );
1218
-
1219
- let msg = ReplicationMessage::new(envelope, "node1".to_string(), "node2".to_string(), 1);
1220
-
1221
- assert_eq!(msg.from_node, "node1");
1222
- assert_eq!(msg.to_node, "node2");
1223
- }
1224
-
1225
- #[test]
1226
- fn test_replica_state_creation() {
1227
- let state = ReplicaState::new(
1228
- "node1".to_string(),
1229
- StateHash::from_hex("hash0".to_string()),
1230
- );
1231
-
1232
- assert_eq!(state.node_id, "node1");
1233
- assert_eq!(state.operations_applied, 0);
1234
- }
1235
-
1236
- #[test]
1237
- fn test_distributed_executor_creation() {
1238
- let executor = DistributedTransactionExecutor::new(
1239
- "node1".to_string(),
1240
- StateHash::from_hex("hash0".to_string()),
1241
- );
1242
-
1243
- assert!(executor.replicas.contains_key("node1"));
1244
- }
1245
-
1246
- #[test]
1247
- fn test_register_replica() {
1248
- let mut executor = DistributedTransactionExecutor::new(
1249
- "node1".to_string(),
1250
- StateHash::from_hex("hash0".to_string()),
1251
- );
1252
-
1253
- executor.register_replica(
1254
- "node2".to_string(),
1255
- StateHash::from_hex("hash0".to_string()),
1256
- );
1257
-
1258
- assert!(executor.replicas.contains_key("node2"));
1259
- }
1260
-
1261
- #[test]
1262
- fn test_convergence_check_empty() {
1263
- let executor = DistributedTransactionExecutor::new(
1264
- "node1".to_string(),
1265
- StateHash::from_hex("hash0".to_string()),
1266
- );
1267
-
1268
- // Single replica should be converged
1269
- assert!(executor.check_convergence());
1270
- }
1271
-
1272
- #[test]
1273
- fn test_convergence_check_multiple_replicas() {
1274
- let mut executor = DistributedTransactionExecutor::new(
1275
- "node1".to_string(),
1276
- StateHash::from_hex("hash0".to_string()),
1277
- );
1278
-
1279
- executor.register_replica(
1280
- "node2".to_string(),
1281
- StateHash::from_hex("hash0".to_string()),
1282
- );
1283
- executor.register_replica(
1284
- "node3".to_string(),
1285
- StateHash::from_hex("hash0".to_string()),
1286
- );
1287
-
1288
- assert!(executor.check_convergence());
1289
- }
1290
-
1291
- #[test]
1292
- fn test_convergence_check_diverged() {
1293
- let mut executor = DistributedTransactionExecutor::new(
1294
- "node1".to_string(),
1295
- StateHash::from_hex("hash0".to_string()),
1296
- );
1297
-
1298
- executor.register_replica(
1299
- "node2".to_string(),
1300
- StateHash::from_hex("hash1".to_string()),
1301
- );
1302
-
1303
- assert!(!executor.check_convergence());
1304
- }
1305
-
1306
- #[test]
1307
- fn test_causal_read_initial_state() {
1308
- let executor = DistributedTransactionExecutor::new(
1309
- "node1".to_string(),
1310
- StateHash::from_hex("hash0".to_string()),
1311
- );
1312
-
1313
- let vc = VectorClock::new();
1314
- assert!(executor.causal_read(&vc));
1315
- }
1316
-
1317
- #[test]
1318
- fn test_envelope_clock_advance() {
1319
- let mut envelope = TransactionEnvelope::new(
1320
- "node1".to_string(),
1321
- 1,
1322
- "txn1".to_string(),
1323
- Vec::new(),
1324
- ConsistencyContract::causal(),
1325
- );
1326
-
1327
- let mut other_clock = VectorClock::new();
1328
- other_clock.increment("node2");
1329
-
1330
- envelope.advance_clock(&other_clock);
1331
-
1332
- assert!(envelope.vector_clock.happens_before(&other_clock) == false);
1333
- }
1334
- }