From 5f9dccace005f01ec2efbcfac03f0039531e9d15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2026 15:36:34 -1000 Subject: [PATCH] [scheduler] Remove deprecated set_retry/cancel_retry set_retry and cancel_retry were deprecated in 2026.2.0 with removal scheduled for 2026.8.0. No internal callers remain. This removes: - RetryResult enum - set_retry/cancel_retry from Scheduler and Component - RetryArgs struct and retry_handler - is_retry bit field from SchedulerItem - match_retry parameter from cancel helpers - Integration tests for retry functionality --- esphome/core/base_automation.h | 4 +- esphome/core/component.cpp | 52 ---- esphome/core/component.h | 38 --- esphome/core/scheduler.cpp | 174 +---------- esphome/core/scheduler.h | 82 +---- .../fixtures/scheduler_numeric_id_test.yaml | 33 +- .../fixtures/scheduler_retry_test.yaml | 287 ------------------ .../test_scheduler_numeric_id_test.py | 45 +-- .../integration/test_scheduler_retry_test.py | 279 ----------------- 9 files changed, 33 insertions(+), 961 deletions(-) delete mode 100644 tests/integration/fixtures/scheduler_retry_test.yaml delete mode 100644 tests/integration/test_scheduler_retry_test.py diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 985f26e711..1f1e0d017e 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -194,7 +194,7 @@ template class DelayAction : public Action, public Compon this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(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 @@ -202,7 +202,7 @@ template class DelayAction : public Action, public Compon App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL, nullptr, static_cast(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; } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 00dda0cc26..51f7e728a1 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -117,36 +117,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 &&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 &&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 &&f) { // NOLINT #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" @@ -194,21 +164,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 &&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(); @@ -370,13 +325,6 @@ void Component::set_timeout(uint32_t timeout, std::function &&f) { // N void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT App.scheduler.set_interval(this, static_cast(nullptr), interval, std::move(f)); } -void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&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 { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || diff --git a/esphome/core/component.h b/esphome/core/component.h index 119681f64c..f920beecf3 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -80,9 +80,6 @@ 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 uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; class Component { @@ -382,41 +379,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 &&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 &&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 &&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 &&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. diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 51cbfb208e..3177a2d429 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -112,55 +112,23 @@ uint32_t Scheduler::calculate_interval_offset_(uint32_t delay) { return static_cast((static_cast(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 &&func, bool is_retry, bool skip_cancel) { + std::function &&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; @@ -169,7 +137,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->callback = 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. @@ -210,8 +177,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_) { @@ -273,125 +239,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 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 &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 - // 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 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(); - 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 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 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 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 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 @@ -723,11 +570,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. @@ -737,8 +584,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; @@ -750,7 +596,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; } @@ -763,7 +609,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) @@ -772,7 +618,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; } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 1e44f41da8..58eabcc10e 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -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 &args); class Scheduler { - // Allow retry_handler to access protected members for internal retry mechanism - friend void ::esphome::retry_handler(const std::shared_ptr &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 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 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 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 inlines correctly on all platforms. std::atomic 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 &&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 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 &&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 &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; diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml index 25decf20f5..ae95e095f6 100644 --- a/tests/integration/fixtures/scheduler_numeric_id_test.yaml +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -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 diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml deleted file mode 100644 index cdf71152bd..0000000000 --- a/tests/integration/fixtures/scheduler_retry_test.yaml +++ /dev/null @@ -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"); diff --git a/tests/integration/test_scheduler_numeric_id_test.py b/tests/integration/test_scheduler_numeric_id_test.py index c1958db685..3591e3014e 100644 --- a/tests/integration/test_scheduler_numeric_id_test.py +++ b/tests/integration/test_scheduler_numeric_id_test.py @@ -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}" diff --git a/tests/integration/test_scheduler_retry_test.py b/tests/integration/test_scheduler_retry_test.py deleted file mode 100644 index 910034e5bb..0000000000 --- a/tests/integration/test_scheduler_retry_test.py +++ /dev/null @@ -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")