Skip to content

Commit c18924c

Browse files
committed
Bound the publish_live_objects graceful flush on stop
Treat a flush write failure or in-loop send failure that races with a concurrent close() as a benign stop (return success), and time-box the post-loop flush as secondary insurance against a slow multi-write flush. Adds flush-fallback, graceful-flush, and no-false-positive tests; the mock transport now fails writes once closed, mirroring picoquic teardown.
1 parent ced45c8 commit c18924c

2 files changed

Lines changed: 177 additions & 4 deletions

File tree

src/transport/moqt_session.cpp

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4134,6 +4134,11 @@ TransportStatus MoqtSession::publish_live_objects(const openmoq::publisher::Live
41344134
next->final_in_subgroup,
41354135
std::span<const std::uint8_t>(next->payload));
41364136
if (!status.ok) {
4137+
// A send failure that races with a concurrent close() is a
4138+
// benign stop, not a transport error: break to the flush.
4139+
if (stop_requested_.load(std::memory_order_acquire)) {
4140+
break;
4141+
}
41374142
return status;
41384143
}
41394144
record_published_object(next->track_name,
@@ -4181,6 +4186,11 @@ TransportStatus MoqtSession::publish_live_objects(const openmoq::publisher::Live
41814186
next->final_in_subgroup,
41824187
std::span<const std::uint8_t>(next->payload));
41834188
if (!status.ok) {
4189+
// A send failure that races with a concurrent close() is a benign
4190+
// stop, not a transport error: break to the flush.
4191+
if (stop_requested_.load(std::memory_order_acquire)) {
4192+
break;
4193+
}
41844194
return status;
41854195
}
41864196
record_published_object(next->track_name,
@@ -4191,15 +4201,35 @@ TransportStatus MoqtSession::publish_live_objects(const openmoq::publisher::Live
41914201
}
41924202
}
41934203

4204+
// Bound the post-loop graceful flush. When a stop is in progress, a flush
4205+
// write can fail (transport torn down by a concurrent close()) or the total
4206+
// flush can run long; in either case return success rather than surfacing a
4207+
// stop-driven flush failure. The deadline is secondary insurance for a
4208+
// multi-write flush that is merely slow.
4209+
const auto flush_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
4210+
const auto flush_budget_exhausted = [&]() {
4211+
return stop_requested_.load(std::memory_order_acquire) &&
4212+
std::chrono::steady_clock::now() >= flush_deadline;
4213+
};
4214+
const auto stopping = [&]() {
4215+
return stop_requested_.load(std::memory_order_acquire);
4216+
};
4217+
41944218
for (auto& [track_name, sender] : sender_by_track) {
41954219
static_cast<void>(track_name);
4220+
if (flush_budget_exhausted()) {
4221+
return TransportStatus::success();
4222+
}
41964223
status = sender.finish_group(transport_);
41974224
if (!status.ok) {
4198-
return status;
4225+
return stopping() ? TransportStatus::success() : status;
41994226
}
42004227
}
42014228

42024229
for (const auto& [request_id, subscribe] : active_subscriptions) {
4230+
if (flush_budget_exhausted()) {
4231+
return TransportStatus::success();
4232+
}
42034233
const auto stream_it = active_subscription_stream_ids.find(request_id);
42044234
const std::uint64_t response_stream_id =
42054235
uses_request_streams(draft_version) && stream_it != active_subscription_stream_ids.end()
@@ -4210,29 +4240,39 @@ TransportStatus MoqtSession::publish_live_objects(const openmoq::publisher::Live
42104240
draft_version, request_id, sender_by_track[subscribe.track_name].stream_count()),
42114241
false);
42124242
if (!status.ok) {
4213-
return status;
4243+
return stopping() ? TransportStatus::success() : status;
42144244
}
42154245
}
42164246
for (const auto& [track_name, request_id] : subscribe_tracks_publish_request_ids) {
42174247
if (!subscribed_tracks.contains(track_name)) {
42184248
continue;
42194249
}
4250+
if (flush_budget_exhausted()) {
4251+
return TransportStatus::success();
4252+
}
42204253
status = write_publish_done_for_request(transport_,
42214254
draft_version,
42224255
control_stream_id_,
42234256
publish_stream_id_by_request_id_,
42244257
request_id,
42254258
sender_by_track[track_name].stream_count());
42264259
if (!status.ok) {
4227-
return status;
4260+
return stopping() ? TransportStatus::success() : status;
42284261
}
42294262
}
42304263

