mirror of
https://github.com/esphome/esphome.git
synced 2025-10-29 22:24:26 +00:00
[scheduler] Fix retry race condition on cancellation (#9788)
This commit is contained in:
@@ -65,7 +65,7 @@ static void validate_static_string(const char *name) {
|
||||
|
||||
// Common implementation for both timeout and interval
|
||||
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string,
|
||||
const void *name_ptr, uint32_t delay, std::function<void()> func) {
|
||||
const void *name_ptr, uint32_t delay, std::function<void()> func, bool is_retry) {
|
||||
// Get the name as const char*
|
||||
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
|
||||
|
||||
@@ -130,6 +130,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
// For retries, check if there's a cancelled timeout first
|
||||
if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT &&
|
||||
(has_cancelled_timeout_in_container_(this->items_, component, name_cstr) ||
|
||||
has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr))) {
|
||||
// Skip scheduling - the retry was cancelled
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// If name is provided, do atomic cancel-and-add
|
||||
// Cancel existing items
|
||||
this->cancel_item_locked_(component, name_cstr, type);
|
||||
@@ -178,12 +190,14 @@ struct RetryArgs {
|
||||
Scheduler *scheduler;
|
||||
};
|
||||
|
||||
static void retry_handler(const std::shared_ptr<RetryArgs> &args) {
|
||||
void retry_handler(const std::shared_ptr<RetryArgs> &args) {
|
||||
RetryResult const retry_result = args->func(--args->retry_countdown);
|
||||
if (retry_result == RetryResult::DONE || args->retry_countdown <= 0)
|
||||
return;
|
||||
// second execution of `func` happens after `initial_wait_time`
|
||||
args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); });
|
||||
args->scheduler->set_timer_common_(
|
||||
args->component, Scheduler::SchedulerItem::TIMEOUT, false, &args->name, args->current_interval,
|
||||
[args]() { retry_handler(args); }, true);
|
||||
// backoff_increase_factor applied to third & later executions
|
||||
args->current_interval *= args->backoff_increase_factor;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,15 @@
|
||||
namespace esphome {
|
||||
|
||||
class Component;
|
||||
struct RetryArgs;
|
||||
|
||||
// Forward declaration of retry_handler - needs to be non-static for friend declaration
|
||||
void retry_handler(const std::shared_ptr<RetryArgs> &args);
|
||||
|
||||
class Scheduler {
|
||||
// Allow retry_handler to access protected members
|
||||
friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
|
||||
|
||||
public:
|
||||
// Public API - accepts std::string for backward compatibility
|
||||
void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> func);
|
||||
@@ -147,7 +154,7 @@ class Scheduler {
|
||||
|
||||
// Common implementation for both timeout and interval
|
||||
void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
|
||||
uint32_t delay, std::function<void()> func);
|
||||
uint32_t delay, std::function<void()> func, bool is_retry = false);
|
||||
|
||||
uint64_t millis_64_(uint32_t now);
|
||||
// Cleanup logically deleted items from the scheduler
|
||||
@@ -170,8 +177,8 @@ class Scheduler {
|
||||
|
||||
// Helper function to check if item matches criteria for cancellation
|
||||
inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
|
||||
SchedulerItem::Type type) {
|
||||
if (item->component != component || item->type != type || item->remove) {
|
||||
SchedulerItem::Type type, bool skip_removed = true) const {
|
||||
if (item->component != component || item->type != type || (skip_removed && item->remove)) {
|
||||
return false;
|
||||
}
|
||||
const char *item_name = item->get_name();
|
||||
@@ -197,6 +204,18 @@ class Scheduler {
|
||||
return item->remove || (item->component != nullptr && item->component->is_failed());
|
||||
}
|
||||
|
||||
// Template helper to check if any item in a container matches our criteria
|
||||
template<typename Container>
|
||||
bool has_cancelled_timeout_in_container_(const Container &container, Component *component,
|
||||
const char *name_cstr) const {
|
||||
for (const auto &item : container) {
|
||||
if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Mutex lock_;
|
||||
std::vector<std::unique_ptr<SchedulerItem>> items_;
|
||||
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
|
||||
|
||||
Reference in New Issue
Block a user