diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 5d0b96c38f2..1dc95c448b6 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -442,10 +442,10 @@ uint32_t HOT Scheduler::call(uint32_t now) { SchedulerNameLog name_log; bool is_cancelled = is_item_removed_(item); - ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", + ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRId64 "ms at %" PRIu64 "%s", LOG_STR_ARG(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(), + (int64_t) (item->get_next_execution() - now_64), item->get_next_execution(), is_cancelled ? LOG_STR_LITERAL(" [CANCELLED]") : LOG_STR_LITERAL("")); old_items.push_back(item); @@ -539,6 +539,7 @@ uint32_t HOT Scheduler::call(uint32_t now) { // We were removed/cancelled in the function call, recycle and continue this->to_remove_decrement_locked_(); this->recycle_item_main_loop_(executed_item); + has_added_items |= !this->to_add_.empty(); continue; } diff --git a/tests/components/core/test_scheduler_cancelled_callback.cpp b/tests/components/core/test_scheduler_cancelled_callback.cpp new file mode 100644 index 00000000000..b4cfd21cc5c --- /dev/null +++ b/tests/components/core/test_scheduler_cancelled_callback.cpp @@ -0,0 +1,44 @@ +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/scheduler.h" + +#include + +#include +#include + +namespace esphome::core::testing { + +class SchedulerTestComponent : public Component {}; + +// On a multi-threaded build next_schedule_in() only sees items_, so a timeout a callback leaves in +// to_add_ would let a loop that sleeps on next_schedule_in() oversleep. The case that used to skip +// the merge is a heap item (a zero timeout would run from the defer queue instead) whose callback +// cancels its own item. +TEST(Scheduler, TimeoutAddedByACancelledCallbackIsVisibleAfterCall) { + Scheduler scheduler; + SchedulerTestComponent component; + bool fired = false; + + scheduler.set_timeout(&component, "first", 1, [&]() { + scheduler.cancel_timeout(&component, "first"); + scheduler.set_timeout(&component, "second", 100, [&fired]() { fired = true; }); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + scheduler.call(millis()); + + auto next = scheduler.next_schedule_in(millis()); + ASSERT_TRUE(next.has_value()); + const uint32_t next_ms = next.has_value() ? *next : 0; + EXPECT_GT(next_ms, 50u); + EXPECT_LE(next_ms, 100u); + EXPECT_FALSE(fired); + + std::this_thread::sleep_for(std::chrono::milliseconds(120)); + scheduler.call(millis()); + EXPECT_TRUE(fired); + EXPECT_FALSE(scheduler.next_schedule_in(millis()).has_value()); + scheduler.trim_freelist(); +} + +} // namespace esphome::core::testing