[core] Scheduler::next_schedule_in: return 0 when defer queue is non-empty

On multi-thread platforms (ESP32/LibreTiny/host), defer() items go
directly into a separate defer_queue_ instead of the main items_ heap
(scheduler.cpp:200). next_schedule_in previously only looked at
items_[0], so a pending defer posted from a background task was
invisible to the loop's sleep-duration calculation: if items_[0] fired
seconds away, the main loop would sleep that long on ulTaskNotifyTake
while the defer sat unexecuted.

In practice this rarely bit anyone because concurrent socket activity
usually woke the loop via the lwip fast-select path, but a defer with
no accompanying network traffic could stall for seconds.

Check defer_empty_() (cheap atomic/volatile load, scheduler.h:594)
before the items_ check and return 0 if pending. process_defer_queue_
runs at the top of every scheduler.call(), so "defer pending" == "run
immediately."

Single-threaded platforms funnel defers into to_add_ like any other
timeout, so the fix is gated on !ESPHOME_THREAD_SINGLE.
This commit is contained in:
J. Nick Koston
2026-04-24 03:26:21 -05:00
parent eceb534895
commit da5c31cb27
+8
View File
@@ -417,6 +417,14 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
// It performs cleanup and accesses items_[0] without holding a lock, which is only
// safe when called from the main thread. Other threads must not call this method.
#ifndef ESPHOME_THREAD_SINGLE
// defer() items live in a separate queue that is drained at the top of every
// loop tick via process_defer_queue_(). If any are pending, the next loop
// iteration has work to do right now -- don't let the caller sleep.
if (!this->defer_empty_())
return 0;
#endif
// If no items, return empty optional
if (!this->cleanup_())
return {};