Skip to content

Add an outbound binary send primitive to all transports - #113

Draft
chrisuthe wants to merge 7 commits into
source-role/01-protocolfrom
source-role/02-binary-send
Draft

Add an outbound binary send primitive to all transports#113
chrisuthe wants to merge 7 commits into
source-role/01-protocolfrom
source-role/02-binary-send

Conversation

@chrisuthe

Copy link
Copy Markdown
Member

Part 2/6 of the source@v1 stack (tracker: #95).

What it adds: send_binary_message() on the connection interface and all four transports (ESP client/server, host client/server). The ESP server transport gets a single-in-flight send slot that is allocation-free in steady state; the completion callback fires exactly once per call.

How it's used: part 3's source task calls it once per audio chunk. No callers yet in this part, so no behavior changes.

@chrisuthe
chrisuthe force-pushed the source-role/02-binary-send branch from ea0ad1c to 2d30ccf Compare September 1, 2026 02:32
@chrisuthe chrisuthe closed this Sep 3, 2026
@chrisuthe chrisuthe reopened this Sep 4, 2026
@chrisuthe chrisuthe added the enhancement New feature or request label Sep 4, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The ESP hot path still allocates, includes a shutdown leak, and lacks synchronized exactly-once verification.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds outbound binary messaging across all connection transports to support the upcoming source role.

Changes:

  • Adds send_binary_message() to the connection interface and four transports.
  • Implements a reusable single-in-flight ESP server send slot.
  • Adds host transport delivery tests and NOT_FINISHED.
File summaries
File Description
src/connection.h Defines the binary-send contract.
src/platform/types.h Adds the busy-operation error code.
src/host/client_connection.h Declares host-client binary sending.
src/host/client_connection.cpp Implements host-client binary sending.
src/host/server_connection.h Declares host-server binary sending.
src/host/server_connection.cpp Implements host-server binary sending.
src/esp/client_connection.h Declares ESP-client binary sending.
src/esp/client_connection.cpp Implements synchronous ESP-client sending.
src/esp/server_connection.h Defines ESP-server send-slot state.
src/esp/server_connection.cpp Implements queued ESP-server binary sending.
tests/test_connection_lifecycle.cpp Tests host binary-frame delivery.
Review details

Suppressed comments (2)

src/esp/server_connection.cpp:243

  • The first binary chunk also allocates the lookup control block in the per-chunk path, so pre-sizing the payload alone would still violate the allocation-free hot-path rule in docs/conventions.md:67-68. Create this reusable context during connection setup (with an explicit allocation-failure path) rather than lazily in send_binary_message().
    if (this->binary_send_lookup_ == nullptr) {
        this->binary_send_lookup_ = std::make_shared<BinarySendLookup>();
        this->binary_send_lookup_->conn =
            std::static_pointer_cast<SendspinServerConnection>(this->shared_from_this());

tests/test_connection_lifecycle.cpp:768

  • Like the client-side test, this asserts immediately after the first frame and can miss a duplicate delivered just afterward. Synchronize with a fully drained/stopped peer before asserting the final frame count so the test actually protects the exactly-once behavior.
    {
        const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
        while (binary_frames.load() == 0 && std::chrono::steady_clock::now() < deadline) {
            std::this_thread::sleep_for(std::chrono::milliseconds(5));
        }
    }
    ASSERT_EQ(binary_frames.load(), 1);
  • Files reviewed: 11/11 changed files
  • Comments generated: 5
  • Review effort level: Balanced

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/esp/server_connection.cpp Outdated
Comment thread src/esp/server_connection.cpp
Comment thread tests/test_connection_lifecycle.cpp
Comment thread src/connection.h
Comment thread src/esp/server_connection.cpp
Track engaged binary send lookups in a registry reclaimed after
httpd_stop so discarded work cannot strand keep-alive cycles across
server restarts, allocate the lookup block at connection construction
instead of on the first send, document the completion callback's
execution contexts on the interface, and drain the peer's close before
the wire tests re-assert their exactly-once counts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Global ESP work reclamation can invalidate queued work belonging to another live server.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/connection.h:159

  • This contract says binary frames are hello-gated, but the host client/server and ESP client implementations only check is_connected() and send immediately; only the queued ESP-server worker checks client_hello_sent_. A role using this interface could therefore rely on ordering that three transports do not provide. Either enforce the gate consistently or document that synchronous transports require the caller to wait for handshake completion.
    src/esp/server_connection.h:119
  • The slot is also released on immediate allocation failure and httpd_queue_work failure, so “only by the worker's completion or the destructor” is inaccurate. Describe release as occurring on every immediate-failure/completion path or destruction so this lifecycle contract matches lines 250-289.
  • Files reviewed: 12/12 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/esp/server_connection.cpp Outdated
Comment thread src/esp/server_connection.cpp
The process-global sweep could break another live server's still-queued
worker keep-alive (a cross-server use-after-free window) and raced the
worker's unclaimed self-move. The transitions now live in an owner-
scoped, host-tested registry: engage parks the keep-alive under the
connection's httpd handle, the worker claims it back under the registry
lock, and a stopped server reclaims only its own blocks. The interface
doc regains the synchronous-transport hello-gate nuance and the slot
doc names the immediate-failure release paths.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

ESP queue-loss and failed-shutdown paths can violate the completion and reclamation guarantees.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 15/15 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread src/esp/server_connection.cpp
Comment thread src/esp/ws_server.cpp Outdated
Comment thread src/esp/server_connection.cpp Outdated
httpd_stop can return ESP_FAIL with its shutdown control message
unsent, leaving the httpd task able to run queued workers; reclaiming
their lookup blocks then would violate reclaim()'s no-worker-can-run
precondition, so reclaim only on ESP_OK and leave the blocks (a bounded
leak) otherwise. Also corrects the completion comment: the callback
wakes the source task to send the next chunk rather than re-entering
the connection itself, which the interface forbids.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The ESP binary-message hot path can still allocate when the registry vector grows.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/binary_send_registry.h:45

  • engage() runs once per queued binary frame, but push_back allocates on the first frame and again whenever concurrent sends exceed the vector's previous high-water mark. That violates the hot-path rule in docs/conventions.md:67-71 and undermines the ESP transport's allocation-free claim. Use an intrusive or fixed-capacity registry sized outside the send path, with explicit overflow handling.
  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants