1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00

[scheduler] Eliminate heap allocations for std::string names and add uint32_t ID API (#13200)

This commit is contained in:
J. Nick Koston
2026-01-14 04:15:31 -10:00
committed by GitHub
parent 9c5f4e5288
commit d5f557ad1c
11 changed files with 816 additions and 256 deletions

View File

@@ -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();

View File

@@ -191,15 +191,15 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, 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<Ts...>::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);
}
}

View File

@@ -118,7 +118,10 @@ void Component::setup() {}
void Component::loop() {}
void Component::set_interval(const std::string &name, uint32_t interval, std::function<void()> &&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<void()> &&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<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
#pragma GCC diagnostic pop
}
void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
@@ -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<void()> &&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<void()> &&f) { // NOLINT
@@ -160,13 +175,36 @@ void Component::set_timeout(const char *name, uint32_t timeout, std::function<vo
}
bool Component::cancel_timeout(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_timeout(const char *name) { // NOLINT
return App.scheduler.cancel_timeout(this, name);
}
// uint32_t (numeric ID) overloads - zero heap allocation
void Component::set_timeout(uint32_t id, uint32_t timeout, std::function<void()> &&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<void()> &&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<RetryResult(uint8_t)> &&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<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, static_cast<const char *>(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<void()> &&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<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, name, 0, std::move(f));

View File

@@ -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<void()> &&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<void()> &&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<void()> &&f); // NOLINT
void set_interval(uint32_t interval, std::function<void()> &&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<RetryResult(uint8_t)> &&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<RetryResult(uint8_t)> &&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<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, // NOLINT
float backoff_increase_factor = 1.0f); // NOLINT
@@ -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<void()> &&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<void()> &&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<void()> &&f); // NOLINT
void set_timeout(uint32_t timeout, std::function<void()> &&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<void()> &&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<void()> &&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};

View File

@@ -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

View File

@@ -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 <algorithm>
#include <cinttypes>
#include <cstring>
@@ -32,6 +33,34 @@ static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::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<void()> 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<void()> 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<SchedulerItem> 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<SchedulerItem>();
#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<void()> 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<void()> 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<void()> 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<void()> 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<void()> 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<void()> 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<RetryResult(uint8_t)> 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<RetryArgs> &args) {
@@ -226,31 +273,38 @@ void retry_handler(const std::shared_ptr<RetryArgs> &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<RetryArgs>
// args->name_ is owned by the shared_ptr<RetryArgs>
// which is captured in the lambda and outlives the SchedulerItem
const char *static_name = (args->name_type == Scheduler::NameType::STATIC_STRING) ? args->name_.static_name : nullptr;
uint32_t hash_or_id = (args->name_type != Scheduler::NameType::STATIC_STRING) ? args->name_.hash_or_id : 0;
args->scheduler->set_timer_common_(
args->component, Scheduler::SchedulerItem::TIMEOUT, 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<RetryResult(uint8_t)> 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<RetryArgs>
// 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<RetryResult(uint8_t)> func, float backoff_increase_factor) {
this->set_retry_common_(component, NameType::STATIC_STRING, name, 0, initial_wait_time, max_attempts, std::move(func),
backoff_increase_factor);
}
bool HOT Scheduler::cancel_retry_(Component *component, NameType name_type, const char *static_name,
uint32_t hash_or_id) {
return this->cancel_item_(component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT,
/* match_retry= */ true);
}
bool HOT Scheduler::cancel_retry(Component *component, const char *name) {
return this->cancel_retry_(component, NameType::STATIC_STRING, name, 0);
}
void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time,
uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
float backoff_increase_factor) {
this->set_retry_common_(component, 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<RetryResult(uint8_t)> 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<RetryResult(uint8_t)> func, float backoff_increase_factor) {
this->set_retry_common_(component, NameType::NUMERIC_ID, nullptr, id, initial_wait_time, max_attempts,
std::move(func), backoff_increase_factor);
}
bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) {
return this->cancel_retry_(component, NameType::NUMERIC_ID, nullptr, id);
}
optional<uint32_t> 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<SchedulerItem> 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<SchedulerItem> 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<uint32_t>(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::SchedulerItem> Scheduler::get_item_from_pool_locked_() {
std::unique_ptr<SchedulerItem> 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<SchedulerItem>();
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Allocated new item (pool empty)");
#endif
}
return item;
}
} // namespace esphome

View File

@@ -1,9 +1,10 @@
#pragma once
#include "esphome/core/defines.h"
#include <vector>
#include <memory>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
#include <atomic>
#endif
@@ -29,7 +30,9 @@ class Scheduler {
template<typename... Ts> 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<void()> 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<void()> func);
/// Set a timeout with a numeric ID (zero heap allocation)
void set_timeout(Component *component, uint32_t id, uint32_t timeout, std::function<void()> 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<void()> 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<void()> func);
/// Set an interval with a numeric ID (zero heap allocation)
void set_interval(Component *component, uint32_t id, uint32_t interval, std::function<void()> 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<RetryResult(uint8_t)> 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<RetryResult(uint8_t)> 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<RetryResult(uint8_t)> 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<bool> separately since it can't be packed with bit fields
std::atomic<bool> 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<bool>{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<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &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<void()> 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<void()> 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<RetryResult(uint8_t)> 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<RetryResult(uint8_t)> 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<SchedulerItem> 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<SchedulerItem> 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<const char *>(name_ptr) : static_cast<const std::string *>(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<SchedulerItem> &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<typename Container>
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<typename Container>
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;
}
}

View File

@@ -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

View File

@@ -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");

View File

@@ -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}"
)

View File

@@ -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)