[core] coerce set_interval(0) to 1ms to prevent scheduler spin

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.
This commit is contained in:
J. Nick Koston
2026-04-16 17:34:05 -10:00
parent a0ac226e6c
commit 71636ba8a1
+9 -5
View File
@@ -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