[core] RAII guard for component loop phase (#15897)

This commit is contained in:
J. Nick Koston
2026-04-22 14:04:29 +02:00
committed by GitHub
parent f6bf6dc8e5
commit c399cd2fa2
2 changed files with 29 additions and 17 deletions

View File

@@ -95,16 +95,16 @@ void Application::setup() {
// interrupts during setup. During setup we always run the component
// phase (no loop_interval_ gate), so call both helpers unconditionally.
this->scheduler_tick_(MillisInternal::get());
this->before_component_phase_();
{
ComponentPhaseGuard phase_guard{*this};
for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component
this->loop_component_start_time_ = MillisInternal::get();
this->components_[j]->call();
this->feed_wdt();
for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component
this->loop_component_start_time_ = MillisInternal::get();
this->components_[j]->call();
this->feed_wdt();
}
}
this->after_component_phase_();
yield();
} while (!component->can_proceed() && !component->is_failed());
}

View File

@@ -425,8 +425,20 @@ class Application {
void enable_pending_loops_();
void activate_looping_component_(uint16_t index);
inline uint32_t ESPHOME_ALWAYS_INLINE scheduler_tick_(uint32_t now);
inline void ESPHOME_ALWAYS_INLINE before_component_phase_();
inline void ESPHOME_ALWAYS_INLINE after_component_phase_() { this->in_loop_ = false; }
// RAII guard for a component loop phase. Constructor processes any pending
// enable_loop requests from ISRs and marks in_loop_ so reentrant
// modifications during component.loop() are safe; destructor clears in_loop_.
class ComponentPhaseGuard {
public:
inline ESPHOME_ALWAYS_INLINE explicit ComponentPhaseGuard(Application &app);
inline ESPHOME_ALWAYS_INLINE ~ComponentPhaseGuard() { this->app_.in_loop_ = false; }
ComponentPhaseGuard(const ComponentPhaseGuard &) = delete;
ComponentPhaseGuard &operator=(const ComponentPhaseGuard &) = delete;
private:
Application &app_;
};
/// Process dump_config output one component per loop iteration.
/// Extracted from loop() to keep cold startup/reconnect logging out of the hot path.
@@ -595,10 +607,10 @@ inline uint32_t ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now)
// Phase B entry: only invoked when a component loop phase is about to run.
// Processes pending enable_loop requests from ISRs and marks in_loop_ so
// reentrant modifications during component.loop() are safe.
inline void ESPHOME_ALWAYS_INLINE Application::before_component_phase_() {
inline ESPHOME_ALWAYS_INLINE Application::ComponentPhaseGuard::ComponentPhaseGuard(Application &app) : app_(app) {
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
if (this->has_pending_enable_loop_requests_) {
if (this->app_.has_pending_enable_loop_requests_) {
// Clear flag BEFORE processing to avoid race condition
// If ISR sets it during processing, we'll catch it next loop iteration
// This is safe because:
@@ -606,12 +618,12 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_component_phase_() {
// 2. If we can't process a component (wrong state), enable_pending_loops_()
// will set this flag back to true
// 3. Any new ISR requests during processing will set the flag again
this->has_pending_enable_loop_requests_ = false;
this->enable_pending_loops_();
this->app_.has_pending_enable_loop_requests_ = false;
this->app_.enable_pending_loops_();
}
// Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
this->app_.in_loop_ = true;
}
inline void ESPHOME_ALWAYS_INLINE Application::loop() {
@@ -665,7 +677,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
const bool do_component_phase = high_frequency || woke || (elapsed >= this->loop_interval_);
if (do_component_phase) {
this->before_component_phase_();
ComponentPhaseGuard phase_guard{*this};
uint32_t last_op_end_time = now;
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
@@ -690,7 +702,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
#endif
this->last_loop_ = last_op_end_time;
now = last_op_end_time;
this->after_component_phase_();
// phase_guard destructor clears in_loop_ at scope exit
}
#ifdef USE_RUNTIME_STATS