Skip to content

Commit c6f1e3e

Browse files
committed
web: fence broadcastAndWait on write completion, not strand post
Replace the two-callback SessionRegistry pattern (SendFn + PostFn) with a combined SendAndWaitFn whose completion fires after async_write finishes. This ensures the shutdown message is on the wire before stop() tears down the io_context. Also store the WebLogSink as a WebServer member so it is properly removed from the Logger on shutdown, and fix a missing writing_=false reset on the write-error path. Signed-off-by: Matt Liberty <mliberty@precisioninno.com>
1 parent 5b042a2 commit c6f1e3e

6 files changed

Lines changed: 97 additions & 52 deletions

File tree

src/web/include/web/web.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ namespace sta {
2525
class dbSta;
2626
}
2727

28+
namespace spdlog::sinks {
29+
class sink;
30+
}
31+
2832
namespace web {
2933

3034
struct Color;
@@ -111,6 +115,7 @@ class WebServer
111115
int num_threads_ = 0;
112116
std::shared_ptr<TileGenerator> generator_;
113117
std::unique_ptr<WebViewerHook> viewer_hook_;
118+
std::shared_ptr<spdlog::sinks::sink> log_sink_;
114119

115120
// Background I/O context and worker threads (non-null while running).
116121
std::unique_ptr<boost::asio::io_context> ioc_;

src/web/src/web.cpp

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,13 @@ class WebSocketSession : public std::enable_shared_from_this<WebSocketSession>
310310
DRCHandler drc_handler_;
311311

312312
// Write serialization: strand + queue ensures one async_write at a time
313+
struct PendingWrite
314+
{
315+
std::vector<unsigned char> frame;
316+
std::function<void()> on_complete;
317+
};
313318
net::strand<net::any_io_executor> strand_;
314-
std::deque<std::vector<unsigned char>> write_queue_;
319+
std::deque<PendingWrite> write_queue_;
315320
bool writing_ = false;
316321

317322
// Background search index initialization
@@ -339,7 +344,8 @@ class WebSocketSession : public std::enable_shared_from_this<WebSocketSession>
339344
void on_accept(beast::error_code ec);
340345
void do_read();
341346
void on_read(beast::error_code ec);
342-
void queue_response(const WebSocketResponse& resp);
347+
void queue_response(const WebSocketResponse& resp,
348+
std::function<void()> on_complete = {});
343349
void do_write();
344350
};
345351

@@ -435,15 +441,19 @@ void WebSocketSession::on_accept(beast::error_code ec)
435441
resp.payload.assign(json.begin(), json.end());
436442
self->queue_response(resp);
437443
},
438-
// PostFnpost an arbitrary callable onto this session's strand.
439-
// Used by broadcastAndWait() to fence after the write is queued.
440-
[weak_self](std::function<void()> fn) {
444+
// SendAndWaitFnqueue a JSON push message and invoke the callback
445+
// after async_write completes.
446+
[weak_self](const std::string& json, std::function<void()> fn) {
441447
auto self = weak_self.lock();
442448
if (!self) {
443449
fn(); // session gone — signal fence immediately
444450
return;
445451
}
446-
net::post(self->strand_, std::move(fn));
452+
WebSocketResponse resp;
453+
resp.id = 0;
454+
resp.type = 0; // JSON
455+
resp.payload.assign(json.begin(), json.end());
456+
self->queue_response(resp, std::move(fn));
447457
});
448458

449459
// Flush any log output that accumulated before this client
@@ -789,18 +799,23 @@ void WebSocketSession::on_read(beast::error_code ec)
789799
do_read();
790800
}
791801

792-
void WebSocketSession::queue_response(const WebSocketResponse& resp)
802+
void WebSocketSession::queue_response(const WebSocketResponse& resp,
803+
std::function<void()> on_complete)
793804
{
794805
std::vector<unsigned char> frame = serialize_response(resp);
795806

796807
// Post to the strand to serialize write queue access
797-
net::post(strand_,
798-
[self = shared_from_this(), frame = std::move(frame)]() mutable {
799-
self->write_queue_.push_back(std::move(frame));
800-
if (!self->writing_) {
801-
self->do_write();
802-
}
803-
});
808+
net::post(
809+
strand_,
810+
[self = shared_from_this(),
811+
frame = std::move(frame),
812+
on_complete = std::move(on_complete)]() mutable {
813+
self->write_queue_.push_back(PendingWrite{
814+
.frame = std::move(frame), .on_complete = std::move(on_complete)});
815+
if (!self->writing_) {
816+
self->do_write();
817+
}
818+
});
804819
}
805820

