Skip to content

feat(orchestrator): C2 real-time command dispatch#43

Merged
martinjms merged 1 commit intomainfrom
feat/c2-command-dispatch
May 2, 2026
Merged

feat(orchestrator): C2 real-time command dispatch#43
martinjms merged 1 commit intomainfrom
feat/c2-command-dispatch

Conversation

@martinjms
Copy link
Copy Markdown
Collaborator

Implements #22 — orchestrator component C2 (real-time command dispatch). Makes the `tests/command_dispatch.rs` acceptance tests pass.

What this does

`CommandDispatcher` fans out `OrchestratorCommand` values from any submitter to every `Active` driver concurrently, bounded by a configurable wall-clock deadline, and records per-driver acks + missing in an append-only command log keyed by submitter identity.

  • Domain-neutral. Initial command set: `SetPlayers(N)`, `SetSpawnDelayMs(ms)`, `Stop`. The dispatcher carries no benchmark vocabulary.
  • Slow drivers don't block fast ones. Acks land in the result as they arrive; the deadline cuts off the wait but doesn't drop already-collected acks.
  • Multi-submitter. Multiple controllers can submit concurrently; each log entry carries submitter id + monotonic seq.
  • Generic over transport. `DriverChannel` trait abstracts the wire so production can wire mpsc channels populated by the WS server while tests use `MockDriverChannel`.

What's deliberately out of this PR

Test plan

  • `cargo build -p arcane-swarm-orchestrator` passes
  • `cargo test -p arcane-swarm-orchestrator` — 45 passed, 0 failed (8 new C2 tests + existing 37)
  • `cargo clippy -p arcane-swarm-orchestrator --all-targets -- -D warnings` clean
  • `cargo fmt --all -- --check` clean

Acceptance tests covered

All 7 scenarios from issue #22 + 1 wire-fairness test:

  • `set_players_broadcasts_to_all_active_drivers_within_100ms`
  • `per_driver_acknowledgment_recorded`
  • `stale_driver_does_not_block_broadcast_to_others`
  • `unknown_command_returns_typed_error_no_broadcast` (closed-enum interpretation: `NoActiveDrivers`)
  • `multiple_controllers_can_submit_concurrently`
  • `set_spawn_delay_ms_propagates_to_drivers`
  • `stop_command_is_broadcast_and_logged`
  • `slow_driver_excluded_from_acks_when_past_deadline`

Closes #22.

🤖 Generated with Claude Code

Implements component C2 of the orchestrator MVP — domain-neutral command
broadcast plane. The dispatcher fans out OrchestratorCommand values
(SetPlayers, SetSpawnDelayMs, Stop) from any submitter to every Active
driver concurrently, bounded by a configurable wall-clock deadline, and
records per-driver acks + missing in an append-only command log keyed by
submitter identity.

Design notes:
- Generic over a DriverChannel trait so production wires per-driver mpsc
  channels populated by the WS server, while tests inject MockDriverChannel.
- Slow drivers don't block the broadcast: fan-out collects acks via mpsc
  and breaks at the deadline, leaving partial results intact (no all-or-
  nothing timeout cancellation).
- Stale drivers are filtered out at submit time; if no driver is Active
  the dispatcher returns DispatchError::NoActiveDrivers without logging.
- Multiple controllers can submit concurrently; each entry in the log
  carries the submitter id and a monotonic seq.

WS server wiring (driver-side ack push, channel registration on Register)
is intentionally a follow-up — keeps this PR's blast radius to the
dispatcher logic itself, with mock-driven tests as the spec.

Closes #22.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@martinjms martinjms merged commit 1d61621 into main May 2, 2026
1 check passed
@martinjms martinjms deleted the feat/c2-command-dispatch branch May 2, 2026 16:14
martinjms added a commit that referenced this pull request May 2, 2026
Completes the wire layer that PR #43 (in-process dispatcher) deliberately
deferred. A real driver can now register, receive server-pushed Command
envelopes, and ack them — and the dispatcher's submit() resolves
end-to-end against a real WebSocket.

Protocol additions (backward-compatible):
- DriverMessage::CommandAck(CommandAck): driver→orchestrator ack frame
- OrchestratorResponse::Command(CommandEnvelope { seq, command }):
  server-pushed command frame
- DriverChannel::send takes a `seq` parameter so the dispatcher's
  monotonic sequence number flows through to the wire envelope and
  back via the matching ack.

Server changes (server.rs):
- DriverServer::with_dispatcher constructor wires an Arc<CommandDispatcher>
  through to handle_connection. Old `new()` (no dispatcher) preserved for
  call sites that don't need command dispatch.
- handle_connection refactored into a reader/writer pair. The writer
  multiplexes command envelopes + handshake responses onto the WS sink;
  the reader handles register/heartbeat/deregister/ack messages and
  routes acks to per-seq oneshots.
- WsDriverChannel: production DriverChannel impl backed by a per-
  connection mpsc + oneshot pair; populated on Register, dropped on
  Deregister or disconnect.

Tests:
- Two new e2e tests (tests/ws_command_e2e.rs) drive a real WS round-trip:
  one asserts the dispatcher's submit returns acks routed back through
  the wire; the other asserts the dispatcher's channel is dropped when
  a driver deregisters.

This is the orchestrator-side counterpart to issue #27 (driver-side
extension); together they form the full C2 wire path.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Orchestrator C2: real-time command dispatch (replaces tier ramp coordinator)

1 participant