Skip to content

Commit b3cb1e0

Browse files
committed
Register outlet consumers before completing the inlet handshake
1 parent 19c439f commit b3cb1e0

3 files changed

Lines changed: 68 additions & 24 deletions

File tree

‎src/tcp_server.cpp‎

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,15 @@ class sync_write_handler {
100100
close_sockets(sockets_swapped_);
101101
}
102102

103-
/// Add a socket for sync writes (called from client_session after handshake)
103+
/// Send the handshake and register a socket atomically with respect to sample writes.
104104
void add_socket(tcp_socket::native_handle_type handle, tcp_socket::protocol_type protocol,
105-
bool reverse_byte_order) {
105+
bool reverse_byte_order, const asio::streambuf::const_buffers_type &header) {
106106
std::lock_guard<std::mutex> lock(mutex_);
107107
auto sock = std::make_unique<tcp_socket>(io_ctx_, protocol, handle);
108108
apply_send_timeout(*sock);
109+
// An inlet may return from open_stream() as soon as these bytes arrive.
110+
// Keep sample writes locked out until the socket is registered below.
111+
asio::write(*sock, header);
109112
if (reverse_byte_order) {
110113
sockets_swapped_.push_back(std::move(sock));
111114
LOG_F(INFO, "Added sync socket (swapped endian), now have %zu native + %zu swapped",
@@ -259,7 +262,8 @@ class client_session : public std::enable_shared_from_this<client_session> {
259262
int request_protocol_version, const std::string &request_uid, err_t err);
260263

261264
/// Handler that gets called sending the feedheader has completed.
262-
void handle_send_feedheader_outcome(err_t err, std::size_t n);
265+
void handle_send_feedheader_outcome(
266+
err_t err, std::size_t n, std::shared_ptr<consumer_queue> queue);
263267

264268
/// Transfers samples from the server's send buffer into the async send queues of IO threads
265269
void transfer_samples_thread(std::shared_ptr<client_session> /*keepalive*/,
@@ -706,18 +710,34 @@ void client_session::handle_read_feedparams(
706710
*outarch_ << *temp;
707711
}
708712

709-
// send off the newly created feedheader
710-
async_write(
711-
sock_, feedbuf_.data(), [shared_this = shared_from_this()](err_t err, std::size_t len) {
712-
shared_this->handle_send_feedheader_outcome(err, len);
713+
if (max_buffered_ > 0 && serv->is_sync_mode()) {
714+
// The synchronous writer sends the header under its sample-write lock,
715+
// then registers the socket before an immediate push can acquire it.
716+
auto protocol = sock_.local_endpoint().protocol();
717+
serv->sync_handler_->add_socket(
718+
sock_.release(), protocol, reverse_byte_order_, feedbuf_.data());
719+
serv->unregister_inflight_session(this);
720+
return;
721+
}
722+
723+
// Subscribe before sending the handshake: receiving its test patterns is
724+
// what lets the inlet return from open_stream(). Queue samples until the
725+
// header write completes so data cannot overtake the handshake.
726+
std::shared_ptr<consumer_queue> queue;
727+
if (max_buffered_ > 0) queue = serv->send_buffer_->new_consumer(max_buffered_);
728+
async_write(sock_, feedbuf_.data(),
729+
[shared_this = shared_from_this(), queue = std::move(queue)](
730+
err_t err, std::size_t len) mutable {
731+
shared_this->handle_send_feedheader_outcome(err, len, std::move(queue));
713732
});
714733
DLOG_F(2, "%p sent test pattern samples", this);
715734
} catch (std::exception &e) {
716735
LOG_F(WARNING, "Unexpected error while serializing the feed header: %s", e.what());
717736
}
718737
}
719738

720-
void client_session::handle_send_feedheader_outcome(err_t err, std::size_t n) {
739+
void client_session::handle_send_feedheader_outcome(
740+
err_t err, std::size_t n, std::shared_ptr<consumer_queue> queue) {
721741
try {
722742
if (err) return;
723743

@@ -730,22 +750,6 @@ void client_session::handle_send_feedheader_outcome(err_t err, std::size_t n) {
730750
// convenient for unit tests
731751
if (max_buffered_ <= 0) return;
732752

733-
// If server is in sync mode, hand off the socket to the sync handler
734-
if (serv->is_sync_mode()) {
735-
LOG_F(INFO, "Handing off socket to sync handler for zero-copy transfer (reverse_byte_order=%d)",
736-
reverse_byte_order_);
737-
auto protocol = sock_.local_endpoint().protocol();
738-
// Release the socket from this io_context and add to sync handler
739-
// See https://stackoverflow.com/q/52671836/73299
740-
serv->sync_handler_->add_socket(sock_.release(), protocol, reverse_byte_order_);
741-
// Unregister this session since we're handing off the socket
742-
serv->unregister_inflight_session(this);
743-
return;
744-
}
745-
746-
// determine transfer parameters
747-
auto queue = serv->send_buffer_->new_consumer(max_buffered_);
748-
749753
// determine the maximum chunk size
750754
int max_samples_per_chunk = std::numeric_limits<int>::max();
751755
if (chunk_granularity_)

‎testing/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ target_link_libraries(common PUBLIC Catch2::Catch2)
6969
add_executable(lsl_test_exported
7070
ext/DataType.cpp
7171
ext/discovery.cpp
72+
ext/inlet_open.cpp
7273
ext/move.cpp
7374
ext/streaminfo.cpp
7475
ext/sync_outlet.cpp

‎testing/ext/inlet_open.cpp‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#include <catch2/catch_all.hpp>
2+
#include <lsl_cpp.h>
3+
#include <string>
4+
5+
TEST_CASE("The first sample after open_stream is delivered", "[inlet][open][basic]") {
6+
// Cover the original string-stream report, ordinary numeric outlets, and
7+
// synchronous numeric outlets. Each iteration uses a newly created inlet.
8+
const int mode = GENERATE(0, 1, 2);
9+
const bool strings = mode == 2;
10+
const bool synchronous = mode == 1;
11+
CAPTURE(strings, synchronous);
12+
const auto source_id = "open-ready-" + std::to_string(lsl::local_clock());
13+
lsl::stream_info info("open-ready", "test", 1, 0,
14+
strings ? lsl::cf_string : lsl::cf_float32, source_id);
15+
lsl::stream_outlet outlet(info, 0, 360,
16+
synchronous ? transp_sync_blocking : transp_default);
17+
auto found = lsl::resolve_stream("source_id", source_id, 1, 2);
18+
REQUIRE(found.size() == 1);
19+
20+
for (int trial = 0; trial < 30; ++trial) {
21+
CAPTURE(trial);
22+
lsl::stream_inlet inlet(found[0]);
23+
const float sent_number = static_cast<float>(trial);
24+
const std::string sent_string = std::to_string(trial);
25+
inlet.open_stream(2);
26+
// No sleep, wait_for_consumers(), retry, or extra push between these calls.
27+
if (strings) {
28+
outlet.push_sample(&sent_string);
29+
std::string received;
30+
REQUIRE(inlet.pull_sample(&received, 1, 2) != 0);
31+
CHECK(received == sent_string);
32+
} else {
33+
outlet.push_sample(&sent_number);
34+
float received = -1;
35+
REQUIRE(inlet.pull_sample(&received, 1, 2) != 0);
36+
CHECK(received == sent_number);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)