@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,1474 +0,0 @@
1
- //! A FeltDB replication node, as an operating-system process.
2
- //!
3
- //! This exists so replication can be proved across a real process and network
4
- //! boundary rather than between executors sharing a heap. It is a harness, not
5
- //! a product surface: it speaks a line protocol on stdin/stdout so a test can
6
- //! drive it, and it implements no replication semantics of its own. Every
7
- //! decision about ordering, deduplication, durability, and convergence is made
8
- //! by `DistributedTransactionExecutor`; this binary only moves bytes between
9
- //! processes and reports what happened.
10
- //!
11
- //! ## Links are directed, one TCP connection each
12
- //!
13
- //! `TcpTransport` holds a single stream, and its `receive()` holds the stream
14
- //! lock while awaiting the next message. A bidirectional link would therefore
15
- //! deadlock the first `send()` behind the receive that is parked on the socket.
16
- //!
17
- //! So each direction gets its own connection: for a link X -> Y, Y binds a port
18
- //! dedicated to X and only ever receives on it, and X connects to that port and
19
- //! only ever sends. Three nodes in a full mesh is six directed links.
20
- //!
21
- //! That is a topology choice made here in the harness. It changes nothing about
22
- //! the transport or the protocol.
23
- //!
24
- //! ## Usage
25
- //!
26
- //! ```text
27
- //! feltdb_node --node-id A --data DIR --recv B=127.0.0.1:9111 --send B=127.0.0.1:9121
28
- //! ```
29
- //!
30
- //! `--recv PEER=ADDR` binds ADDR and accepts the connection PEER makes to it.
31
- //! `--send PEER=ADDR` connects to the address PEER is listening on for us.
32
- //!
33
- //! Commands on stdin, one per line:
34
- //!
35
- //! ```text
36
- //! execute-tx RECORD commit a local transaction and replicate it to every peer
37
- //! resend-last put the identical envelope back on the wire, to test dedup
38
- //! query-state the local replica's state hash and applied-operation count
39
- //! metrics per-link byte and message counters, straight from the transport
40
- //! causal-state applied frontier, and what is durable but held back
41
- //! hold-inbound PEER park messages from PEER instead of delivering them
42
- //! release-inbound PEER [reverse]
43
- //! deliver what was parked, optionally newest-first
44
- //! peers per-link connection state
45
- //! shutdown exit cleanly
46
- //! ```
47
-
48
- use feltdb::authority_failover::{AuthorityStore, ClaimResult, FencingToken, Heartbeat};
49
- use feltdb::convergence::VectorClock;
50
- use feltdb::distributed_transactions::{
51
- CausalCapacity, DistributedTransactionExecutor, ReceiveOutcome, ReplicationMessage,
52
- SubmitOutcome,
53
- };
54
- use feltdb::replica_acknowledgements::{
55
- AcknowledgementStore, ReclamationEvidence, ReplicaAcknowledgement,
56
- };
57
- use feltdb::replica_membership::MembershipStore;
58
- use feltdb::replication_protocol::ProtocolTransport;
59
- use feltdb::state_hash::StateHash;
60
- use feltdb::tcp_transport::TcpTransport;
61
- use feltdb::transactions::{
62
- ConsistencyContract, Operation, OperationCommand, OperationId, StateVersion,
63
- };
64
- use serde_json::json;
65
- use std::collections::{HashMap, HashSet};
66
- use std::io::{self, BufRead, Write};
67
- use std::path::PathBuf;
68
- use std::sync::atomic::{AtomicBool, Ordering};
69
- use std::sync::Arc;
70
- use tokio::sync::{mpsc, Mutex};
71
- use tokio::time::{sleep, Duration};
72
-
73
- /// One outbound connection to a peer, used only for sending.
74
- ///
75
- /// `control` exists because `connect` needs `&mut self`. Everything else goes
76
- /// through `io`, a clone: `TcpTransport` holds its stream and metrics behind
77
- /// `Arc`s, so a clone is the same connection, and reading counters or sending
78
- /// does not queue behind whatever else holds the mutex.
79
- #[derive(Clone)]
80
- struct SendLink {
81
- peer: String,
82
- addr: String,
83
- control: Arc<Mutex<TcpTransport>>,
84
- io: TcpTransport,
85
- }
86
-
87
- /// One inbound connection from a peer, used only for receiving.
88
- ///
89
- /// The distinction matters more here: `receive()` parks on the socket holding
90
- /// the transport's internal stream lock, so anything sharing an outer mutex
91
- /// with it would block until a message happened to arrive.
92
- #[derive(Clone)]
93
- struct RecvLink {
94
- peer: String,
95
- #[allow(dead_code)]
96
- addr: String,
97
- control: Arc<Mutex<TcpTransport>>,
98
- io: TcpTransport,
99
- }
100
-
101
- /// Hand one replicated message to the executor and record what it decided.
102
- ///
103
- /// The harness makes no ordering decision of its own here. It calls the same
104
- /// `receive_replicated` the shipping receive path calls, and reports the typed
105
- /// outcome back so a test can assert on "applied" versus "held back" rather
106
- /// than inferring it from state.
107
- fn deliver(
108
- executor: &mut DistributedTransactionExecutor,
109
- version: &mut StateVersion,
110
- message: ReplicationMessage,
111
- ) -> serde_json::Value {
112
- let origin = message.envelope.originating_node.clone();
113
- let key = format!(
114
- "{}:{}",
115
- message.envelope.envelope_id.originating_node, message.envelope.envelope_id.sequence
116
- );
117
- let parent = version.clone();
118
- match executor.receive_replicated(message, parent) {
119
- Ok(ReceiveOutcome::Applied { state_hash }) => {
120
- // Only a real application advances the chained version, so a held
121
- // or duplicated message cannot make the node look like it moved.
122
- version.state_hash = state_hash.to_string();
123
- version.vector_clock.increment(&origin);
124
- json!({ "envelope": key, "outcome": "applied" })
125
- }
126
- Ok(ReceiveOutcome::PendingDependencies { missing }) => {
127
- json!({ "envelope": key, "outcome": "pending", "missing": missing })
128
- }
129
- Ok(ReceiveOutcome::AlreadyKnown) => {
130
- json!({ "envelope": key, "outcome": "already_known" })
131
- }
132
- Ok(ReceiveOutcome::CapacityDeferred {
133
- missing,
134
- pending_entries,
135
- pending_bytes,
136
- }) => {
137
- // Durable, but not buffered. The caller's obligation is to stop
138
- // reading from this peer; the envelope is admitted from the log
139
- // once the backlog drains.
140
- json!({
141
- "envelope": key,
142
- "outcome": "capacity_deferred",
143
- "missing": missing,
144
- "pending_entries": pending_entries,
145
- "pending_bytes": pending_bytes,
146
- })
147
- }
148
- Err(error) => json!({ "envelope": key, "outcome": "error", "error": error }),
149
- }
150
- }
151
-
152
- fn parse_link(value: &str) -> Result<(String, String), String> {
153
- let (peer, addr) = value
154
- .split_once('=')
155
- .ok_or_else(|| format!("expected PEER=ADDR, got {value}"))?;
156
- if peer.is_empty() || addr.is_empty() {
157
- return Err(format!("expected PEER=ADDR, got {value}"));
158
- }
159
- Ok((peer.to_string(), addr.to_string()))
160
- }
161
-
162
- /// Connect with retries: the peer's listener may not be bound yet, and after a
163
- /// peer restarts its old socket is gone and this is how the link comes back.
164
- async fn connect_with_retry(link: &SendLink, attempts: u32) -> bool {
165
- for attempt in 0..attempts {
166
- let mut control = link.control.lock().await;
167
- if control.connect(&link.addr).await.is_ok() {
168
- return true;
169
- }
170
- drop(control);
171
- sleep(Duration::from_millis(25 * (attempt as u64 + 1))).await;
172
- }
173
- false
174
- }
175
-
176
- #[tokio::main]
177
- async fn main() -> Result<(), Box<dyn std::error::Error>> {
178
- let args: Vec<String> = std::env::args().collect();
179
-
180
- let mut node_id = String::new();
181
- let mut data_dir: Option<PathBuf> = None;
182
- let mut send_specs: Vec<(String, String)> = Vec::new();
183
- let mut recv_specs: Vec<(String, String)> = Vec::new();
184
- let mut max_pending_entries: Option<usize> = None;
185
- let mut max_pending_bytes: Option<usize> = None;
186
- let mut authority_priority: u32 = 0;
187
- let mut heartbeat_interval_ms: u64 = 1_000;
188
- let mut heartbeat_failure_threshold: u32 = 5;
189
-
190
- let mut index = 1;
191
- while index < args.len() {
192
- match args[index].as_str() {
193
- "--node-id" => {
194
- index += 1;
195
- node_id = args.get(index).ok_or("--node-id requires a value")?.clone();
196
- }
197
- "--data" => {
198
- index += 1;
199
- data_dir = Some(PathBuf::from(
200
- args.get(index).ok_or("--data requires a value")?,
201
- ));
202
- }
203
- "--send" => {
204
- index += 1;
205
- send_specs.push(parse_link(
206
- args.get(index).ok_or("--send requires PEER=ADDR")?,
207
- )?);
208
- }
209
- "--recv" => {
210
- index += 1;
211
- recv_specs.push(parse_link(
212
- args.get(index).ok_or("--recv requires PEER=ADDR")?,
213
- )?);
214
- }
215
- // The bound is settable so an acceptance test can reach it through
216
- // the production path. Filling the shipping default would mean
217
- // sending ten thousand blocked envelopes over TCP; a test that
218
- // instead constructed a barrier and filled it directly would be
219
- // measuring a component nothing runs, which is the defect this
220
- // whole line of work exists to stop repeating.
221
- "--max-pending-entries" => {
222
- index += 1;
223
- max_pending_entries = Some(
224
- args.get(index)
225
- .ok_or("--max-pending-entries requires a value")?
226
- .parse::<usize>()
227
- .map_err(|e| format!("--max-pending-entries: {e}"))?,
228
- );
229
- }
230
- "--max-pending-bytes" => {
231
- index += 1;
232
- max_pending_bytes = Some(
233
- args.get(index)
234
- .ok_or("--max-pending-bytes requires a value")?
235
- .parse::<usize>()
236
- .map_err(|e| format!("--max-pending-bytes: {e}"))?,
237
- );
238
- }
239
- // Static priority for authority election. Higher priority nodes are
240
- // preferred during failover when multiple candidates have equal state.
241
- "--authority-priority" => {
242
- index += 1;
243
- authority_priority = args
244
- .get(index)
245
- .ok_or("--authority-priority requires a value")?
246
- .parse::<u32>()
247
- .map_err(|e| format!("--authority-priority: {e}"))?;
248
- }
249
- "--heartbeat-interval-ms" => {
250
- index += 1;
251
- heartbeat_interval_ms = args
252
- .get(index)
253
- .ok_or("--heartbeat-interval-ms requires a value")?
254
- .parse::<u64>()
255
- .map_err(|e| format!("--heartbeat-interval-ms: {e}"))?;
256
- }
257
- "--heartbeat-failure-threshold" => {
258
- index += 1;
259
- heartbeat_failure_threshold = args
260
- .get(index)
261
- .ok_or("--heartbeat-failure-threshold requires a value")?
262
- .parse::<u32>()
263
- .map_err(|e| format!("--heartbeat-failure-threshold: {e}"))?;
264
- }
265
- other => return Err(format!("unknown argument {other}").into()),
266
- }
267
- index += 1;
268
- }
269
-
270
- if node_id.is_empty() {
271
- return Err("--node-id is required".into());
272
- }
273
-
274
- // Durability is the executor's, not this binary's. The log path is per
275
- // node, so each process owns its own directory on disk.
276
- let log_path = data_dir.as_ref().map(|dir| {
277
- std::fs::create_dir_all(dir).ok();
278
- dir.join("operations.jsonl")
279
- });
280
-
281
- // Membership lives beside the log and is loaded from disk, never rebuilt
282
- // from --send/--recv. A node's connection flags say who it talks to; they
283
- // say nothing about who is owed history, and conflating the two is what
284
- // this store exists to stop.
285
- let mut membership = MembershipStore::open(
286
- data_dir
287
- .as_ref()
288
- .map(|dir| dir.join("membership.json"))
289
- .unwrap_or_else(|| PathBuf::from("membership.json")),
290
- )
291
- .map_err(|e| e.to_string())?;
292
-
293
- // Acknowledgements are durable and shared with the receive tasks, because
294
- // they arrive on the socket rather than through a command.
295
- let acknowledgements = Arc::new(Mutex::new(
296
- AcknowledgementStore::open(
297
- data_dir
298
- .as_ref()
299
- .map(|dir| dir.join("acknowledgements.json"))
300
- .unwrap_or_else(|| PathBuf::from("acknowledgements.json")),
301
- )
302
- .map_err(|e| e.to_string())?,
303
- ));
304
-
305
- // Authority state tracks who holds write authority in the cluster. The store
306
- // is durable and survives restarts, so a node that crashes can rejoin and
307
- // recover its previous role. The cluster ID is set by the first `authority-claim`
308
- // command, so it starts as the node's own ID.
309
- let authority = Arc::new(Mutex::new(
310
- AuthorityStore::open_with_failure_detector(
311
- data_dir
312
- .as_ref()
313
- .map(|dir| dir.join("authority.json"))
314
- .unwrap_or_else(|| PathBuf::from("authority.json")),
315
- &node_id, // cluster_id defaults to node_id until set
316
- &node_id,
317
- authority_priority,
318
- heartbeat_interval_ms,
319
- heartbeat_failure_threshold,
320
- )
321
- .map_err(|e| e.to_string())?,
322
- ));
323
-
324
- let initial_state = StateHash::from_hex("0".repeat(64));
325
- let mut executor = DistributedTransactionExecutor::with_log(
326
- node_id.clone(),
327
- initial_state.clone(),
328
- log_path.clone(),
329
- )?;
330
-
331
- // The bound is set before recovery, not after, because recovery restores
332
- // the backlog and has to respect the same limit the live path does.
333
- // Otherwise a node could restart into a backlog it would never have
334
- // accepted while running, and the bound would hold only until the first
335
- // crash.
336
- let default_capacity = CausalCapacity::default();
337
- executor.set_causal_capacity(CausalCapacity::new(
338
- max_pending_entries.unwrap_or(default_capacity.max_pending_entries),
339
- max_pending_bytes.unwrap_or(default_capacity.max_pending_bytes),
340
- ));
341
-
342
- // Replaying the log restores the deduplication set, so a restarted node
343
- // does not re-apply operations it already durably recorded.
344
- let mut replayed = 0usize;
345
- if let Some(path) = &log_path {
346
- if path.exists() {
347
- replayed = executor.load_from_disk(path.clone())?;
348
- }
349
- }
350
-
351
- for (peer, _) in send_specs.iter().chain(recv_specs.iter()) {
352
- if peer != &node_id {
353
- executor.register_replica(peer.clone(), initial_state.clone());
354
- }
355
- }
356
-
357
- // The running state version, advanced by every applied transaction.
358
- //
359
- // This matters more than it looks. `execute_transaction` derives the next
360
- // hash as H(parent_hash, command), so passing a fresh zero-hash parent each
361
- // time — as this binary and the old in-process tests both did — makes the
362
- // resulting "state hash" a function of the last command alone, carrying no
363
- // history. Two nodes that applied completely different sets of operations
364
- // then report identical hashes as long as the most recent one matches, and
365
- // a convergence check built on that comparison cannot fail.
366
- //
367
- // Chaining it makes the hash mean what its name says, and makes a node that
368
- // missed an operation observably divergent. It lives beside the executor
369
- // under one lock so a local commit and an arriving replication cannot
370
- // interleave and chain onto a parent that is already stale.
371
- let recovered_version = executor
372
- .get_replica_state(&node_id)
373
- .map(|replica| StateVersion::new(replica.vector_clock, replica.state_hash.to_string()))
374
- .unwrap_or_else(|| StateVersion::new(VectorClock::new(), "0".repeat(64)));
375
- let core = Arc::new(Mutex::new((executor, recovered_version)));
376
- let should_exit = Arc::new(AtomicBool::new(false));
377
- // Per-peer delivery hold.
378
- //
379
- // `Some(queue)` means messages arriving from that peer are received off the
380
- // socket as normal and parked instead of being handed to the executor.
381
- // This is how a test produces out-of-causal-order arrival without faking
382
- // the network: the bytes really do cross TCP, and only the order in which
383
- // the receiver is offered them is controlled. A real network reorders
384
- // delivery this way; the harness just does it on demand.
385
- let held: Arc<Mutex<HashMap<String, Vec<ReplicationMessage>>>> =
386
- Arc::new(Mutex::new(HashMap::new()));
387
- // Peers this node has stopped reading from because the causal backlog is
388
- // at its bound.
389
- //
390
- // This is the backpressure the bounding decision commits to, and not
391
- // reading is the whole mechanism: the socket's receive buffer fills, the
392
- // TCP window closes, and the sender blocks. Nothing is dropped and nothing
393
- // is refused, so no retransmission protocol is needed -- which is good,
394
- // because there isn't one.
395
- let backpressured: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
396
- // Kept so `resend-last` can put the identical envelope back on the wire.
397
- let last_envelope: Arc<Mutex<Option<feltdb::distributed_transactions::TransactionEnvelope>>> =
398
- Arc::new(Mutex::new(None));
399
-
400
- let send_links: Vec<SendLink> = send_specs
401
- .into_iter()
402
- .map(|(peer, addr)| {
403
- let transport = TcpTransport::new("127.0.0.1:0".to_string());
404
- SendLink {
405
- peer,
406
- addr,
407
- control: Arc::new(Mutex::new(transport.clone())),
408
- io: transport,
409
- }
410
- })
411
- .collect();
412
-
413
- let recv_links: Vec<RecvLink> = recv_specs
414
- .into_iter()
415
- .map(|(peer, addr)| {
416
- let transport = TcpTransport::new(addr.clone());
417
- RecvLink {
418
- peer,
419
- addr,
420
- control: Arc::new(Mutex::new(transport.clone())),
421
- io: transport,
422
- }
423
- })
424
- .collect();
425
-
426
- // Accept, serve, and accept again. One task per link, because each parks
427
- // on its own socket and must not block the others.
428
- //
429
- // The re-listen is what makes a peer restart survivable. `TcpTransport`'s
430
- // `listen()` binds, accepts exactly one connection, and drops the listener,
431
- // so after that first accept the port is closed and a peer that reconnects
432
- // is refused. Looping back to `listen()` once the connection ends rebinds
433
- // the port for the next one. That is a harness-level fix: the transport
434
- // keeps its single-connection shape and the protocol is untouched.
435
- for link in &recv_links {
436
- let control = link.control.clone();
437
- let transport = link.io.clone();
438
- let core = core.clone();
439
- let exit = should_exit.clone();
440
- let peer = link.peer.clone();
441
- let held = held.clone();
442
- let backpressured = backpressured.clone();
443
- let acknowledgements = acknowledgements.clone();
444
- tokio::spawn(async move {
445
- while !exit.load(Ordering::Relaxed) {
446
- {
447
- let mut control = control.lock().await;
448
- if let Err(error) = control.listen().await {
449
- eprintln!("listen for {peer} failed: {error}");
450
- drop(control);
451
- sleep(Duration::from_millis(100)).await;
452
- continue;
453
- }
454
- }
455
-
456
- // Serve this connection until it ends, then rebind for the next.
457
- loop {
458
- if exit.load(Ordering::Relaxed) {
459
- return;
460
- }
461
-
462
- // Do not read from a backpressured peer. While paused, keep
463
- // trying to drain: the work that frees capacity may arrive
464
- // on another connection, and a deferred envelope is already
465
- // durable, so draining needs nothing from this socket.
466
- while backpressured.lock().await.contains(&peer) {
467
- if exit.load(Ordering::Relaxed) {
468
- return;
469
- }
470
- {
471
- let mut guard = core.lock().await;
472
- let (executor, version) = &mut *guard;
473
- let parent = version.clone();
474
- let _ = executor.drain_backlog(parent);
475
- if executor.pending_causal_count()
476
- < executor.causal_capacity().max_pending_entries
477
- {
478
- backpressured.lock().await.remove(&peer);
479
- }
480
- }
481
- sleep(Duration::from_millis(25)).await;
482
- }
483
-
484
- match transport.receive().await {
485
- Ok(messages) => {
486
- // Acknowledgements arrive interleaved with
487
- // envelopes on the same connection. They are
488
- // durable before they count as evidence.
489
- for ack in transport.take_acknowledgements().await {
490
- let mut store = acknowledgements.lock().await;
491
- if let Err(error) = store.record(ack) {
492
- eprintln!("acknowledgement not recorded: {error}");
493
- }
494
- }
495
- for message in messages {
496
- {
497
- let mut hold = held.lock().await;
498
- if let Some(queue) = hold.get_mut(&peer) {
499
- queue.push(message);
500
- continue;
501
- }
502
- }
503
- let outcome = {
504
- let mut guard = core.lock().await;
505
- let (executor, version) = &mut *guard;
506
- deliver(executor, version, message)
507
- };
508
- if outcome.get("outcome").and_then(|value| value.as_str())
509
- == Some("capacity_deferred")
510
- {
511
- backpressured.lock().await.insert(peer.clone());
512
- }
513
- }
514
- }
515
- Err(_) => break,
516
- }
517
- }
518
- }
519
- });
520
- }
521
-
522
- // Establish outbound links before announcing readiness, so a test that
523
- // writes immediately is not racing the topology.
524
- for link in &send_links {
525
- connect_with_retry(link, 20).await;
526
- }
527
-
528
- println!(
529
- "STARTED {}",
530
- json!({
531
- "node": node_id,
532
- "replayed": replayed,
533
- "send": send_links.iter().map(|l| l.peer.clone()).collect::<Vec<_>>(),
534
- "recv": recv_links.iter().map(|l| l.peer.clone()).collect::<Vec<_>>(),
535
- })
536
- );
537
- io::stdout().flush().ok();
538
-
539
- let (commands, mut incoming) = mpsc::channel::<String>(100);
540
- tokio::task::spawn_blocking(move || {
541
- for line in io::stdin().lock().lines().map_while(Result::ok) {
542
- let line = line.trim().to_string();
543
- if !line.is_empty() && commands.blocking_send(line).is_err() {
544
- break;
545
- }
546
- }
547
- });
548
-
549
- while let Some(command) = incoming.recv().await {
550
- let (verb, argument) = match command.split_once(' ') {
551
- Some((verb, rest)) => (verb, rest.trim().to_string()),
552
- None => (command.as_str(), String::new()),
553
- };
554
-
555
- match verb {
556
- "execute-tx" | "authority-write" => {
557
- let record = if verb == "authority-write" {
558
- let mut parts = argument.splitn(2, ' ');
559
- let token = parts.next().and_then(|value| value.parse::<u64>().ok());
560
- let record = parts.next().unwrap_or("item").to_string();
561
- let validation = match token {
562
- Some(token) => authority
563
- .lock()
564
- .await
565
- .validate_quorum_write(FencingToken(token), 2),
566
- None => Err(feltdb::authority_failover::AuthorityError::StaleToken {
567
- provided: FencingToken(0),
568
- current: authority.lock().await.state().current_token(),
569
- }),
570
- };
571
- if let Err(error) = validation {
572
- println!(
573
- "AUTHORITY_WRITE_REJECTED {}",
574
- json!({ "record": record, "reason": error.to_string() })
575
- );
576
- io::stdout().flush().ok();
577
- continue;
578
- }
579
- record
580
- } else if argument.is_empty() {
581
- "item".to_string()
582
- } else {
583
- argument
584
- };
585
-
586
- let mut fields = HashMap::new();
587
- fields.insert("record".to_string(), json!(record.clone()));
588
-
589
- // The identity comes from the executor, which derives it from the
590
- // durable log. This binary used to keep its own counter,
591
- // starting at 1 on every launch, so a restarted node re-issued
592
- // identities its own earlier operations already held.
593
- let (parent, seq) = {
594
- let guard = core.lock().await;
595
- (guard.1.clone(), guard.0.next_origin_sequence())
596
- };
597
- let operation = Operation::new(
598
- OperationId::new(node_id.clone(), seq),
599
- parent.clone(),
600
- format!("tx_{record}"),
601
- OperationCommand {
602
- op_type: "set".to_string(),
603
- collection: "items".to_string(),
604
- record_id: record.clone(),
605
- fields,
606
- },
607
- node_id.clone(),
608
- );
609
-
610
- let envelope = {
611
- let mut guard = core.lock().await;
612
- let (executor, version) = &mut *guard;
613
- // The admitting entry point: local work is refused when the
614
- // causal backlog is at its bound, before anything is
615
- // executed or persisted, so a refusal leaves nothing to
616
- // undo.
617
- let result = executor.submit_local_transaction(
618
- format!("tx_{record}"),
619
- version.clone(),
620
- vec![operation],
621
- ConsistencyContract::local(),
622
- );
623
- if matches!(result, Ok(SubmitOutcome::Admitted(_))) {
624
- if let Some(replica) = executor.get_replica_state(&node_id) {
625
- version.state_hash = replica.state_hash.to_string();
626
- version.vector_clock.increment(&node_id);
627
- }
628
- }
629
- result
630
- };
631
-
632
- match envelope {
633
- Ok(SubmitOutcome::CapacityExceeded {
634
- pending_entries,
635
- pending_bytes,
636
- limit_entries,
637
- limit_bytes,
638
- }) => {
639
- // A distinct outcome, not an error: the caller should
640
- // retry, and nothing was admitted or lost.
641
- println!(
642
- "TX_REFUSED {}",
643
- json!({
644
- "record": record,
645
- "reason": "CAPACITY_EXCEEDED",
646
- "pending_entries": pending_entries,
647
- "pending_bytes": pending_bytes,
648
- "limit_entries": limit_entries,
649
- "limit_bytes": limit_bytes,
650
- })
651
- );
652
- }
653
- Ok(SubmitOutcome::Admitted(envelope)) => {
654
- *last_envelope.lock().await = Some(envelope.clone());
655
- let mut delivered = Vec::new();
656
- let mut failed = Vec::new();
657
- for link in &send_links {
658
- let message = ReplicationMessage::new(
659
- envelope.clone(),
660
- node_id.clone(),
661
- link.peer.clone(),
662
- seq,
663
- );
664
- let sent = link.io.send(message.clone()).await;
665
- match sent {
666
- Ok(()) => delivered.push(link.peer.clone()),
667
- Err(_) => {
668
- // The peer may have restarted, which drops
669
- // our end of the socket. Reconnect and try
670
- // once more before reporting a failure.
671
- if connect_with_retry(link, 3).await {
672
- if link.io.send(message).await.is_ok() {
673
- delivered.push(link.peer.clone());
674
- continue;
675
- }
676
- }
677
- failed.push(link.peer.clone());
678
- }
679
- }
680
- }
681
- println!(
682
- "TX_EXECUTED {}",
683
- json!({
684
- "record": record,
685
- // The identity the executor actually issued.
686
- "sequence": envelope.envelope_id.sequence,
687
- "delivered": delivered,
688
- "failed": failed,
689
- })
690
- );
691
- }
692
- Err(error) => println!("ERROR {}", json!({ "reason": error })),
693
- }
694
- io::stdout().flush().ok();
695
- }
696
-
697
- // Re-send the most recent envelope, unchanged, to every peer.
698
- //
699
- // Deduplication is a claimed property of the executor, and it is
700
- // only interesting if the duplicate actually crosses the wire.
701
- // Issuing another `execute-tx` would not test it: that produces a
702
- // new envelope id and is a different transaction.
703
- // `resend SEQUENCE` — put one specific envelope from this node's
704
- // durable log back on the wire.
705
- //
706
- // A peer re-sending an operation another node is missing is the
707
- // primitive catch-up is built from. This is not catch-up: nothing
708
- // decides *what* to send, a test does. It exists so the receiver's
709
- // behaviour can be exercised on a gap that a restart left behind.
710
- // `forward ORIGIN SEQUENCE` -- put an envelope this node holds, but
711
- // did not originate, back on the wire.
712
- //
713
- // `resend` deliberately only replays this node's own envelopes. A
714
- // peer relaying an operation that a *third* node is missing is a
715
- // different primitive, and it is the one a backpressured receiver
716
- // needs: when a node stops reading from the peer whose envelopes it
717
- // deferred, the dependency it is waiting for cannot arrive on that
718
- // link. It has to come from somewhere else, which is precisely the
719
- // case the read loop's keep-draining-while-paused behaviour exists
720
- // to serve.
721
- "forward" => {
722
- let mut parts = argument.split_whitespace();
723
- let origin = parts.next().unwrap_or_default().to_string();
724
- let wanted: Option<u64> = parts.next().and_then(|value| value.parse().ok());
725
- let envelope = {
726
- let guard = core.lock().await;
727
- let (executor, _) = &*guard;
728
- executor.operation_log.as_ref().and_then(|log| {
729
- log.load_all().ok().and_then(|envelopes| {
730
- envelopes.into_iter().find(|candidate| {
731
- candidate.envelope_id.originating_node == origin
732
- && Some(candidate.envelope_id.sequence) == wanted
733
- })
734
- })
735
- })
736
- };
737
- match envelope {
738
- Some(envelope) => {
739
- let mut delivered = Vec::new();
740
- for link in &send_links {
741
- let message = ReplicationMessage::new(
742
- envelope.clone(),
743
- node_id.clone(),
744
- link.peer.clone(),
745
- envelope.envelope_id.sequence,
746
- );
747
- // Forwarding, like re-sending, is an explicit
748
- // recovery operation. Prefer a fresh connection so
749
- // a peer restart cannot leave this write accepted
750
- // by a stale local socket and delivered nowhere.
751
- let _ = connect_with_retry(link, 3).await;
752
- if link.io.send(message).await.is_ok() {
753
- delivered.push(link.peer.clone());
754
- }
755
- }
756
- println!(
757
- "FORWARDED {}",
758
- json!({
759
- "origin": origin,
760
- "sequence": wanted,
761
- "delivered": delivered,
762
- })
763
- );
764
- }
765
- None => println!(
766
- "FORWARDED {}",
767
- json!({ "origin": origin, "sequence": wanted, "delivered": [], "found": false })
768
- ),
769
- }
770
- io::stdout().flush().ok();
771
- }
772
-
773
- "resend" => {
774
- let wanted: Option<u64> = argument.trim().parse().ok();
775
- let envelope = {
776
- let guard = core.lock().await;
777
- let (executor, _) = &*guard;
778
- executor.operation_log.as_ref().and_then(|log| {
779
- log.load_all().ok().and_then(|envelopes| {
780
- envelopes.into_iter().find(|candidate| {
781
- candidate.envelope_id.originating_node == node_id
782
- && Some(candidate.envelope_id.sequence) == wanted
783
- })
784
- })
785
- })
786
- };
787
- match envelope {
788
- Some(envelope) => {
789
- let mut delivered = Vec::new();
790
- for link in &send_links {
791
- let message = ReplicationMessage::new(
792
- envelope.clone(),
793
- node_id.clone(),
794
- link.peer.clone(),
795
- envelope.envelope_id.sequence,
796
- );
797
- // Re-sending is the recovery primitive used after a
798
- // peer restart. A write to the old TCP socket can
799
- // still return success after the peer has exited,
800
- // because that only confirms acceptance by the
801
- // local kernel. Try to establish a fresh connection
802
- // first. If the peer never restarted, the connect
803
- // attempt leaves the established stream untouched.
804
- let _ = connect_with_retry(link, 3).await;
805
- if link.io.send(message).await.is_ok() {
806
- delivered.push(link.peer.clone());
807
- }
808
- }
809
- println!(
810
- "RESENT {}",
811
- json!({ "envelope": envelope.envelope_id, "delivered": delivered })
812
- );
813
- }
814
- None => println!(
815
- "ERROR {}",
816
- json!({ "reason": format!("no envelope {argument} in this node's log") })
817
- ),
818
- }
819
- io::stdout().flush().ok();
820
- }
821
-
822
- "resend-last" => {
823
- let envelope = last_envelope.lock().await.clone();
824
- match envelope {
825
- Some(envelope) => {
826
- let mut delivered = Vec::new();
827
- for link in &send_links {
828
- let message = ReplicationMessage::new(
829
- envelope.clone(),
830
- node_id.clone(),
831
- link.peer.clone(),
832
- envelope.envelope_id.sequence,
833
- );
834
- if link.io.send(message).await.is_ok() {
835
- delivered.push(link.peer.clone());
836
- }
837
- }
838
- println!(
839
- "RESENT {}",
840
- json!({ "envelope": envelope.envelope_id, "delivered": delivered })
841
- );
842
- }
843
- None => println!("ERROR {}", json!({ "reason": "nothing sent yet" })),
844
- }
845
- io::stdout().flush().ok();
846
- }
847
-
848
- "query-state" => {
849
- let guard = core.lock().await;
850
- let (executor, _) = &*guard;
851
- let canonical = executor
852
- .canonical_state()
853
- .map(|state| state.hash().to_string())
854
- .unwrap_or_else(|error| format!("error: {error}"));
855
- match executor.get_replica_state(&node_id) {
856
- Some(replica) => println!(
857
- "STATE {}",
858
- json!({
859
- "hash": replica.state_hash.to_string(),
860
- "operations_applied": replica.operations_applied,
861
- // Order-independent, and therefore the one to
862
- // compare across replicas. The chained hash above
863
- // detects a missing operation in an ordered
864
- // history; it calls two nodes that applied the same
865
- // concurrent operations in different orders
866
- // divergent, which they are not.
867
- "canonical_hash": canonical,
868
- })
869
- ),
870
- None => println!("ERROR {}", json!({ "reason": "no local replica" })),
871
- }
872
- io::stdout().flush().ok();
873
- }
874
-
875
- // What the causal receiver is holding, and why.
876
- // Membership commands. Deliberately no compaction frontier and no
877
- // acknowledgement tracking: this establishes who exists and who is
878
- // required, and reclaims nothing.
879
- "cluster-create" => {
880
- let result = membership.create_cluster(argument.trim());
881
- println!(
882
- "CLUSTER {}",
883
- match result {
884
- Ok(()) => json!({ "cluster_id": argument.trim(), "created": true }),
885
- Err(error) => json!({ "created": false, "error": error.to_string() }),
886
- }
887
- );
888
- io::stdout().flush().ok();
889
- }
890
-
891
- "replica-join" | "replica-approve" | "replica-suspect" | "replica-seen"
892
- | "replica-remove" => {
893
- let mut parts = argument.split_whitespace();
894
- let replica = parts.next().unwrap_or_default().to_string();
895
- let result = match verb {
896
- "replica-join" => {
897
- let node = parts.next().unwrap_or(&replica).to_string();
898
- membership.join_replica(&replica, &node)
899
- }
900
- "replica-approve" => membership.approve_replica(&replica),
901
- "replica-suspect" => membership.suspect_replica(&replica),
902
- "replica-seen" => membership.mark_seen(&replica),
903
- _ => membership.remove_replica(&replica),
904
- };
905
- println!(
906
- "REPLICA {}",
907
- match result {
908
- Ok(()) => json!({ "replica_id": replica, "action": verb, "ok": true }),
909
- Err(error) =>
910
- json!({ "replica_id": replica, "action": verb, "ok": false, "error": error.to_string() }),
911
- }
912
- );
913
- io::stdout().flush().ok();
914
- }
915
-
916
- // Report what this node has durably applied, to every peer.
917
- //
918
- // The frontier is read from the executor, where it advances only in
919
- // record_applied -- after an fsynced envelope is applied. So the
920
- // statement "durably applied through this frontier" is true by
921
- // construction rather than by assertion.
922
- "ack-broadcast" => {
923
- let (frontier, marks) = {
924
- let guard = core.lock().await;
925
- let (executor, _) = &*guard;
926
- (
927
- executor.causal_frontier().clone(),
928
- executor.origin_sequence_high_water(),
929
- )
930
- };
931
- let view: Vec<String> = membership
932
- .get_members()
933
- .into_iter()
934
- .map(|replica| replica.replica_id)
935
- .collect();
936
- let replica_id = membership
937
- .get_members()
938
- .into_iter()
939
- .find(|replica| replica.node_id == node_id)
940
- .map(|replica| replica.replica_id)
941
- .unwrap_or_else(|| node_id.clone());
942
-
943
- let ack = ReplicaAcknowledgement::new(
944
- replica_id.clone(),
945
- node_id.clone(),
946
- frontier,
947
- marks,
948
- view,
949
- );
950
-
951
- // A node's own acknowledgement is evidence it holds first-hand:
952
- // it knows its own durable frontier without being told. Without
953
- // this a node would be permanently blocked on itself, since
954
- // nothing else can report on its behalf.
955
- {
956
- let mut store = acknowledgements.lock().await;
957
- let _ = store.record(ack.clone());
958
- }
959
-
960
- let mut delivered = Vec::new();
961
- for link in &send_links {
962
- // Acknowledgements are durable recovery evidence and are
963
- // commonly rebroadcast after a peer restart. Refresh the
964
- // connection first for the same reason as explicit
965
- // envelope replay.
966
- let _ = connect_with_retry(link, 3).await;
967
- if link.io.send_acknowledgement(ack.clone()).await.is_ok() {
968
- delivered.push(link.peer.clone());
969
- }
970
- }
971
- println!(
972
- "ACK_SENT {}",
973
- json!({
974
- "replica_id": replica_id,
975
- "frontier": ack.applied_frontier.clocks,
976
- "delivered": delivered,
977
- })
978
- );
979
- io::stdout().flush().ok();
980
- }
981
-
982
- "acknowledgements" => {
983
- let store = acknowledgements.lock().await;
984
- println!("ACKS {}", json!({ "acknowledgements": store.all() }));
985
- io::stdout().flush().ok();
986
- }
987
-
988
- // What this node's evidence permits. Computes; reclaims nothing.
989
- "reclamation-evidence" => {
990
- let local: Vec<String> = membership
991
- .get_required_replicas()
992
- .into_iter()
993
- .map(|replica| replica.replica_id)
994
- .collect();
995
- let store = acknowledgements.lock().await;
996
- let report = match store.evidence(&local) {
997
- ReclamationEvidence::Blocked { waiting_on } => json!({
998
- "state": "BLOCKED",
999
- "waiting_on": waiting_on,
1000
- "required_union": store.required_union(&local),
1001
- }),
1002
- ReclamationEvidence::SafeThrough { frontier, covered } => json!({
1003
- "state": "SAFE_THROUGH",
1004
- "frontier": frontier.clocks,
1005
- "covered": covered,
1006
- "required_union": store.required_union(&local),
1007
- }),
1008
- };
1009
- println!("EVIDENCE {}", report);
1010
- io::stdout().flush().ok();
1011
- }
1012
-
1013
- "members" => {
1014
- println!(
1015
- "MEMBERS {}",
1016
- json!({
1017
- "cluster_id": membership.cluster().map(|c| c.cluster_id.clone()),
1018
- "members": membership.get_members(),
1019
- "all": membership.all_replicas(),
1020
- })
1021
- );
1022
- io::stdout().flush().ok();
1023
- }
1024
-
1025
- "required-replicas" => {
1026
- println!(
1027
- "REQUIRED {}",
1028
- json!({ "required": membership.get_required_replicas() })
1029
- );
1030
- io::stdout().flush().ok();
1031
- }
1032
-
1033
- "causal-state" => {
1034
- let guard = core.lock().await;
1035
- let (executor, _) = &*guard;
1036
- let paused = {
1037
- let mut peers: Vec<String> =
1038
- backpressured.lock().await.iter().cloned().collect();
1039
- peers.sort();
1040
- peers
1041
- };
1042
- println!(
1043
- "CAUSAL {}",
1044
- json!({
1045
- "frontier": executor.causal_frontier().clocks,
1046
- "pending": executor.pending_causal_keys(),
1047
- "pending_count": executor.pending_causal_count(),
1048
- "pending_metadata_bytes": executor.pending_causal_metadata_bytes(),
1049
- // Occupancy against the bound that is now enforced.
1050
- // Current and maximum, for both dimensions, because a
1051
- // backlog that filled and drained between two reads is
1052
- // invisible to a current-value gauge.
1053
- "pending_payload_bytes": executor.pending_causal_payload_bytes(),
1054
- "deferred_count": executor.deferred_causal_count(),
1055
- "deferred": executor.deferred_causal_keys(),
1056
- "max_pending_entries_seen": executor.max_causal_occupancy().0,
1057
- "max_pending_payload_bytes_seen": executor.max_causal_occupancy().1,
1058
- "limit_entries": executor.causal_capacity().max_pending_entries,
1059
- "limit_bytes": executor.causal_capacity().max_pending_bytes,
1060
- // Peers this node has stopped reading from. This is the
1061
- // observable for backpressure: the pause lives in the
1062
- // socket read loop, so without it a test can only infer
1063
- // the behaviour from timing.
1064
- //
1065
- // Locked after `core`, matching the order the read loop
1066
- // takes them in.
1067
- "backpressured": paused,
1068
- })
1069
- );
1070
- io::stdout().flush().ok();
1071
- }
1072
-
1073
- // Park messages from a peer instead of delivering them, so a test
1074
- // can choose the order the receiver is offered them in.
1075
- "hold-inbound" => {
1076
- let mut hold = held.lock().await;
1077
- hold.entry(argument.clone()).or_default();
1078
- println!("HELD {}", json!({ "peer": argument }));
1079
- io::stdout().flush().ok();
1080
- }
1081
-
1082
- // Deliver what was parked, optionally newest-first, then resume
1083
- // normal delivery. Each message goes through the same production
1084
- // receiver as an unheld one.
1085
- // `release-inbound PEER [reverse] [COUNT]`
1086
- //
1087
- // COUNT releases only that many and keeps holding the rest, which
1088
- // is what lets a test offer an operation while deliberately
1089
- // withholding the dependency it needs.
1090
- "release-inbound" => {
1091
- let mut parts = argument.split_whitespace();
1092
- let peer = parts.next().unwrap_or_default().to_string();
1093
- let mut reverse = false;
1094
- let mut limit: Option<usize> = None;
1095
- for token in parts {
1096
- if token == "reverse" {
1097
- reverse = true;
1098
- } else if let Ok(value) = token.parse::<usize>() {
1099
- limit = Some(value);
1100
- }
1101
- }
1102
-
1103
- let mut queued = { held.lock().await.remove(&peer).unwrap_or_default() };
1104
- if reverse {
1105
- queued.reverse();
1106
- }
1107
- let take = limit.unwrap_or(queued.len()).min(queued.len());
1108
- let remainder = queued.split_off(take);
1109
-
1110
- let mut outcomes = Vec::new();
1111
- for message in queued {
1112
- let mut guard = core.lock().await;
1113
- let (executor, version) = &mut *guard;
1114
- outcomes.push(deliver(executor, version, message));
1115
- }
1116
-
1117
- // Anything not released stays held, so the hold is still in
1118
- // force for messages that arrive afterwards.
1119
- if !remainder.is_empty() || limit.is_some() {
1120
- held.lock().await.insert(peer.clone(), remainder);
1121
- }
1122
-
1123
- println!(
1124
- "RELEASED {}",
1125
- json!({ "peer": peer, "reverse": reverse, "outcomes": outcomes })
1126
- );
1127
- io::stdout().flush().ok();
1128
- }
1129
-
1130
- // The proof that the network carried the work. Counters come from
1131
- // the transport itself, so a test can assert bytes actually moved
1132
- // rather than inferring it from converged state.
1133
- "metrics" => {
1134
- let mut sent = json!({});
1135
- let mut received = json!({});
1136
- for link in &send_links {
1137
- let metrics = link.io.metrics();
1138
- sent[&link.peer] = json!({
1139
- "bytes": metrics.get_bytes_sent(),
1140
- "messages": metrics.get_messages_sent(),
1141
- "connection_changes": metrics.get_connection_state_changes(),
1142
- });
1143
- }
1144
- for link in &recv_links {
1145
- let metrics = link.io.metrics();
1146
- received[&link.peer] = json!({
1147
- "bytes": metrics.get_bytes_received(),
1148
- "messages": metrics.get_messages_received(),
1149
- "connection_changes": metrics.get_connection_state_changes(),
1150
- });
1151
- }
1152
- println!("METRICS {}", json!({ "sent": sent, "received": received }));
1153
- io::stdout().flush().ok();
1154
- }
1155
-
1156
- "list-operations" => {
1157
- let guard = core.lock().await;
1158
- let (executor, _) = &*guard;
1159
-
1160
- // Get all operations from the log
1161
- let mut all_ops = Vec::new();
1162
- let mut applied_ops = Vec::new();
1163
- let pending_ops = executor.pending_causal_keys();
1164
- let deferred_ops = executor.deferred_causal_keys();
1165
-
1166
- if let Some(ref log) = executor.operation_log {
1167
- if let Ok(envelopes) = log.load_all() {
1168
- for env in &envelopes {
1169
- let key = format!(
1170
- "{}:{}",
1171
- env.envelope_id.originating_node, env.envelope_id.sequence
1172
- );
1173
- all_ops.push(key.clone());
1174
- }
1175
- }
1176
- }
1177
-
1178
- // Get applied operations from the barrier
1179
- for key in executor.causal_barrier.applied_keys() {
1180
- applied_ops.push(key.to_string());
1181
- }
1182
-
1183
- println!(
1184
- "OPERATIONS {}",
1185
- json!({
1186
- "all": all_ops,
1187
- "applied": applied_ops,
1188
- "pending": pending_ops,
1189
- "deferred": deferred_ops,
1190
- "operations_applied": executor.get_replica_state(&node_id).map(|r| r.operations_applied).unwrap_or(0),
1191
- })
1192
- );
1193
- io::stdout().flush().ok();
1194
- }
1195
-
1196
- "operation-inventory" => {
1197
- let guard = core.lock().await;
1198
- let (executor, _) = &*guard;
1199
-
1200
- // Get all operations from the log
1201
- let mut operations = json!({
1202
- "applied": [],
1203
- "pending": [],
1204
- "deferred": [],
1205
- });
1206
-
1207
- if let Some(ref log) = executor.operation_log {
1208
- if let Ok(envelopes) = log.load_all() {
1209
- let applied_keys = executor.causal_barrier.applied_keys();
1210
- let pending_keys = executor.pending_causal_keys();
1211
- let deferred_keys = executor.deferred_causal_keys();
1212
-
1213
- for env in &envelopes {
1214
- let key = format!(
1215
- "{}:{}",
1216
- env.envelope_id.originating_node, env.envelope_id.sequence
1217
- );
1218
-
1219
- if applied_keys.contains(&key) {
1220
- operations["applied"].as_array_mut().unwrap().push(json!({
1221
- "id": key,
1222
- "origin": env.envelope_id.originating_node,
1223
- "sequence": env.envelope_id.sequence,
1224
- }));
1225
- } else if pending_keys.contains(&key) {
1226
- operations["pending"].as_array_mut().unwrap().push(json!({
1227
- "id": key,
1228
- "origin": env.envelope_id.originating_node,
1229
- "sequence": env.envelope_id.sequence,
1230
- }));
1231
- } else if deferred_keys.contains(&key) {
1232
- operations["deferred"].as_array_mut().unwrap().push(json!({
1233
- "id": key,
1234
- "origin": env.envelope_id.originating_node,
1235
- "sequence": env.envelope_id.sequence,
1236
- }));
1237
- }
1238
- }
1239
- }
1240
- }
1241
-
1242
- println!("INVENTORY {}", operations);
1243
- io::stdout().flush().ok();
1244
- }
1245
-
1246
- "peers" => {
1247
- let mut state = json!({});
1248
- for link in &send_links {
1249
- state[format!("send:{}", link.peer)] = json!(link.io.is_connected());
1250
- }
1251
- for link in &recv_links {
1252
- state[format!("recv:{}", link.peer)] = json!(link.io.is_connected());
1253
- }
1254
- println!("PEERS {state}");
1255
- io::stdout().flush().ok();
1256
- }
1257
-
1258
- // Authority failover commands.
1259
- //
1260
- // These commands implement the distributed authority protocol that prevents
1261
- // split-brain and allows automatic failover when the authority node fails.
1262
-
1263
- // Claim authority for this node. This generates a new fencing token that
1264
- // supersedes any previous authority. The claim succeeds only if no other
1265
- // node has a higher token and appears healthy.
1266
- "authority-claim" => {
1267
- let mut guard = authority.lock().await;
1268
- let result = guard.claim_authority();
1269
- match result {
1270
- Ok(ClaimResult::Granted { token }) => {
1271
- println!(
1272
- "AUTHORITY_CLAIMED {}",
1273
- json!({
1274
- "node": node_id,
1275
- "fencing_token": token.0,
1276
- "role": "AUTHORITY",
1277
- })
1278
- );
1279
- }
1280
- Ok(ClaimResult::Rejected {
1281
- current_authority,
1282
- current_token,
1283
- }) => {
1284
- println!(
1285
- "AUTHORITY_REJECTED {}",
1286
- json!({
1287
- "current_authority": current_authority,
1288
- "current_token": current_token.0,
1289
- "reason": "another authority is still active",
1290
- })
1291
- );
1292
- }
1293
- Ok(ClaimResult::Fenced { token }) => {
1294
- println!(
1295
- "AUTHORITY_FENCED {}",
1296
- json!({
1297
- "node": node_id,
1298
- "fencing_token": token.0,
1299
- "reason": "this node is fenced",
1300
- })
1301
- );
1302
- }
1303
- Err(e) => {
1304
- println!("ERROR {}", json!({ "reason": e.to_string() }));
1305
- }
1306
- }
1307
- io::stdout().flush().ok();
1308
- }
1309
-
1310
- // Accept a new authority announced by another node.
1311
- // Format: authority-accept NODE_ID TOKEN
1312
- "authority-accept" => {
1313
- let mut parts = argument.split_whitespace();
1314
- let authority_node = parts.next().unwrap_or_default().to_string();
1315
- let token_value: u64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
1316
- let token = FencingToken(token_value);
1317
-
1318
- let mut guard = authority.lock().await;
1319
- let result = guard.accept_authority(&authority_node, token);
1320
- match result {
1321
- Ok(()) => {
1322
- let state = guard.state();
1323
- println!(
1324
- "AUTHORITY_ACCEPTED {}",
1325
- json!({
1326
- "authority_node": authority_node,
1327
- "fencing_token": token.0,
1328
- "local_role": format!("{:?}", state.local_role),
1329
- })
1330
- );
1331
- }
1332
- Err(e) => {
1333
- println!("ERROR {}", json!({ "reason": e.to_string() }));
1334
- }
1335
- }
1336
- io::stdout().flush().ok();
1337
- }
1338
-
1339
- // Query the current authority state.
1340
- "authority-state" => {
1341
- let guard = authority.lock().await;
1342
- let state = guard.state();
1343
- let operations_applied = {
1344
- let core_guard = core.lock().await;
1345
- let (executor, _) = &*core_guard;
1346
- executor
1347
- .get_replica_state(&node_id)
1348
- .map(|r| r.operations_applied)
1349
- .unwrap_or(0)
1350
- };
1351
- println!(
1352
- "AUTHORITY {}",
1353
- json!({
1354
- "cluster_id": state.cluster_id,
1355
- "local_node": state.local_node_id,
1356
- "local_role": format!("{:?}", state.local_role),
1357
- "is_authority": state.is_authority(),
1358
- "is_fenced": state.is_fenced(),
1359
- "authority_node": state.authority_node(),
1360
- "current_token": state.current_token().0,
1361
- "highest_seen_token": state.highest_seen_token.0,
1362
- "operations_applied": operations_applied,
1363
- })
1364
- );
1365
- io::stdout().flush().ok();
1366
- }
1367
-
1368
- // Record a heartbeat from a peer. This is used for failure detection.
1369
- // Format: heartbeat NODE_ID [TOKEN] [OPS_APPLIED] [IS_AUTHORITY]
1370
- "heartbeat" => {
1371
- let mut parts = argument.split_whitespace();
1372
- let from_node = parts.next().unwrap_or_default().to_string();
1373
-
1374
- if from_node.is_empty() {
1375
- println!(
1376
- "ERROR {}",
1377
- json!({ "reason": "heartbeat requires NODE_ID" })
1378
- );
1379
- } else {
1380
- let mut guard = authority.lock().await;
1381
- guard.heartbeat(&from_node);
1382
- println!(
1383
- "HEARTBEAT_RECORDED {}",
1384
- json!({
1385
- "from_node": from_node,
1386
- })
1387
- );
1388
- }
1389
- io::stdout().flush().ok();
1390
- }
1391
-
1392
- // Check if the current authority has failed based on heartbeat timeout.
1393
- "authority-failed" => {
1394
- let guard = authority.lock().await;
1395
- let failed = guard.authority_failed();
1396
- let failed_nodes = guard.failed_nodes();
1397
- let state = guard.state();
1398
- println!(
1399
- "AUTHORITY_CHECK {}",
1400
- json!({
1401
- "authority_failed": failed,
1402
- "current_authority": state.authority_node(),
1403
- "failed_nodes": failed_nodes,
1404
- })
1405
- );
1406
- io::stdout().flush().ok();
1407
- }
1408
-
1409
- // Step down from authority role voluntarily.
1410
- "authority-stepdown" => {
1411
- let mut guard = authority.lock().await;
1412
- let was_authority = guard.state().is_authority();
1413
- let result = guard.step_down();
1414
- match result {
1415
- Ok(()) => {
1416
- println!(
1417
- "AUTHORITY_STEPDOWN {}",
1418
- json!({
1419
- "node": node_id,
1420
- "was_authority": was_authority,
1421
- "now_role": format!("{:?}", guard.state().local_role),
1422
- })
1423
- );
1424
- }
1425
- Err(e) => {
1426
- println!("ERROR {}", json!({ "reason": e.to_string() }));
1427
- }
1428
- }
1429
- io::stdout().flush().ok();
1430
- }
1431
-
1432
- // Recover from fenced state after catching up.
1433
- "authority-recover" => {
1434
- let mut guard = authority.lock().await;
1435
- let was_fenced = guard.state().is_fenced();
1436
- let result = guard.recover_from_fenced();
1437
- match result {
1438
- Ok(()) => {
1439
- println!(
1440
- "AUTHORITY_RECOVERED {}",
1441
- json!({
1442
- "node": node_id,
1443
- "was_fenced": was_fenced,
1444
- "now_role": format!("{:?}", guard.state().local_role),
1445
- })
1446
- );
1447
- }
1448
- Err(e) => {
1449
- println!("ERROR {}", json!({ "reason": e.to_string() }));
1450
- }
1451
- }
1452
- io::stdout().flush().ok();
1453
- }
1454
-
1455
- "shutdown" => {
1456
- should_exit.store(true, Ordering::Relaxed);
1457
- println!("SHUTDOWN");
1458
- io::stdout().flush().ok();
1459
- sleep(Duration::from_millis(50)).await;
1460
- std::process::exit(0);
1461
- }
1462
-
1463
- _ => {
1464
- println!(
1465
- "ERROR {}",
1466
- json!({ "reason": format!("unknown command {verb}") })
1467
- );
1468
- io::stdout().flush().ok();
1469
- }
1470
- }
1471
- }
1472
-
1473
- Ok(())
1474
- }