1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-30 14:43:51 +00:00

Fix scheduler race conditions and add comprehensive test suite (#9348)

This commit is contained in:
J. Nick Koston
2025-07-07 14:57:55 -05:00
committed by GitHub
parent 138ff749f3
commit 3ef392d433
45 changed files with 2686 additions and 102 deletions

View File

@@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace(
"scheduler_rapid_cancellation_component"
)
SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_(
"SchedulerRapidCancellationComponent", cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -0,0 +1,80 @@
#include "rapid_cancellation_component.h"
#include "esphome/core/log.h"
#include <thread>
#include <vector>
#include <chrono>
#include <random>
#include <sstream>
namespace esphome {
namespace scheduler_rapid_cancellation_component {
static const char *const TAG = "scheduler_rapid_cancellation";
void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); }
void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() {
ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names");
// Reset counters
this->total_scheduled_ = 0;
this->total_executed_ = 0;
static constexpr int NUM_THREADS = 4; // Number of threads to create
static constexpr int NUM_NAMES = 10; // Only 10 unique names
static constexpr int OPERATIONS_PER_THREAD = 100; // Each thread does 100 operations
// Create threads that will all fight over the same timeout names
std::vector<std::thread> threads;
threads.reserve(NUM_THREADS);
for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) {
threads.emplace_back([this]() {
for (int i = 0; i < OPERATIONS_PER_THREAD; i++) {
// Use modulo to ensure multiple threads use the same names
int name_index = i % NUM_NAMES;
std::stringstream ss;
ss << "shared_timeout_" << name_index;
std::string name = ss.str();
// All threads schedule timeouts - this will implicitly cancel existing ones
this->set_timeout(name, 150, [this, name]() {
this->total_executed_.fetch_add(1);
ESP_LOGI(TAG, "Executed callback '%s'", name.c_str());
});
this->total_scheduled_.fetch_add(1);
// Small delay to increase chance of race conditions
if (i % 10 == 0) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
});
}
// Wait for all threads to complete
for (auto &t : threads) {
t.join();
}
ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load());
// Give some time for any remaining callbacks to execute
this->set_timeout("final_timeout", 200, [this]() {
ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:");
ESP_LOGI(TAG, " Total scheduled: %d", this->total_scheduled_.load());
ESP_LOGI(TAG, " Total executed: %d", this->total_executed_.load());
// Calculate implicit cancellations (timeouts replaced when scheduling same name)
int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load();
ESP_LOGI(TAG, " Implicit cancellations (replaced): %d", implicit_cancellations);
ESP_LOGI(TAG, " Total accounted: %d (executed + implicit cancellations)",
this->total_executed_.load() + implicit_cancellations);
// Final message to signal test completion - ensures all stats are logged before test ends
ESP_LOGI(TAG, "Test finished - all statistics reported");
});
}
} // namespace scheduler_rapid_cancellation_component
} // namespace esphome

View File

@@ -0,0 +1,22 @@
#pragma once
#include "esphome/core/component.h"
#include <atomic>
namespace esphome {
namespace scheduler_rapid_cancellation_component {
class SchedulerRapidCancellationComponent : public Component {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::LATE; }
void run_rapid_cancellation_test();
private:
std::atomic<int> total_scheduled_{0};
std::atomic<int> total_executed_{0};
};
} // namespace scheduler_rapid_cancellation_component
} // namespace esphome