mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 13:34:54 +01:00
Lock scheduler items while modifying them (#4410)
* Cosmetic fixes to scheduler code * Add generic Mutex API * Lock scheduler items while modifying them * Always defer MQTT callbacks on Arduino
This commit is contained in:
parent
1a9141877d
commit
86c0e6114f
5 changed files with 100 additions and 12 deletions
|
@ -485,16 +485,16 @@ static bool topic_match(const char *message, const char *subscription) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
|
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ARDUINO
|
||||||
// on ESP8266, this is called in LWiP thread; some components do not like running
|
// on Arduino, this is called in lwIP/AsyncTCP task; some components do not like running
|
||||||
// in an ISR.
|
// from a different task.
|
||||||
this->defer([this, topic, payload]() {
|
this->defer([this, topic, payload]() {
|
||||||
#endif
|
#endif
|
||||||
for (auto &subscription : this->subscriptions_) {
|
for (auto &subscription : this->subscriptions_) {
|
||||||
if (topic_match(topic.c_str(), subscription.topic.c_str()))
|
if (topic_match(topic.c_str(), subscription.topic.c_str()))
|
||||||
subscription.callback(topic, payload);
|
subscription.callback(topic, payload);
|
||||||
}
|
}
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ARDUINO
|
||||||
});
|
});
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
@ -393,6 +393,18 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green
|
||||||
}
|
}
|
||||||
|
|
||||||
// System APIs
|
// System APIs
|
||||||
|
#if defined(USE_ESP8266)
|
||||||
|
// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
|
||||||
|
Mutex::Mutex() {}
|
||||||
|
void Mutex::lock() {}
|
||||||
|
bool Mutex::try_lock() { return true; }
|
||||||
|
void Mutex::unlock() {}
|
||||||
|
#elif defined(USE_ESP32) || defined(USE_RP2040)
|
||||||
|
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
|
||||||
|
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
|
||||||
|
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
|
||||||
|
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP8266)
|
#if defined(USE_ESP8266)
|
||||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
||||||
|
|
|
@ -14,6 +14,14 @@
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_ESP32)
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#elif defined(USE_RP2040)
|
||||||
|
#include <FreeRTOS.h>
|
||||||
|
#include <semphr.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define HOT __attribute__((hot))
|
#define HOT __attribute__((hot))
|
||||||
#define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg)))
|
#define ESPDEPRECATED(msg, when) __attribute__((deprecated(msg)))
|
||||||
#define ALWAYS_INLINE __attribute__((always_inline))
|
#define ALWAYS_INLINE __attribute__((always_inline))
|
||||||
|
@ -516,6 +524,39 @@ template<typename T> class Parented {
|
||||||
/// @name System APIs
|
/// @name System APIs
|
||||||
///@{
|
///@{
|
||||||
|
|
||||||
|
/** Mutex implementation, with API based on the unavailable std::mutex.
|
||||||
|
*
|
||||||
|
* @note This mutex is non-recursive, so take care not to try to obtain the mutex while it is already taken.
|
||||||
|
*/
|
||||||
|
class Mutex {
|
||||||
|
public:
|
||||||
|
Mutex();
|
||||||
|
Mutex(const Mutex &) = delete;
|
||||||
|
void lock();
|
||||||
|
bool try_lock();
|
||||||
|
void unlock();
|
||||||
|
|
||||||
|
Mutex &operator=(const Mutex &) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
#if defined(USE_ESP32) || defined(USE_RP2040)
|
||||||
|
SemaphoreHandle_t handle_;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Helper class that wraps a mutex with a RAII-style API.
|
||||||
|
*
|
||||||
|
* This behaves like std::lock_guard: as long as the object is alive, the mutex is held.
|
||||||
|
*/
|
||||||
|
class LockGuard {
|
||||||
|
public:
|
||||||
|
LockGuard(Mutex &mutex) : mutex_{mutex} { mutex_.lock(); }
|
||||||
|
~LockGuard() { mutex_.unlock(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Mutex &mutex_;
|
||||||
|
};
|
||||||
|
|
||||||
/** Helper class to disable interrupts.
|
/** Helper class to disable interrupts.
|
||||||
*
|
*
|
||||||
* This behaves like std::lock_guard: as long as the object is alive, all interrupts are disabled.
|
* This behaves like std::lock_guard: as long as the object is alive, all interrupts are disabled.
|
||||||
|
|
|
@ -13,6 +13,11 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
|
||||||
// Uncomment to debug scheduler
|
// Uncomment to debug scheduler
|
||||||
// #define ESPHOME_DEBUG_SCHEDULER
|
// #define ESPHOME_DEBUG_SCHEDULER
|
||||||
|
|
||||||
|
// A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to
|
||||||
|
// them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task,
|
||||||
|
// iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to
|
||||||
|
// avoid the main thread modifying the list while it is being accessed.
|
||||||
|
|
||||||
void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
|
void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
|
||||||
std::function<void()> func) {
|
std::function<void()> func) {
|
||||||
const uint32_t now = this->millis_();
|
const uint32_t now = this->millis_();
|
||||||
|
@ -121,7 +126,7 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin
|
||||||
args->backoff_increase_factor = backoff_increase_factor;
|
args->backoff_increase_factor = backoff_increase_factor;
|
||||||
args->scheduler = this;
|
args->scheduler = this;
|
||||||
|
|
||||||
// First exectuion of `func` immediately
|
// First execution of `func` immediately
|
||||||
this->set_timeout(component, args->name, 0, [args]() { retry_handler(args); });
|
this->set_timeout(component, args->name, 0, [args]() { retry_handler(args); });
|
||||||
}
|
}
|
||||||
bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) {
|
bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) {
|
||||||
|
@ -150,30 +155,42 @@ void HOT Scheduler::call() {
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> old_items;
|
std::vector<std::unique_ptr<SchedulerItem>> old_items;
|
||||||
ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now);
|
ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now);
|
||||||
while (!this->empty_()) {
|
while (!this->empty_()) {
|
||||||
|
this->lock_.lock();
|
||||||
auto item = std::move(this->items_[0]);
|
auto item = std::move(this->items_[0]);
|
||||||
|
this->pop_raw_();
|
||||||
|
this->lock_.unlock();
|
||||||
|
|
||||||
ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(),
|
ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(),
|
||||||
item->name.c_str(), item->interval, item->last_execution, item->last_execution_major,
|
item->name.c_str(), item->interval, item->last_execution, item->last_execution_major,
|
||||||
item->next_execution(), item->next_execution_major());
|
item->next_execution(), item->next_execution_major());
|
||||||
|
|
||||||
this->pop_raw_();
|
|
||||||
old_items.push_back(std::move(item));
|
old_items.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
ESP_LOGVV(TAG, "\n");
|
ESP_LOGVV(TAG, "\n");
|
||||||
|
|
||||||
|
{
|
||||||
|
LockGuard guard{this->lock_};
|
||||||
this->items_ = std::move(old_items);
|
this->items_ = std::move(old_items);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endif // ESPHOME_DEBUG_SCHEDULER
|
#endif // ESPHOME_DEBUG_SCHEDULER
|
||||||
|
|
||||||
auto to_remove_was = to_remove_;
|
auto to_remove_was = to_remove_;
|
||||||
auto items_was = items_.size();
|
auto items_was = this->items_.size();
|
||||||
// If we have too many items to remove
|
// If we have too many items to remove
|
||||||
if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
|
if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> valid_items;
|
std::vector<std::unique_ptr<SchedulerItem>> valid_items;
|
||||||
while (!this->empty_()) {
|
while (!this->empty_()) {
|
||||||
|
LockGuard guard{this->lock_};
|
||||||
auto item = std::move(this->items_[0]);
|
auto item = std::move(this->items_[0]);
|
||||||
this->pop_raw_();
|
this->pop_raw_();
|
||||||
valid_items.push_back(std::move(item));
|
valid_items.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
LockGuard guard{this->lock_};
|
||||||
this->items_ = std::move(valid_items);
|
this->items_ = std::move(valid_items);
|
||||||
|
}
|
||||||
|
|
||||||
// The following should not happen unless I'm missing something
|
// The following should not happen unless I'm missing something
|
||||||
if (to_remove_ != 0) {
|
if (to_remove_ != 0) {
|
||||||
|
@ -198,6 +215,7 @@ void HOT Scheduler::call() {
|
||||||
|
|
||||||
// Don't run on failed components
|
// Don't run on failed components
|
||||||
if (item->component != nullptr && item->component->is_failed()) {
|
if (item->component != nullptr && item->component->is_failed()) {
|
||||||
|
LockGuard guard{this->lock_};
|
||||||
this->pop_raw_();
|
this->pop_raw_();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -217,6 +235,8 @@ void HOT Scheduler::call() {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
this->lock_.lock();
|
||||||
|
|
||||||
// new scope, item from before might have been moved in the vector
|
// new scope, item from before might have been moved in the vector
|
||||||
auto item = std::move(this->items_[0]);
|
auto item = std::move(this->items_[0]);
|
||||||
|
|
||||||
|
@ -224,6 +244,8 @@ void HOT Scheduler::call() {
|
||||||
// during the function call and know if we were cancelled.
|
// during the function call and know if we were cancelled.
|
||||||
this->pop_raw_();
|
this->pop_raw_();
|
||||||
|
|
||||||
|
this->lock_.unlock();
|
||||||
|
|
||||||
if (item->remove) {
|
if (item->remove) {
|
||||||
// We were removed/cancelled in the function call, stop
|
// We were removed/cancelled in the function call, stop
|
||||||
to_remove_--;
|
to_remove_--;
|
||||||
|
@ -246,6 +268,7 @@ void HOT Scheduler::call() {
|
||||||
this->process_to_add();
|
this->process_to_add();
|
||||||
}
|
}
|
||||||
void HOT Scheduler::process_to_add() {
|
void HOT Scheduler::process_to_add() {
|
||||||
|
LockGuard guard{this->lock_};
|
||||||
for (auto &it : this->to_add_) {
|
for (auto &it : this->to_add_) {
|
||||||
if (it->remove) {
|
if (it->remove) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -263,15 +286,24 @@ void HOT Scheduler::cleanup_() {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
to_remove_--;
|
to_remove_--;
|
||||||
|
|
||||||
|
{
|
||||||
|
LockGuard guard{this->lock_};
|
||||||
this->pop_raw_();
|
this->pop_raw_();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
void HOT Scheduler::pop_raw_() {
|
void HOT Scheduler::pop_raw_() {
|
||||||
std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||||
this->items_.pop_back();
|
this->items_.pop_back();
|
||||||
}
|
}
|
||||||
void HOT Scheduler::push_(std::unique_ptr<Scheduler::SchedulerItem> item) { this->to_add_.push_back(std::move(item)); }
|
void HOT Scheduler::push_(std::unique_ptr<Scheduler::SchedulerItem> item) {
|
||||||
|
LockGuard guard{this->lock_};
|
||||||
|
this->to_add_.push_back(std::move(item));
|
||||||
|
}
|
||||||
bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
|
bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
|
||||||
|
// obtain lock because this function iterates and can be called from non-loop task context
|
||||||
|
LockGuard guard{this->lock_};
|
||||||
bool ret = false;
|
bool ret = false;
|
||||||
for (auto &it : this->items_) {
|
for (auto &it : this->items_) {
|
||||||
if (it->component == component && it->name == name && it->type == type && !it->remove) {
|
if (it->component == component && it->name == name && it->type == type && !it->remove) {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
class Component;
|
class Component;
|
||||||
|
@ -71,6 +73,7 @@ class Scheduler {
|
||||||
return this->items_.empty();
|
return this->items_.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mutex lock_;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> items_;
|
std::vector<std::unique_ptr<SchedulerItem>> items_;
|
||||||
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
|
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
|
||||||
uint32_t last_millis_{0};
|
uint32_t last_millis_{0};
|
||||||
|
|
Loading…
Reference in a new issue