806821
void WebSocketSession::do_write()
@@ -812,19 +827,24 @@ void WebSocketSession::do_write()
812827
writing_ = true;
813828
websocket_.binary(true);
814829
websocket_.async_write(
815-
net::buffer(write_queue_.front()),
830+
net::buffer(write_queue_.front().frame),
816831
[self = shared_from_this()](beast::error_code ec, std::size_t) {
817832
net::post(self->strand_, [self, ec]() {
833+
auto on_complete = std::move(self->write_queue_.front().on_complete);
834+
self->write_queue_.pop_front();
835+
if (on_complete) {
836+
on_complete();
837+
}
818838
if (ec) {
819839
debugPrint(self->logger_,
820840
utl::WEB,
821841
"websocket",
822842
1,
823843
"websocket write error: {}",
824844
ec.message());
845+
self->writing_ = false;
825846
return;
826847
}
827-
self->write_queue_.pop_front();
828848
self->do_write();
829849
});
830850
});

src/web/src/web_serve.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,10 @@ void WebServer::serve(int port)
140140
});
141141

142142
auto log_sink = std::make_shared<WebLogSink>(viewer_hook_.get());
143-
logger_->addSink(log_sink);
144-
viewer_hook_->setDrainLogsFn([log_sink]() { log_sink->drainToClients(); });
143+
log_sink_ = log_sink;
144+
logger_->addSink(log_sink_);
145+
viewer_hook_->setDrainLogsFn(
146+
[log_sink = std::move(log_sink)]() { log_sink->drainToClients(); });
145147

