From 71636ba8a1ddde159d8610ea746dc742d0fca2af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 16 Apr 2026 17:34:05 -1000 Subject: [PATCH] [core] coerce set_interval(0) to 1ms to prevent scheduler spin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users (notably PollingComponents with update_interval: 0ms) have historically relied on set_interval(0) as a pseudo-loop() mechanism. A literal interval=0 causes Scheduler::call() to spin — the item is always 'due now' after re-scheduling, so the scheduler loop never returns, starves the main loop, and triggers a WDT reset in the field. Coerce interval=0 to 1ms at creation so existing code keeps working at ~1kHz instead of spinning, while still emitting the warning pointing authors at HighFrequencyLoopRequester (the intended mechanism for running fast in the main loop). Zero-delay timeouts (defer/set_timeout) remain legitimate one-shots and are unaffected — defer is a one-shot, not a spin risk. --- esphome/core/scheduler.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index fdb96c7343..168230c274 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -144,13 +144,17 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // An interval of 0 means "fire every tick forever," which is misuse: it - // asks the main loop to spin unbounded. The correct mechanism for running - // fast in the main loop is HighFrequencyLoopRequester, not a zero-delay - // interval. Zero-delay timeouts (defer) remain legitimate one-shots. + // An interval of 0 means "fire every tick forever," which is misuse: the + // item would always be due, causing Scheduler::call() to spin and starve + // the main loop (WDT reset in the field). Coerce to 1ms so existing code + // using update_interval=0ms as a pseudo-loop() continues to work at ~1kHz, + // and warn so authors can migrate to HighFrequencyLoopRequester which is + // the intended mechanism for running fast in the main loop. Zero-delay + // timeouts (defer) remain legitimate one-shots and are not affected. if (type == SchedulerItem::INTERVAL && delay == 0) [[unlikely]] { - ESP_LOGW(TAG, "[%s] set_interval(0) is misuse — use HighFrequencyLoopRequester", + ESP_LOGW(TAG, "[%s] set_interval(0) is misuse — coercing to 1ms. Use HighFrequencyLoopRequester instead.", component ? LOG_STR_ARG(component->get_component_log_str()) : LOG_STR_LITERAL("?")); + delay = 1; } // Take lock early to protect scheduler_item_pool_ access and retry-cancelled check