Keep MQTT and WebConfig stable under internal memory pressure
This commit is contained in:
1 parent
2be068cafa
commit
2fc2f409d4
22 files changed
+1494
-406
No files matched your search
@@ -3,6 +3,10 @@
|
||||
#include "PsychicMqttClient.h"
|
||||
|
||||
#include <string.h>
|
||||
#if defined(BOARD_HAS_PSRAM)
|
||||
#include <esp_heap_caps.h>
|
||||
#include <helpers/MQTTConnectionAdmission.h>
|
||||
#endif
|
||||
|
||||
static const char *TAG = "[OCTOPUS]";
|
||||
|
||||
@@ -27,11 +31,16 @@ PsychicMqttClient::~PsychicMqttClient()
|
||||
{
|
||||
esp_mqtt_client_destroy(_client);
|
||||
_client = nullptr;
|
||||
_outbox_size.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
if (_buffer != nullptr)
|
||||
{
|
||||
#if defined(BOARD_HAS_PSRAM)
|
||||
heap_caps_free(_buffer);
|
||||
#else
|
||||
free(_buffer);
|
||||
#endif
|
||||
_buffer = nullptr;
|
||||
_buffer_capacity = 0;
|
||||
}
|
||||
@@ -428,7 +437,23 @@ esp_err_t PsychicMqttClient::connect()
|
||||
if (_buffer == nullptr)
|
||||
{
|
||||
_buffer_capacity = (size_t)desired_buffer;
|
||||
#if defined(BOARD_HAS_PSRAM)
|
||||
// This character buffer is CPU-only; no atomics, DMA or ISR access.
|
||||
// Retain it through reconnects just like the ordinary heap allocation.
|
||||
const size_t bytes = _buffer_capacity + 1;
|
||||
_buffer = static_cast<char*>(heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
if (_buffer == nullptr)
|
||||
{
|
||||
const uint32_t dma_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA;
|
||||
if (MQTTConnectionAdmission::canFallback(bytes,
|
||||
heap_caps_get_free_size(dma_caps), heap_caps_get_largest_free_block(dma_caps)))
|
||||
{
|
||||
_buffer = static_cast<char*>(heap_caps_malloc(bytes, dma_caps | MALLOC_CAP_8BIT));
|
||||
}
|
||||
}
|
||||
#else
|
||||
_buffer = (char *)malloc(_buffer_capacity + 1);
|
||||
#endif
|
||||
if (_buffer == nullptr)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to allocate reassembly buffer (%u bytes)", (unsigned)_buffer_capacity);
|
||||
@@ -517,6 +542,7 @@ void PsychicMqttClient::disconnect()
|
||||
}
|
||||
_connected = false;
|
||||
_started = false;
|
||||
_outbox_size.store(0, std::memory_order_relaxed);
|
||||
ESP_LOGI(TAG, "MQTT client stopped.");
|
||||
}
|
||||
|
||||
@@ -561,9 +587,16 @@ void PsychicMqttClient::forceStop()
|
||||
{
|
||||
ESP_LOGI(TAG, "Forced stop MQTT client.");
|
||||
}
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_mqtt_client_stop(_client));
|
||||
const esp_err_t result = esp_mqtt_client_stop(_client);
|
||||
ESP_ERROR_CHECK_WITHOUT_ABORT(result);
|
||||
if (result != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "Forced MQTT stop failed: %s", esp_err_to_name(result));
|
||||
return;
|
||||
}
|
||||
_connected = false;
|
||||
_started = false;
|
||||
_outbox_size.store(0, std::memory_order_relaxed);
|
||||
ESP_LOGI(TAG, "MQTT client forcefully stopped.");
|
||||
}
|
||||
|
||||
@@ -604,18 +637,18 @@ int PsychicMqttClient::publish(const char *topic, int qos, bool retain, const ch
|
||||
// own (esp-mqtt frees entries only on send-ack or time-based expiry), so on a
|
||||
// stalled uplink QoS0 frames pile up on internal heap without limit. Cap it
|
||||
// here: if the outbox is already at/over _outbox_limit, drop this QoS0 message
|
||||
// and report -2 ("outbox full") so the caller can retry/drop. QoS1/2 (durable,
|
||||
// low-rate) are never gated.
|
||||
if (qos == 0 && _outbox_limit > 0 && _client != nullptr &&
|
||||
esp_mqtt_client_get_outbox_size(_client) >= _outbox_limit)
|
||||
// and report -2 ("outbox full") so the caller can retry/drop. Apply the
|
||||
// same bound to durable status messages on a stalled connection.
|
||||
const size_t outbox_size = refreshOutboxSize();
|
||||
if (_outbox_limit > 0 && _client != nullptr && outbox_size >= _outbox_limit)
|
||||
{
|
||||
_outbox_drops++;
|
||||
static unsigned long last_full_log = 0;
|
||||
unsigned long now = millis();
|
||||
if (now - last_full_log > 5000)
|
||||
{
|
||||
ESP_LOGW(TAG, "Outbox at cap (%u bytes); dropping QoS0 message to topic %s",
|
||||
(unsigned)_outbox_limit, topic);
|
||||
ESP_LOGW(TAG, "Outbox at cap (%u bytes); dropping QoS%d message to topic %s",
|
||||
(unsigned)_outbox_limit, qos, topic);
|
||||
last_full_log = now;
|
||||
}
|
||||
return -2;
|
||||
@@ -625,6 +658,7 @@ int PsychicMqttClient::publish(const char *topic, int qos, bool retain, const ch
|
||||
bool store_in_outbox = true;
|
||||
int result = esp_mqtt_client_enqueue(_client, topic, payload, length, qos, retain, store_in_outbox);
|
||||
if (result < 0) _publish_err++; else _publish_ok++;
|
||||
refreshOutboxSize();
|
||||
return result;
|
||||
}
|
||||
else
|
||||
@@ -635,6 +669,7 @@ int PsychicMqttClient::publish(const char *topic, int qos, bool retain, const ch
|
||||
// callers can surface delivery health without per-message logging.
|
||||
int result = esp_mqtt_client_publish(_client, topic, payload, length, qos, retain);
|
||||
if (result < 0) _publish_err++; else _publish_ok++;
|
||||
if (qos > 0) refreshOutboxSize(); else serviceOutboxDiagnostics();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -661,10 +696,24 @@ PsychicMqttClient &PsychicMqttClient::setOutboxLimit(size_t bytes)
|
||||
|
||||
size_t PsychicMqttClient::getOutboxSize()
|
||||
{
|
||||
if (_client == nullptr)
|
||||
return 0;
|
||||
int size = esp_mqtt_client_get_outbox_size(_client);
|
||||
return size > 0 ? (size_t)size : 0;
|
||||
return _outbox_size.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
size_t PsychicMqttClient::refreshOutboxSize()
|
||||
{
|
||||
const int size = _client ? esp_mqtt_client_get_outbox_size(_client) : 0;
|
||||
const size_t bytes = size > 0 ? static_cast<size_t>(size) : 0;
|
||||
_outbox_size.store(bytes, std::memory_order_relaxed);
|
||||
_outbox_sampled_at.store(static_cast<uint32_t>(millis()), std::memory_order_relaxed);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void PsychicMqttClient::serviceOutboxDiagnostics()
|
||||
{
|
||||
if (!_started.load(std::memory_order_relaxed) || !getOutboxSize()) return;
|
||||
const uint32_t now = static_cast<uint32_t>(millis());
|
||||
if (now - _outbox_sampled_at.load(std::memory_order_relaxed) >= 1000U)
|
||||
refreshOutboxSize();
|
||||
}
|
||||
|
||||
size_t PsychicMqttClient::getOutboxLimit()
|
||||
@@ -862,6 +911,9 @@ void PsychicMqttClient::_onMessage(esp_mqtt_event_handle_t &event)
|
||||
|
||||
void PsychicMqttClient::_onPublish(esp_mqtt_event_handle_t &event)
|
||||
{
|
||||
// The SDK removes the acknowledged item before dispatch and its API mutex
|
||||
// is recursive, so this callback can sample without waiting on another task.
|
||||
refreshOutboxSize();
|
||||
_publish_ack.fetch_add(1, std::memory_order_relaxed);
|
||||
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
|
||||
for (uint8_t i = 0; i < _onPublishUserCallbackCount; ++i)
|
||||
|
||||
@@ -436,15 +436,15 @@ public:
|
||||
esp_mqtt_client_config_t *getMqttConfig();
|
||||
|
||||
/**
|
||||
* @brief Caps the size of the esp-mqtt outbox for async QoS 0 publishes.
|
||||
* @brief Caps the size of the esp-mqtt outbox for asynchronous publishes.
|
||||
*
|
||||
* QoS 0 async publishes are forced into the esp-mqtt outbox (store=true) so
|
||||
* packet topics keep flowing, but the outbox has no size bound of its own --
|
||||
* it only frees entries on send-ack or time-based expiry. On a stalled uplink
|
||||
* the entries accumulate on internal heap without limit. When the current
|
||||
* outbox size is at/over this cap, publish() drops the new QoS 0 message
|
||||
* outbox size is at/over this cap, publish() drops the new message
|
||||
* (returns -2) instead of enqueuing it, applying backpressure. 0 disables the
|
||||
* cap. QoS 1/2 publishes are never gated by this.
|
||||
* cap. The publish owner samples the SDK immediately before admission.
|
||||
*
|
||||
* @param bytes Maximum outbox size in bytes, or 0 to disable.
|
||||
* @return A reference to the PsychicMqttClient instance.
|
||||
@@ -452,13 +452,18 @@ public:
|
||||
PsychicMqttClient &setOutboxLimit(size_t bytes);
|
||||
|
||||
/**
|
||||
* @brief Returns the current esp-mqtt outbox size in bytes (0 if the client
|
||||
* is not initialized). Useful for diagnostics/backpressure monitoring.
|
||||
* @brief Returns the last owner-sampled outbox size without taking the SDK
|
||||
* mutex. Publish/ACK events update it; nonempty idle outboxes refresh within
|
||||
* one second when the owner calls serviceOutboxDiagnostics().
|
||||
*
|
||||
* @return Current outbox size in bytes.
|
||||
*/
|
||||
size_t getOutboxSize();
|
||||
|
||||
// Call only from the client's network owner, never a radio/CLI reader.
|
||||
// Expiry can remove queued items without a broker acknowledgment event.
|
||||
void serviceOutboxDiagnostics();
|
||||
|
||||
/**
|
||||
* @brief Returns the configured outbox cap in bytes (0 = disabled). Lets
|
||||
* callers confirm the cap is actually applied to this client.
|
||||
@@ -466,7 +471,7 @@ public:
|
||||
size_t getOutboxLimit();
|
||||
|
||||
/**
|
||||
* @brief Returns the cumulative count of QoS0 messages dropped because the
|
||||
* @brief Returns the cumulative count of async messages dropped because the
|
||||
* outbox was at/over the cap. Monotonic; useful for backpressure diagnostics.
|
||||
*/
|
||||
unsigned long getOutboxDrops();
|
||||
@@ -500,10 +505,12 @@ private:
|
||||
bool _config_dirty = true;
|
||||
std::atomic<bool> _started{false};
|
||||
|
||||
// Runtime cap on the esp-mqtt outbox for QoS 0 async publishes (bytes).
|
||||
// Runtime cap on the esp-mqtt outbox for async publishes (bytes).
|
||||
// 0 = disabled. Enforced in publish(); not an esp-mqtt config field.
|
||||
size_t _outbox_limit = 0;
|
||||
// Cumulative count of QoS0 publishes dropped because the outbox hit the cap.
|
||||
std::atomic<size_t> _outbox_size{0};
|
||||
std::atomic<uint32_t> _outbox_sampled_at{0};
|
||||
// Cumulative count of async publishes dropped because the outbox hit the cap.
|
||||
unsigned long _outbox_drops = 0;
|
||||
// Cumulative publish accept/fail counts (any QoS, sync or async path).
|
||||
unsigned long _publish_ok = 0;
|
||||
@@ -513,6 +520,8 @@ private:
|
||||
|
||||
// Multipart message reassembly. _buffer is lazily allocated at connect() time
|
||||
// to match the configured buffer size, then reused for the client's lifetime.
|
||||
// PSRAM builds prefer external byte storage; internal fallback preserves
|
||||
// the network/AES DMA reserve. The client object and task stacks stay internal.
|
||||
// _topic is inline storage, never heap-allocated.
|
||||
char *_buffer = nullptr;
|
||||
size_t _buffer_capacity = 0;
|
||||
@@ -523,6 +532,7 @@ private:
|
||||
// Configuration is transactional: never start/reconnect on stale credentials
|
||||
// after a failed esp_mqtt_set_config().
|
||||
esp_err_t applyConfig();
|
||||
size_t refreshOutboxSize();
|
||||
bool _isTopicMatch(const char *topic, const char *subscription);
|
||||
|
||||
// Fixed-size callback storage. Each slot is "used" once registered; we never
|
||||
|
||||
Reference in new issue
Block a user