4231-
return write_namespace_done_for_request(transport_,
4264+
if (flush_budget_exhausted()) {
4265+
return TransportStatus::success();
4266+
}
4267+
const auto namespace_done_status = write_namespace_done_for_request(transport_,
42324268
draft_version,
42334269
control_stream_id_,
42344270
namespace_stream_id_,
42354271
namespace_message);
4272+
if (!namespace_done_status.ok && stopping()) {
4273+
return TransportStatus::success();
4274+
}
4275+
return namespace_done_status;
42364276
}
42374277

42384278
TransportStatus MoqtSession::close(std::uint64_t application_error_code) {

tests/moqt_session_test.cpp

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ struct MockTransport final : PublisherTransport {
180180
TransportStatus write_stream(std::uint64_t stream_id,
181181
std::span<const std::uint8_t> bytes,
182182
bool fin) override {
183+
// A blocked write is released either explicitly or by transport close,
184+
// mirroring how a real picoquic write unblocks when the connection is
185+
// torn down.
186+
while (block_writes.load() && !release_writes.load() &&
187+
state_ != ConnectionState::kClosed) {
188+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
189+
}
190+
if (state_ == ConnectionState::kClosed) {
191+
return TransportStatus::failure("transport closed");
192+
}
183193
writes.push_back({
184194
.stream_id = stream_id,
185195
.bytes = std::vector<std::uint8_t>(bytes.begin(), bytes.end()),
@@ -250,6 +260,8 @@ struct MockTransport final : PublisherTransport {
250260
std::map<std::uint64_t, std::vector<std::vector<std::uint8_t>>> reads;
251261
std::set<std::uint64_t> accepted_streams;
252262
std::function<void(MockTransport&, std::uint64_t)> on_read;
263+
std::atomic<bool> block_writes{false};
264+
std::atomic<bool> release_writes{false};
253265
};
254266

255267
std::vector<std::size_t> object_write_indices(const MockTransport& transport) {
@@ -2712,5 +2724,126 @@ int main() {
27122724
}
27132725
}
27142726

2727+
{
2728+
// Flush fallback: if post-stop flush writes block, publish_live_objects
2729+
// must still return promptly. close() tears down the transport, which
2730+
// makes the blocked write fail; the stop-swallow path returns success.
2731+
MockTransport transport;
2732+
transport.reads[0].push_back(encode_server_setup_message({
2733+
.draft = DraftVersion::kDraft14,
2734+
.max_request_id = 8,
2735+
}));
2736+
transport.reads[0].push_back(encode_publish_namespace_ok_message(DraftVersion::kDraft14, 0));
2737+
MoqtSession session(transport, std::string(kTestTrackNamespace),
2738+
/*auto_forward=*/true, /*publish_catalog=*/false,
2739+
/*paced=*/false, std::chrono::seconds(1));
2740+
ok &= expect(session.connect(endpoint, tls).ok,
2741+
"expected flush-fallback session connect to succeed");
2742+
2743+
LiveObjectSource source;
2744+
source.tracks = {LiveTrack{.track_name = "events"}};
2745+
std::atomic<int> object_calls{0};
2746+
source.next_object = [&object_calls]() -> std::optional<LiveObject> {
2747+
const int n = object_calls.fetch_add(1);
2748+
return LiveObject{.track_name = "events", .group_id = 1,
2749+
.object_id = static_cast<std::size_t>(n),
2750+
.payload = {'O', 'K'}};
2751+
};
2752+
2753+
std::promise<TransportStatus> result_promise;
2754+
auto result_future = result_promise.get_future();
2755+
std::thread worker([&]() {
2756+
result_promise.set_value(session.publish_live_objects(source, DraftVersion::kDraft14));
2757+
});
2758+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
2759+
transport.block_writes.store(true); // any flush write now blocks
2760+
session.close(0); // tears down transport -> unblocks via failure
2761+
2762+
const auto wait_status = result_future.wait_for(std::chrono::seconds(4));
2763+
ok &= expect(wait_status == std::future_status::ready,
2764+
"expected publish_live_objects to return promptly despite blocked flush");
2765+
transport.release_writes.store(true); // belt-and-suspenders unblock
2766+
if (wait_status == std::future_status::ready) {
2767+
worker.join();
2768+
ok &= expect(result_future.get().ok,
2769+
"expected stop with blocked flush to still return success");
2770+
} else {
2771+
worker.detach();
2772+
}
2773+
}
2774+
2775+
{
2776+
// Graceful flush: with a healthy transport, a stop still returns success
2777+
// and the loop served at least one object.
2778+
MockTransport transport;
2779+
transport.reads[0].push_back(encode_server_setup_message({
2780+
.draft = DraftVersion::kDraft14,
2781+
.max_request_id = 8,
2782+
}));
2783+
transport.reads[0].push_back(encode_publish_namespace_ok_message(DraftVersion::kDraft14, 0));
2784+
MoqtSession session(transport, std::string(kTestTrackNamespace),
2785+
/*auto_forward=*/true, /*publish_catalog=*/false,
2786+
/*paced=*/false, std::chrono::seconds(1));
2787+
ok &= expect(session.connect(endpoint, tls).ok,
2788+
"expected graceful-flush session connect to succeed");
2789+
2790+
LiveObjectSource source;
2791+
source.tracks = {LiveTrack{.track_name = "events"}};
2792+
std::atomic<int> object_calls{0};
2793+
source.next_object = [&object_calls]() -> std::optional<LiveObject> {
2794+
const int n = object_calls.fetch_add(1);
2795+
return LiveObject{.track_name = "events", .group_id = 1,
2796+
.object_id = static_cast<std::size_t>(n),
2797+
.payload = {'O', 'K'}};
2798+
};
2799+
2800+
std::promise<TransportStatus> result_promise;
2801+
auto result_future = result_promise.get_future();
2802+
std::thread worker([&]() {
2803+
result_promise.set_value(session.publish_live_objects(source, DraftVersion::kDraft14));
2804+
});
2805+
std::this_thread::sleep_for(std::chrono::milliseconds(50));
2806+
session.close(0);
2807+
const auto wait_status = result_future.wait_for(std::chrono::seconds(3));
2808+
ok &= expect(wait_status == std::future_status::ready,
2809+
"expected graceful-flush publish to return after close()");
2810+
if (wait_status == std::future_status::ready) {
2811+
worker.join();
2812+
ok &= expect(result_future.get().ok, "expected graceful-flush return success");
2813+
ok &= expect(object_calls.load() > 0,
2814+
"expected graceful flush after serving objects");
2815+
} else {
2816+
worker.detach();
2817+
}
2818+
}
2819+
2820+
{
2821+
// No false positive: a transport error WITHOUT a stop request must still
2822+
// return failure. next_object returns an object for an unknown track,
2823+
// which the loop rejects with failure at runtime; close() is never called.
2824+
MockTransport transport;
2825+
transport.reads[0].push_back(encode_server_setup_message({
2826+
.draft = DraftVersion::kDraft14,
2827+
.max_request_id = 8,
2828+
}));
2829+
transport.reads[0].push_back(encode_publish_namespace_ok_message(DraftVersion::kDraft14, 0));
2830+
MoqtSession session(transport, std::string(kTestTrackNamespace),
2831+
/*auto_forward=*/true, /*publish_catalog=*/false,
2832+
/*paced=*/false, std::chrono::seconds(1));
2833+
ok &= expect(session.connect(endpoint, tls).ok,
2834+
"expected no-false-positive session connect to succeed");
2835+
2836+
LiveObjectSource source;
2837+
source.tracks = {LiveTrack{.track_name = "events"}};
2838+
source.next_object = []() -> std::optional<LiveObject> {
2839+
return LiveObject{.track_name = "not_a_declared_track", .group_id = 1,
2840+
.object_id = 0, .payload = {'X'}};
2841+
};
2842+
2843+
const TransportStatus result = session.publish_live_objects(source, DraftVersion::kDraft14);
2844+
ok &= expect(!result.ok,
2845+
"expected an unknown-track error without stop to return failure");
2846+
}
2847+
27152848
return ok ? 0 : 1;
27162849
}

0 commit comments

Comments
 (0)