mirror of
https://github.com/esphome/esphome.git
synced 2026-06-25 08:20:23 +00:00
Compare commits
4 Commits
multi-inte
...
remove-set
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8938728b8 | ||
|
|
f2fa97bfda | ||
|
|
80028ea1ad | ||
|
|
5f9dccace0 |
@@ -201,7 +201,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr,
|
||||
static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION), this->delay_.value(),
|
||||
[this]() { this->play_next_(); },
|
||||
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
|
||||
/* skip_cancel= */ this->num_running_ > 1);
|
||||
} else {
|
||||
// For delays with arguments, capture by value to preserve argument values
|
||||
// Arguments must be copied because original references may be invalid after delay
|
||||
@@ -209,7 +209,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL,
|
||||
nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION),
|
||||
this->delay_.value(x...), std::move(f),
|
||||
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
|
||||
/* skip_cancel= */ this->num_running_ > 1);
|
||||
}
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
@@ -112,36 +112,6 @@ bool Component::cancel_interval(const char *name) { // NOLINT
|
||||
return App.scheduler.cancel_interval(this, name);
|
||||
}
|
||||
|
||||
void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
bool Component::cancel_retry(const std::string &name) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return App.scheduler.cancel_retry(this, name);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
bool Component::cancel_retry(const char *name) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return App.scheduler.cancel_retry(this, name);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
@@ -189,21 +159,6 @@ void Component::set_interval(InternalSchedulerID id, uint32_t interval, std::fun
|
||||
|
||||
bool Component::cancel_interval(InternalSchedulerID id) { return App.scheduler.cancel_interval(this, id); }
|
||||
|
||||
void Component::set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
App.scheduler.set_retry(this, id, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
bool Component::cancel_retry(uint32_t id) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return App.scheduler.cancel_retry(this, id);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
void Component::call_setup() { this->setup(); }
|
||||
void Component::call_dump_config_() {
|
||||
this->dump_config();
|
||||
@@ -362,13 +317,6 @@ void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // N
|
||||
void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_interval(this, static_cast<const char *>(nullptr), interval, std::move(f));
|
||||
}
|
||||
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
|
||||
float backoff_increase_factor) { // NOLINT
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
bool Component::is_ready() const {
|
||||
// Bitmask check: valid states are SETUP(1), LOOP(2), LOOP_DONE(4)
|
||||
// (1 << state) & 0b10110 checks membership in one instruction
|
||||
|
||||
@@ -89,8 +89,6 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08;
|
||||
inline constexpr uint8_t STATUS_LED_ERROR = 0x10;
|
||||
// Component loop override flag uses bit 5 (set at registration time)
|
||||
inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20;
|
||||
// Remove before 2026.8.0
|
||||
enum class RetryResult { DONE, RETRY };
|
||||
|
||||
inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms)
|
||||
|
||||
@@ -422,41 +420,6 @@ class Component {
|
||||
bool cancel_interval(uint32_t id); // NOLINT
|
||||
bool cancel_interval(InternalSchedulerID id); // NOLINT
|
||||
|
||||
/// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0.
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
|
||||
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, // NOLINT
|
||||
float backoff_increase_factor = 1.0f); // NOLINT
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(const std::string &name); // NOLINT
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(const char *name); // NOLINT
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(uint32_t id); // NOLINT
|
||||
|
||||
/** Set a timeout function with a unique name.
|
||||
*
|
||||
* Similar to javascript's setTimeout(). Empty name means no cancelling possible.
|
||||
|
||||
@@ -112,55 +112,23 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) {
|
||||
return static_cast<uint32_t>((static_cast<uint64_t>(random_uint32()) * max_offset) >> 32);
|
||||
}
|
||||
|
||||
// Check if a retry was already cancelled in items_ or to_add_
|
||||
// Extracted from set_timer_common_ to reduce code size - retry path is cold and deprecated
|
||||
// Remove before 2026.8.0 along with all retry code
|
||||
bool Scheduler::is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name,
|
||||
uint32_t hash_or_id) {
|
||||
for (auto *container : {&this->items_, &this->to_add_}) {
|
||||
for (auto *item : *container) {
|
||||
if (item != nullptr && this->is_item_removed_locked_(item) &&
|
||||
this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT,
|
||||
/* match_retry= */ true, /* skip_removed= */ false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Common implementation for both timeout and interval
|
||||
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
|
||||
void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type,
|
||||
const char *static_name, uint32_t hash_or_id, uint32_t delay,
|
||||
std::function<void()> &&func, bool is_retry, bool skip_cancel) {
|
||||
std::function<void()> &&func, bool skip_cancel) {
|
||||
if (delay == SCHEDULER_DONT_RUN) {
|
||||
// Still need to cancel existing timer if we have a name/id
|
||||
if (!skip_cancel) {
|
||||
LockGuard guard{this->lock_};
|
||||
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false,
|
||||
/* find_first= */ true);
|
||||
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* find_first= */ true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Take lock early to protect scheduler_item_pool_ access and retry-cancelled check
|
||||
// Take lock early to protect scheduler_item_pool_ access
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
// For retries, check if there's a cancelled timeout first - before allocating an item.
|
||||
// Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name
|
||||
// Skip check for defer (delay=0) - deferred retries bypass the cancellation check
|
||||
if (is_retry && delay != 0 && (name_type != NameType::STATIC_STRING || static_name != nullptr) &&
|
||||
type == SchedulerItem::TIMEOUT &&
|
||||
this->is_retry_cancelled_locked_(component, name_type, static_name, hash_or_id)) {
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
SchedulerNameLog skip_name_log;
|
||||
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item",
|
||||
skip_name_log.format(name_type, static_name, hash_or_id));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and populate the scheduler item
|
||||
SchedulerItem *item = this->get_item_from_pool_locked_();
|
||||
item->component = component;
|
||||
@@ -175,7 +143,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
new (&item->callback) std::function<void()>(std::move(func));
|
||||
// Reset remove flag - recycled items may have been cancelled (remove=true) in previous use
|
||||
this->set_item_removed_(item, false);
|
||||
item->is_retry = is_retry;
|
||||
|
||||
// Determine target container: defer_queue_ for deferred items, to_add_ for everything else.
|
||||
// Using a pointer lets both paths share the cancel + push_back epilogue.
|
||||
@@ -216,8 +183,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
|
||||
// Common epilogue: atomic cancel-and-add (unless skip_cancel is true)
|
||||
if (!skip_cancel) {
|
||||
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false,
|
||||
/* find_first= */ true);
|
||||
this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* find_first= */ true);
|
||||
}
|
||||
target->push_back(item);
|
||||
if (target == &this->to_add_) {
|
||||
@@ -279,125 +245,6 @@ bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) {
|
||||
return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL);
|
||||
}
|
||||
|
||||
// Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation.
|
||||
// Remove before 2026.8.0 along with all retry code.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
struct RetryArgs {
|
||||
// Ordered to minimize padding on 32-bit systems
|
||||
std::function<RetryResult(uint8_t)> func;
|
||||
Component *component;
|
||||
Scheduler *scheduler;
|
||||
// Union for name storage - only one is used based on name_type
|
||||
union {
|
||||
const char *static_name; // For STATIC_STRING
|
||||
uint32_t hash_or_id; // For HASHED_STRING or NUMERIC_ID
|
||||
} name_;
|
||||
uint32_t current_interval;
|
||||
float backoff_increase_factor;
|
||||
Scheduler::NameType name_type; // Discriminator for name_ union
|
||||
uint8_t retry_countdown;
|
||||
};
|
||||
|
||||
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->name_ is owned by the shared_ptr<RetryArgs>
|
||||
// which is captured in the lambda and outlives the SchedulerItem
|
||||
const char *static_name = (args->name_type == Scheduler::NameType::STATIC_STRING) ? args->name_.static_name : nullptr;
|
||||
uint32_t hash_or_id = (args->name_type != Scheduler::NameType::STATIC_STRING) ? args->name_.hash_or_id : 0;
|
||||
args->scheduler->set_timer_common_(
|
||||
args->component, Scheduler::SchedulerItem::TIMEOUT, args->name_type, static_name, hash_or_id,
|
||||
args->current_interval, [args]() { retry_handler(args); },
|
||||
/* is_retry= */ true);
|
||||
// backoff_increase_factor applied to third & later executions
|
||||
args->current_interval *= args->backoff_increase_factor;
|
||||
}
|
||||
|
||||
void HOT Scheduler::set_retry_common_(Component *component, NameType name_type, const char *static_name,
|
||||
uint32_t hash_or_id, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor) {
|
||||
this->cancel_retry_(component, name_type, static_name, hash_or_id);
|
||||
|
||||
if (initial_wait_time == SCHEDULER_DONT_RUN)
|
||||
return;
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
{
|
||||
SchedulerNameLog name_log;
|
||||
ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)",
|
||||
name_log.format(name_type, static_name, hash_or_id), initial_wait_time, max_attempts,
|
||||
backoff_increase_factor);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (backoff_increase_factor < 0.0001) {
|
||||
ESP_LOGE(TAG, "set_retry: backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor,
|
||||
(name_type == NameType::STATIC_STRING && static_name) ? static_name : "");
|
||||
backoff_increase_factor = 1;
|
||||
}
|
||||
|
||||
auto args = std::make_shared<RetryArgs>();
|
||||
args->func = std::move(func);
|
||||
args->component = component;
|
||||
args->scheduler = this;
|
||||
args->name_type = name_type;
|
||||
if (name_type == NameType::STATIC_STRING) {
|
||||
args->name_.static_name = static_name;
|
||||
} else {
|
||||
args->name_.hash_or_id = hash_or_id;
|
||||
}
|
||||
args->current_interval = initial_wait_time;
|
||||
args->backoff_increase_factor = backoff_increase_factor;
|
||||
args->retry_countdown = max_attempts;
|
||||
|
||||
// First execution of `func` immediately - use set_timer_common_ with is_retry=true
|
||||
this->set_timer_common_(
|
||||
component, SchedulerItem::TIMEOUT, name_type, static_name, hash_or_id, 0, [args]() { retry_handler(args); },
|
||||
/* is_retry= */ true);
|
||||
}
|
||||
|
||||
void HOT Scheduler::set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor) {
|
||||
this->set_retry_common_(component, NameType::STATIC_STRING, name, 0, initial_wait_time, max_attempts, std::move(func),
|
||||
backoff_increase_factor);
|
||||
}
|
||||
|
||||
bool HOT Scheduler::cancel_retry_(Component *component, NameType name_type, const char *static_name,
|
||||
uint32_t hash_or_id) {
|
||||
return this->cancel_item_(component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT,
|
||||
/* match_retry= */ true);
|
||||
}
|
||||
bool HOT Scheduler::cancel_retry(Component *component, const char *name) {
|
||||
return this->cancel_retry_(component, NameType::STATIC_STRING, name, 0);
|
||||
}
|
||||
|
||||
void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time,
|
||||
uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
|
||||
float backoff_increase_factor) {
|
||||
this->set_retry_common_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), initial_wait_time,
|
||||
max_attempts, std::move(func), backoff_increase_factor);
|
||||
}
|
||||
|
||||
bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) {
|
||||
return this->cancel_retry_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name));
|
||||
}
|
||||
|
||||
void HOT Scheduler::set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor) {
|
||||
this->set_retry_common_(component, NameType::NUMERIC_ID, nullptr, id, initial_wait_time, max_attempts,
|
||||
std::move(func), backoff_increase_factor);
|
||||
}
|
||||
|
||||
bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) {
|
||||
return this->cancel_retry_(component, NameType::NUMERIC_ID, nullptr, id);
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop // End suppression of deprecated RetryResult warnings
|
||||
|
||||
optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
||||
// IMPORTANT: This method should only be called from the main thread (loop task).
|
||||
// It performs cleanup and accesses items_[0] without holding a lock, which is only
|
||||
@@ -729,11 +576,11 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
|
||||
|
||||
// Common implementation for cancel operations - handles locking
|
||||
bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
|
||||
SchedulerItem::Type type, bool match_retry) {
|
||||
SchedulerItem::Type type) {
|
||||
LockGuard guard{this->lock_};
|
||||
// Public cancel path uses default find_first=false to cancel ALL matches because
|
||||
// DelayAction parallel mode (skip_cancel=true) can create multiple items with the same key.
|
||||
return this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, match_retry);
|
||||
return this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type);
|
||||
}
|
||||
|
||||
// Helper to cancel matching items - must be called with lock held.
|
||||
@@ -743,8 +590,7 @@ bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const
|
||||
// public cancel path where DelayAction parallel mode can create duplicates).
|
||||
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
|
||||
bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type, const char *static_name,
|
||||
uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry,
|
||||
bool find_first) {
|
||||
uint32_t hash_or_id, SchedulerItem::Type type, bool find_first) {
|
||||
// Early return if static string name is invalid
|
||||
if (name_type == NameType::STATIC_STRING && static_name == nullptr) {
|
||||
return false;
|
||||
@@ -756,7 +602,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
|
||||
// Mark items in defer queue as cancelled (they'll be skipped when processed)
|
||||
if (type == SchedulerItem::TIMEOUT) {
|
||||
total_cancelled += this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_type, static_name,
|
||||
hash_or_id, type, match_retry, find_first);
|
||||
hash_or_id, type, find_first);
|
||||
if (find_first && total_cancelled > 0)
|
||||
return true;
|
||||
}
|
||||
@@ -769,7 +615,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
|
||||
// Only the main loop in call() should recycle items after execution completes.
|
||||
if (!this->items_.empty()) {
|
||||
size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name,
|
||||
hash_or_id, type, match_retry, find_first);
|
||||
hash_or_id, type, find_first);
|
||||
total_cancelled += heap_cancelled;
|
||||
this->to_remove_add_(heap_cancelled);
|
||||
if (find_first && total_cancelled > 0)
|
||||
@@ -778,7 +624,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type
|
||||
|
||||
// Cancel items in to_add_
|
||||
total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_type, static_name,
|
||||
hash_or_id, type, match_retry, find_first);
|
||||
hash_or_id, type, find_first);
|
||||
|
||||
return total_cancelled > 0;
|
||||
}
|
||||
|
||||
@@ -16,14 +16,8 @@
|
||||
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 for internal retry mechanism
|
||||
friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
|
||||
// Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays.
|
||||
// This is needed to fix issue #10264 where parallel scripts with delays interfere with each other.
|
||||
// We use friend instead of a public API because skip_cancel is dangerous - it can cause delays
|
||||
@@ -91,32 +85,6 @@ class Scheduler {
|
||||
SchedulerItem::INTERVAL);
|
||||
}
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
|
||||
"2026.2.0")
|
||||
void set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(Component *component, const std::string &name);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(Component *component, const char *name);
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
bool cancel_retry(Component *component, uint32_t id);
|
||||
|
||||
/// Get 64-bit millisecond timestamp (handles 32-bit millis() rollover)
|
||||
uint64_t millis_64() { return esphome::millis_64(); }
|
||||
|
||||
@@ -181,19 +149,17 @@ class Scheduler {
|
||||
// std::atomic<uint8_t> inlines correctly on all platforms.
|
||||
std::atomic<uint8_t> remove{0};
|
||||
|
||||
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
|
||||
// Bit-packed fields (3 bits used, 5 bits padding in 1 byte)
|
||||
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
|
||||
NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum)
|
||||
bool is_retry : 1; // True if this is a retry timeout
|
||||
// 4 bits padding
|
||||
// 5 bits padding
|
||||
#else
|
||||
// Single-threaded or multi-threaded without atomics: can pack all fields together
|
||||
// Bit-packed fields (5 bits used, 3 bits padding in 1 byte)
|
||||
// Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
|
||||
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
|
||||
bool remove : 1;
|
||||
NameType name_type_ : 2; // Discriminator for name_ union (0–3, see NameType enum)
|
||||
bool is_retry : 1; // True if this is a retry timeout
|
||||
// 3 bits padding
|
||||
// 4 bits padding
|
||||
#endif
|
||||
|
||||
// Constructor
|
||||
@@ -205,13 +171,11 @@ class Scheduler {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
// remove is initialized in the member declaration
|
||||
type(TIMEOUT),
|
||||
name_type_(NameType::STATIC_STRING),
|
||||
is_retry(false) {
|
||||
name_type_(NameType::STATIC_STRING) {
|
||||
#else
|
||||
type(TIMEOUT),
|
||||
remove(false),
|
||||
name_type_(NameType::STATIC_STRING),
|
||||
is_retry(false) {
|
||||
name_type_(NameType::STATIC_STRING) {
|
||||
#endif
|
||||
name_.static_name = nullptr;
|
||||
}
|
||||
@@ -269,19 +233,7 @@ class Scheduler {
|
||||
// Common implementation for both timeout and interval
|
||||
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
|
||||
void set_timer_common_(Component *component, SchedulerItem::Type type, NameType name_type, const char *static_name,
|
||||
uint32_t hash_or_id, uint32_t delay, std::function<void()> &&func, bool is_retry = false,
|
||||
bool skip_cancel = false);
|
||||
|
||||
// Common implementation for retry - Remove before 2026.8.0
|
||||
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
void set_retry_common_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
|
||||
uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
|
||||
float backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
// Common implementation for cancel_retry
|
||||
bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
|
||||
uint32_t hash_or_id, uint32_t delay, std::function<void()> &&func, bool skip_cancel = false);
|
||||
|
||||
// Extend a 32-bit millis() value to 64-bit. Use when the caller already has a fresh now.
|
||||
// On platforms with native 64-bit time, ignores now and uses millis_64() directly.
|
||||
@@ -327,11 +279,11 @@ class Scheduler {
|
||||
// mode where skip_cancel=true allows multiple items with the same key).
|
||||
// name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id
|
||||
bool cancel_item_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
|
||||
SchedulerItem::Type type, bool match_retry = false, bool find_first = false);
|
||||
SchedulerItem::Type type, bool find_first = false);
|
||||
|
||||
// Common implementation for cancel operations - handles locking
|
||||
bool cancel_item_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
|
||||
SchedulerItem::Type type, bool match_retry = false);
|
||||
SchedulerItem::Type type);
|
||||
|
||||
// Helper to check if two static string names match
|
||||
inline bool HOT names_match_static_(const char *name1, const char *name2) const {
|
||||
@@ -347,14 +299,13 @@ class Scheduler {
|
||||
// IMPORTANT: Must be called with scheduler lock held
|
||||
inline bool HOT matches_item_locked_(SchedulerItem *item, Component *component, NameType name_type,
|
||||
const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type,
|
||||
bool match_retry, bool skip_removed = true) const {
|
||||
bool skip_removed = true) const {
|
||||
// THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded
|
||||
// platforms, items can be nulled in defer_queue_ during processing.
|
||||
// Fixes: https://github.com/esphome/esphome/issues/11940
|
||||
if (item == nullptr)
|
||||
return false;
|
||||
if (item->component != component || item->type != type || (skip_removed && this->is_item_removed_locked_(item)) ||
|
||||
(match_retry && !item->is_retry)) {
|
||||
if (item->component != component || item->type != type || (skip_removed && this->is_item_removed_locked_(item))) {
|
||||
return false;
|
||||
}
|
||||
// Name type must match
|
||||
@@ -390,13 +341,6 @@ class Scheduler {
|
||||
// IMPORTANT: Must not be inlined - called only for intervals, keeping it out of the hot path saves flash.
|
||||
uint32_t __attribute__((noinline)) calculate_interval_offset_(uint32_t delay);
|
||||
|
||||
// Helper to check if a retry was already cancelled - extracted to reduce code size of set_timer_common_
|
||||
// Remove before 2026.8.0 along with all retry code.
|
||||
// IMPORTANT: Must not be inlined - retry path is cold and deprecated.
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
bool __attribute__((noinline))
|
||||
is_retry_cancelled_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
// Helper for debug logging in set_timer_common_ - extracted to reduce code size
|
||||
void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id,
|
||||
@@ -498,11 +442,11 @@ class Scheduler {
|
||||
__attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector<SchedulerItem *> &container,
|
||||
Component *component, NameType name_type,
|
||||
const char *static_name, uint32_t hash_or_id,
|
||||
SchedulerItem::Type type, bool match_retry,
|
||||
SchedulerItem::Type type,
|
||||
bool find_first = false) {
|
||||
size_t count = 0;
|
||||
for (auto *item : container) {
|
||||
if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) {
|
||||
if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type)) {
|
||||
this->set_item_removed_(item, true);
|
||||
if (find_first)
|
||||
return 1;
|
||||
|
||||
@@ -18,9 +18,6 @@ globals:
|
||||
- id: interval_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: retry_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: defer_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
@@ -118,29 +115,7 @@ script:
|
||||
id(timeout_counter) += 1;
|
||||
});
|
||||
|
||||
// Test 10: set_retry with numeric ID
|
||||
App.scheduler.set_retry(component1, 6001U, 50, 3,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(retry_counter)++;
|
||||
ESP_LOGI("test", "Numeric retry 6001 attempt %d (countdown=%d)",
|
||||
id(retry_counter), retry_countdown);
|
||||
if (id(retry_counter) >= 2) {
|
||||
ESP_LOGI("test", "Numeric retry 6001 done");
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
|
||||
// Test 11: cancel_retry with numeric ID
|
||||
App.scheduler.set_retry(component1, 6002U, 100, 5,
|
||||
[](uint8_t retry_countdown) {
|
||||
ESP_LOGE("test", "ERROR: Numeric retry 6002 should have been cancelled");
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
App.scheduler.cancel_retry(component1, 6002U);
|
||||
ESP_LOGI("test", "Cancelled numeric retry 6002");
|
||||
|
||||
// Test 12: defer with numeric ID (Component method)
|
||||
// Test 10: defer with numeric ID (Component method)
|
||||
class TestDeferComponent : public Component {
|
||||
public:
|
||||
void test_defer_methods() {
|
||||
@@ -161,7 +136,7 @@ script:
|
||||
static TestDeferComponent test_defer_component;
|
||||
test_defer_component.test_defer_methods();
|
||||
|
||||
// Test 13: cancel_defer with numeric ID (Component method)
|
||||
// Test 11: cancel_defer with numeric ID (Component method)
|
||||
class TestCancelDeferComponent : public Component {
|
||||
public:
|
||||
void test_cancel_defer() {
|
||||
@@ -181,8 +156,8 @@ script:
|
||||
- id: report_results
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d",
|
||||
id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter));
|
||||
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Defers: %d",
|
||||
id(timeout_counter), id(interval_counter), id(defer_counter));
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
esphome:
|
||||
debug_scheduler: true # Enable scheduler leak detection
|
||||
name: scheduler-retry-test
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- logger.log: "Starting scheduler retry tests"
|
||||
# Run all tests sequentially with delays
|
||||
- script.execute: run_all_tests
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
globals:
|
||||
- id: simple_retry_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: backoff_retry_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: backoff_last_attempt_time
|
||||
type: uint32_t
|
||||
initial_value: '0'
|
||||
- id: immediate_done_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: cancel_retry_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: empty_name_retry_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: script_retry_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: multiple_same_name_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: const_char_retry_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
- id: static_char_retry_counter
|
||||
type: int
|
||||
initial_value: '0'
|
||||
|
||||
# Using different component types for each test to ensure isolation
|
||||
sensor:
|
||||
- platform: template
|
||||
name: Simple Retry Test Sensor
|
||||
id: simple_retry_sensor
|
||||
lambda: return 1.0;
|
||||
update_interval: never
|
||||
|
||||
- platform: template
|
||||
name: Backoff Retry Test Sensor
|
||||
id: backoff_retry_sensor
|
||||
lambda: return 2.0;
|
||||
update_interval: never
|
||||
|
||||
- platform: template
|
||||
name: Immediate Done Test Sensor
|
||||
id: immediate_done_sensor
|
||||
lambda: return 3.0;
|
||||
update_interval: never
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
name: Cancel Retry Test Binary Sensor
|
||||
id: cancel_retry_binary_sensor
|
||||
lambda: return false;
|
||||
|
||||
- platform: template
|
||||
name: Empty Name Test Binary Sensor
|
||||
id: empty_name_binary_sensor
|
||||
lambda: return true;
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
name: Script Retry Test Switch
|
||||
id: script_retry_switch
|
||||
optimistic: true
|
||||
|
||||
- platform: template
|
||||
name: Multiple Same Name Test Switch
|
||||
id: multiple_same_name_switch
|
||||
optimistic: true
|
||||
|
||||
script:
|
||||
- id: run_all_tests
|
||||
then:
|
||||
# Test 1: Simple retry
|
||||
- logger.log: "=== Test 1: Simple retry ==="
|
||||
- lambda: |-
|
||||
auto *component = id(simple_retry_sensor);
|
||||
App.scheduler.set_retry(component, "simple_retry", 50, 3,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(simple_retry_counter)++;
|
||||
ESP_LOGI("test", "Simple retry attempt %d (countdown=%d)",
|
||||
id(simple_retry_counter), retry_countdown);
|
||||
|
||||
if (id(simple_retry_counter) >= 2) {
|
||||
ESP_LOGI("test", "Simple retry succeeded on attempt %d", id(simple_retry_counter));
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
|
||||
# Test 2: Backoff retry
|
||||
- logger.log: "=== Test 2: Retry with backoff ==="
|
||||
- lambda: |-
|
||||
auto *component = id(backoff_retry_sensor);
|
||||
|
||||
App.scheduler.set_retry(component, "backoff_retry", 50, 4,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(backoff_retry_counter)++;
|
||||
uint32_t now = millis();
|
||||
uint32_t interval = 0;
|
||||
|
||||
// Only calculate interval after first attempt
|
||||
if (id(backoff_retry_counter) > 1) {
|
||||
interval = now - id(backoff_last_attempt_time);
|
||||
}
|
||||
id(backoff_last_attempt_time) = now;
|
||||
|
||||
ESP_LOGI("test", "Backoff retry attempt %d (countdown=%d, interval=%dms)",
|
||||
id(backoff_retry_counter), retry_countdown, interval);
|
||||
|
||||
if (id(backoff_retry_counter) == 1) {
|
||||
ESP_LOGI("test", "First call was immediate");
|
||||
} else if (id(backoff_retry_counter) == 2) {
|
||||
ESP_LOGI("test", "Second call interval: %dms (expected ~50ms)", interval);
|
||||
} else if (id(backoff_retry_counter) == 3) {
|
||||
ESP_LOGI("test", "Third call interval: %dms (expected ~100ms)", interval);
|
||||
} else if (id(backoff_retry_counter) == 4) {
|
||||
ESP_LOGI("test", "Fourth call interval: %dms (expected ~200ms)", interval);
|
||||
ESP_LOGI("test", "Backoff retry completed");
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
|
||||
return RetryResult::RETRY;
|
||||
}, 2.0f);
|
||||
|
||||
# Test 3: Immediate done
|
||||
- logger.log: "=== Test 3: Immediate done ==="
|
||||
- lambda: |-
|
||||
auto *component = id(immediate_done_sensor);
|
||||
App.scheduler.set_retry(component, "immediate_done", 50, 5,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(immediate_done_counter)++;
|
||||
ESP_LOGI("test", "Immediate done retry called (countdown=%d)", retry_countdown);
|
||||
return RetryResult::DONE;
|
||||
});
|
||||
|
||||
# Test 4: Cancel retry
|
||||
- logger.log: "=== Test 4: Cancel retry ==="
|
||||
- lambda: |-
|
||||
auto *component = id(cancel_retry_binary_sensor);
|
||||
App.scheduler.set_retry(component, "cancel_test", 30, 10,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(cancel_retry_counter)++;
|
||||
ESP_LOGI("test", "Cancel test retry attempt %d", id(cancel_retry_counter));
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
|
||||
// Cancel it after 100ms
|
||||
App.scheduler.set_timeout(component, "cancel_timer", 100, []() {
|
||||
bool cancelled = App.scheduler.cancel_retry(id(cancel_retry_binary_sensor), "cancel_test");
|
||||
ESP_LOGI("test", "Retry cancellation result: %s", cancelled ? "true" : "false");
|
||||
ESP_LOGI("test", "Cancel retry ran %d times before cancellation", id(cancel_retry_counter));
|
||||
});
|
||||
|
||||
# Test 5: Empty name retry
|
||||
- logger.log: "=== Test 5: Empty name retry ==="
|
||||
- lambda: |-
|
||||
auto *component = id(empty_name_binary_sensor);
|
||||
App.scheduler.set_retry(component, "", 100, 5,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(empty_name_retry_counter)++;
|
||||
ESP_LOGI("test", "Empty name retry attempt %d", id(empty_name_retry_counter));
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
|
||||
// Try to cancel after 150ms
|
||||
App.scheduler.set_timeout(component, "empty_cancel_timer", 150, []() {
|
||||
bool cancelled = App.scheduler.cancel_retry(id(empty_name_binary_sensor), "");
|
||||
ESP_LOGI("test", "Empty name retry cancel result: %s",
|
||||
cancelled ? "true" : "false");
|
||||
ESP_LOGI("test", "Empty name retry ran %d times", id(empty_name_retry_counter));
|
||||
});
|
||||
|
||||
# Test 6: Component method
|
||||
- logger.log: "=== Test 6: Component::set_retry method ==="
|
||||
- lambda: |-
|
||||
class TestRetryComponent : public Component {
|
||||
public:
|
||||
void test_retry() {
|
||||
this->set_retry(50, 3,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(script_retry_counter)++;
|
||||
ESP_LOGI("test", "Component retry attempt %d", id(script_retry_counter));
|
||||
if (id(script_retry_counter) >= 2) {
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
}, 1.5f);
|
||||
}
|
||||
};
|
||||
|
||||
static TestRetryComponent test_component;
|
||||
test_component.test_retry();
|
||||
|
||||
# Test 7: Multiple same name
|
||||
- logger.log: "=== Test 7: Multiple retries with same name ==="
|
||||
- lambda: |-
|
||||
auto *component = id(multiple_same_name_switch);
|
||||
|
||||
// Set first retry
|
||||
App.scheduler.set_retry(component, "duplicate_retry", 100, 5,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(multiple_same_name_counter) += 1;
|
||||
ESP_LOGI("test", "First duplicate retry - should not run");
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
|
||||
// Set second retry with same name (should cancel first)
|
||||
App.scheduler.set_retry(component, "duplicate_retry", 50, 3,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(multiple_same_name_counter) += 10;
|
||||
ESP_LOGI("test", "Second duplicate retry attempt (counter=%d)",
|
||||
id(multiple_same_name_counter));
|
||||
if (id(multiple_same_name_counter) >= 20) {
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
return RetryResult::RETRY;
|
||||
});
|
||||
|
||||
# Test 8: Const char* overloads
|
||||
- logger.log: "=== Test 8: Const char* overloads ==="
|
||||
- lambda: |-
|
||||
auto *component = id(simple_retry_sensor);
|
||||
|
||||
// Test 8a: Direct string literal
|
||||
App.scheduler.set_retry(component, "const_char_test", 30, 2,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(const_char_retry_counter)++;
|
||||
ESP_LOGI("test", "Const char retry %d", id(const_char_retry_counter));
|
||||
return RetryResult::DONE;
|
||||
});
|
||||
|
||||
# Test 9: Static const char* variable
|
||||
- logger.log: "=== Test 9: Static const char* ==="
|
||||
- lambda: |-
|
||||
auto *component = id(backoff_retry_sensor);
|
||||
|
||||
static const char* STATIC_NAME = "static_retry_test";
|
||||
App.scheduler.set_retry(component, STATIC_NAME, 20, 1,
|
||||
[](uint8_t retry_countdown) {
|
||||
id(static_char_retry_counter)++;
|
||||
ESP_LOGI("test", "Static const char retry %d", id(static_char_retry_counter));
|
||||
return RetryResult::DONE;
|
||||
});
|
||||
|
||||
// Cancel with same static const char*
|
||||
App.scheduler.set_timeout(component, "static_cancel", 10, []() {
|
||||
static const char* STATIC_NAME = "static_retry_test";
|
||||
bool result = App.scheduler.cancel_retry(id(backoff_retry_sensor), STATIC_NAME);
|
||||
ESP_LOGI("test", "Static cancel result: %s", result ? "true" : "false");
|
||||
});
|
||||
|
||||
# Wait for all tests to complete before reporting
|
||||
- delay: 500ms
|
||||
|
||||
# Final report
|
||||
- logger.log: "=== Retry Test Results ==="
|
||||
- lambda: |-
|
||||
ESP_LOGI("test", "Simple retry counter: %d (expected 2)", id(simple_retry_counter));
|
||||
ESP_LOGI("test", "Backoff retry counter: %d (expected 4)", id(backoff_retry_counter));
|
||||
ESP_LOGI("test", "Immediate done counter: %d (expected 1)", id(immediate_done_counter));
|
||||
ESP_LOGI("test", "Cancel retry counter: %d (expected 2-4)", id(cancel_retry_counter));
|
||||
ESP_LOGI("test", "Empty name retry counter: %d (expected 1-2)", id(empty_name_retry_counter));
|
||||
ESP_LOGI("test", "Component retry counter: %d (expected 2)", id(script_retry_counter));
|
||||
ESP_LOGI("test", "Multiple same name counter: %d (expected 20+)", id(multiple_same_name_counter));
|
||||
ESP_LOGI("test", "Const char retry counter: %d (expected 1)", id(const_char_retry_counter));
|
||||
ESP_LOGI("test", "Static char retry counter: %d (expected 1)", id(static_char_retry_counter));
|
||||
ESP_LOGI("test", "All retry tests completed");
|
||||
@@ -18,7 +18,6 @@ async def test_scheduler_numeric_id_test(
|
||||
# Track counts
|
||||
timeout_count = 0
|
||||
interval_count = 0
|
||||
retry_count = 0
|
||||
defer_count = 0
|
||||
|
||||
# Events for each test completion
|
||||
@@ -32,8 +31,6 @@ async def test_scheduler_numeric_id_test(
|
||||
component_interval_fired = asyncio.Event()
|
||||
zero_id_timeout_fired = asyncio.Event()
|
||||
max_id_timeout_fired = asyncio.Event()
|
||||
numeric_retry_done = asyncio.Event()
|
||||
numeric_retry_cancelled = asyncio.Event()
|
||||
numeric_defer_7001_fired = asyncio.Event()
|
||||
numeric_defer_7002_fired = asyncio.Event()
|
||||
numeric_defer_cancelled = asyncio.Event()
|
||||
@@ -41,11 +38,10 @@ async def test_scheduler_numeric_id_test(
|
||||
|
||||
# Track interval counts
|
||||
numeric_interval_count = 0
|
||||
numeric_retry_count = 0
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
nonlocal timeout_count, interval_count, retry_count, defer_count
|
||||
nonlocal numeric_interval_count, numeric_retry_count
|
||||
nonlocal timeout_count, interval_count, defer_count
|
||||
nonlocal numeric_interval_count
|
||||
|
||||
# Strip ANSI color codes
|
||||
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
||||
@@ -97,18 +93,6 @@ async def test_scheduler_numeric_id_test(
|
||||
max_id_timeout_fired.set()
|
||||
timeout_count += 1
|
||||
|
||||
# Check for numeric retry tests
|
||||
elif "Numeric retry 6001 attempt" in clean_line:
|
||||
match = re.search(r"attempt (\d+)", clean_line)
|
||||
if match:
|
||||
numeric_retry_count = int(match.group(1))
|
||||
|
||||
elif "Numeric retry 6001 done" in clean_line:
|
||||
numeric_retry_done.set()
|
||||
|
||||
elif "Cancelled numeric retry 6002" in clean_line:
|
||||
numeric_retry_cancelled.set()
|
||||
|
||||
# Check for numeric defer tests
|
||||
elif "Component numeric defer 7001 fired" in clean_line:
|
||||
numeric_defer_7001_fired.set()
|
||||
@@ -122,14 +106,13 @@ async def test_scheduler_numeric_id_test(
|
||||
# Check for final results
|
||||
elif "Final results" in clean_line:
|
||||
match = re.search(
|
||||
r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+), Defers: (\d+)",
|
||||
r"Timeouts: (\d+), Intervals: (\d+), Defers: (\d+)",
|
||||
clean_line,
|
||||
)
|
||||
if match:
|
||||
timeout_count = int(match.group(1))
|
||||
interval_count = int(match.group(2))
|
||||
retry_count = int(match.group(3))
|
||||
defer_count = int(match.group(4))
|
||||
defer_count = int(match.group(3))
|
||||
final_results_logged.set()
|
||||
|
||||
async with (
|
||||
@@ -200,23 +183,6 @@ async def test_scheduler_numeric_id_test(
|
||||
except TimeoutError:
|
||||
pytest.fail("Max ID timeout did not fire within 0.5 seconds")
|
||||
|
||||
# Wait for numeric retry tests
|
||||
try:
|
||||
await asyncio.wait_for(numeric_retry_done.wait(), timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Numeric retry 6001 did not complete. Count: {numeric_retry_count}"
|
||||
)
|
||||
|
||||
assert numeric_retry_count >= 2, (
|
||||
f"Expected at least 2 numeric retry attempts, got {numeric_retry_count}"
|
||||
)
|
||||
|
||||
# Verify numeric retry was cancelled
|
||||
assert numeric_retry_cancelled.is_set(), (
|
||||
"Numeric retry 6002 should have been cancelled"
|
||||
)
|
||||
|
||||
# Wait for numeric defer tests
|
||||
try:
|
||||
await asyncio.wait_for(numeric_defer_7001_fired.wait(), timeout=0.5)
|
||||
@@ -245,7 +211,4 @@ async def test_scheduler_numeric_id_test(
|
||||
assert interval_count >= 3, (
|
||||
f"Expected at least 3 interval fires, got {interval_count}"
|
||||
)
|
||||
assert retry_count >= 2, (
|
||||
f"Expected at least 2 retry attempts, got {retry_count}"
|
||||
)
|
||||
assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}"
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
"""Test scheduler retry functionality."""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scheduler_retry_test(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that scheduler retry functionality works correctly."""
|
||||
# Track test progress
|
||||
simple_retry_done = asyncio.Event()
|
||||
backoff_retry_done = asyncio.Event()
|
||||
immediate_done_done = asyncio.Event()
|
||||
cancel_retry_done = asyncio.Event()
|
||||
empty_name_retry_done = asyncio.Event()
|
||||
component_retry_done = asyncio.Event()
|
||||
multiple_name_done = asyncio.Event()
|
||||
const_char_done = asyncio.Event()
|
||||
static_char_done = asyncio.Event()
|
||||
test_complete = asyncio.Event()
|
||||
|
||||
# Track retry counts
|
||||
simple_retry_count = 0
|
||||
backoff_retry_count = 0
|
||||
immediate_done_count = 0
|
||||
cancel_retry_count = 0
|
||||
empty_name_retry_count = 0
|
||||
component_retry_count = 0
|
||||
multiple_name_count = 0
|
||||
const_char_retry_count = 0
|
||||
static_char_retry_count = 0
|
||||
|
||||
# Track specific test results
|
||||
cancel_result = None
|
||||
empty_cancel_result = None
|
||||
backoff_intervals = []
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
nonlocal simple_retry_count, backoff_retry_count, immediate_done_count
|
||||
nonlocal cancel_retry_count, empty_name_retry_count, component_retry_count
|
||||
nonlocal multiple_name_count, const_char_retry_count, static_char_retry_count
|
||||
nonlocal cancel_result, empty_cancel_result
|
||||
|
||||
# Strip ANSI color codes
|
||||
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
||||
|
||||
# Simple retry test
|
||||
if "Simple retry attempt" in clean_line:
|
||||
if match := re.search(r"Simple retry attempt (\d+)", clean_line):
|
||||
simple_retry_count = int(match.group(1))
|
||||
|
||||
elif "Simple retry succeeded on attempt" in clean_line:
|
||||
simple_retry_done.set()
|
||||
|
||||
# Backoff retry test
|
||||
elif "Backoff retry attempt" in clean_line:
|
||||
if match := re.search(
|
||||
r"Backoff retry attempt (\d+).*interval=(\d+)ms", clean_line
|
||||
):
|
||||
backoff_retry_count = int(match.group(1))
|
||||
interval = int(match.group(2))
|
||||
if backoff_retry_count > 1: # Skip first (immediate) call
|
||||
backoff_intervals.append(interval)
|
||||
|
||||
elif "Backoff retry completed" in clean_line:
|
||||
backoff_retry_done.set()
|
||||
|
||||
# Immediate done test
|
||||
elif "Immediate done retry called" in clean_line:
|
||||
immediate_done_count += 1
|
||||
immediate_done_done.set()
|
||||
|
||||
# Cancel retry test
|
||||
elif "Cancel test retry attempt" in clean_line:
|
||||
cancel_retry_count += 1
|
||||
|
||||
elif "Retry cancellation result:" in clean_line:
|
||||
cancel_result = "true" in clean_line
|
||||
cancel_retry_done.set()
|
||||
|
||||
# Empty name retry test
|
||||
elif "Empty name retry attempt" in clean_line:
|
||||
if match := re.search(r"Empty name retry attempt (\d+)", clean_line):
|
||||
empty_name_retry_count = int(match.group(1))
|
||||
|
||||
elif "Empty name retry cancel result:" in clean_line:
|
||||
empty_cancel_result = "true" in clean_line
|
||||
|
||||
elif "Empty name retry ran" in clean_line:
|
||||
empty_name_retry_done.set()
|
||||
|
||||
# Component retry test
|
||||
elif "Component retry attempt" in clean_line:
|
||||
if match := re.search(r"Component retry attempt (\d+)", clean_line):
|
||||
component_retry_count = int(match.group(1))
|
||||
if component_retry_count >= 2:
|
||||
component_retry_done.set()
|
||||
|
||||
# Multiple same name test
|
||||
elif "Second duplicate retry attempt" in clean_line:
|
||||
if match := re.search(r"counter=(\d+)", clean_line):
|
||||
multiple_name_count = int(match.group(1))
|
||||
if multiple_name_count >= 20:
|
||||
multiple_name_done.set()
|
||||
|
||||
# Const char retry test
|
||||
elif "Const char retry" in clean_line:
|
||||
if match := re.search(r"Const char retry (\d+)", clean_line):
|
||||
const_char_retry_count = int(match.group(1))
|
||||
const_char_done.set()
|
||||
|
||||
# Static const char retry test
|
||||
elif "Static const char retry" in clean_line:
|
||||
if match := re.search(r"Static const char retry (\d+)", clean_line):
|
||||
static_char_retry_count = int(match.group(1))
|
||||
static_char_done.set()
|
||||
|
||||
elif "Static cancel result:" in clean_line:
|
||||
# This is part of test 9, but we don't track it separately
|
||||
pass
|
||||
|
||||
# Test completion
|
||||
elif "All retry tests completed" in clean_line:
|
||||
test_complete.set()
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify we can connect
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "scheduler-retry-test"
|
||||
|
||||
# Wait for simple retry test
|
||||
try:
|
||||
await asyncio.wait_for(simple_retry_done.wait(), timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Simple retry test did not complete. Count: {simple_retry_count}"
|
||||
)
|
||||
|
||||
assert simple_retry_count == 2, (
|
||||
f"Expected 2 simple retry attempts, got {simple_retry_count}"
|
||||
)
|
||||
|
||||
# Wait for backoff retry test
|
||||
try:
|
||||
await asyncio.wait_for(backoff_retry_done.wait(), timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Backoff retry test did not complete. Count: {backoff_retry_count}"
|
||||
)
|
||||
|
||||
assert backoff_retry_count == 4, (
|
||||
f"Expected 4 backoff retry attempts, got {backoff_retry_count}"
|
||||
)
|
||||
|
||||
# Verify backoff intervals (allowing for timing variations)
|
||||
assert len(backoff_intervals) >= 2, (
|
||||
f"Expected at least 2 intervals, got {len(backoff_intervals)}"
|
||||
)
|
||||
if len(backoff_intervals) >= 3:
|
||||
# First interval should be ~50ms (very wide tolerance for heavy system load)
|
||||
assert 20 <= backoff_intervals[0] <= 150, (
|
||||
f"First interval {backoff_intervals[0]}ms not ~50ms"
|
||||
)
|
||||
# Second interval should be ~100ms (50ms * 2.0)
|
||||
assert 50 <= backoff_intervals[1] <= 250, (
|
||||
f"Second interval {backoff_intervals[1]}ms not ~100ms"
|
||||
)
|
||||
# Third interval should be ~200ms (100ms * 2.0)
|
||||
assert 100 <= backoff_intervals[2] <= 500, (
|
||||
f"Third interval {backoff_intervals[2]}ms not ~200ms"
|
||||
)
|
||||
|
||||
# Wait for immediate done test
|
||||
try:
|
||||
await asyncio.wait_for(immediate_done_done.wait(), timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Immediate done test did not complete. Count: {immediate_done_count}"
|
||||
)
|
||||
|
||||
assert immediate_done_count == 1, (
|
||||
f"Expected 1 immediate done call, got {immediate_done_count}"
|
||||
)
|
||||
|
||||
# Wait for cancel retry test
|
||||
try:
|
||||
await asyncio.wait_for(cancel_retry_done.wait(), timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Cancel retry test did not complete. Count: {cancel_retry_count}"
|
||||
)
|
||||
|
||||
assert cancel_result is True, "Retry cancellation should have succeeded"
|
||||
assert 2 <= cancel_retry_count <= 5, (
|
||||
f"Expected 2-5 cancel retry attempts before cancellation, got {cancel_retry_count}"
|
||||
)
|
||||
|
||||
# Wait for empty name retry test
|
||||
try:
|
||||
await asyncio.wait_for(empty_name_retry_done.wait(), timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Empty name retry test did not complete. Count: {empty_name_retry_count}"
|
||||
)
|
||||
|
||||
# Empty name retry should run at least once before being cancelled
|
||||
assert 1 <= empty_name_retry_count <= 3, (
|
||||
f"Expected 1-3 empty name retry attempts, got {empty_name_retry_count}"
|
||||
)
|
||||
assert empty_cancel_result is True, (
|
||||
"Empty name retry cancel should have succeeded"
|
||||
)
|
||||
|
||||
# Wait for component retry test
|
||||
try:
|
||||
await asyncio.wait_for(component_retry_done.wait(), timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Component retry test did not complete. Count: {component_retry_count}"
|
||||
)
|
||||
|
||||
assert component_retry_count >= 2, (
|
||||
f"Expected at least 2 component retry attempts, got {component_retry_count}"
|
||||
)
|
||||
|
||||
# Wait for multiple same name test
|
||||
try:
|
||||
await asyncio.wait_for(multiple_name_done.wait(), timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Multiple same name test did not complete. Count: {multiple_name_count}"
|
||||
)
|
||||
|
||||
# Should be 20+ (only second retry should run)
|
||||
assert multiple_name_count >= 20, (
|
||||
f"Expected multiple name count >= 20 (second retry only), got {multiple_name_count}"
|
||||
)
|
||||
|
||||
# Wait for const char retry test
|
||||
try:
|
||||
await asyncio.wait_for(const_char_done.wait(), timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Const char retry test did not complete. Count: {const_char_retry_count}"
|
||||
)
|
||||
|
||||
assert const_char_retry_count == 1, (
|
||||
f"Expected 1 const char retry call, got {const_char_retry_count}"
|
||||
)
|
||||
|
||||
# Wait for static char retry test
|
||||
try:
|
||||
await asyncio.wait_for(static_char_done.wait(), timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Static char retry test did not complete. Count: {static_char_retry_count}"
|
||||
)
|
||||
|
||||
assert static_char_retry_count == 1, (
|
||||
f"Expected 1 static char retry call, got {static_char_retry_count}"
|
||||
)
|
||||
|
||||
# Wait for test completion
|
||||
try:
|
||||
await asyncio.wait_for(test_complete.wait(), timeout=1.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Test did not complete within timeout")
|
||||
Reference in New Issue
Block a user