fix: serialize MQTT ws I/O and dewcouple keepalive

This commit is contained in:
Ian Chua
2026-09-24 20:28:13 +08:00
parent 220508534a
commit 87eb8de001
4 changed files with 260 additions and 133 deletions
+13 -5
View File
@@ -127,6 +127,9 @@ public:
// MQTT CONNECTs seen; increments again after a reconnect.
int connect_count() const { return m_connect_count.load(); }
// MQTT PINGREQs seen while the client has no other traffic.
int ping_count() const { return m_ping_count.load(); }
private:
void run()
{
@@ -231,6 +234,7 @@ private:
return true;
}
case 0xc0: // PINGREQ
++m_ping_count;
write_packet(stream, {0xd0, 0x00});
return true;
case 0xe0: // DISCONNECT
@@ -277,8 +281,8 @@ private:
}
// Every write - the worker's own replies and push_report() from the test
// thread - is serialised by m_mutex. beast permits a writer while the worker
// is blocked in read(), which is the same arrangement OrcaMqttConnection uses.
// thread - is serialised by m_mutex. The production client keeps all of its
// WebSocket operations on its MQTT worker instead.
void write_packet(ws::stream<beast::tcp_stream>& stream, const std::vector<std::uint8_t>& packet)
{
std::lock_guard<std::mutex> lock(m_mutex);
@@ -294,11 +298,14 @@ private:
m_stream_ready = false;
boost::system::error_code ec;
auto& socket = beast::get_lowest_layer(*m_stream).socket();
socket.cancel(ec);
if (socket.cancel(ec))
return;
// shutdown() before close() is what actually wakes a blocking read on the
// worker thread; close() alone does not on POSIX.
socket.shutdown(tcp::socket::shutdown_both, ec);
socket.close(ec);
if (socket.shutdown(tcp::socket::shutdown_both, ec))
return;
if (socket.close(ec))
return;
}
const bool m_refuse_auth;
@@ -308,6 +315,7 @@ private:
std::thread m_thread;
std::atomic_bool m_stopping{false};
std::atomic<int> m_connect_count{0};
std::atomic<int> m_ping_count{0};
mutable std::mutex m_mutex;
std::optional<ws::stream<beast::tcp_stream>> m_stream; // guarded by m_mutex
bool m_stream_ready = false; // guarded by m_mutex
@@ -182,6 +182,20 @@ static void run_round_trip(bool use_tls_flag_only) {
TEST_CASE("OrcaMqtt round-trip — LAN-style config", "[OrcaMqtt][.integration]") { run_round_trip(false); }
TEST_CASE("OrcaMqtt round-trip — cloud-style config", "[OrcaMqtt][.integration]") { run_round_trip(true); }
TEST_CASE("OrcaMqtt keepalive runs while the connection is idle", "[OrcaMqtt][.integration]") {
orca_mqtt_test::MockBroker broker;
OrcaMqttConnection conn;
OrcaMqttConnection::Config cfg;
cfg.url = broker.ws_url();
cfg.keepalive_seconds = 2;
REQUIRE(conn.start(cfg, [](const std::string&, const std::string&) {}, [](bool, bool) {}));
for (int i = 0; i < 200 && broker.ping_count() == 0; ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
CHECK(broker.ping_count() > 0);
conn.stop();
}
TEST_CASE("OrcaMqtt reconnects and re-subscribes after a socket drop", "[OrcaMqtt][.integration]") {
orca_mqtt_test::MockBroker broker;
OrcaMqttConnection conn;