diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a1fe33edb2..a4eeb4dd5e 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -645,18 +645,18 @@ uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConn this->active_action_calls_.push_back({action_call_id, client_call_id, conn}); // Schedule automatic cleanup after timeout (client will have given up by then) - this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS, - [this, action_call_id]() { - ESP_LOGD(TAG, "Action call %u timed out", action_call_id); - this->unregister_active_action_call(action_call_id); - }); + // Uses numeric ID overload to avoid heap allocation from str_sprintf + this->set_timeout(action_call_id, USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() { + ESP_LOGD(TAG, "Action call %u timed out", action_call_id); + this->unregister_active_action_call(action_call_id); + }); return action_call_id; } void APIServer::unregister_active_action_call(uint32_t action_call_id) { - // Cancel the timeout for this action call - this->cancel_timeout(str_sprintf("action_call_%u", action_call_id)); + // Cancel the timeout for this action call (uses numeric ID overload) + this->cancel_timeout(action_call_id); // Swap-and-pop is more efficient than remove_if for unordered vectors for (size_t i = 0; i < this->active_action_calls_.size(); i++) { @@ -672,8 +672,8 @@ void APIServer::unregister_active_action_calls_for_connection(APIConnection *con // Remove all active action calls for disconnected connection using swap-and-pop for (size_t i = 0; i < this->active_action_calls_.size();) { if (this->active_action_calls_[i].connection == conn) { - // Cancel the timeout for this action call - this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id)); + // Cancel the timeout for this action call (uses numeric ID overload) + this->cancel_timeout(this->active_action_calls_[i].action_call_id); std::swap(this->active_action_calls_[i], this->active_action_calls_.back()); this->active_action_calls_.pop_back(); diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index e8878ac251..19d0ccf972 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -191,15 +191,15 @@ template class DelayAction : public Action, public Compon // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) if constexpr (sizeof...(Ts) == 0) { App.scheduler.set_timer_common_( - this, Scheduler::SchedulerItem::TIMEOUT, - /* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); }, + this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::STATIC_STRING, "delay", 0, this->delay_.value(), + [this]() { this->play_next_(); }, /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } else { // For delays with arguments, use std::bind to preserve argument values // Arguments must be copied because original references may be invalid after delay auto f = std::bind(&DelayAction::play_next_, this, x...); - App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, - /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::STATIC_STRING, + "delay", 0, this->delay_.value(x...), std::move(f), /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 90be6cf646..2f61f7d195 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -118,7 +118,10 @@ void Component::setup() {} void Component::loop() {} void Component::set_interval(const std::string &name, uint32_t interval, std::function &&f) { // NOLINT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" App.scheduler.set_interval(this, name, interval, std::move(f)); +#pragma GCC diagnostic pop } void Component::set_interval(const char *name, uint32_t interval, std::function &&f) { // NOLINT @@ -126,7 +129,10 @@ void Component::set_interval(const char *name, uint32_t interval, std::function< } bool Component::cancel_interval(const std::string &name) { // NOLINT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" return App.scheduler.cancel_interval(this, name); +#pragma GCC diagnostic pop } bool Component::cancel_interval(const char *name) { // NOLINT @@ -135,7 +141,10 @@ bool Component::cancel_interval(const char *name) { // NOLINT 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, @@ -144,7 +153,10 @@ void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t } 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 @@ -152,7 +164,10 @@ bool Component::cancel_retry(const char *name) { // NOLINT } 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" App.scheduler.set_timeout(this, name, timeout, std::move(f)); +#pragma GCC diagnostic pop } void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) { // NOLINT @@ -160,13 +175,36 @@ void Component::set_timeout(const char *name, uint32_t timeout, std::function &&f) { // NOLINT + App.scheduler.set_timeout(this, id, timeout, std::move(f)); +} + +bool Component::cancel_timeout(uint32_t id) { return App.scheduler.cancel_timeout(this, id); } + +void Component::set_interval(uint32_t id, uint32_t interval, std::function &&f) { // NOLINT + App.scheduler.set_interval(this, id, interval, std::move(f)); +} + +bool Component::cancel_interval(uint32_t 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 + App.scheduler.set_retry(this, id, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); +} + +bool Component::cancel_retry(uint32_t id) { return App.scheduler.cancel_retry(this, id); } + void Component::call_loop() { this->loop(); } void Component::call_setup() { this->setup(); } void Component::call_dump_config() { @@ -301,10 +339,19 @@ void Component::defer(std::function &&f) { // NOLINT App.scheduler.set_timeout(this, static_cast(nullptr), 0, std::move(f)); } bool Component::cancel_defer(const std::string &name) { // NOLINT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return App.scheduler.cancel_timeout(this, name); +#pragma GCC diagnostic pop +} +bool Component::cancel_defer(const char *name) { // NOLINT return App.scheduler.cancel_timeout(this, name); } void Component::defer(const std::string &name, std::function &&f) { // NOLINT +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" App.scheduler.set_timeout(this, name, 0, std::move(f)); +#pragma GCC diagnostic pop } void Component::defer(const char *name, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); diff --git a/esphome/core/component.h b/esphome/core/component.h index 32f594d6f8..49349d4199 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -306,6 +306,8 @@ class Component { * * @see cancel_interval() */ + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(const std::string &name, uint32_t interval, std::function &&f); // NOLINT /** Set an interval function with a const char* name. @@ -324,6 +326,14 @@ class Component { */ void set_interval(const char *name, uint32_t interval, std::function &&f); // NOLINT + /** Set an interval function with a numeric ID (zero heap allocation). + * + * @param id The numeric identifier for this interval function + * @param interval The interval in ms + * @param f The function to call + */ + void set_interval(uint32_t id, uint32_t interval, std::function &&f); // NOLINT + void set_interval(uint32_t interval, std::function &&f); // NOLINT /** Cancel an interval function. @@ -331,8 +341,11 @@ class Component { * @param name The identifier for this interval function. * @return Whether an interval functions was deleted. */ + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_interval(const std::string &name); // NOLINT bool cancel_interval(const char *name); // NOLINT + bool cancel_interval(uint32_t id); // NOLINT /** Set an retry function with a unique name. Empty name means no cancelling possible. * @@ -364,12 +377,25 @@ class Component { * @param backoff_increase_factor time between retries is multiplied by this factor on every retry after the first * @see cancel_retry() */ + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.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 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 + /** Set a retry function with a numeric ID (zero heap allocation). + * + * @param id The numeric identifier for this retry function + * @param initial_wait_time The wait time after the first execution + * @param max_attempts The max number of attempts + * @param f The function to call + * @param backoff_increase_factor The factor to increase the retry interval by + */ + 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 + void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, // NOLINT float backoff_increase_factor = 1.0f); // NOLINT @@ -378,8 +404,11 @@ class Component { * @param name The identifier for this retry function. * @return Whether a retry function was deleted. */ + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_retry(const std::string &name); // NOLINT bool cancel_retry(const char *name); // NOLINT + bool cancel_retry(uint32_t id); // NOLINT /** Set a timeout function with a unique name. * @@ -395,6 +424,8 @@ class Component { * * @see cancel_timeout() */ + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT /** Set a timeout function with a const char* name. @@ -413,6 +444,14 @@ class Component { */ void set_timeout(const char *name, uint32_t timeout, std::function &&f); // NOLINT + /** Set a timeout function with a numeric ID (zero heap allocation). + * + * @param id The numeric identifier for this timeout function + * @param timeout The timeout in ms + * @param f The function to call + */ + void set_timeout(uint32_t id, uint32_t timeout, std::function &&f); // NOLINT + void set_timeout(uint32_t timeout, std::function &&f); // NOLINT /** Cancel a timeout function. @@ -420,8 +459,11 @@ class Component { * @param name The identifier for this timeout function. * @return Whether a timeout functions was deleted. */ + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(const std::string &name); // NOLINT bool cancel_timeout(const char *name); // NOLINT + bool cancel_timeout(uint32_t id); // NOLINT /** Defer a callback to the next loop() call. * @@ -430,6 +472,8 @@ class Component { * @param name The name of the defer function. * @param f The callback. */ + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") void defer(const std::string &name, std::function &&f); // NOLINT /** Defer a callback to the next loop() call with a const char* name. @@ -451,7 +495,10 @@ class Component { void defer(std::function &&f); // NOLINT /// Cancel a defer callback using the specified name, name must not be empty. + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_defer(const std::string &name); // NOLINT + bool cancel_defer(const char *name); // NOLINT // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index d1594f47e7..fe9c9b5a75 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -8,11 +8,15 @@ // ESP8266 uses Arduino macros #define ESPHOME_F(string_literal) F(string_literal) #define ESPHOME_PGM_P PGM_P +#define ESPHOME_PSTR(s) PSTR(s) #define ESPHOME_strncpy_P strncpy_P #define ESPHOME_strncat_P strncat_P +#define ESPHOME_snprintf_P snprintf_P #else #define ESPHOME_F(string_literal) (string_literal) #define ESPHOME_PGM_P const char * +#define ESPHOME_PSTR(s) (s) #define ESPHOME_strncpy_P strncpy #define ESPHOME_strncat_P strncat +#define ESPHOME_snprintf_P snprintf #endif diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index b28cb947c7..047bf4ef17 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -5,6 +5,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include #include #include @@ -32,6 +33,34 @@ static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() // max delay to start an interval sequence static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; +#if defined(ESPHOME_LOG_HAS_VERBOSE) || defined(ESPHOME_DEBUG_SCHEDULER) +// Helper struct for formatting scheduler item names consistently in logs +// Uses a stack buffer to avoid heap allocation +// Uses ESPHOME_snprintf_P/ESPHOME_PSTR for ESP8266 to keep format strings in flash +struct SchedulerNameLog { + char buffer[20]; // Enough for "id:4294967295" or "hash:0xFFFFFFFF" or "(null)" + + // Format a scheduler item name for logging + // Returns pointer to formatted string (either static_name or internal buffer) + const char *format(Scheduler::NameType name_type, const char *static_name, uint32_t hash_or_id) { + using NameType = Scheduler::NameType; + if (name_type == NameType::STATIC_STRING) { + if (static_name) + return static_name; + // Copy "(null)" to buffer to keep it in flash on ESP8266 + ESPHOME_strncpy_P(buffer, ESPHOME_PSTR("(null)"), sizeof(buffer)); + return buffer; + } else if (name_type == NameType::HASHED_STRING) { + ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("hash:0x%08" PRIX32), hash_or_id); + return buffer; + } else { // NUMERIC_ID + ESPHOME_snprintf_P(buffer, sizeof(buffer), ESPHOME_PSTR("id:%" PRIu32), hash_or_id); + return buffer; + } + } +}; +#endif + // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER @@ -76,17 +105,15 @@ static void validate_static_string(const char *name) { // avoid the main thread modifying the list while it is being accessed. // Common implementation for both timeout and interval -void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, - const void *name_ptr, uint32_t delay, std::function func, bool is_retry, - bool skip_cancel) { - // Get the name as const char* - const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); - +// 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) { if (delay == SCHEDULER_DONT_RUN) { - // Still need to cancel existing timer if name is not empty + // 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_cstr, type); + this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } return; } @@ -98,23 +125,19 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type LockGuard guard{this->lock_}; // Create and populate the scheduler item - std::unique_ptr item; - if (!this->scheduler_item_pool_.empty()) { - // Reuse from pool - item = std::move(this->scheduler_item_pool_.back()); - this->scheduler_item_pool_.pop_back(); -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); -#endif - } else { - // Allocate new if pool is empty - item = make_unique(); -#ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Allocated new item (pool empty)"); -#endif - } + auto item = this->get_item_from_pool_locked_(); item->component = component; - item->set_name(name_cstr, !is_static_string); + switch (name_type) { + case NameType::STATIC_STRING: + item->set_static_name(static_name); + break; + case NameType::HASHED_STRING: + item->set_hashed_name(hash_or_id); + break; + case NameType::NUMERIC_ID: + item->set_numeric_id(hash_or_id); + break; + } item->type = type; item->callback = std::move(func); // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use @@ -127,7 +150,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution if (!skip_cancel) { - this->cancel_item_locked_(component, name_cstr, type); + this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } this->defer_queue_.push_back(std::move(item)); return; @@ -141,24 +164,32 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Calculate random offset (0 to min(interval/2, 5s)) uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); item->set_next_execution(now + offset); - ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay, - offset); +#ifdef ESPHOME_LOG_HAS_VERBOSE + SchedulerNameLog name_log; + ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", + name_log.format(name_type, static_name, hash_or_id), delay, offset); +#endif } else { item->interval = 0; item->set_next_execution(now + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item.get(), is_static_string, name_cstr, type, delay, now); + this->debug_log_timer_(item.get(), name_type, static_name, hash_or_id, type, delay, now); #endif /* ESPHOME_DEBUG_SCHEDULER */ // For retries, check if there's a cancelled timeout first - if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) || - has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + // Skip check for anonymous retries (STATIC_STRING with nullptr) - they can't be cancelled by name + if (is_retry && (name_type != NameType::STATIC_STRING || static_name != nullptr) && type == SchedulerItem::TIMEOUT && + (has_cancelled_timeout_in_container_locked_(this->items_, component, name_type, static_name, hash_or_id, + /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_type, static_name, hash_or_id, + /* match_retry= */ true))) { // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER - ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); + 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; } @@ -166,7 +197,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // If name is provided, do atomic cancel-and-add (unless skip_cancel is true) // Cancel existing items if (!skip_cancel) { - this->cancel_item_locked_(component, name_cstr, type); + this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type); } // Add new item directly to to_add_ // since we have the lock held @@ -174,33 +205,51 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } void HOT Scheduler::set_timeout(Component *component, const char *name, uint32_t timeout, std::function func) { - this->set_timer_common_(component, SchedulerItem::TIMEOUT, true, name, timeout, std::move(func)); + this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::STATIC_STRING, name, 0, timeout, + std::move(func)); } void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func) { - this->set_timer_common_(component, SchedulerItem::TIMEOUT, false, &name, timeout, std::move(func)); + this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), + timeout, std::move(func)); +} +void HOT Scheduler::set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function func) { + this->set_timer_common_(component, SchedulerItem::TIMEOUT, NameType::NUMERIC_ID, nullptr, id, timeout, + std::move(func)); } bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) { - return this->cancel_item_(component, false, &name, SchedulerItem::TIMEOUT); + return this->cancel_item_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), SchedulerItem::TIMEOUT); } bool HOT Scheduler::cancel_timeout(Component *component, const char *name) { - return this->cancel_item_(component, true, name, SchedulerItem::TIMEOUT); + return this->cancel_item_(component, NameType::STATIC_STRING, name, 0, SchedulerItem::TIMEOUT); +} +bool HOT Scheduler::cancel_timeout(Component *component, uint32_t id) { + return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::TIMEOUT); } void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, std::function func) { - this->set_timer_common_(component, SchedulerItem::INTERVAL, false, &name, interval, std::move(func)); + this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), + interval, std::move(func)); } void HOT Scheduler::set_interval(Component *component, const char *name, uint32_t interval, std::function func) { - this->set_timer_common_(component, SchedulerItem::INTERVAL, true, name, interval, std::move(func)); + this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::STATIC_STRING, name, 0, interval, + std::move(func)); +} +void HOT Scheduler::set_interval(Component *component, uint32_t id, uint32_t interval, std::function func) { + this->set_timer_common_(component, SchedulerItem::INTERVAL, NameType::NUMERIC_ID, nullptr, id, interval, + std::move(func)); } bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) { - return this->cancel_item_(component, false, &name, SchedulerItem::INTERVAL); + return this->cancel_item_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name), SchedulerItem::INTERVAL); } bool HOT Scheduler::cancel_interval(Component *component, const char *name) { - return this->cancel_item_(component, true, name, SchedulerItem::INTERVAL); + return this->cancel_item_(component, NameType::STATIC_STRING, name, 0, SchedulerItem::INTERVAL); +} +bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) { + return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL); } struct RetryArgs { @@ -208,17 +257,15 @@ struct RetryArgs { std::function func; Component *component; Scheduler *scheduler; - const char *name; // Points to static string or owned copy + // 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; - bool name_is_dynamic; // True if name needs delete[] - - ~RetryArgs() { - if (this->name_is_dynamic && this->name) { - delete[] this->name; - } - } }; void retry_handler(const std::shared_ptr &args) { @@ -226,31 +273,38 @@ void retry_handler(const std::shared_ptr &args) { if (retry_result == RetryResult::DONE || args->retry_countdown <= 0) return; // second execution of `func` happens after `initial_wait_time` - // Pass is_static_string=true because args->name is owned by the shared_ptr + // 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, true, args->name, args->current_interval, - [args]() { retry_handler(args); }, /* is_retry= */ true); + 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, bool is_static_string, const void *name_ptr, - uint32_t initial_wait_time, uint8_t max_attempts, +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) { - const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); - - if (name_cstr != nullptr) - this->cancel_retry(component, name_cstr); + this->cancel_retry_(component, name_type, static_name, hash_or_id); if (initial_wait_time == SCHEDULER_DONT_RUN) return; - ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)", - name_cstr ? name_cstr : "", initial_wait_time, max_attempts, backoff_increase_factor); +#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, "backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, name_cstr ? name_cstr : ""); + 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; } @@ -258,56 +312,56 @@ void HOT Scheduler::set_retry_common_(Component *component, bool is_static_strin 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; - // Store name - either as static pointer or owned copy - if (name_cstr == nullptr || name_cstr[0] == '\0') { - // Empty or null name - use empty string literal - args->name = ""; - args->name_is_dynamic = false; - } else if (is_static_string) { - // Static string - just store the pointer - args->name = name_cstr; - args->name_is_dynamic = false; - } else { - // Dynamic string - make a copy - size_t len = strlen(name_cstr); - char *copy = new char[len + 1]; - memcpy(copy, name_cstr, len + 1); - args->name = copy; - args->name_is_dynamic = true; - } - // First execution of `func` immediately - use set_timer_common_ with is_retry=true - // Pass is_static_string=true because args->name is owned by the shared_ptr - // which is captured in the lambda and outlives the SchedulerItem this->set_timer_common_( - component, SchedulerItem::TIMEOUT, true, args->name, 0, [args]() { retry_handler(args); }, + 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, false, &name, initial_wait_time, max_attempts, std::move(func), - 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); } -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, true, 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, name.c_str()); + return this->cancel_retry_(component, NameType::HASHED_STRING, nullptr, fnv1a_hash(name)); } -bool HOT Scheduler::cancel_retry(Component *component, const char *name) { - // Cancel timeouts that have is_retry flag set - LockGuard guard{this->lock_}; - return this->cancel_item_locked_(component, name, SchedulerItem::TIMEOUT, /* match_retry= */ true); +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); } optional HOT Scheduler::next_schedule_in(uint32_t now) { @@ -391,10 +445,11 @@ void HOT Scheduler::call(uint32_t now) { item = this->pop_raw_locked_(); } - const char *name = item->get_name(); + SchedulerNameLog name_log; bool is_cancelled = is_item_removed_(item.get()); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", - item->get_type_str(), LOG_STR_ARG(item->get_source()), name ? name : "(null)", item->interval, + item->get_type_str(), LOG_STR_ARG(item->get_source()), + name_log.format(item->get_name_type(), item->get_name(), item->get_name_hash_or_id()), item->interval, item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : ""); old_items.push_back(std::move(item)); @@ -458,10 +513,13 @@ void HOT Scheduler::call(uint32_t now) { #endif #ifdef ESPHOME_DEBUG_SCHEDULER - const char *item_name = item->get_name(); - ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval, - item->get_next_execution(), now_64); + { + SchedulerNameLog name_log; + ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", + item->get_type_str(), LOG_STR_ARG(item->get_source()), + name_log.format(item->get_name_type(), item->get_name(), item->get_name_hash_or_id()), item->interval, + item->get_next_execution(), now_64); + } #endif /* ESPHOME_DEBUG_SCHEDULER */ // Warning: During callback(), a lot of stuff can happen, including: @@ -560,33 +618,29 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { return guard.finish(); } -// Common implementation for cancel operations -bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, const void *name_ptr, - SchedulerItem::Type type) { - // Get the name as const char* - const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); - - // obtain lock because this function iterates and can be called from non-loop task context +// 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) { LockGuard guard{this->lock_}; - return this->cancel_item_locked_(component, name_cstr, type); + return this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, match_retry); } -// Helper to cancel items by name - must be called with lock held -bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type, - bool match_retry) { - // Early return if name is invalid - no items to cancel - if (name_cstr == nullptr) { +// Helper to cancel items - must be called with lock held +// 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) { + // Early return if static string name is invalid + if (name_type == NameType::STATIC_STRING && static_name == nullptr) { return false; } size_t total_cancelled = 0; - // Check all containers for matching items #ifndef ESPHOME_THREAD_SINGLE // 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_cstr, type, match_retry); + total_cancelled += this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_type, static_name, + hash_or_id, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -596,14 +650,15 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // would destroy the callback while it's running (use-after-free). // 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_cstr, type, match_retry); + size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, + hash_or_id, type, match_retry); total_cancelled += heap_cancelled; this->to_remove_ += heap_cancelled; } // Cancel items in to_add_ - total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry); + total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_type, static_name, + hash_or_id, type, match_retry); return total_cancelled > 0; } @@ -785,8 +840,6 @@ void Scheduler::recycle_item_main_loop_(std::unique_ptr item) { if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) { // Clear callback to release captured resources item->callback = nullptr; - // Clear dynamic name if any - item->clear_dynamic_name(); this->scheduler_item_pool_.push_back(std::move(item)); #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size()); @@ -800,24 +853,44 @@ void Scheduler::recycle_item_main_loop_(std::unique_ptr item) { } #ifdef ESPHOME_DEBUG_SCHEDULER -void Scheduler::debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, - SchedulerItem::Type type, uint32_t delay, uint64_t now) { +void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, uint32_t delay, uint64_t now) { // Validate static strings in debug mode - if (is_static_string && name_cstr != nullptr) { - validate_static_string(name_cstr); + if (name_type == NameType::STATIC_STRING && static_name != nullptr) { + validate_static_string(static_name); } // Debug logging + SchedulerNameLog name_log; const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; if (type == SchedulerItem::TIMEOUT) { ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), - name_cstr ? name_cstr : "(null)", type_str, delay); + name_log.format(name_type, static_name, hash_or_id), type_str, delay); } else { ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), - name_cstr ? name_cstr : "(null)", type_str, delay, + name_log.format(name_type, static_name, hash_or_id), type_str, delay, static_cast(item->get_next_execution() - now)); } } #endif /* ESPHOME_DEBUG_SCHEDULER */ +// Helper to get or create a scheduler item from the pool +// IMPORTANT: Caller must hold the scheduler lock before calling this function. +std::unique_ptr Scheduler::get_item_from_pool_locked_() { + std::unique_ptr item; + if (!this->scheduler_item_pool_.empty()) { + item = std::move(this->scheduler_item_pool_.back()); + this->scheduler_item_pool_.pop_back(); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); +#endif + } else { + item = make_unique(); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Allocated new item (pool empty)"); +#endif + } + return item; +} + } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 5bf3d19adb..8c2e349180 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -1,9 +1,10 @@ #pragma once #include "esphome/core/defines.h" -#include -#include #include +#include +#include +#include #ifdef ESPHOME_THREAD_MULTI_ATOMICS #include #endif @@ -29,7 +30,9 @@ class Scheduler { template friend class DelayAction; public: - // Public API - accepts std::string for backward compatibility + // std::string overload - deprecated, use const char* or uint32_t instead + // Remove before 2026.7.0 + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); /** Set a timeout with a const char* name. @@ -39,14 +42,17 @@ class Scheduler { * - A string literal (e.g., "update") * - A static const char* variable * - A pointer with lifetime >= the scheduled task - * - * For dynamic strings, use the std::string overload instead. */ void set_timeout(Component *component, const char *name, uint32_t timeout, std::function func); + /// Set a timeout with a numeric ID (zero heap allocation) + void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function func); + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(Component *component, const std::string &name); bool cancel_timeout(Component *component, const char *name); + bool cancel_timeout(Component *component, uint32_t id); + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); /** Set an interval with a const char* name. @@ -56,19 +62,29 @@ class Scheduler { * - A string literal (e.g., "update") * - A static const char* variable * - A pointer with lifetime >= the scheduled task - * - * For dynamic strings, use the std::string overload instead. */ void set_interval(Component *component, const char *name, uint32_t interval, std::function func); + /// Set an interval with a numeric ID (zero heap allocation) + void set_interval(Component *component, uint32_t id, uint32_t interval, std::function func); + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_interval(Component *component, const std::string &name); bool cancel_interval(Component *component, const char *name); + bool cancel_interval(Component *component, uint32_t id); + + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.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); 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); + /// Set a retry with a numeric ID (zero heap allocation) + 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); + + ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_retry(Component *component, const std::string &name); bool cancel_retry(Component *component, const char *name); + bool cancel_retry(Component *component, uint32_t id); // Calculate when the next scheduled item should run // @param now Fresh timestamp from millis() - must not be stale/cached @@ -83,14 +99,22 @@ class Scheduler { void process_to_add(); + // Name storage type discriminator for SchedulerItem + // Used to distinguish between static strings, hashed strings, and numeric IDs + enum class NameType : uint8_t { + STATIC_STRING = 0, // const char* pointer to static/flash storage + HASHED_STRING = 1, // uint32_t FNV-1a hash of a runtime string + NUMERIC_ID = 2 // uint32_t numeric identifier + }; + protected: struct SchedulerItem { // Ordered by size to minimize padding Component *component; - // Optimized name storage using tagged union + // Optimized name storage using tagged union - zero heap allocation union { - const char *static_name; // For string literals (no allocation) - char *dynamic_name; // For allocated strings + const char *static_name; // For STATIC_STRING (string literals, no allocation) + uint32_t hash_or_id; // For HASHED_STRING or NUMERIC_ID } name_; uint32_t interval; // Split time to handle millis() rollover. The scheduler combines the 32-bit millis() @@ -109,19 +133,19 @@ class Scheduler { // Place atomic separately since it can't be packed with bit fields std::atomic remove{false}; - // Bit-packed fields (3 bits used, 5 bits padding in 1 byte) - enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; - bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) - bool is_retry : 1; // True if this is a retry timeout - // 5 bits padding -#else - // Single-threaded or multi-threaded without atomics: can pack all fields together // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; + NameType name_type_ : 2; // Discriminator for name_ union (STATIC_STRING, HASHED_STRING, NUMERIC_ID) + bool is_retry : 1; // True if this is a retry timeout + // 4 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) + enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; bool remove : 1; - bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) - bool is_retry : 1; // True if this is a retry timeout - // 4 bits padding + NameType name_type_ : 2; // Discriminator for name_ union (STATIC_STRING, HASHED_STRING, NUMERIC_ID) + bool is_retry : 1; // True if this is a retry timeout + // 3 bits padding #endif // Constructor @@ -133,19 +157,19 @@ class Scheduler { #ifdef ESPHOME_THREAD_MULTI_ATOMICS // remove is initialized in the member declaration as std::atomic{false} type(TIMEOUT), - name_is_dynamic(false), + name_type_(NameType::STATIC_STRING), is_retry(false) { #else type(TIMEOUT), remove(false), - name_is_dynamic(false), + name_type_(NameType::STATIC_STRING), is_retry(false) { #endif name_.static_name = nullptr; } - // Destructor to clean up dynamic names - ~SchedulerItem() { clear_dynamic_name(); } + // Destructor - no dynamic memory to clean up + ~SchedulerItem() = default; // Delete copy operations to prevent accidental copies SchedulerItem(const SchedulerItem &) = delete; @@ -155,36 +179,31 @@ class Scheduler { SchedulerItem(SchedulerItem &&) = delete; SchedulerItem &operator=(SchedulerItem &&) = delete; - // Helper to get the name regardless of storage type - const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } + // Helper to get the static name (only valid for STATIC_STRING type) + const char *get_name() const { return (name_type_ == NameType::STATIC_STRING) ? name_.static_name : nullptr; } - // Helper to clear dynamic name if allocated - void clear_dynamic_name() { - if (name_is_dynamic && name_.dynamic_name) { - delete[] name_.dynamic_name; - name_.dynamic_name = nullptr; - name_is_dynamic = false; - } + // Helper to get the hash or numeric ID (only valid for HASHED_STRING or NUMERIC_ID types) + uint32_t get_name_hash_or_id() const { return (name_type_ != NameType::STATIC_STRING) ? name_.hash_or_id : 0; } + + // Helper to get the name type + NameType get_name_type() const { return name_type_; } + + // Helper to set a static string name (no allocation) + void set_static_name(const char *name) { + name_.static_name = name; + name_type_ = NameType::STATIC_STRING; } - // Helper to set name with proper ownership - void set_name(const char *name, bool make_copy = false) { - // Clean up old dynamic name if any - clear_dynamic_name(); + // Helper to set a hashed string name (hash computed from std::string) + void set_hashed_name(uint32_t hash) { + name_.hash_or_id = hash; + name_type_ = NameType::HASHED_STRING; + } - if (!name) { - // nullptr case - no name provided - name_.static_name = nullptr; - } else if (make_copy) { - // Make a copy for dynamic strings (including empty strings) - size_t len = strlen(name); - name_.dynamic_name = new char[len + 1]; - memcpy(name_.dynamic_name, name, len + 1); - name_is_dynamic = true; - } else { - // Use static string directly (including empty strings) - name_.static_name = name; - } + // Helper to set a numeric ID name + void set_numeric_id(uint32_t id) { + name_.hash_or_id = id; + name_type_ = NameType::NUMERIC_ID; } static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); @@ -207,12 +226,18 @@ class Scheduler { }; // Common implementation for both timeout and interval - void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, - uint32_t delay, std::function func, bool is_retry = false, bool skip_cancel = false); + // 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 - void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time, - uint8_t max_attempts, std::function func, float backoff_increase_factor); + // name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id + 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); + // Common implementation for cancel_retry + bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id); uint64_t millis_64_(uint32_t now); // Cleanup logically deleted items from the scheduler @@ -222,21 +247,22 @@ class Scheduler { // Remove and return the front item from the heap // IMPORTANT: Caller must hold the scheduler lock before calling this function. std::unique_ptr pop_raw_locked_(); + // Get or create a scheduler item from the pool + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + std::unique_ptr get_item_from_pool_locked_(); private: - // Helper to cancel items by name - must be called with lock held - bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type, bool match_retry = false); + // Helper to cancel items - must be called with lock held + // 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); - // Helper to extract name as const char* from either static string or std::string - inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) { - return is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); - } + // 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); - // Common implementation for cancel operations - bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); - - // Helper to check if two scheduler item names match - inline bool HOT names_match_(const char *name1, const char *name2) const { + // Helper to check if two static string names match + inline bool HOT names_match_static_(const char *name1, const char *name2) const { // Check pointer equality first (common for static strings), then string contents // The core ESPHome codebase uses static strings (const char*) for component names, // making pointer comparison effective. The std::string overloads exist only for @@ -245,10 +271,11 @@ class Scheduler { } // Helper function to check if item matches criteria for cancellation + // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // IMPORTANT: Must be called with scheduler lock held inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, - const char *name_cstr, SchedulerItem::Type type, bool match_retry, - bool skip_removed = true) const { + NameType name_type, const char *static_name, uint32_t hash_or_id, + SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and @@ -261,7 +288,14 @@ class Scheduler { (match_retry && !item->is_retry)) { return false; } - return this->names_match_(item->get_name(), name_cstr); + // Name type must match + if (item->get_name_type() != name_type) + return false; + // For static strings, compare the string content; for hash/ID, compare the value + if (name_type == NameType::STATIC_STRING) { + return this->names_match_static_(item->get_name(), static_name); + } + return item->get_name_hash_or_id() == hash_or_id; } // Helper to execute a scheduler item @@ -283,7 +317,7 @@ class Scheduler { #ifdef ESPHOME_DEBUG_SCHEDULER // Helper for debug logging in set_timer_common_ - extracted to reduce code size - void debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, uint32_t delay, uint64_t now); #endif /* ESPHOME_DEBUG_SCHEDULER */ @@ -410,11 +444,13 @@ class Scheduler { } // Helper to mark matching items in a container as removed + // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal // IMPORTANT: Must be called with scheduler lock held template - size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry) { + size_t mark_matching_items_removed_locked_(Container &container, Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, + bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) @@ -423,8 +459,7 @@ class Scheduler { // the vector can still contain nullptr items from the processing loop. This check prevents crashes. if (!item) continue; - if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) { - // Mark item for removal (platform-specific) + if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { this->set_item_removed_(item.get(), true); count++; } @@ -433,10 +468,12 @@ class Scheduler { } // Template helper to check if any item in a container matches our criteria + // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // IMPORTANT: Must be called with scheduler lock held template - bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, - const char *name_cstr, bool match_retry) const { + bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, + bool match_retry) const { for (const auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the @@ -445,8 +482,8 @@ class Scheduler { if (!item) continue; if (is_item_removed_(item.get()) && - this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT, + match_retry, /* skip_removed= */ false)) { return true; } } diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml new file mode 100644 index 0000000000..bf60f2fda9 --- /dev/null +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -0,0 +1,173 @@ +esphome: + name: scheduler-numeric-id-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler numeric ID tests" + +host: +api: +logger: + level: VERBOSE + +globals: + - id: timeout_counter + type: int + initial_value: '0' + - id: interval_counter + type: int + initial_value: '0' + - id: retry_counter + type: int + initial_value: '0' + - id: tests_done + type: bool + initial_value: 'false' + - id: results_reported + type: bool + initial_value: 'false' + +script: + - id: test_numeric_ids + then: + - logger.log: "Testing numeric ID timeouts and intervals" + - lambda: |- + auto *component1 = id(test_sensor1); + + // Test 1: Numeric ID with set_timeout (uint32_t) + App.scheduler.set_timeout(component1, 1001U, 50, []() { + ESP_LOGI("test", "Numeric timeout 1001 fired"); + id(timeout_counter) += 1; + }); + + // Test 2: Another numeric ID timeout + App.scheduler.set_timeout(component1, 1002U, 100, []() { + ESP_LOGI("test", "Numeric timeout 1002 fired"); + id(timeout_counter) += 1; + }); + + // Test 3: Numeric ID with set_interval + App.scheduler.set_interval(component1, 2001U, 200, []() { + ESP_LOGI("test", "Numeric interval 2001 fired, count: %d", id(interval_counter)); + id(interval_counter) += 1; + if (id(interval_counter) >= 3) { + App.scheduler.cancel_interval(id(test_sensor1), 2001U); + ESP_LOGI("test", "Cancelled numeric interval 2001"); + } + }); + + // Test 4: Cancel timeout with numeric ID + App.scheduler.set_timeout(component1, 3001U, 5000, []() { + ESP_LOGE("test", "ERROR: Timeout 3001 should have been cancelled"); + }); + App.scheduler.cancel_timeout(component1, 3001U); + ESP_LOGI("test", "Cancelled numeric timeout 3001"); + + // Test 5: Multiple timeouts with same numeric ID - only last should execute + for (int i = 0; i < 5; i++) { + App.scheduler.set_timeout(component1, 4001U, 300 + i*10, [i]() { + ESP_LOGI("test", "Duplicate numeric timeout %d fired", i); + id(timeout_counter) += 1; + }); + } + ESP_LOGI("test", "Created 5 timeouts with same numeric ID 4001"); + + // Test 6: Cancel non-existent numeric ID + bool cancelled_nonexistent = App.scheduler.cancel_timeout(component1, 9999U); + ESP_LOGI("test", "Cancel non-existent numeric ID result: %s", + cancelled_nonexistent ? "true (unexpected!)" : "false (expected)"); + + // Test 7: Component method uint32_t overloads + class TestNumericComponent : public Component { + public: + void test_numeric_methods() { + // Test set_timeout with uint32_t ID + this->set_timeout(5001U, 150, []() { + ESP_LOGI("test", "Component numeric timeout 5001 fired"); + id(timeout_counter) += 1; + }); + + // Test set_interval with uint32_t ID + // Capture 'this' pointer so we can cancel with correct component + auto *self = this; + this->set_interval(5002U, 400, [self]() { + ESP_LOGI("test", "Component numeric interval 5002 fired"); + id(interval_counter) += 1; + // Cancel after first fire - must use same component pointer + App.scheduler.cancel_interval(self, 5002U); + }); + } + }; + + static TestNumericComponent test_component; + test_component.test_numeric_methods(); + + // Test 8: Zero ID (edge case) + App.scheduler.set_timeout(component1, 0U, 200, []() { + ESP_LOGI("test", "Numeric timeout with ID 0 fired"); + id(timeout_counter) += 1; + }); + + // Test 9: Max uint32_t ID (edge case) + App.scheduler.set_timeout(component1, 0xFFFFFFFFU, 250, []() { + ESP_LOGI("test", "Numeric timeout with max ID fired"); + 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"); + + - id: report_results + then: + - lambda: |- + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d", + id(timeout_counter), id(interval_counter), id(retry_counter)); + +sensor: + - platform: template + name: Test Sensor 1 + id: test_sensor1 + lambda: return 1.0; + update_interval: never + +interval: + # Run numeric ID tests after boot + - interval: 0.1s + then: + - if: + condition: + lambda: 'return id(tests_done) == false;' + then: + - lambda: 'id(tests_done) = true;' + - script.execute: test_numeric_ids + - logger.log: "Started numeric ID tests" + + # Report results after tests complete + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(tests_done) && !id(results_reported);' + then: + - lambda: 'id(results_reported) = true;' + - delay: 1.5s + - script.execute: report_results diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml index 11fff6c395..ffe9082a69 100644 --- a/tests/integration/fixtures/scheduler_retry_test.yaml +++ b/tests/integration/fixtures/scheduler_retry_test.yaml @@ -43,9 +43,6 @@ globals: - id: static_char_retry_counter type: int initial_value: '0' - - id: mixed_cancel_result - type: bool - initial_value: 'false' # Using different component types for each test to ensure isolation sensor: @@ -271,23 +268,6 @@ script: ESP_LOGI("test", "Static cancel result: %s", result ? "true" : "false"); }); - # Test 10: Mix string and const char* cancel - - logger.log: "=== Test 10: Mixed string/const char* ===" - - lambda: |- - auto *component = id(immediate_done_sensor); - - // Set with std::string - std::string str_name = "mixed_retry"; - App.scheduler.set_retry(component, str_name, 40, 3, - [](uint8_t retry_countdown) { - ESP_LOGI("test", "Mixed retry - should be cancelled"); - return RetryResult::RETRY; - }); - - // Cancel with const char* - id(mixed_cancel_result) = App.scheduler.cancel_retry(component, "mixed_retry"); - ESP_LOGI("test", "Mixed cancel result: %s", id(mixed_cancel_result) ? "true" : "false"); - # Wait for all tests to complete before reporting - delay: 500ms @@ -303,5 +283,4 @@ script: 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", "Mixed cancel result: %s (expected true)", id(mixed_cancel_result) ? "true" : "false"); 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 new file mode 100644 index 0000000000..510256b9a4 --- /dev/null +++ b/tests/integration/test_scheduler_numeric_id_test.py @@ -0,0 +1,217 @@ +"""Test scheduler numeric ID (uint32_t) overloads.""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_numeric_id_test( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduler handles numeric IDs (uint32_t) correctly.""" + # Track counts + timeout_count = 0 + interval_count = 0 + retry_count = 0 + + # Events for each test completion + numeric_timeout_1001_fired = asyncio.Event() + numeric_timeout_1002_fired = asyncio.Event() + numeric_interval_2001_fired = asyncio.Event() + numeric_interval_cancelled = asyncio.Event() + numeric_timeout_cancelled = asyncio.Event() + duplicate_timeout_fired = asyncio.Event() + component_timeout_fired = asyncio.Event() + 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() + final_results_logged = asyncio.Event() + + # 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 + nonlocal numeric_interval_count, numeric_retry_count + + # Strip ANSI color codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + # Check for numeric timeout completions + if "Numeric timeout 1001 fired" in clean_line: + numeric_timeout_1001_fired.set() + timeout_count += 1 + + elif "Numeric timeout 1002 fired" in clean_line: + numeric_timeout_1002_fired.set() + timeout_count += 1 + + # Check for numeric interval + elif "Numeric interval 2001 fired" in clean_line: + match = re.search(r"count: (\d+)", clean_line) + if match: + numeric_interval_count = int(match.group(1)) + numeric_interval_2001_fired.set() + + elif "Cancelled numeric interval 2001" in clean_line: + numeric_interval_cancelled.set() + + elif "Cancelled numeric timeout 3001" in clean_line: + numeric_timeout_cancelled.set() + + # Check for duplicate timeout (only last should fire) + elif "Duplicate numeric timeout" in clean_line: + match = re.search(r"timeout (\d+) fired", clean_line) + if match and match.group(1) == "4": + duplicate_timeout_fired.set() + timeout_count += 1 + + # Check for component method tests + elif "Component numeric timeout 5001 fired" in clean_line: + component_timeout_fired.set() + timeout_count += 1 + + elif "Component numeric interval 5002 fired" in clean_line: + component_interval_fired.set() + interval_count += 1 + + # Check for edge case tests + elif "Numeric timeout with ID 0 fired" in clean_line: + zero_id_timeout_fired.set() + timeout_count += 1 + + elif "Numeric timeout with max ID fired" in clean_line: + 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 final results + elif "Final results" in clean_line: + match = re.search( + r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+)", clean_line + ) + if match: + timeout_count = int(match.group(1)) + interval_count = int(match.group(2)) + retry_count = int(match.group(3)) + final_results_logged.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-numeric-id-test" + + # Wait for numeric timeout tests + try: + await asyncio.wait_for(numeric_timeout_1001_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric timeout 1001 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(numeric_timeout_1002_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric timeout 1002 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(numeric_interval_2001_fired.wait(), timeout=1.0) + except TimeoutError: + pytest.fail("Numeric interval 2001 did not fire within 1 second") + + try: + await asyncio.wait_for(numeric_interval_cancelled.wait(), timeout=2.0) + except TimeoutError: + pytest.fail("Numeric interval 2001 was not cancelled within 2 seconds") + + # Verify numeric interval ran at least twice + assert numeric_interval_count >= 2, ( + f"Expected numeric interval to run at least 2 times, got {numeric_interval_count}" + ) + + # Verify numeric timeout was cancelled + assert numeric_timeout_cancelled.is_set(), ( + "Numeric timeout 3001 should have been cancelled" + ) + + # Wait for duplicate timeout (only last one should fire) + try: + await asyncio.wait_for(duplicate_timeout_fired.wait(), timeout=1.0) + except TimeoutError: + pytest.fail("Duplicate numeric timeout did not fire within 1 second") + + # Wait for component method tests + try: + await asyncio.wait_for(component_timeout_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Component numeric timeout did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(component_interval_fired.wait(), timeout=1.0) + except TimeoutError: + pytest.fail("Component numeric interval did not fire within 1 second") + + # Wait for edge case tests + try: + await asyncio.wait_for(zero_id_timeout_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Zero ID timeout did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(max_id_timeout_fired.wait(), timeout=0.5) + 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 final results + try: + await asyncio.wait_for(final_results_logged.wait(), timeout=3.0) + except TimeoutError: + pytest.fail("Final results were not logged within 3 seconds") + + # Verify results + assert timeout_count >= 6, f"Expected at least 6 timeouts, got {timeout_count}" + 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}" + ) diff --git a/tests/integration/test_scheduler_retry_test.py b/tests/integration/test_scheduler_retry_test.py index c04b7197c9..910034e5bb 100644 --- a/tests/integration/test_scheduler_retry_test.py +++ b/tests/integration/test_scheduler_retry_test.py @@ -25,7 +25,6 @@ async def test_scheduler_retry_test( multiple_name_done = asyncio.Event() const_char_done = asyncio.Event() static_char_done = asyncio.Event() - mixed_cancel_done = asyncio.Event() test_complete = asyncio.Event() # Track retry counts @@ -42,14 +41,13 @@ async def test_scheduler_retry_test( # Track specific test results cancel_result = None empty_cancel_result = None - mixed_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, mixed_cancel_result + nonlocal cancel_result, empty_cancel_result # Strip ANSI color codes clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) @@ -129,11 +127,6 @@ async def test_scheduler_retry_test( # This is part of test 9, but we don't track it separately pass - # Mixed cancel test - elif "Mixed cancel result:" in clean_line: - mixed_cancel_result = "true" in clean_line - mixed_cancel_done.set() - # Test completion elif "All retry tests completed" in clean_line: test_complete.set() @@ -279,16 +272,6 @@ async def test_scheduler_retry_test( f"Expected 1 static char retry call, got {static_char_retry_count}" ) - # Wait for mixed cancel test - try: - await asyncio.wait_for(mixed_cancel_done.wait(), timeout=1.0) - except TimeoutError: - pytest.fail("Mixed cancel test did not complete") - - assert mixed_cancel_result is True, ( - "Mixed string/const char cancel should have succeeded" - ) - # Wait for test completion try: await asyncio.wait_for(test_complete.wait(), timeout=1.0)