Add an outbound binary send primitive to all transports - #113
Conversation
ea0ad1c to
2d30ccf
Compare
There was a problem hiding this comment.
🟡 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 insend_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.
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.
There was a problem hiding this comment.
🟡 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 checksclient_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_workfailure, 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
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.
There was a problem hiding this comment.
🟡 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
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.
There was a problem hiding this comment.
🔵 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, butpush_backallocates on the first frame and again whenever concurrent sends exceed the vector's previous high-water mark. That violates the hot-path rule indocs/conventions.md:67-71and 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
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.