146148
TileGenerator::setDebugOverlayCallback(
147149
[weak_gen = std::weak_ptr<TileGenerator>(generator_),
@@ -215,9 +217,8 @@ void WebServer::waitForStop()
215217
lock.unlock();
216218

217219
// Notify connected browsers so they can show "Server stopped" and
218-
// disable auto-reconnect. broadcastAndWait() posts the message and
219-
// then waits for a strand-fence on each session, guaranteeing the
220-
// write is queued before stop() tears down the io_context.
220+
// disable auto-reconnect. broadcastAndWait() waits for the write to
221+
// complete before stop() tears down the io_context.
221222
if (viewer_hook_) {
222223
constexpr auto kShutdownFlushTimeout = std::chrono::seconds(2);
223224
viewer_hook_->sessions().broadcastAndWait(R"({"type":"shutdown"})",
@@ -249,6 +250,10 @@ void WebServer::stop()
249250
}
250251
gui::Gui::get()->setChartFactory({});
251252
}
253+
if (log_sink_) {
254+
logger_->removeSink(log_sink_);
255+
log_sink_.reset();
256+
}
252257

253258
if (shutdown_listener_) {
254259
shutdown_listener_();

src/web/src/web_viewer_hook.cpp

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ constexpr auto kMaxPauseTimeout = std::chrono::minutes(10);
3030
// SessionRegistry
3131
//------------------------------------------------------------------------------
3232

33-
std::size_t SessionRegistry::add(SendFn send, PostFn post)
33+
std::size_t SessionRegistry::add(SendFn send, SendAndWaitFn send_and_wait)
3434
{
3535
std::size_t token;
3636
{
3737
std::lock_guard<std::mutex> lock(mutex_);
3838
token = next_token_++;
39-
senders_.emplace(token, SessionCallbacks{std::move(send), std::move(post)});
39+
senders_.emplace(token,
40+
SessionCallbacks{
41+
.send = std::move(send),
42+
.send_and_wait = std::move(send_and_wait),
43+
});
4044
}
4145
client_cv_.notify_all();
4246
return token;
@@ -103,10 +107,10 @@ bool SessionRegistry::broadcastAndWait(const std::string& json,
103107
}
104108
}
105109

106-
// Count sessions that support fencing (have a PostFn).
110+
// Count sessions that support write-completion fencing.
107111
std::size_t fence_count = 0;
108112
for (const auto& cb : to_send) {
109-
if (cb.post) {
113+
if (cb.send_and_wait) {
110114
++fence_count;
111115
}
112116
}
@@ -119,8 +123,8 @@ bool SessionRegistry::broadcastAndWait(const std::string& json,
119123
return true;
120124
}
121125

122-
// Shared state for the fence: each session's PostFn decrements the
123-
// counter and notifies when all fences have fired.
126+
// Shared state for the fence: each session's SendAndWaitFn decrements
127+
// the counter and notifies when all writes have completed.
124128
struct FenceState
125129
{
126130
std::mutex mutex;
@@ -131,14 +135,15 @@ bool SessionRegistry::broadcastAndWait(const std::string& json,
131135
state->remaining = fence_count;
132136

133137
for (const auto& cb : to_send) {
134-
cb.send(json);
135-
if (cb.post) {
136-
cb.post([state]() {
138+
if (cb.send_and_wait) {
139+
cb.send_and_wait(json, [state]() {
137140
std::lock_guard<std::mutex> lock(state->mutex);
138141
if (--state->remaining == 0) {
139142
state->cv.notify_one();
140143
}
141144
});
145+
} else {
146+
cb.send(json);
142147
}
143148
}
144149

src/web/src/web_viewer_hook.h

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ class SessionRegistry
2727
{
2828
public:
2929
using SendFn = std::function<void(const std::string& json)>;
30-
// Posts an arbitrary callable onto the session's strand.
31-
using PostFn = std::function<void(std::function<void()>)>;
30+
// Sends JSON and invokes the callback after the session write completes.
31+
using SendAndWaitFn
32+
= std::function<void(const std::string& json, std::function<void()>)>;
3233
using WaitInterruptFn = std::function<bool()>;
3334

3435
// Register a send callback. Returns a token the caller must pass to
35-
// remove() during teardown. The optional PostFn lets broadcastAndWait()
36-
// post a fence lambda onto the session's strand.
37-
std::size_t add(SendFn send, PostFn post = {});
36+
// remove() during teardown. The optional SendAndWaitFn lets
37+
// broadcastAndWait() wait until a write has completed.
38+
std::size_t add(SendFn send, SendAndWaitFn send_and_wait = {});
3839
void remove(std::size_t token);
3940

4041
// True if at least one session is registered.
@@ -51,17 +52,16 @@ class SessionRegistry
5152
// Deliver the JSON string to every currently-registered session.
5253
void broadcast(const std::string& json);
5354

54-
// Like broadcast(), but waits until every session's strand has executed
55-
// the queued write (or timeout expires). Returns true if all fences
56-
// completed, false on timeout.
55+
// Like broadcast(), but waits until every fenceable session has completed
56+
// the queued write (or timeout expires).
5757
bool broadcastAndWait(const std::string& json,
5858
std::chrono::milliseconds timeout);
5959

6060
private:
6161
struct SessionCallbacks
6262
{
6363
SendFn send;
64-
PostFn post; // may be empty for legacy callers
64+
SendAndWaitFn send_and_wait; // may be empty for legacy callers
6565
};
6666

6767
mutable std::mutex mutex_;

src/web/test/cpp/TestDebugGraphics.cpp

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
#include <atomic>
55
#include <chrono>
66
#include <cstddef>
7+
#include <functional>
78
#include <memory>
89
#include <mutex>
910
#include <string>
1011
#include <thread>
12+
#include <utility>
1113
#include <variant>
1214
#include <vector>
1315

@@ -121,12 +123,15 @@ TEST(SessionRegistryTest, BroadcastAndWaitFencesComplete)
121123
SessionRegistry registry;
122124
std::string received;
123125

124-
// Register a session whose PostFn invokes the fence on a background thread
125-
// (simulating a real strand dispatch).
126+
// Register a session whose SendAndWaitFn invokes the fence only after
127+
// the simulated write has completed.
126128
auto token = registry.add(
127129
[&received](const std::string& json) { received = json; },
128-
[](std::function<void()> fence) {
129-
std::thread([fence = std::move(fence)]() { fence(); }).detach();
130+
[&received](const std::string& json, std::function<void()> fence) {
131+
std::thread([&received, json, fence = std::move(fence)]() {
132+
received = json;
133+
fence();
134+
}).detach();
130135
});
131136

132137
EXPECT_TRUE(registry.broadcastAndWait(R"({"type":"shutdown"})",
@@ -139,10 +144,12 @@ TEST(SessionRegistryTest, BroadcastAndWaitTimesOut)
139144
{
140145
SessionRegistry registry;
141146

142-
// Register a session whose PostFn silently drops the fence.
143-
auto token = registry.add(
144-
[](const std::string&) {},
145-
[](std::function<void()> /*fence*/) { /* never called */ });
147+
// Register a session whose SendAndWaitFn silently drops the fence.
148+
auto token
149+
= registry.add([](const std::string&) {},
150+
[](const std::string&, std::function<void()> /*fence*/) {
151+
/* never called */
152+
});
146153

147154
const auto t0 = std::chrono::steady_clock::now();
148155
EXPECT_FALSE(registry.broadcastAndWait("{}", std::chrono::milliseconds(200)));
@@ -159,11 +166,14 @@ TEST(SessionRegistryTest, BroadcastAndWaitDeadSession)
159166
SessionRegistry registry;
160167
std::atomic<bool> send_called{false};
161168

162-
// Register a session whose PostFn calls the fence immediately
169+
// Register a session whose SendAndWaitFn calls the fence immediately
163170
// (simulating a dead session that signals right away).
164-
auto token
165-
= registry.add([&send_called](const std::string&) { send_called = true; },
166-
[](std::function<void()> fence) { fence(); });
171+
auto token = registry.add(
172+
[&send_called](const std::string&) { send_called = true; },
173+
[&send_called](const std::string&, std::function<void()> fence) {
174+
send_called = true;
175+
fence();
176+
});
167177

168178
EXPECT_TRUE(registry.broadcastAndWait("{}", std::chrono::milliseconds(100)));
169179
EXPECT_TRUE(send_called);
@@ -175,7 +185,7 @@ TEST(SessionRegistryTest, BroadcastAndWaitLegacySendOnly)
175185
SessionRegistry registry;
176186
std::string received;
177187

178-
// Register with SendFn only (no PostFn) — legacy path.
188+
// Register with SendFn only (no SendAndWaitFn) — legacy path.
179189
auto token
180190
= registry.add([&received](const std::string& json) { received = json; });
181191

0 commit comments

Comments
 (0)