Protect MQTT publishing headroom and credit replies before disconnect
This commit is contained in:
1 parent
2be068cafa
commit
d1a2fd74ba
11 files changed
+377
-96
No files matched your search
@@ -607,10 +607,12 @@ int main() {
|
||||
self.assertEqual(profile["maximum_slot_stack_buffer_allowance_bytes"], potential)
|
||||
self.assertEqual(profile["cold_start_allocation_allowance_bytes"], 16384)
|
||||
self.assertEqual(profile["cold_start_dma_admission_free_bytes"], 32768)
|
||||
self.assertEqual(profile["cold_start_dma_admission_contiguous_bytes"], 8192)
|
||||
self.assertEqual(profile["cold_start_dma_admission_contiguous_bytes"],
|
||||
16000 if active == 5 else 8192)
|
||||
self.assertEqual(profile["restart_allocation_allowance_bytes"], 8192)
|
||||
self.assertEqual(profile["restart_dma_admission_free_bytes"], 24576)
|
||||
self.assertEqual(profile["restart_dma_admission_contiguous_bytes"], 8192)
|
||||
self.assertEqual(profile["restart_dma_admission_contiguous_bytes"],
|
||||
16000 if active == 5 else 8192)
|
||||
self.assertFalse(profile["maximum_runtime_load_qualified"])
|
||||
self.assertFalse(profile["physical_validation_performed"])
|
||||
ordinary = ram.requirements("ESP32_PLATFORM", {"BOARD_HAS_PSRAM": 1}, "v4_companion")
|
||||
@@ -707,16 +709,31 @@ int main() {
|
||||
("client_task_stack_bytes", "kClientTaskStackBytes"),
|
||||
("client_task_priority", "kClientTaskPriority"),
|
||||
("cold_start_allocation_allowance_bytes", "kColdStartAllowanceBytes"),
|
||||
("cold_start_dma_admission_contiguous_bytes", "kColdStartLargestBytes"),
|
||||
("restart_allocation_allowance_bytes", "kRestartAllowanceBytes"),
|
||||
("restart_dma_admission_contiguous_bytes", "kRestartLargestBytes"),
|
||||
("dma_admission_free_bytes", "kDmaReserveBytes"),
|
||||
("dma_admission_contiguous_bytes", "kDmaLargestBytes"),
|
||||
("publish_contiguous_without_psram_bytes", "kInternalPublishLargestBytes"),
|
||||
("publish_contiguous_with_psram_bytes", "kPsramPublishLargestBytes"),
|
||||
("tls_internal_admission_free_bytes", "kTlsInternalFreeBytes"),
|
||||
("tls_internal_admission_contiguous_bytes", "kTlsRecordAllocationBytes"),
|
||||
("tls_record_allocation_bytes", "kTlsRecordAllocationBytes"),
|
||||
):
|
||||
with self.subTest(key=key):
|
||||
self.assertIn(f"{name} = {profile[key]};", source)
|
||||
for psram in (False, True):
|
||||
flags = {"WITH_MQTT_BRIDGE": 1}
|
||||
if psram:
|
||||
flags["BOARD_HAS_PSRAM"] = 1
|
||||
actual = ram.requirements("ESP32_PLATFORM", flags, "v4_companion")["mqtt"]
|
||||
floor = 16000 if psram else 8000
|
||||
self.assertEqual(actual["publish_contiguous_bytes"], floor)
|
||||
self.assertEqual(actual["dma_admission_contiguous_bytes"], floor)
|
||||
self.assertEqual(actual["cold_start_dma_admission_contiguous_bytes"], max(floor, 8192))
|
||||
self.assertEqual(actual["additional_broker_cold_start_dma_admission_contiguous_bytes"], floor + 16384)
|
||||
self.assertEqual(actual["restart_dma_admission_contiguous_bytes"], max(floor, 8192))
|
||||
self.assertEqual(actual["additional_broker_restart_dma_admission_contiguous_bytes"], floor + 8192)
|
||||
self.assertEqual(actual["additional_broker_cold_start_tls_internal_admission_contiguous_bytes"], 41792)
|
||||
self.assertEqual(actual["additional_broker_restart_tls_internal_admission_contiguous_bytes"], 33600)
|
||||
self.assertEqual(actual["tls_internal_admission_contiguous_bytes"], 17408)
|
||||
self.assertEqual(actual["additional_broker_tls_internal_admission_contiguous_bytes"], 25408)
|
||||
self.assertEqual(profile["cold_start_dma_admission_free_bytes"],
|
||||
profile["dma_admission_free_bytes"] + profile["cold_start_allocation_allowance_bytes"])
|
||||
self.assertEqual(profile["restart_dma_admission_free_bytes"],
|
||||
|
||||
@@ -14,6 +14,7 @@ POLICY_PREAMBLE = r'''
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include "helpers/MQTTConnectionAdmission.h"
|
||||
#include "helpers/MQTTTransportHealth.h"
|
||||
namespace Admission = MQTTConnectionAdmission;
|
||||
@@ -62,6 +63,7 @@ struct Client {
|
||||
};
|
||||
struct MQTTBridge {
|
||||
struct MQTTSlot {
|
||||
bool enabled = true, connected = false;
|
||||
Client* client = nullptr;
|
||||
const Preset* preset = nullptr;
|
||||
char broker_uri[128] = {}, host[64] = "broker";
|
||||
@@ -97,46 +99,112 @@ class MqttConnectionAdmissionTest(unittest.TestCase):
|
||||
self.compile_run(POLICY_PREAMBLE + r'''
|
||||
int main() {
|
||||
// A large aggregate heap cannot compensate for a fragmented DMA region.
|
||||
assert(!Admission::canStart(true, true, MemoryState::Started, 100000, 4095));
|
||||
assert(!Admission::canStart(true, true, MemoryState::Started, 16383, 20000));
|
||||
assert(Admission::canStart(true, true, MemoryState::Started, 16384, 4096));
|
||||
assert(Admission::canStart(false, false, MemoryState::Started, 16384, 4096));
|
||||
assert(!Admission::canStart(false, false, MemoryState::Started, 16383, 4096));
|
||||
assert(!Admission::canStart(false, false, MemoryState::Started, 16384, 4095));
|
||||
assert(!Admission::canStart(true, true, MemoryState::Started, 100000, 15999, true));
|
||||
assert(!Admission::canStart(true, true, MemoryState::Started, 16383, 16000, true));
|
||||
assert(Admission::canStart(true, true, MemoryState::Started, 16384, 16000, true));
|
||||
assert(Admission::canStart(false, false, MemoryState::Started, 16384, 8000, true));
|
||||
assert(!Admission::canStart(false, false, MemoryState::Started, 16383, 8000, true));
|
||||
assert(!Admission::canStart(false, false, MemoryState::Started, 16384, 7999, true));
|
||||
|
||||
// Without PSRAM, TLS also needs a full record allocation and session budget.
|
||||
// Internal TLS must leave a publication block beside its new TLS record and
|
||||
// ordinary SDK startup allocations, even for a retained running task.
|
||||
const MemoryState states[] = {MemoryState::Uninitialized, MemoryState::Stopped,
|
||||
MemoryState::Started};
|
||||
for (MemoryState state : states) {
|
||||
assert(!Admission::canStart(false, true, state, 61439, 20000));
|
||||
assert(!Admission::canStart(false, true, state, 100000, 17407));
|
||||
assert(Admission::canStart(false, true, state, 61440, 17408));
|
||||
const size_t internal_tls_largest[] = {41792, 33600, 25408};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
assert(!Admission::canStart(false, true, states[i], 61439, 100000, true));
|
||||
assert(!Admission::canStart(false, true, states[i], 100000, internal_tls_largest[i] - 1, true));
|
||||
assert(Admission::canStart(false, true, states[i], 61440, internal_tls_largest[i], true));
|
||||
}
|
||||
assert(Admission::canStart(true, false, MemoryState::Started, 16384, 4096));
|
||||
assert(!Admission::canStart(true, false, static_cast<MemoryState>(255), SIZE_MAX, SIZE_MAX));
|
||||
assert(Admission::canStart(true, false, MemoryState::Started, 16384, 16000, true));
|
||||
assert(!Admission::canStart(true, false, static_cast<MemoryState>(255), SIZE_MAX, SIZE_MAX, true));
|
||||
|
||||
// A new SDK task needs its stack and ordinary startup allocations in
|
||||
// addition to the DMA reserve, including plaintext and PSRAM-backed TLS.
|
||||
assert(Admission::kClientTaskStackBytes == 6144);
|
||||
assert(Admission::kClientTaskPriority == 5);
|
||||
assert(Admission::kColdStartAllowanceBytes == 16384);
|
||||
assert(Admission::kColdStartLargestBytes == 8192);
|
||||
assert(Admission::kRestartAllowanceBytes == 8192);
|
||||
assert(Admission::kRestartLargestBytes == 8192);
|
||||
for (int psram = 0; psram <= 1; ++psram) {
|
||||
const size_t floor = psram ? 16000 : 8000;
|
||||
for (int tls = 0; tls <= 1; ++tls) {
|
||||
if (!psram && tls) continue; // Larger TLS thresholds checked above.
|
||||
assert(!Admission::canStart(psram, tls, MemoryState::Uninitialized, 32767, 20000));
|
||||
assert(!Admission::canStart(psram, tls, MemoryState::Uninitialized, 100000, 8191));
|
||||
assert(Admission::canStart(psram, tls, MemoryState::Uninitialized, 32768, 8192));
|
||||
// An initialized handle retains its ordinary SDK buffers across a stop.
|
||||
// Only the replacement task/TCB is charged beside the DMA reserve.
|
||||
assert(!Admission::canStart(psram, tls, MemoryState::Stopped, 24575, 20000));
|
||||
assert(!Admission::canStart(psram, tls, MemoryState::Stopped, 100000, 8191));
|
||||
assert(Admission::canStart(psram, tls, MemoryState::Stopped, 24576, 8192));
|
||||
assert(!Admission::canStart(psram, tls, MemoryState::Uninitialized, 32767, 100000, true));
|
||||
assert(!Admission::canStart(psram, tls, MemoryState::Uninitialized, 100000, floor + 16383, true));
|
||||
assert(Admission::canStart(psram, tls, MemoryState::Uninitialized, 32768, floor + 16384, true));
|
||||
// An initialized handle retains its buffers, but recreates its task.
|
||||
assert(!Admission::canStart(psram, tls, MemoryState::Stopped, 24575, 100000, true));
|
||||
assert(!Admission::canStart(psram, tls, MemoryState::Stopped, 100000, floor + 8191, true));
|
||||
assert(Admission::canStart(psram, tls, MemoryState::Stopped, 24576, floor + 8192, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
''')
|
||||
|
||||
def test_additional_broker_cannot_spend_live_packet_publishing_headroom(self):
|
||||
self.compile_run(POLICY_PREAMBLE + r'''
|
||||
int main() {
|
||||
for (bool psram : {false, true}) {
|
||||
const size_t floor = psram ? 16000 : 8000;
|
||||
assert(!Admission::hasPublishHeadroom(psram, floor - 1));
|
||||
assert(Admission::hasPublishHeadroom(psram, floor));
|
||||
// Previously admitted: 8 KiB largest was enough to start another client,
|
||||
// even though publishing on a PSRAM board was already suppressed there.
|
||||
assert(!Admission::canStart(psram, false, MemoryState::Uninitialized, 100000, 8192, true));
|
||||
assert(!Admission::canStart(psram, true, MemoryState::Stopped, 100000, 8192, true));
|
||||
const size_t allowances[] = {16384, 8192, 0};
|
||||
const MemoryState states[] = {MemoryState::Uninitialized, MemoryState::Stopped,
|
||||
MemoryState::Started};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
const size_t minimum = floor + allowances[i];
|
||||
assert(!Admission::canStart(psram, false, states[i], 100000, minimum - 1, true));
|
||||
assert(Admission::canStart(psram, false, states[i], 100000, minimum, true));
|
||||
// Spending the complete bounded SDK startup allowance still leaves a
|
||||
// block accepted by the exact production packet-publishing policy.
|
||||
assert(Admission::hasPublishHeadroom(psram, minimum - allowances[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
''')
|
||||
|
||||
def test_actual_packet_publish_guard_uses_detected_psram_and_throttles_sampling(self):
|
||||
source = (ROOT / "src/helpers/bridges/MQTTBridge.cpp").read_text()
|
||||
start = source.index(" #ifdef ESP32\n", source.index("bool MQTTBridge::publishPacket("))
|
||||
end = source.index(" #endif\n", start) + len(" #endif\n")
|
||||
guard = source[start:end]
|
||||
self.compile_run(POLICY_PREAMBLE + r'''
|
||||
#define ESP32 1
|
||||
#define BOARD_HAS_PSRAM 1
|
||||
#define MQTT_DEBUG_PRINTLN(...) ((void)0)
|
||||
static unsigned long now_ms = 6000;
|
||||
unsigned long millis() { return now_ms; }
|
||||
struct {
|
||||
size_t psram = 2097152, largest = 16000, queries = 0;
|
||||
size_t getPsramSize() const { return psram; }
|
||||
size_t getMaxAllocHeap() { ++queries; return largest; }
|
||||
} ESP;
|
||||
struct Bridge {
|
||||
unsigned long _last_memory_check = 0;
|
||||
bool _memory_pressure = false;
|
||||
int _skipped_publishes = 0;
|
||||
bool publish() {
|
||||
''' + guard + r'''
|
||||
return true;
|
||||
}
|
||||
};
|
||||
int main() {
|
||||
Bridge bridge;
|
||||
assert(bridge.publish() && ESP.queries == 1);
|
||||
ESP.largest = 15999;
|
||||
now_ms += 5001;
|
||||
assert(!bridge.publish() && bridge._skipped_publishes == 1 && ESP.queries == 2);
|
||||
assert(!bridge.publish() && bridge._skipped_publishes == 2 && ESP.queries == 2);
|
||||
now_ms += 5001; ESP.largest = 16000;
|
||||
assert(bridge.publish() && ESP.queries == 3);
|
||||
// Compiling BOARD_HAS_PSRAM does not establish that PSRAM initialized.
|
||||
now_ms += 5001; ESP.psram = 0; ESP.largest = 8000;
|
||||
assert(bridge.publish() && ESP.queries == 4);
|
||||
now_ms += 5001; ESP.largest = 7999;
|
||||
assert(!bridge.publish() && bridge._skipped_publishes == 3);
|
||||
}
|
||||
''')
|
||||
|
||||
def test_internal_fallback_preserves_dma_reserve_and_rejects_fragmentation(self):
|
||||
@@ -292,35 +360,35 @@ int main() {
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
dma_free = 100000; dma_largest = 17407;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
ESP.size = 2097152; dma_free = 16384; dma_largest = 4096;
|
||||
ESP.size = 2097152; dma_free = 16384; dma_largest = 16000;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
Client client;
|
||||
bridge._slots[0].client = &client;
|
||||
assert(!bridge.canStartSlotConnection(0)); // Wrapper allocated; SDK not initialized.
|
||||
dma_free = 32767; dma_largest = 20000;
|
||||
dma_free = 32767; dma_largest = 16000;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
dma_free = 32768; dma_largest = 8191;
|
||||
dma_free = 32768; dma_largest = 15999;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
dma_largest = 8192;
|
||||
dma_largest = 16000;
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
|
||||
// An initialized but stopped handle needs its task again, not new SDK buffers.
|
||||
client.initialized = true; dma_free = 24575; dma_largest = 20000;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
dma_free = 24576; dma_largest = 8191;
|
||||
dma_free = 24576; dma_largest = 15999;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
dma_largest = 8192;
|
||||
dma_largest = 16000;
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
|
||||
// A warm reconnect retains the SDK task/buffers and uses the smaller floor.
|
||||
client.started = true; dma_free = 16384; dma_largest = 4096;
|
||||
client.started = true; dma_free = 16384; dma_largest = 16000;
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
dma_largest = 4095;
|
||||
dma_largest = 15999;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
|
||||
// An explicit plaintext URI overrides a TLS port. The effective URI wins
|
||||
// over the raw host; a preset's URI wins over both custom fields.
|
||||
ESP.size = 0; dma_free = 16384; dma_largest = 4096;
|
||||
ESP.size = 0; dma_free = 16384; dma_largest = 16000;
|
||||
strcpy(bridge._slots[0].host, "mqtt://broker:8883");
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
strcpy(bridge._slots[0].broker_uri, "wss://broker/mqtt");
|
||||
@@ -334,6 +402,32 @@ int main() {
|
||||
ESP.size = 2097152;
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
|
||||
// Reproduce the reporter's failed-state heap after all SDK tasks stopped.
|
||||
// One broker must be able to recover instead of waiting forever for a
|
||||
// contiguous 24,192-byte block pinned apart by retained SDK buffers.
|
||||
client.started = false;
|
||||
dma_free = 93024; dma_largest = 20468;
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
// Once another broker is connected, an additional task must leave its
|
||||
// publication headroom beside the restart allowance.
|
||||
bridge._slots[1].connected = true;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
dma_largest = 24191;
|
||||
assert(!bridge.canStartSlotConnection(0));
|
||||
dma_largest = 24192;
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
// Health reconciliation removing the other failed connection restores
|
||||
// bootstrap admission. The target never counts itself as a live peer.
|
||||
dma_largest = 20468;
|
||||
bridge._slots[1].connected = false;
|
||||
bridge._slots[0].connected = true;
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
bridge._slots[0].connected = false;
|
||||
bridge._slots[1].enabled = false;
|
||||
bridge._slots[1].connected = true;
|
||||
assert(bridge.canStartSlotConnection(0));
|
||||
bridge._slots[1].connected = false;
|
||||
|
||||
// Every pending TLS attempt, including the target's own attempt, blocks all
|
||||
// other starts until an event callback clears it. No heap walk is needed.
|
||||
for (int pending = 0; pending < RUNTIME_MQTT_SLOTS; ++pending) {
|
||||
|
||||
@@ -54,7 +54,7 @@ struct esp_mqtt_error_codes {
|
||||
constexpr uint32_t MALLOC_CAP_INTERNAL = 1, MALLOC_CAP_DMA = 2;
|
||||
static uint32_t clock_ms = 1000;
|
||||
static unsigned long wall_seconds = 1800000000;
|
||||
static size_t dma_free = 100000, dma_largest = 20000;
|
||||
static size_t dma_free = 100000, dma_largest = 100000;
|
||||
static unsigned long s_wifi_connected_at = 0;
|
||||
uint32_t millis() { return clock_ms; }
|
||||
unsigned long fake_time(void*) { return wall_seconds; }
|
||||
@@ -155,7 +155,7 @@ struct PsychicMqttClient {
|
||||
void softDisconnect() {
|
||||
++bounces; live = false;
|
||||
// Releasing the old internal TLS session creates room for its replacement.
|
||||
dma_free += 44000; dma_largest = 20000;
|
||||
dma_free += 44000; dma_largest = 50000;
|
||||
}
|
||||
void setCredentials(const char*, const char*) { ++credentials; }
|
||||
};
|
||||
@@ -421,11 +421,11 @@ int main() {
|
||||
bridge.maintainSlotConnections();
|
||||
assert(bridge.setups.empty() && bridge._slots[0].last_reconnect_attempt == 0);
|
||||
assert(bridge._slots[0].enabled && bridge.activatedSlotCount() == 0);
|
||||
dma_free = 32768; dma_largest = 8191;
|
||||
dma_free = 32768; dma_largest = 15999;
|
||||
bridge.maintainSlotConnections();
|
||||
assert(bridge.setups.empty() && bridge._slots[0].last_reconnect_attempt == 0);
|
||||
assert(!bridge.hasPendingSlotConnection() && bridge.activatedSlotCount() == 0);
|
||||
dma_largest = 8192;
|
||||
dma_largest = 16000;
|
||||
bridge.maintainSlotConnections();
|
||||
assert(bridge.setups == std::vector<int>{0} && bridge.activatedSlotCount() == 1);
|
||||
assert(bridge.clients[0].starts == 1 && bridge.clients[0].reconnects == 0);
|
||||
@@ -438,7 +438,7 @@ int main() {
|
||||
MQTTBridge bridge;
|
||||
for (int i = 2; i < RUNTIME_MQTT_SLOTS; ++i) bridge._slots[i].enabled = false;
|
||||
clock_ms = 70000;
|
||||
dma_free = 16384; dma_largest = 4096;
|
||||
dma_free = 16384; dma_largest = 16000;
|
||||
auto& stopped = bridge._slots[0];
|
||||
auto& retained = bridge._slots[1];
|
||||
stopped.client = &bridge.clients[0];
|
||||
@@ -462,7 +462,7 @@ int main() {
|
||||
// with no extra delay introduced by the earlier memory-only deferral.
|
||||
bridge.complete(1);
|
||||
clock_ms += 15000;
|
||||
dma_free = 24576; dma_largest = 8192;
|
||||
dma_free = 24576; dma_largest = 24192;
|
||||
bridge.maintainSlotConnections();
|
||||
assert(stopped.client->starts == 1 && stopped.client->reconnects == 0);
|
||||
assert(stopped.client->isStarted() && bridge._slot_attempt_pending[0]);
|
||||
@@ -536,14 +536,14 @@ int main() {
|
||||
// headroom is inadequate. Neither failure consumes retry/backoff state.
|
||||
dma_free = 24575;
|
||||
bridge.maintainSlotConnections();
|
||||
dma_free = 24576; dma_largest = 8191;
|
||||
dma_free = 24576; dma_largest = 15999;
|
||||
bridge.maintainSlotConnections();
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
assert(bridge.clients[i].starts == 0 && bridge._slots[i].last_reconnect_attempt == 0);
|
||||
assert(bridge._slots[i].reconnect_backoff == 0 && bridge._slots[i].start_failures == 0);
|
||||
}
|
||||
|
||||
dma_free = 42376; dma_largest = 22516;
|
||||
dma_free = 42376; dma_largest = 24192;
|
||||
bridge.maintainSlotConnections();
|
||||
assert(bridge.clients[0].starts == 1 && dma_free == 35840);
|
||||
assert(bridge.hasPendingSlotConnection());
|
||||
@@ -837,10 +837,10 @@ int main() {
|
||||
bridge.maintainSlotConnections();
|
||||
assert(slot.client->starts == 0 && slot.reconnect_backoff == 3);
|
||||
assert(slot.last_reconnect_attempt == 70000 && slot.start_failures == 0);
|
||||
dma_free = 24576; dma_largest = 8191;
|
||||
dma_free = 24576; dma_largest = 15999;
|
||||
bridge.maintainSlotConnections();
|
||||
assert(slot.client->starts == 0 && slot.reconnect_backoff == 3);
|
||||
dma_largest = 8192;
|
||||
dma_largest = 16000;
|
||||
bridge.maintainSlotConnections();
|
||||
assert(slot.client->starts == 1 && bridge._slot_attempt_pending[0]);
|
||||
assert(slot.reconnect_backoff == 4 && slot.max_backoff_failures == 1);
|
||||
|
||||
@@ -104,7 +104,7 @@ RECONFIGURE_PREAMBLE = r'''
|
||||
#define pdMS_TO_TICKS(value) (value)
|
||||
constexpr int RUNTIME_MQTT_SLOTS = 2, MQTT_AUTH_JWT = 1;
|
||||
constexpr uint32_t MALLOC_CAP_INTERNAL = 1, MALLOC_CAP_DMA = 2;
|
||||
static size_t dma_free = 100000, dma_largest = 20000;
|
||||
static size_t dma_free = 100000, dma_largest = 100000;
|
||||
size_t heap_caps_get_free_size(uint32_t caps) {
|
||||
assert(caps == (MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA)); return dma_free;
|
||||
}
|
||||
@@ -228,7 +228,7 @@ int main() {
|
||||
bridge.applySlotPreset(0, "plain");
|
||||
assert(slot.client && !slot.client->isStarted() && !slot.initial_connect_done);
|
||||
assert(init_calls == 1 && destroy_calls == 0);
|
||||
dma_free = 24576;
|
||||
dma_free = 24576; dma_largest = 24192;
|
||||
assert(bridge.setupSlot(0));
|
||||
assert(slot.client->_buffer_capacity == 896 && init_calls == 1);
|
||||
const int destructions_before_guard = destroy_calls;
|
||||
|
||||
@@ -612,6 +612,41 @@ static void publishAcknowledgmentProvesRecoveryImmediately() {
|
||||
"an old PUBACK repeatedly renewed a disconnected broker");
|
||||
}
|
||||
|
||||
static void disconnectedPublishAcknowledgmentRenewsGraceOnce() {
|
||||
for (uint32_t start : {100U, UINT32_MAX - 10000U}) {
|
||||
resetEnvironment();
|
||||
MQTTBridge bridge;
|
||||
require(bridge.serviceNetworkPower(start), "initial outage did not run");
|
||||
require(!bridge.serviceNetworkPower(start + grace), "outage did not suspend");
|
||||
const uint32_t resumed = start + grace + interval;
|
||||
require(bridge.serviceNetworkPower(resumed), "probe did not resume");
|
||||
const uint32_t replied = resumed + probe - 1U;
|
||||
// The SDK delivered PUBACK and then DISCONNECTED between owner passes.
|
||||
// A real broker reply still proves service; a CONNACK by itself does not.
|
||||
bridge.clients[0].publish_acks = 1U;
|
||||
require(!bridge._slots[0].connected, "fixture unexpectedly connected");
|
||||
require(bridge.serviceNetworkPower(replied), "new PUBACK was discarded");
|
||||
require(bridge.serviceNetworkPower(resumed + probe),
|
||||
"disconnect hid PUBACK and retained the expired probe deadline");
|
||||
require(bridge.serviceNetworkPower(replied + grace - 1U),
|
||||
"disconnected PUBACK did not renew normal grace");
|
||||
require(!bridge.serviceNetworkPower(replied + grace),
|
||||
"unchanged PUBACK kept a failed service awake forever");
|
||||
}
|
||||
resetEnvironment();
|
||||
{
|
||||
MQTTBridge bridge;
|
||||
bridge.serviceNetworkPower(100U);
|
||||
bridge._slots[0].enabled = false;
|
||||
bridge.clients[0].publish_acks = 1U;
|
||||
require(!bridge.serviceNetworkPower(100U + grace),
|
||||
"a disabled broker renewed the outage budget");
|
||||
bridge._slots[0].enabled = true;
|
||||
require(!bridge.serviceNetworkPower(101U + grace),
|
||||
"enabling a broker replayed its already consumed PUBACK");
|
||||
}
|
||||
}
|
||||
|
||||
static void quietConnectionHasExactThirtySecondHealthBoundary() {
|
||||
resetEnvironment();
|
||||
{
|
||||
@@ -743,6 +778,7 @@ int main(int argc, char** argv) {
|
||||
else if (name == "channel-repair") deferredEspNowChannelRepair();
|
||||
else if (name == "healthy") stableBrokerCountsButFlappingDoesNot();
|
||||
else if (name == "puback") publishAcknowledgmentProvesRecoveryImmediately();
|
||||
else if (name == "disconnected-puback") disconnectedPublishAcknowledgmentRenewsGraceOnce();
|
||||
else if (name == "quiet-boundary") quietConnectionHasExactThirtySecondHealthBoundary();
|
||||
else if (name == "ntp-retry") sntpFailureAndBusyLeaseRetries();
|
||||
else if (name == "ntp-rotation") everyNtpServerGetsTheLeadingTimeoutSlot();
|
||||
@@ -891,6 +927,9 @@ class MqttWiFiNetworkPowerTests(unittest.TestCase):
|
||||
def test_publish_ack_proves_recovery_immediately_and_is_consumed_once(self):
|
||||
self.run_case("puback")
|
||||
|
||||
def test_puback_followed_by_disconnect_renews_grace_once_across_wrap(self):
|
||||
self.run_case("disconnected-puback")
|
||||
|
||||
def test_quiet_broker_health_requires_exactly_thirty_seconds(self):
|
||||
self.run_case("quiet-boundary")
|
||||
|
||||
|
||||
Reference in new issue
Block a user