ESPHome  2025.4.0
scheduler.cpp
Go to the documentation of this file.
1 #include "scheduler.h"
2 #include "esphome/core/defines.h"
3 #include "esphome/core/log.h"
4 #include "esphome/core/helpers.h"
5 #include "esphome/core/hal.h"
6 #include <algorithm>
7 #include <cinttypes>
8 
9 namespace esphome {
10 
11 static const char *const TAG = "scheduler";
12 
13 static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
14 
15 // Uncomment to debug scheduler
16 // #define ESPHOME_DEBUG_SCHEDULER
17 
18 // A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to
19 // them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task,
20 // iterating over them from the loop task is fine; but iterating from any other context requires the lock to be held to
21 // avoid the main thread modifying the list while it is being accessed.
22 
23 void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout,
24  std::function<void()> func) {
25  const auto now = this->millis_();
26 
27  if (!name.empty())
28  this->cancel_timeout(component, name);
29 
30  if (timeout == SCHEDULER_DONT_RUN)
31  return;
32 
33  auto item = make_unique<SchedulerItem>();
34  item->component = component;
35  item->name = name;
36  item->type = SchedulerItem::TIMEOUT;
37  item->next_execution_ = now + timeout;
38  item->callback = std::move(func);
39  item->remove = false;
40 #ifdef ESPHOME_DEBUG_SCHEDULER
41  ESP_LOGD(TAG, "set_timeout(name='%s/%s', timeout=%" PRIu32 ")", item->get_source(), name.c_str(), timeout);
42 #endif
43  this->push_(std::move(item));
44 }
45 bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name) {
46  return this->cancel_item_(component, name, SchedulerItem::TIMEOUT);
47 }
48 void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval,
49  std::function<void()> func) {
50  const auto now = this->millis_();
51 
52  if (!name.empty())
53  this->cancel_interval(component, name);
54 
55  if (interval == SCHEDULER_DONT_RUN)
56  return;
57 
58  // only put offset in lower half
59  uint32_t offset = 0;
60  if (interval != 0)
61  offset = (random_uint32() % interval) / 2;
62 
63  auto item = make_unique<SchedulerItem>();
64  item->component = component;
65  item->name = name;
66  item->type = SchedulerItem::INTERVAL;
67  item->interval = interval;
68  item->next_execution_ = now + offset;
69  item->callback = std::move(func);
70  item->remove = false;
71 #ifdef ESPHOME_DEBUG_SCHEDULER
72  ESP_LOGD(TAG, "set_interval(name='%s/%s', interval=%" PRIu32 ", offset=%" PRIu32 ")", item->get_source(),
73  name.c_str(), interval, offset);
74 #endif
75  this->push_(std::move(item));
76 }
77 bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
78  return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
79 }
80 
81 struct RetryArgs {
82  std::function<RetryResult(uint8_t)> func;
83  uint8_t retry_countdown;
84  uint32_t current_interval;
85  Component *component;
86  std::string name;
87  float backoff_increase_factor;
88  Scheduler *scheduler;
89 };
90 
91 static void retry_handler(const std::shared_ptr<RetryArgs> &args) {
92  RetryResult const retry_result = args->func(--args->retry_countdown);
93  if (retry_result == RetryResult::DONE || args->retry_countdown <= 0)
94  return;
95  // second execution of `func` happens after `initial_wait_time`
96  args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); });
97  // backoff_increase_factor applied to third & later executions
98  args->current_interval *= args->backoff_increase_factor;
99 }
100 
101 void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time,
102  uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
103  float backoff_increase_factor) {
104  if (!name.empty())
105  this->cancel_retry(component, name);
106 
107  if (initial_wait_time == SCHEDULER_DONT_RUN)
108  return;
109 
110  ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)",
111  name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor);
112 
113  if (backoff_increase_factor < 0.0001) {
114  ESP_LOGE(TAG,
115  "set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead",
116  name.c_str(), backoff_increase_factor);
117  backoff_increase_factor = 1;
118  }
119 
120  auto args = std::make_shared<RetryArgs>();
121  args->func = std::move(func);
122  args->retry_countdown = max_attempts;
123  args->current_interval = initial_wait_time;
124  args->component = component;
125  args->name = "retry$" + name;
126  args->backoff_increase_factor = backoff_increase_factor;
127  args->scheduler = this;
128 
129  // First execution of `func` immediately
130  this->set_timeout(component, args->name, 0, [args]() { retry_handler(args); });
131 }
132 bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) {
133  return this->cancel_timeout(component, "retry$" + name);
134 }
135 
137  if (this->empty_())
138  return {};
139  auto &item = this->items_[0];
140  const auto now = this->millis_();
141  if (item->next_execution_ < now)
142  return 0;
143  return item->next_execution_ - now;
144 }
145 void HOT Scheduler::call() {
146  const auto now = this->millis_();
147  this->process_to_add();
148 
149 #ifdef ESPHOME_DEBUG_SCHEDULER
150  static uint64_t last_print = 0;
151 
152  if (now - last_print > 2000) {
153  last_print = now;
154  std::vector<std::unique_ptr<SchedulerItem>> old_items;
155  ESP_LOGD(TAG, "Items: count=%u, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_,
156  this->last_millis_);
157  while (!this->empty_()) {
158  this->lock_.lock();
159  auto item = std::move(this->items_[0]);
160  this->pop_raw_();
161  this->lock_.unlock();
162 
163  ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
164  item->get_type_str(), item->get_source(), item->name.c_str(), item->interval,
165  item->next_execution_ - now, item->next_execution_);
166 
167  old_items.push_back(std::move(item));
168  }
169  ESP_LOGD(TAG, "\n");
170 
171  {
172  LockGuard guard{this->lock_};
173  this->items_ = std::move(old_items);
174  }
175  }
176 #endif // ESPHOME_DEBUG_SCHEDULER
177 
178  auto to_remove_was = to_remove_;
179  auto items_was = this->items_.size();
180  // If we have too many items to remove
181  if (to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
182  std::vector<std::unique_ptr<SchedulerItem>> valid_items;
183  while (!this->empty_()) {
184  LockGuard guard{this->lock_};
185  auto item = std::move(this->items_[0]);
186  this->pop_raw_();
187  valid_items.push_back(std::move(item));
188  }
189 
190  {
191  LockGuard guard{this->lock_};
192  this->items_ = std::move(valid_items);
193  }
194 
195  // The following should not happen unless I'm missing something
196  if (to_remove_ != 0) {
197  ESP_LOGW(TAG, "to_remove_ was %" PRIu32 " now: %" PRIu32 " items where %zu now %zu. Please report this",
198  to_remove_was, to_remove_, items_was, items_.size());
199  to_remove_ = 0;
200  }
201  }
202 
203  while (!this->empty_()) {
204  // use scoping to indicate visibility of `item` variable
205  {
206  // Don't copy-by value yet
207  auto &item = this->items_[0];
208  if (item->next_execution_ > now) {
209  // Not reached timeout yet, done for this call
210  break;
211  }
212  // Don't run on failed components
213  if (item->component != nullptr && item->component->is_failed()) {
214  LockGuard guard{this->lock_};
215  this->pop_raw_();
216  continue;
217  }
218 
219 #ifdef ESPHOME_DEBUG_SCHEDULER
220  ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
221  item->get_type_str(), item->get_source(), item->name.c_str(), item->interval, item->next_execution_,
222  now);
223 #endif
224 
225  // Warning: During callback(), a lot of stuff can happen, including:
226  // - timeouts/intervals get added, potentially invalidating vector pointers
227  // - timeouts/intervals get cancelled
228  {
229  WarnIfComponentBlockingGuard guard{item->component};
230  item->callback();
231  }
232  }
233 
234  {
235  this->lock_.lock();
236 
237  // new scope, item from before might have been moved in the vector
238  auto item = std::move(this->items_[0]);
239 
240  // Only pop after function call, this ensures we were reachable
241  // during the function call and know if we were cancelled.
242  this->pop_raw_();
243 
244  this->lock_.unlock();
245 
246  if (item->remove) {
247  // We were removed/cancelled in the function call, stop
248  to_remove_--;
249  continue;
250  }
251 
252  if (item->type == SchedulerItem::INTERVAL) {
253  item->next_execution_ = now + item->interval;
254  this->push_(std::move(item));
255  }
256  }
257  }
258 
259  this->process_to_add();
260 }
262  LockGuard guard{this->lock_};
263  for (auto &it : this->to_add_) {
264  if (it->remove) {
265  continue;
266  }
267 
268  this->items_.push_back(std::move(it));
269  std::push_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
270  }
271  this->to_add_.clear();
272 }
274  while (!this->items_.empty()) {
275  auto &item = this->items_[0];
276  if (!item->remove)
277  return;
278 
279  to_remove_--;
280 
281  {
282  LockGuard guard{this->lock_};
283  this->pop_raw_();
284  }
285  }
286 }
288  std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
289  this->items_.pop_back();
290 }
291 void HOT Scheduler::push_(std::unique_ptr<Scheduler::SchedulerItem> item) {
292  LockGuard guard{this->lock_};
293  this->to_add_.push_back(std::move(item));
294 }
295 bool HOT Scheduler::cancel_item_(Component *component, const std::string &name, Scheduler::SchedulerItem::Type type) {
296  // obtain lock because this function iterates and can be called from non-loop task context
297  LockGuard guard{this->lock_};
298  bool ret = false;
299  for (auto &it : this->items_) {
300  if (it->component == component && it->name == name && it->type == type && !it->remove) {
301  to_remove_++;
302  it->remove = true;
303  ret = true;
304  }
305  }
306  for (auto &it : this->to_add_) {
307  if (it->component == component && it->name == name && it->type == type) {
308  it->remove = true;
309  ret = true;
310  }
311  }
312 
313  return ret;
314 }
315 uint64_t Scheduler::millis_() {
316  const uint32_t now = millis();
317  if (now < this->last_millis_) {
318  this->millis_major_++;
319  ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms",
320  now + (static_cast<uint64_t>(this->millis_major_) << 32));
321  }
322  this->last_millis_ = now;
323  return now + (static_cast<uint64_t>(this->millis_major_) << 32);
324 }
325 
326 bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
327  const std::unique_ptr<SchedulerItem> &b) {
328  return a->next_execution_ > b->next_execution_;
329 }
330 
331 } // namespace esphome
uint32_t to_remove_
Definition: scheduler.h:71
const char * name
Definition: stm32flash.h:78
RetryResult
Definition: component.h:66
void push_(std::unique_ptr< SchedulerItem > item)
Definition: scheduler.cpp:291
uint16_t millis_major_
Definition: scheduler.h:70
uint32_t random_uint32()
Return a random 32-bit unsigned integer.
Definition: helpers.cpp:196
bool cancel_timeout(Component *component, const std::string &name)
Definition: scheduler.cpp:45
optional< uint32_t > next_schedule_in()
Definition: scheduler.cpp:136
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
const char *const TAG
Definition: spi.cpp:8
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)
Definition: scheduler.cpp:101
uint64_t millis_()
Definition: scheduler.cpp:315
std::vector< std::unique_ptr< SchedulerItem > > to_add_
Definition: scheduler.h:68
bool cancel_retry(Component *component, const std::string &name)
Definition: scheduler.cpp:132
uint8_t type
uint32_t last_millis_
Definition: scheduler.h:69
bool cancel_item_(Component *component, const std::string &name, SchedulerItem::Type type)
Definition: scheduler.cpp:295
static bool cmp(const std::unique_ptr< SchedulerItem > &a, const std::unique_ptr< SchedulerItem > &b)
Definition: scheduler.cpp:326
bool cancel_interval(Component *component, const std::string &name)
Definition: scheduler.cpp:77
void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function< void()> func)
Definition: scheduler.cpp:23
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
void unlock()
Definition: helpers.cpp:650
std::vector< std::unique_ptr< SchedulerItem > > items_
Definition: scheduler.h:67
Helper class that wraps a mutex with a RAII-style API.
Definition: helpers.h:585
void set_interval(Component *component, const std::string &name, uint32_t interval, std::function< void()> func)
Definition: scheduler.cpp:48