Keep the console responsive during MQTT shutdown
This commit is contained in:
1 parent
d17519fe7f
commit
f05524a4a2
15 files changed
+634
-63
No files matched your search
Vendored
+33
-4
@@ -112,6 +112,7 @@ struct MQTTNodeInfo {
|
||||
};
|
||||
class MQTTBridge : public AbstractBridge {
|
||||
bool running = false;
|
||||
bool stopping = false;
|
||||
public:
|
||||
inline static unsigned live = 0, fail_starts = 0;
|
||||
unsigned begins = 0, ends = 0, loops = 0, sent = 0, received = 0;
|
||||
@@ -127,9 +128,11 @@ public:
|
||||
if (fail_starts) { --fail_starts; return; }
|
||||
running = true;
|
||||
}
|
||||
void end() override { ++ends; running = false; }
|
||||
void end() override { ++ends; running = stopping = false; }
|
||||
void requestStop() { stopping = running; }
|
||||
bool isStopping() const { return stopping; }
|
||||
bool isRunning() const override { return running; }
|
||||
void loop() override { ++loops; }
|
||||
void loop() override { ++loops; if (stopping) { ++ends; running = stopping = false; } }
|
||||
void sendPacket(mesh::Packet* p) override { if (allow(p)) ++sent; }
|
||||
void onPacketReceived(mesh::Packet* p) { if (allow(p)) ++received; }
|
||||
};
|
||||
@@ -171,6 +174,8 @@ struct Callbacks {
|
||||
virtual bool restartEspNowBridge() = 0;
|
||||
virtual bool isMqttBridgeRunning() = 0;
|
||||
virtual bool setMqttBridgeState(bool) = 0;
|
||||
virtual bool requestMqttBridgeStop() = 0;
|
||||
virtual bool isMqttBridgeStopping() = 0;
|
||||
virtual bool restartMqttBridge() = 0;
|
||||
};
|
||||
class MyMesh : public Callbacks {
|
||||
@@ -277,7 +282,7 @@ int main() {
|
||||
assert(mesh.bridge->sent == 1 && mesh.bridge->loops == 1);
|
||||
assert(mesh.espnow_bridge.sent == 1 && mesh.espnow_bridge.loops == 1);
|
||||
assert(mesh.mqtt_bridge->received == 1 && mesh.mqtt_bridge->sent == 1);
|
||||
assert(mesh.mqtt_bridge->loops == 0); // Its actual task services MQTT independently.
|
||||
assert(mesh.mqtt_bridge->loops == 1); // Core 1 services stop acknowledgement only.
|
||||
assert(mesh.filter_checks == 4);
|
||||
packet.allowed = false;
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet);
|
||||
@@ -342,12 +347,34 @@ int main() {
|
||||
cli.set("bridge.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && !mesh.isEspNowBridgeRunning());
|
||||
assert(mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
|
||||
// A failed intent save must not signal an uncancellable worker stop.
|
||||
cli.fail_save = true;
|
||||
cli.set("mqtt.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && !mesh.isMqttBridgeRunning());
|
||||
assert(strstr(reply, "not saved; unchanged") && mesh._prefs.bridge_enabled);
|
||||
assert(mesh.isMqttBridgeRunning() && !mesh.isMqttBridgeStopping());
|
||||
cli.fail_save = false;
|
||||
cli.set("mqtt.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isMqttBridgeRunning());
|
||||
assert(mesh.isMqttBridgeStopping() && !mesh._prefs.bridge_enabled);
|
||||
expect_get(cli, "mqtt.running", "> on");
|
||||
expect_get(cli, "mqtt.stopping", "> on");
|
||||
const unsigned stopping_saves = cli.saves;
|
||||
cli.set("mqtt.enabled on", reply);
|
||||
assert(strstr(reply, "MQTT is stopping") && !mesh._prefs.bridge_enabled);
|
||||
assert(cli.saves == stopping_saves && !mesh.setMqttBridgeState(true));
|
||||
cli.set("mqtt.enabled off", reply); // a repeated request is accepted
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isMqttBridgeStopping());
|
||||
assert(mesh.isRs232BridgeRunning() && mesh.bridgesPreventSleep());
|
||||
const unsigned uart_tx_before = mesh.bridge->sent;
|
||||
mesh.routeRx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge->sent == uart_tx_before + 1);
|
||||
assert(!mesh.isMqttBridgeRunning() && !mesh.isMqttBridgeStopping());
|
||||
expect_get(cli, "mqtt.stopping", "> off");
|
||||
cli.fail_save = true;
|
||||
cli.set("mqtt.enabled on", reply);
|
||||
assert(strstr(reply, "not saved; unchanged") && !mesh._prefs.bridge_enabled);
|
||||
assert(!mesh.isMqttBridgeRunning());
|
||||
cli.fail_save = false;
|
||||
cli.set("espnow.enabled on", reply); assert(strcmp(reply, "OK") == 0);
|
||||
cli.set("mqtt.enabled on", reply); assert(strcmp(reply, "OK") == 0);
|
||||
mesh.serviceBridges(); assert(mesh.isEspNowBridgeRunning());
|
||||
@@ -379,6 +406,8 @@ int main() {
|
||||
assert(mesh.isRs232BridgeRunning() && RS232Bridge::live == 1);
|
||||
}
|
||||
cli.set("rs232.enabled off", reply); cli.set("espnow.enabled off", reply); cli.set("mqtt.enabled off", reply);
|
||||
assert(mesh.isMqttBridgeStopping() && mesh.bridgesPreventSleep());
|
||||
mesh.serviceBridges();
|
||||
assert(!mesh.bridgesPreventSleep() && RS232Bridge::live == 0);
|
||||
cli.run("start ota", reply); cli.run("stop ota", reply); mesh.serviceBridges();
|
||||
assert(!mesh.isRs232BridgeRunning() && !mesh.isMqttBridgeRunning() && !mesh.isEspNowBridgeRunning());
|
||||
|
||||
@@ -68,6 +68,7 @@ OBSERVER_QUERY_EXCEPTIONS = {
|
||||
'mqtt.ntp.diag': 'NTP diagnostics',
|
||||
'mqtt.presets': 'List of available presets',
|
||||
'mqtt.running': 'Live state; set mqtt.enabled controls intent',
|
||||
'mqtt.stopping': 'Pending worker-owned shutdown; set mqtt.enabled off requests it',
|
||||
'mqtt.stats': 'Runtime MQTT statistics',
|
||||
'wifi.status': 'Live connection status',
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@ namespace mesh {
|
||||
void setUsbLoggingEnabled(bool value) { live_usb = value; ++usb_calls; }
|
||||
}
|
||||
struct Callbacks {
|
||||
bool live_wifi = false, apply_ok = true;
|
||||
bool live_wifi = false, apply_ok = true, stopping = false;
|
||||
unsigned calls = 0;
|
||||
bool setMqttBridgeState(bool value) {
|
||||
++calls;
|
||||
if (apply_ok) live_wifi = value;
|
||||
return apply_ok;
|
||||
}
|
||||
bool requestMqttBridgeStop() { return setMqttBridgeState(false); }
|
||||
bool isMqttBridgeStopping() { return stopping; }
|
||||
};
|
||||
struct CLI {
|
||||
Prefs prefs, persisted;
|
||||
@@ -88,6 +90,15 @@ int main() {
|
||||
assert(cli.persisted.usb_logging_enabled && cli.persisted.bridge_enabled);
|
||||
assert(live_usb && !cli.callbacks.live_wifi);
|
||||
assert(strstr(reply, "saved, but MQTT runtime change failed"));
|
||||
// A restart during cleanup must not save ON or change either live output.
|
||||
cli.callbacks.stopping = true;
|
||||
cli.prefs = cli.persisted = {0, 0};
|
||||
live_usb = false;
|
||||
const unsigned before_saves = cli.saves, before_calls = cli.callbacks.calls;
|
||||
cli.set("logging.output both", reply);
|
||||
assert(strstr(reply, "MQTT is stopping"));
|
||||
assert(cli.saves == before_saves && cli.callbacks.calls == before_calls);
|
||||
assert(!cli.prefs.bridge_enabled && !cli.persisted.bridge_enabled && !live_usb);
|
||||
#endif
|
||||
}
|
||||
'''
|
||||
|
||||
@@ -26,6 +26,7 @@ def method(path, signature):
|
||||
|
||||
|
||||
PREAMBLE = r'''
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
@@ -90,6 +91,7 @@ struct MQTTBridge {
|
||||
};
|
||||
MQTTSlot _slots[RUNTIME_MQTT_SLOTS];
|
||||
volatile bool _slot_attempt_pending[RUNTIME_MQTT_SLOTS] = {};
|
||||
std::atomic<bool> _stop_requested{false};
|
||||
esp_err_t reconnectSlotClient(int index);
|
||||
};
|
||||
'''
|
||||
@@ -152,12 +154,391 @@ int main() {
|
||||
sdk_callback=nullptr;
|
||||
reconnect_result=ESP_OK;
|
||||
assert(b.reconnectSlotClient(0)==ESP_OK && b._slot_attempt_pending[0]);
|
||||
const int stopped_starts = start_calls, stopped_reconnects = reconnect_calls;
|
||||
b._stop_requested = true;
|
||||
assert(b.reconnectSlotClient(0)==ESP_ERR_INVALID_STATE);
|
||||
assert(start_calls==stopped_starts && reconnect_calls==stopped_reconnects);
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
TASK_PREAMBLE = r'''
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#define PORTABLE_MQTT_OBSERVER 1
|
||||
#define MQTT_DEBUG_PRINTLN(...) ((void)0)
|
||||
using TickType_t = uint32_t;
|
||||
constexpr int RUNTIME_MQTT_SLOTS = 3, WL_CONNECTED = 1;
|
||||
constexpr uint32_t STATUS_RETRY_INTERVAL = 30000;
|
||||
static uint32_t tick_ms = 1, first_tick = 0, first_millis = 0;
|
||||
static uint64_t elapsed_ms = 0, stop_at_ms = UINT64_MAX;
|
||||
static int scenario = 0;
|
||||
static std::vector<uint32_t> task_sleeps;
|
||||
struct MQTTBridge;
|
||||
static MQTTBridge* active_bridge = nullptr;
|
||||
#define pdMS_TO_TICKS(ms) static_cast<TickType_t>((ms) / tick_ms)
|
||||
TickType_t xTaskGetTickCount() {
|
||||
return first_tick + static_cast<TickType_t>(elapsed_ms / tick_ms);
|
||||
}
|
||||
uint32_t millis() { return first_millis + static_cast<uint32_t>(elapsed_ms); }
|
||||
void vTaskDelay(TickType_t ticks);
|
||||
struct { int status() { return WL_CONNECTED; } } WiFi;
|
||||
bool mqttNtpRefreshDue(unsigned long, unsigned long, unsigned long) { return false; }
|
||||
struct MQTTPrefs {
|
||||
char mqtt_slot_preset[RUNTIME_MQTT_SLOTS][16] = {};
|
||||
bool mqtt_status_enabled = true;
|
||||
};
|
||||
struct MQTTBridge {
|
||||
struct Slot {
|
||||
bool enabled = true, initial_connect_done = false, connected = false;
|
||||
unsigned long last_reconnect_attempt = 0;
|
||||
} _slots[RUNTIME_MQTT_SLOTS];
|
||||
struct Latch {
|
||||
void noteGotIp() {}
|
||||
bool consumeIfConnected(bool) { return false; }
|
||||
} _ntp_reconnect_latch;
|
||||
MQTTPrefs prefs;
|
||||
MQTTPrefs* _obs = &prefs;
|
||||
std::atomic<bool> _ntp_synced{true};
|
||||
bool _ntp_sync_pending = false, _slots_setup_done = false;
|
||||
bool _ntp_force_requested = false, _ntp_force_result = false, _ntp_force_done = false;
|
||||
bool _ntp_refresh_pending = false, _cached_has_connected_slots = true;
|
||||
std::atomic<bool> _stop_requested{false}, _stop_acked{false};
|
||||
bool _slot_reconfigure_pending[RUNTIME_MQTT_SLOTS] = {true, true, true};
|
||||
bool _status_publish_pending[RUNTIME_MQTT_SLOTS] = {true, true, true};
|
||||
unsigned long _last_ntp_sync = 0, _ntp_refresh_retry_at = 0;
|
||||
unsigned long _last_status_retry = 0, _last_status_publish = 0, _status_interval = 300000;
|
||||
int _max_active_slots = RUNTIME_MQTT_SLOTS, _queue_count = 0;
|
||||
int late_work = 0, reconfigures = 0, publishes = 0, queue_calls = 0;
|
||||
std::vector<int> setups;
|
||||
std::vector<uint64_t> setup_times;
|
||||
std::vector<std::string> teardown;
|
||||
void work() {
|
||||
if (_stop_requested) ++late_work;
|
||||
assert(!_stop_acked);
|
||||
}
|
||||
void initializeWiFiInTask() {}
|
||||
bool handleWiFiConnection(unsigned long) { work(); return false; }
|
||||
bool syncTimeWithNTP(bool = false, bool = false) {
|
||||
work();
|
||||
if (scenario == 6 || scenario == 7) {
|
||||
_ntp_synced = true;
|
||||
_stop_requested = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool canActivateSlot(int) { work(); return true; }
|
||||
bool isSlotReady(int, char*, size_t) { work(); return true; }
|
||||
bool setupSlot(int index) {
|
||||
work(); setups.push_back(index); setup_times.push_back(elapsed_ms);
|
||||
if (scenario == 5) _stop_requested = true;
|
||||
return true;
|
||||
}
|
||||
void applySlotPreset(int, const char*) {
|
||||
work(); ++reconfigures;
|
||||
if (scenario == 8) _stop_requested = true;
|
||||
}
|
||||
void publishStatusToSlot(int) {
|
||||
work(); ++publishes;
|
||||
if (scenario == 9) _stop_requested = true;
|
||||
}
|
||||
void maintainSlotConnections() {
|
||||
work();
|
||||
if (scenario == 10) _stop_requested = true;
|
||||
}
|
||||
void processPacketQueue() {
|
||||
work(); ++queue_calls;
|
||||
if (scenario == 11) _stop_requested = true;
|
||||
}
|
||||
void checkConfigurationMismatch() { work(); }
|
||||
void pollNtpRefresh(unsigned long) { work(); }
|
||||
void refreshNTP() { work(); }
|
||||
bool publishStatus() { work(); ++publishes; return true; }
|
||||
void updateCachedConnectionStatus() { work(); }
|
||||
void cancelNtpRefresh() {
|
||||
assert(_stop_requested && !_stop_acked);
|
||||
teardown.push_back("ntp");
|
||||
}
|
||||
void teardownSlot(int index) {
|
||||
assert(_stop_requested && !_stop_acked);
|
||||
teardown.push_back("slot" + std::to_string(index));
|
||||
}
|
||||
void destroySlotClients() {
|
||||
assert(!_stop_acked);
|
||||
assert(teardown == std::vector<std::string>({"ntp", "slot0", "slot1", "slot2"}));
|
||||
teardown.push_back("destroy");
|
||||
}
|
||||
bool waitUnlessStopping(uint32_t delay_ms);
|
||||
void mqttTaskLoop();
|
||||
};
|
||||
void vTaskDelay(TickType_t ticks) {
|
||||
assert(ticks > 0);
|
||||
const uint32_t duration = ticks * tick_ms;
|
||||
task_sleeps.push_back(duration);
|
||||
elapsed_ms += duration;
|
||||
if (elapsed_ms >= stop_at_ms) active_bridge->_stop_requested = true;
|
||||
// The no-stop scenario exits after one complete worker iteration. Stop is
|
||||
// raised during its ordinary idle delay, after all setup and publications.
|
||||
if (scenario == 3 && active_bridge->setups.size() == RUNTIME_MQTT_SLOTS)
|
||||
active_bridge->_stop_requested = true;
|
||||
assert(elapsed_ms < 30000); // bound a missing cooperative exit deterministically
|
||||
}
|
||||
'''
|
||||
|
||||
TASK_MAIN = r'''
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 4);
|
||||
scenario = std::atoi(argv[1]);
|
||||
tick_ms = static_cast<uint32_t>(std::atoi(argv[2]));
|
||||
if (std::atoi(argv[3])) {
|
||||
first_tick = UINT32_MAX - 511;
|
||||
first_millis = UINT32_MAX - 511;
|
||||
}
|
||||
MQTTBridge bridge;
|
||||
active_bridge = &bridge;
|
||||
switch (scenario) {
|
||||
case 0: stop_at_ms = 187; break; // stop while WiFi settles
|
||||
case 1: stop_at_ms = 1173; break; // stop during first TLS stagger
|
||||
case 2: stop_at_ms = 6191; break; // stop during second TLS stagger
|
||||
case 3: break; // preserve every normal delay
|
||||
case 4: bridge._stop_requested = true; break;
|
||||
case 5: break; // setup callback requests stop
|
||||
case 6: bridge._ntp_force_requested = true; break;
|
||||
case 7: bridge._ntp_sync_pending = true; bridge._ntp_synced = false; break;
|
||||
case 8: case 9: case 10: case 11: break; // stop raised during worker callbacks
|
||||
default: assert(false);
|
||||
}
|
||||
bridge.mqttTaskLoop();
|
||||
assert(bridge._stop_acked && bridge.late_work == 0);
|
||||
assert(bridge.teardown == std::vector<std::string>({
|
||||
"ntp", "slot0", "slot1", "slot2", "destroy"}));
|
||||
if (scenario == 3) {
|
||||
assert(bridge.setups == std::vector<int>({0, 1, 2}));
|
||||
assert(bridge.setup_times == std::vector<uint64_t>({1000, 6000, 11000}));
|
||||
assert(bridge.reconfigures == RUNTIME_MQTT_SLOTS);
|
||||
assert(bridge.publishes >= RUNTIME_MQTT_SLOTS && bridge.queue_calls == 1);
|
||||
} else {
|
||||
const size_t wanted = scenario >= 8 ? RUNTIME_MQTT_SLOTS
|
||||
: scenario == 0 || scenario == 4 || scenario >= 6 ? 0
|
||||
: scenario == 2 ? 2 : 1;
|
||||
assert(bridge.setups.size() == wanted);
|
||||
if (scenario >= 8) {
|
||||
assert(bridge.reconfigures == (scenario == 8 ? 1 : RUNTIME_MQTT_SLOTS));
|
||||
assert(bridge.publishes == (scenario == 8 ? 0 : scenario == 9 ? 1 : RUNTIME_MQTT_SLOTS));
|
||||
assert(bridge.queue_calls == (scenario == 11 ? 1 : 0));
|
||||
} else {
|
||||
assert(bridge.reconfigures == 0 && bridge.publishes == 0 && bridge.queue_calls == 0);
|
||||
}
|
||||
const uint32_t poll_bound = std::max(uint32_t{20}, tick_ms);
|
||||
assert(std::all_of(task_sleeps.begin(), task_sleeps.end(),
|
||||
[poll_bound](uint32_t duration) { return duration <= poll_bound; }));
|
||||
if (scenario == 4) assert(elapsed_ms == 0 && task_sleeps.empty());
|
||||
else if (scenario >= 8) assert(elapsed_ms == 11000);
|
||||
else if (scenario >= 5) assert(elapsed_ms == 1000);
|
||||
else assert(elapsed_ms >= stop_at_ms && elapsed_ms - stop_at_ms < poll_bound);
|
||||
}
|
||||
// A completed zero-duration wait preserves its caller's stop decision.
|
||||
const auto sleeps_before = task_sleeps.size();
|
||||
bridge._stop_requested = false;
|
||||
assert(bridge.waitUnlessStopping(0));
|
||||
bridge._stop_requested = true;
|
||||
assert(!bridge.waitUnlessStopping(0));
|
||||
assert(task_sleeps.size() == sleeps_before);
|
||||
}
|
||||
'''
|
||||
|
||||
ASYNC_STOP_PREAMBLE = r'''
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include "helpers/MQTTLifecycle.h"
|
||||
#define ESP_PLATFORM 1
|
||||
#define MQTT_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define pdMS_TO_TICKS(ms) (ms)
|
||||
constexpr int RUNTIME_MQTT_SLOTS = 3;
|
||||
static constexpr uint32_t MQTT_STOP_TIMEOUT_BASE_MS = 5000;
|
||||
static constexpr uint32_t MQTT_STOP_TIMEOUT_PER_SLOT_MS = 8000;
|
||||
static uint32_t clock_ms = 0, sleeps = 0;
|
||||
static uint64_t elapsed_ms = 0, ack_after_ms = UINT64_MAX;
|
||||
uint32_t millis() { return clock_ms; }
|
||||
struct MQTTBridge;
|
||||
static MQTTBridge* s_mqtt_bridge_instance = nullptr;
|
||||
static MQTTBridge* bridge_waiting = nullptr;
|
||||
void vTaskDelay(uint32_t duration);
|
||||
struct MQTTBridge {
|
||||
struct Slot { bool enabled = true; } _slots[RUNTIME_MQTT_SLOTS];
|
||||
struct LifecycleOps : MQTTLifecycle::Ops {
|
||||
MQTTBridge* _b;
|
||||
explicit LifecycleOps(MQTTBridge* bridge) : _b(bridge) {}
|
||||
uint32_t nowMs() override;
|
||||
void startTask() override;
|
||||
void deliverStop() override;
|
||||
void releaseResources() override;
|
||||
void onStopComplete(bool clean) override;
|
||||
};
|
||||
struct RecordingOps : LifecycleOps {
|
||||
int signals = 0;
|
||||
explicit RecordingOps(MQTTBridge* bridge) : LifecycleOps(bridge) {}
|
||||
void deliverStop() override { ++signals; LifecycleOps::deliverStop(); }
|
||||
} ops{this};
|
||||
MQTTLifecycle::Coordinator _lifecycle{ops, 1};
|
||||
std::atomic<bool> _stop_requested{false}, _stop_acked{false};
|
||||
bool _initialized = true, resources_owned = true;
|
||||
bool _slots_setup_done = true, _staged_raw_valid = true;
|
||||
bool _ntp_estimate_requested = true, _ntp_estimate_done = true, _ntp_estimate_ok = true;
|
||||
uint32_t _ntp_estimate_epoch = 123;
|
||||
int releases = 0, cancels = 0, completions = 0;
|
||||
bool clean_completion = false;
|
||||
MQTTBridge() {
|
||||
assert(_lifecycle.requestStart());
|
||||
assert(_lifecycle.onTaskStarted());
|
||||
s_mqtt_bridge_instance = this;
|
||||
}
|
||||
void cancelNtpRefresh() { ++cancels; }
|
||||
void requestStop();
|
||||
void end();
|
||||
void finishStopped();
|
||||
void loop();
|
||||
};
|
||||
void MQTTBridge::LifecycleOps::releaseResources() {
|
||||
assert(_b->_stop_acked.load() || _b->_lifecycle.stopTimedOut());
|
||||
assert(_b->resources_owned && _b->_initialized);
|
||||
_b->resources_owned = false;
|
||||
++_b->releases;
|
||||
}
|
||||
void MQTTBridge::LifecycleOps::onStopComplete(bool clean) {
|
||||
++_b->completions;
|
||||
_b->clean_completion = clean;
|
||||
}
|
||||
void vTaskDelay(uint32_t duration) {
|
||||
assert(duration > 0);
|
||||
++sleeps;
|
||||
clock_ms += duration;
|
||||
elapsed_ms += duration;
|
||||
if (elapsed_ms >= ack_after_ms)
|
||||
bridge_waiting->_stop_acked.store(true, std::memory_order_release);
|
||||
assert(elapsed_ms < 60000);
|
||||
}
|
||||
static void assert_finished(MQTTBridge& bridge, bool clean) {
|
||||
assert(!bridge._initialized && !bridge.resources_owned);
|
||||
assert(bridge.releases == 1 && bridge.completions == 1);
|
||||
assert(bridge.clean_completion == clean);
|
||||
assert(!bridge._lifecycle.isStopInProgress() && bridge._lifecycle.mayRestart());
|
||||
assert(bridge._lifecycle.mayBeginFlash() == clean);
|
||||
assert(!bridge._slots_setup_done && !bridge._staged_raw_valid);
|
||||
assert(!bridge._ntp_estimate_requested && !bridge._ntp_estimate_done);
|
||||
assert(!bridge._ntp_estimate_ok && bridge._ntp_estimate_epoch == 0);
|
||||
assert(bridge.cancels == 1);
|
||||
}
|
||||
'''
|
||||
|
||||
ASYNC_STOP_MAIN = r'''
|
||||
int main() {
|
||||
{
|
||||
MQTTBridge bridge;
|
||||
bridge._stop_acked = true; // stale prior-epoch ACK must be cleared exactly once
|
||||
bridge.requestStop();
|
||||
assert(sleeps == 0 && elapsed_ms == 0 && bridge.ops.signals == 1);
|
||||
assert(bridge._stop_requested && !bridge._stop_acked);
|
||||
assert(bridge._initialized && bridge.resources_owned && bridge.releases == 0);
|
||||
assert(s_mqtt_bridge_instance == nullptr);
|
||||
const auto timeout = bridge._lifecycle.stopTimeoutMs();
|
||||
assert(timeout > 5000);
|
||||
for (int pass = 0; pass < 10; ++pass) {
|
||||
clock_ms += timeout; // ordinary loop must never force-free a slow worker
|
||||
bridge.requestStop();
|
||||
bridge.loop();
|
||||
assert(bridge.ops.signals == 1 && sleeps == 0);
|
||||
assert(bridge._initialized && bridge.resources_owned && bridge.releases == 0);
|
||||
assert(bridge._lifecycle.isStopInProgress() && !bridge._lifecycle.stopTimedOut());
|
||||
}
|
||||
bridge._stop_acked.store(true, std::memory_order_release);
|
||||
bridge.requestStop();
|
||||
assert(bridge._stop_acked && bridge.ops.signals == 1); // duplicate cannot revoke ACK
|
||||
bridge.loop();
|
||||
assert_finished(bridge, true);
|
||||
bridge.loop();
|
||||
bridge.requestStop();
|
||||
bridge.end();
|
||||
assert(bridge.releases == 1 && bridge.ops.signals == 1 && bridge.cancels == 1);
|
||||
}
|
||||
for (int at_deadline = 0; at_deadline < 2; ++at_deadline) {
|
||||
MQTTBridge bridge;
|
||||
clock_ms = UINT32_MAX - 500; elapsed_ms = 0; sleeps = 0;
|
||||
bridge_waiting = &bridge;
|
||||
bridge.requestStop();
|
||||
ack_after_ms = at_deadline ? bridge._lifecycle.stopTimeoutMs() : 8000;
|
||||
bridge.end(); // synchronous OTA barrier joins the existing request
|
||||
assert(elapsed_ms == ack_after_ms && sleeps > 0 && bridge.ops.signals == 1);
|
||||
assert(!bridge._lifecycle.stopTimedOut());
|
||||
assert_finished(bridge, true);
|
||||
}
|
||||
{
|
||||
MQTTBridge bridge;
|
||||
clock_ms = 0; elapsed_ms = 0; sleeps = 0;
|
||||
bridge_waiting = &bridge; ack_after_ms = UINT64_MAX;
|
||||
bridge.end(); // reviewed timeout remains confined to the explicit barrier
|
||||
assert(bridge.ops.signals == 1 && bridge._lifecycle.stopTimedOut());
|
||||
assert(elapsed_ms == bridge._lifecycle.stopTimeoutMs());
|
||||
assert_finished(bridge, false);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class MqttTransportResultsTests(unittest.TestCase):
|
||||
def test_actual_async_stop_preserves_resources_until_ack_and_ota_joins(self):
|
||||
compiler = shutil.which("g++") or shutil.which("c++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
bridge = "src/helpers/bridges/MQTTBridge.cpp"
|
||||
source = ASYNC_STOP_PREAMBLE + "\n".join(method(bridge, signature) for signature in (
|
||||
"static inline uint32_t mqttStopTimeoutForSlots(int slots)",
|
||||
"void MQTTBridge::requestStop()", "void MQTTBridge::end()",
|
||||
"void MQTTBridge::finishStopped()", "void MQTTBridge::loop()",
|
||||
"uint32_t MQTTBridge::LifecycleOps::nowMs()",
|
||||
"void MQTTBridge::LifecycleOps::startTask()",
|
||||
"void MQTTBridge::LifecycleOps::deliverStop()")) + ASYNC_STOP_MAIN
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-mqtt-async-stop-") as temp:
|
||||
path = Path(temp) / "stop.cpp"
|
||||
path.write_text(source, encoding="utf-8")
|
||||
exe = Path(temp) / ("stop.exe" if os.name == "nt" else "stop")
|
||||
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
"-I", str(ROOT / "src"), str(path), "-o", str(exe)],
|
||||
text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = subprocess.run([str(exe)], text=True, capture_output=True, timeout=5)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_actual_worker_interrupts_startup_and_stagger_without_late_work(self):
|
||||
compiler = shutil.which("g++") or shutil.which("c++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
bridge = "src/helpers/bridges/MQTTBridge.cpp"
|
||||
source = TASK_PREAMBLE + method(bridge,
|
||||
"bool MQTTBridge::waitUnlessStopping(uint32_t delay_ms)")
|
||||
source += method(bridge, "void MQTTBridge::mqttTaskLoop()") + TASK_MAIN
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-mqtt-stop-") as temp:
|
||||
path = Path(temp) / "worker.cpp"
|
||||
path.write_text(source, encoding="utf-8")
|
||||
exe = Path(temp) / ("worker.exe" if os.name == "nt" else "worker")
|
||||
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
str(path), "-o", str(exe)], text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
for scenario in range(12):
|
||||
for tick_ms in (1, 10, 50):
|
||||
for wrap in (0, 1):
|
||||
with self.subTest(scenario=scenario, tick_ms=tick_ms, wrap=wrap):
|
||||
result = subprocess.run([str(exe), str(scenario), str(tick_ms), str(wrap)],
|
||||
text=True, capture_output=True, timeout=5)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_real_adapter_and_bridge_methods_for_both_idf_layouts(self):
|
||||
compiler = shutil.which("g++") or shutil.which("c++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
|
||||
@@ -35,7 +35,8 @@ class RS232MQTTRuntimeTests(unittest.TestCase):
|
||||
"bool setRs232BridgeState(bool enable)", "bool restartRs232Bridge()",
|
||||
"bool startSharedEspNowBridgeIfReady()", "bool isEspNowBridgeRunning()",
|
||||
"bool setEspNowBridgeState(bool enable)", "bool restartEspNowBridge()",
|
||||
"bool isMqttBridgeRunning()", "bool setMqttBridgeState(bool enable)",
|
||||
"bool isMqttBridgeRunning()", "bool requestMqttBridgeStop()", "bool isMqttBridgeStopping()",
|
||||
"bool setMqttBridgeState(bool enable)",
|
||||
"bool restartMqttBridge()", "bool setBridgeState(bool enable)", "bool restartBridge()",
|
||||
))
|
||||
defaults_start = implementation.index("// bridge defaults")
|
||||
@@ -49,7 +50,7 @@ class RS232MQTTRuntimeTests(unittest.TestCase):
|
||||
start = implementation.index("#ifdef WITH_MQTT_BRIDGE\n", start)
|
||||
end = implementation.index(" if (_logging)", start)
|
||||
routing[name] = implementation[start:end]
|
||||
service_start = implementation.index("#if defined(WITH_ESPNOW_BRIDGE)",
|
||||
service_start = implementation.index("#if defined(WITH_MQTT_BRIDGE)\n // MQTT owns TLS",
|
||||
implementation.index("MyMesh::servicePostMeshLoop()"))
|
||||
service_end = implementation.index(" if (next_flood_advert", service_start)
|
||||
pending_start = implementation.index("#if defined(WITH_BRIDGE)",
|
||||
@@ -80,7 +81,8 @@ class RS232MQTTRuntimeTests(unittest.TestCase):
|
||||
'if (configKeyEquals(config, "bridge.uart"))',
|
||||
))
|
||||
gets += "\nelse " + "\nelse ".join(extract_braced(observer, signature) for signature in (
|
||||
'if (strcmp(config, "mqtt.enabled") == 0)', 'if (strcmp(config, "mqtt.running") == 0)'))
|
||||
'if (strcmp(config, "mqtt.enabled") == 0)', 'if (strcmp(config, "mqtt.running") == 0)',
|
||||
'if (strcmp(config, "mqtt.stopping") == 0)'))
|
||||
ota_start = cli.index(' } else if (memcmp(command, "start ota", 9)')
|
||||
ota_end = cli.index(' } else if (memcmp(command, "clock", 5)', ota_start)
|
||||
code = (ROOT / "test/fixtures/rs232_mqtt_runtime.cpp").read_text(encoding="ascii")
|
||||
@@ -128,7 +130,8 @@ class RS232MQTTRuntimeTests(unittest.TestCase):
|
||||
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()", "bool endRS232Bridge()",
|
||||
"bool rs232BridgeEnabled() const", "bool isBridgeRunning() const override",
|
||||
"bool isRs232BridgeRunning()", "bool setRs232BridgeState(bool enable)",
|
||||
"bool restartRs232Bridge()", "bool isMqttBridgeRunning()", "bool setBridgeState(bool enable)",
|
||||
"bool restartRs232Bridge()", "bool isMqttBridgeRunning()", "bool requestMqttBridgeStop()",
|
||||
"bool isMqttBridgeStopping()", "bool setBridgeState(bool enable)",
|
||||
"bool restartBridge()"))
|
||||
code = (ROOT / "test/fixtures/rs232_mqtt_runtime.cpp").read_text(encoding="ascii")
|
||||
code = code[:code.index("int main() {")]
|
||||
@@ -144,7 +147,7 @@ class RS232MQTTRuntimeTests(unittest.TestCase):
|
||||
boot_start = implementation.index('mesh::hilStartupTrace("mesh_bridge_begin");')
|
||||
boot_start = implementation.index("#if defined(WITH_BRIDGE)", boot_start)
|
||||
boot_end = implementation.index('mesh::hilStartupTrace("mesh_bridge_ready");', boot_start)
|
||||
service_start = implementation.index("#if defined(WITH_ESPNOW_BRIDGE)", implementation.index("MyMesh::servicePostMeshLoop()"))
|
||||
service_start = implementation.index("#if defined(WITH_MQTT_BRIDGE)\n // MQTT owns TLS", implementation.index("MyMesh::servicePostMeshLoop()"))
|
||||
service_end = implementation.index(" if (next_flood_advert", service_start)
|
||||
pending_start = implementation.index("#if defined(WITH_BRIDGE)", implementation.index("bool MyMesh::hasPendingWork()"))
|
||||
pending_end = implementation.index(" if (radio_driver.isWatchdogObserving()", pending_start)
|
||||
@@ -199,6 +202,8 @@ class RS232MQTTRuntimeTests(unittest.TestCase):
|
||||
cli.run("stop ota", reply);
|
||||
assert(!mesh._cli.board.ota && mesh.isMqttBridgeRunning() && Serial2.begins == uart_starts);
|
||||
cli.set("rs232.enabled off", reply); cli.set("mqtt.enabled off", reply);
|
||||
assert(!mesh.bridge && mesh.isMqttBridgeStopping() && mesh.bridgesPreventSleep());
|
||||
mesh.serviceBridges();
|
||||
assert(!mesh.bridge && !mesh.isMqttBridgeRunning() && !mesh.bridgesPreventSleep());
|
||||
cli.set("rs232.enabled on", reply); mesh.mqtt_bridge->fail_starts = 1;
|
||||
cli.set("mqtt.enabled on", reply);
|
||||
|
||||
Reference in new issue
Block a user