ESPHome 2025.8.0
Loading...
Searching...
No Matches
scheduler.h
Go to the documentation of this file.
1#pragma once
2
4#include <vector>
5#include <memory>
6#include <cstring>
7#include <deque>
8#ifdef ESPHOME_THREAD_MULTI_ATOMICS
9#include <atomic>
10#endif
11
14
15namespace esphome {
16
17class Component;
18struct RetryArgs;
19
20// Forward declaration of retry_handler - needs to be non-static for friend declaration
21void retry_handler(const std::shared_ptr<RetryArgs> &args);
22
23class Scheduler {
24 // Allow retry_handler to access protected members
25 friend void ::esphome::retry_handler(const std::shared_ptr<RetryArgs> &args);
26
27 public:
28 // Public API - accepts std::string for backward compatibility
29 void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function<void()> func);
30
41 void set_timeout(Component *component, const char *name, uint32_t timeout, std::function<void()> func);
42
43 bool cancel_timeout(Component *component, const std::string &name);
44 bool cancel_timeout(Component *component, const char *name);
45
46 void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> func);
47
58 void set_interval(Component *component, const char *name, uint32_t interval, std::function<void()> func);
59
60 bool cancel_interval(Component *component, const std::string &name);
61 bool cancel_interval(Component *component, const char *name);
62 void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
63 std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
64 void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
65 std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
66 bool cancel_retry(Component *component, const std::string &name);
67 bool cancel_retry(Component *component, const char *name);
68
69 // Calculate when the next scheduled item should run
70 // @param now Fresh timestamp from millis() - must not be stale/cached
71 // Returns the time in milliseconds until the next scheduled item, or nullopt if no items
72 // This method performs cleanup of removed items before checking the schedule
73 // IMPORTANT: This method should only be called from the main thread (loop task).
74 optional<uint32_t> next_schedule_in(uint32_t now);
75
76 // Execute all scheduled items that are ready
77 // @param now Fresh timestamp from millis() - must not be stale/cached
78 void call(uint32_t now);
79
80 void process_to_add();
81
82 protected:
83 struct SchedulerItem {
84 // Ordered by size to minimize padding
85 Component *component;
86 uint32_t interval;
87 // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis()
88 // with a 16-bit rollover counter to create a 64-bit time that won't roll over for
89 // billions of years. This ensures correct scheduling even when devices run for months.
90 uint64_t next_execution_;
91
92 // Optimized name storage using tagged union
93 union {
94 const char *static_name; // For string literals (no allocation)
95 char *dynamic_name; // For allocated strings
96 } name_;
97
98 std::function<void()> callback;
99
100#ifdef ESPHOME_THREAD_MULTI_ATOMICS
101 // Multi-threaded with atomics: use atomic for lock-free access
102 // Place atomic<bool> separately since it can't be packed with bit fields
103 std::atomic<bool> remove{false};
104
105 // Bit-packed fields (3 bits used, 5 bits padding in 1 byte)
106 enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
107 bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
108 bool is_retry : 1; // True if this is a retry timeout
109 // 5 bits padding
110#else
111 // Single-threaded or multi-threaded without atomics: can pack all fields together
112 // Bit-packed fields (4 bits used, 4 bits padding in 1 byte)
113 enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
114 bool remove : 1;
115 bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
116 bool is_retry : 1; // True if this is a retry timeout
117 // 4 bits padding
118#endif
119
120 // Constructor
121 SchedulerItem()
122 : component(nullptr),
123 interval(0),
124 next_execution_(0),
125#ifdef ESPHOME_THREAD_MULTI_ATOMICS
126 // remove is initialized in the member declaration as std::atomic<bool>{false}
127 type(TIMEOUT),
128 name_is_dynamic(false),
129 is_retry(false) {
130#else
131 type(TIMEOUT),
132 remove(false),
133 name_is_dynamic(false),
134 is_retry(false) {
135#endif
136 name_.static_name = nullptr;
137 }
138
139 // Destructor to clean up dynamic names
140 ~SchedulerItem() {
141 if (name_is_dynamic) {
142 delete[] name_.dynamic_name;
143 }
144 }
145
146 // Delete copy operations to prevent accidental copies
147 SchedulerItem(const SchedulerItem &) = delete;
148 SchedulerItem &operator=(const SchedulerItem &) = delete;
149
150 // Delete move operations: SchedulerItem objects are only managed via unique_ptr, never moved directly
151 SchedulerItem(SchedulerItem &&) = delete;
152 SchedulerItem &operator=(SchedulerItem &&) = delete;
153
154 // Helper to get the name regardless of storage type
155 const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
156
157 // Helper to set name with proper ownership
158 void set_name(const char *name, bool make_copy = false) {
159 // Clean up old dynamic name if any
160 if (name_is_dynamic && name_.dynamic_name) {
161 delete[] name_.dynamic_name;
162 name_is_dynamic = false;
163 }
164
165 if (!name) {
166 // nullptr case - no name provided
167 name_.static_name = nullptr;
168 } else if (make_copy) {
169 // Make a copy for dynamic strings (including empty strings)
170 size_t len = strlen(name);
171 name_.dynamic_name = new char[len + 1];
172 memcpy(name_.dynamic_name, name, len + 1);
173 name_is_dynamic = true;
174 } else {
175 // Use static string directly (including empty strings)
176 name_.static_name = name;
177 }
178 }
179
180 static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
181 const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
182 const char *get_source() const { return component ? component->get_component_source() : "unknown"; }
183 };
184
185 // Common implementation for both timeout and interval
186 void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
187 uint32_t delay, std::function<void()> func, bool is_retry = false);
188
189 // Common implementation for retry
190 void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
191 uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, float backoff_increase_factor);
192
193 uint64_t millis_64_(uint32_t now);
194 // Cleanup logically deleted items from the scheduler
195 // Returns the number of items remaining after cleanup
196 // IMPORTANT: This method should only be called from the main thread (loop task).
197 size_t cleanup_();
198 void pop_raw_();
199
200 private:
201 // Helper to cancel items by name - must be called with lock held
202 bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type, bool match_retry = false);
203
204 // Helper to extract name as const char* from either static string or std::string
205 inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) {
206 return is_static_string ? static_cast<const char *>(name_ptr) : static_cast<const std::string *>(name_ptr)->c_str();
207 }
208
209 // Common implementation for cancel operations
210 bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
211
212 // Helper function to check if item matches criteria for cancellation
213 inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
214 SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const {
215 if (item->component != component || item->type != type || (skip_removed && item->remove) ||
216 (match_retry && !item->is_retry)) {
217 return false;
218 }
219 const char *item_name = item->get_name();
220 if (item_name == nullptr) {
221 return false;
222 }
223 // Fast path: if pointers are equal
224 // This is effective because the core ESPHome codebase uses static strings (const char*)
225 // for component names. The std::string overloads exist only for compatibility with
226 // external components, but are rarely used in practice.
227 if (item_name == name_cstr) {
228 return true;
229 }
230 // Slow path: compare string contents
231 return strcmp(name_cstr, item_name) == 0;
232 }
233
234 // Helper to execute a scheduler item
235 void execute_item_(SchedulerItem *item, uint32_t now);
236
237 // Helper to check if item should be skipped
238 bool should_skip_item_(const SchedulerItem *item) const {
239 return item->remove || (item->component != nullptr && item->component->is_failed());
240 }
241
242 // Helper to check if item is marked for removal (platform-specific)
243 // Returns true if item should be skipped, handles platform-specific synchronization
244 // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
245 // function.
246 bool is_item_removed_(SchedulerItem *item) const {
247#ifdef ESPHOME_THREAD_MULTI_ATOMICS
248 // Multi-threaded with atomics: use atomic load for lock-free access
249 return item->remove.load(std::memory_order_acquire);
250#else
251 // Single-threaded (ESPHOME_THREAD_SINGLE) or
252 // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct read
253 // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
254 return item->remove;
255#endif
256 }
257
258 // Helper to mark item for removal (platform-specific)
259 // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
260 // function.
261 void mark_item_removed_(SchedulerItem *item) {
262#ifdef ESPHOME_THREAD_MULTI_ATOMICS
263 // Multi-threaded with atomics: use atomic store
264 item->remove.store(true, std::memory_order_release);
265#else
266 // Single-threaded (ESPHOME_THREAD_SINGLE) or
267 // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
268 // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
269 item->remove = true;
270#endif
271 }
272
273 // Template helper to check if any item in a container matches our criteria
274 template<typename Container>
275 bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
276 bool match_retry) const {
277 for (const auto &item : container) {
278 if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
279 /* skip_removed= */ false)) {
280 return true;
281 }
282 }
283 return false;
284 }
285
286 Mutex lock_;
287 std::vector<std::unique_ptr<SchedulerItem>> items_;
288 std::vector<std::unique_ptr<SchedulerItem>> to_add_;
289#ifndef ESPHOME_THREAD_SINGLE
290 // Single-core platforms don't need the defer queue and save 40 bytes of RAM
291 std::deque<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
292#endif /* ESPHOME_THREAD_SINGLE */
293 uint32_t to_remove_{0};
294
295#ifdef ESPHOME_THREAD_MULTI_ATOMICS
296 /*
297 * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
298 *
299 * MEMORY-ORDERING NOTE
300 * --------------------
301 * `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half.
302 * Writers publish `last_millis_` with memory_order_release and readers use
303 * memory_order_acquire. This ensures that once a reader sees the new low word,
304 * it also observes the corresponding increment of `millis_major_`.
305 */
306 std::atomic<uint32_t> last_millis_{0};
307#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
308 // Platforms without atomic support or single-threaded platforms
309 uint32_t last_millis_{0};
310#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
311
312 /*
313 * Upper 16 bits of the 64-bit millis counter. Incremented only while holding
314 * `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race.
315 * Ordering relative to `last_millis_` is provided by its release store and the
316 * corresponding acquire loads.
317 */
318#ifdef ESPHOME_THREAD_MULTI_ATOMICS
319 std::atomic<uint16_t> millis_major_{0};
320#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
321 uint16_t millis_major_{0};
322#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
323};
324
325} // namespace esphome
uint8_t type
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
void retry_handler(const std::shared_ptr< RetryArgs > &args)
uint32_t len