@feltdb/core 0.15.0 → 0.16.1

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 (248) 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 +20 -2
  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 +356 -24
  17. package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +10 -1
  18. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +1 -1
  19. package/dist/db.d.ts +11 -1
  20. package/dist/db.d.ts.map +1 -1
  21. package/dist/db.js +15 -2
  22. package/dist/error-codes.d.ts +5 -0
  23. package/dist/error-codes.d.ts.map +1 -1
  24. package/dist/error-codes.js +9 -1
  25. package/dist/http-db.d.ts +16 -0
  26. package/dist/http-db.d.ts.map +1 -1
  27. package/dist/http-db.js +36 -6
  28. package/dist/managed-recovery.d.ts +6 -0
  29. package/dist/managed-recovery.d.ts.map +1 -1
  30. package/dist/managed-recovery.js +9 -0
  31. package/dist/studio-app/assets/{feltdb_wasm-BjM1ylL3.js → feltdb_wasm-BUIxCuTR.js} +1 -1
  32. package/dist/studio-app/assets/feltdb_wasm_bg-C3D6y6DK.wasm +0 -0
  33. package/dist/studio-app/assets/index-BhXYAhgS.js +37 -0
  34. package/dist/studio-app/index.html +1 -1
  35. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  36. package/dist/wasm/package.json +1 -1
  37. package/dist/workload.d.ts +55 -3
  38. package/dist/workload.d.ts.map +1 -1
  39. package/dist/workload.js +23 -0
  40. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  41. package/dist/workspace/workspace-connection.js +14 -3
  42. package/package.json +1 -1
  43. package/dist/create/server-source/crates/feltdb/Cargo.lock +0 -175
  44. package/dist/create/server-source/crates/feltdb/Cargo.toml +0 -102
  45. package/dist/create/server-source/crates/feltdb/LICENSE +0 -21
  46. package/dist/create/server-source/crates/feltdb/README.md +0 -75
  47. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +0 -370
  48. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +0 -589
  49. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +0 -247
  50. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +0 -440
  51. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +0 -275
  52. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +0 -381
  53. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +0 -467
  54. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +0 -489
  55. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +0 -281
  56. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +0 -606
  57. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +0 -531
  58. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +0 -462
  59. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +0 -505
  60. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +0 -484
  61. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +0 -758
  62. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +0 -421
  63. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +0 -329
  64. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +0 -1705
  65. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +0 -286
  66. package/dist/create/server-source/crates/feltdb/src/admission.rs +0 -185
  67. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +0 -507
  68. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +0 -539
  69. package/dist/create/server-source/crates/feltdb/src/agent_state.rs +0 -320
  70. package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +0 -1545
  71. package/dist/create/server-source/crates/feltdb/src/analytics.rs +0 -521
  72. package/dist/create/server-source/crates/feltdb/src/application.rs +0 -2674
  73. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +0 -1070
  74. package/dist/create/server-source/crates/feltdb/src/authority.rs +0 -479
  75. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +0 -847
  76. package/dist/create/server-source/crates/feltdb/src/authorization.rs +0 -1030
  77. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +0 -1122
  78. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +0 -32
  79. package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +0 -19
  80. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +0 -1474
  81. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +0 -8
  82. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +0 -418
  83. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +0 -482
  84. package/dist/create/server-source/crates/feltdb/src/capability.rs +0 -903
  85. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +0 -263
  86. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -77
  87. package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +0 -496
  88. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +0 -2574
  89. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +0 -212
  90. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +0 -422
  91. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +0 -445
  92. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +0 -465
  93. package/dist/create/server-source/crates/feltdb/src/convergence.rs +0 -634
  94. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +0 -427
  95. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +0 -376
  96. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +0 -365
  97. package/dist/create/server-source/crates/feltdb/src/cron.rs +0 -294
  98. package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +0 -483
  99. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +0 -477
  100. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +0 -1003
  101. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +0 -1334
  102. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +0 -368
  103. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +0 -231
  104. package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +0 -482
  105. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +0 -203
  106. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +0 -317
  107. package/dist/create/server-source/crates/feltdb/src/equality_index.rs +0 -595
  108. package/dist/create/server-source/crates/feltdb/src/execution.rs +0 -426
  109. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +0 -214
  110. package/dist/create/server-source/crates/feltdb/src/indexing.rs +0 -776
  111. package/dist/create/server-source/crates/feltdb/src/lib.rs +0 -7365
  112. package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +0 -234
  113. package/dist/create/server-source/crates/feltdb/src/materialization.rs +0 -184
  114. package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -266
  115. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +0 -310
  116. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +0 -736
  117. package/dist/create/server-source/crates/feltdb/src/observability.rs +0 -379
  118. package/dist/create/server-source/crates/feltdb/src/operation.rs +0 -290
  119. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +0 -439
  120. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +0 -349
  121. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +0 -332
  122. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +0 -718
  123. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +0 -487
  124. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +0 -166
  125. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +0 -289
  126. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +0 -566
  127. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +0 -3391
  128. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +0 -203
  129. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +0 -261
  130. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +0 -502
  131. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +0 -216
  132. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +0 -303
  133. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +0 -323
  134. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +0 -531
  135. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +0 -406
  136. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +0 -436
  137. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +0 -387
  138. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +0 -378
  139. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +0 -359
  140. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +0 -372
  141. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +0 -404
  142. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +0 -376
  143. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +0 -461
  144. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +0 -2125
  145. package/dist/create/server-source/crates/feltdb/src/production_api.rs +0 -418
  146. package/dist/create/server-source/crates/feltdb/src/provenance.rs +0 -207
  147. package/dist/create/server-source/crates/feltdb/src/query_execution_diagnostics.rs +0 -126
  148. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +0 -218
  149. package/dist/create/server-source/crates/feltdb/src/read_basis.rs +0 -257
  150. package/dist/create/server-source/crates/feltdb/src/references.rs +0 -404
  151. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +0 -401
  152. package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +0 -501
  153. package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +0 -680
  154. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +0 -163
  155. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +0 -133
  156. package/dist/create/server-source/crates/feltdb/src/routing.rs +0 -409
  157. package/dist/create/server-source/crates/feltdb/src/semantic_decision.rs +0 -2029
  158. package/dist/create/server-source/crates/feltdb/src/semantic_query.rs +0 -599
  159. package/dist/create/server-source/crates/feltdb/src/sharding.rs +0 -549
  160. package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +0 -516
  161. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +0 -6003
  162. package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +0 -222
  163. package/dist/create/server-source/crates/feltdb/src/state_facade.rs +0 -320
  164. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +0 -291
  165. package/dist/create/server-source/crates/feltdb/src/state_model.rs +0 -4158
  166. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +0 -379
  167. package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +0 -673
  168. package/dist/create/server-source/crates/feltdb/src/storage.rs +0 -273
  169. package/dist/create/server-source/crates/feltdb/src/submission.rs +0 -471
  170. package/dist/create/server-source/crates/feltdb/src/sync.rs +0 -495
  171. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +0 -822
  172. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +0 -433
  173. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +0 -462
  174. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +0 -965
  175. package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +0 -1120
  176. package/dist/create/server-source/crates/feltdb/src/transactions.rs +0 -643
  177. package/dist/create/server-source/crates/feltdb/src/trigger.rs +0 -390
  178. package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +0 -749
  179. package/dist/create/server-source/crates/feltdb/src/versioned_object_substrate.rs +0 -708
  180. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +0 -1269
  181. package/dist/create/server-source/crates/feltdb/src/workflow.rs +0 -713
  182. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +0 -510
  183. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +0 -354
  184. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +0 -594
  185. package/dist/create/server-source/crates/feltdb/src/workload.rs +0 -4222
  186. package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +0 -456
  187. package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +0 -1188
  188. package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +0 -2884
  189. package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +0 -297
  190. package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +0 -534
  191. package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +0 -132
  192. package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +0 -299
  193. package/dist/create/server-source/crates/feltdb/tests/common/mod.rs +0 -96
  194. package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +0 -272
  195. package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +0 -467
  196. package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +0 -309
  197. package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +0 -445
  198. package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +0 -518
  199. package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +0 -392
  200. package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +0 -850
  201. package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +0 -23340
  202. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +0 -1916
  203. package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +0 -1878
  204. package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +0 -1862
  205. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_evidence.rs +0 -123
  206. package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +0 -299
  207. package/dist/create/server-source/crates/feltdb/tests/local_process_ownership.rs +0 -69
  208. package/dist/create/server-source/crates/feltdb/tests/managed_incident_regression.rs +0 -191
  209. package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +0 -278
  210. package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +0 -234
  211. package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +0 -947
  212. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +0 -404
  213. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +0 -916
  214. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +0 -1045
  215. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +0 -347
  216. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +0 -361
  217. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +0 -330
  218. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +0 -93
  219. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +0 -622
  220. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +0 -517
  221. package/dist/create/server-source/crates/feltdb/tests/production_certification.rs +0 -1153
  222. package/dist/create/server-source/crates/feltdb/tests/production_contract.rs +0 -771
  223. package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +0 -1383
  224. package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +0 -868
  225. package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +0 -221
  226. package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +0 -417
  227. package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +0 -338
  228. package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +0 -1039
  229. package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +0 -739
  230. package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +0 -452
  231. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +0 -81
  232. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +0 -438
  233. package/dist/create/server-source/crates/feltdb/tests/semantic_decision.rs +0 -1165
  234. package/dist/create/server-source/crates/feltdb/tests/semantic_query.rs +0 -767
  235. package/dist/create/server-source/crates/feltdb/tests/semantic_query_contract.rs +0 -363
  236. package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +0 -1799
  237. package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +0 -1316
  238. package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +0 -358
  239. package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +0 -365
  240. package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +0 -918
  241. package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +0 -299
  242. package/dist/create/server-source/crates/feltdb/tests/storage_independent_versioned_object_substrate.rs +0 -328
  243. package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +0 -255
  244. package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +0 -249
  245. package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +0 -994
  246. package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +0 -484
  247. package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
  248. 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
- }