mirror of
https://github.com/esphome/esphome.git
synced 2024-11-13 02:37:47 +01:00
Add retry handler (#2721)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
This commit is contained in:
parent
8267f01ccd
commit
448e1690aa
4 changed files with 122 additions and 15 deletions
|
@ -55,6 +55,15 @@ bool Component::cancel_interval(const std::string &name) { // NOLINT
|
||||||
return App.scheduler.cancel_interval(this, name);
|
return App.scheduler.cancel_interval(this, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Component::set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||||
|
std::function<RetryResult()> &&f, float backoff_increase_factor) { // NOLINT
|
||||||
|
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Component::cancel_retry(const std::string &name) { // NOLINT
|
||||||
|
return App.scheduler.cancel_retry(this, name);
|
||||||
|
}
|
||||||
|
|
||||||
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
||||||
return App.scheduler.set_timeout(this, name, timeout, std::move(f));
|
return App.scheduler.set_timeout(this, name, timeout, std::move(f));
|
||||||
}
|
}
|
||||||
|
@ -120,6 +129,10 @@ void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // N
|
||||||
void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // NOLINT
|
void Component::set_interval(uint32_t interval, std::function<void()> &&f) { // NOLINT
|
||||||
App.scheduler.set_interval(this, "", interval, std::move(f));
|
App.scheduler.set_interval(this, "", interval, std::move(f));
|
||||||
}
|
}
|
||||||
|
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult()> &&f,
|
||||||
|
float backoff_increase_factor) { // NOLINT
|
||||||
|
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||||
|
}
|
||||||
bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||||
bool Component::can_proceed() { return true; }
|
bool Component::can_proceed() { return true; }
|
||||||
bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; }
|
bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; }
|
||||||
|
|
|
@ -61,6 +61,8 @@ extern const uint32_t STATUS_LED_OK;
|
||||||
extern const uint32_t STATUS_LED_WARNING;
|
extern const uint32_t STATUS_LED_WARNING;
|
||||||
extern const uint32_t STATUS_LED_ERROR;
|
extern const uint32_t STATUS_LED_ERROR;
|
||||||
|
|
||||||
|
enum RetryResult { DONE, RETRY };
|
||||||
|
|
||||||
class Component {
|
class Component {
|
||||||
public:
|
public:
|
||||||
/** Where the component's initialization should happen.
|
/** Where the component's initialization should happen.
|
||||||
|
@ -180,7 +182,35 @@ class Component {
|
||||||
*/
|
*/
|
||||||
bool cancel_interval(const std::string &name); // NOLINT
|
bool cancel_interval(const std::string &name); // NOLINT
|
||||||
|
|
||||||
void set_timeout(uint32_t timeout, std::function<void()> &&f); // NOLINT
|
/** Set an retry function with a unique name. Empty name means no cancelling possible.
|
||||||
|
*
|
||||||
|
* This will call f. If f returns RetryResult::RETRY f is called again after initial_wait_time ms.
|
||||||
|
* f should return RetryResult::DONE if no repeat is required. The initial wait time will be increased
|
||||||
|
* by backoff_increase_factor for each iteration. Default is doubling the time between iterations
|
||||||
|
* Can be cancelled via cancel_retry().
|
||||||
|
*
|
||||||
|
* IMPORTANT: Do not rely on this having correct timing. This is only called from
|
||||||
|
* loop() and therefore can be significantly delayed.
|
||||||
|
*
|
||||||
|
* @param name The identifier for this retry function.
|
||||||
|
* @param initial_wait_time The time in ms before f is called again
|
||||||
|
* @param max_attempts The maximum number of retries
|
||||||
|
* @param f The function (or lambda) that should be called
|
||||||
|
* @param backoff_increase_factor time between retries is increased by this factor on every retry
|
||||||
|
* @see cancel_retry()
|
||||||
|
*/
|
||||||
|
void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
|
||||||
|
std::function<RetryResult()> &&f, float backoff_increase_factor = 1.0f); // NOLINT
|
||||||
|
|
||||||
|
void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult()> &&f, // NOLINT
|
||||||
|
float backoff_increase_factor = 1.0f); // NOLINT
|
||||||
|
|
||||||
|
/** Cancel a retry function.
|
||||||
|
*
|
||||||
|
* @param name The identifier for this retry function.
|
||||||
|
* @return Whether a retry function was deleted.
|
||||||
|
*/
|
||||||
|
bool cancel_retry(const std::string &name); // NOLINT
|
||||||
|
|
||||||
/** Set a timeout function with a unique name.
|
/** Set a timeout function with a unique name.
|
||||||
*
|
*
|
||||||
|
@ -198,6 +228,8 @@ class Component {
|
||||||
*/
|
*/
|
||||||
void set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f); // NOLINT
|
void set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f); // NOLINT
|
||||||
|
|
||||||
|
void set_timeout(uint32_t timeout, std::function<void()> &&f); // NOLINT
|
||||||
|
|
||||||
/** Cancel a timeout function.
|
/** Cancel a timeout function.
|
||||||
*
|
*
|
||||||
* @param name The identifier for this timeout function.
|
* @param name The identifier for this timeout function.
|
||||||
|
|
|
@ -32,7 +32,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u
|
||||||
item->timeout = timeout;
|
item->timeout = timeout;
|
||||||
item->last_execution = now;
|
item->last_execution = now;
|
||||||
item->last_execution_major = this->millis_major_;
|
item->last_execution_major = this->millis_major_;
|
||||||
item->f = std::move(func);
|
item->void_callback = std::move(func);
|
||||||
item->remove = false;
|
item->remove = false;
|
||||||
this->push_(std::move(item));
|
this->push_(std::move(item));
|
||||||
}
|
}
|
||||||
|
@ -65,13 +65,47 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name,
|
||||||
item->last_execution_major = this->millis_major_;
|
item->last_execution_major = this->millis_major_;
|
||||||
if (item->last_execution > now)
|
if (item->last_execution > now)
|
||||||
item->last_execution_major--;
|
item->last_execution_major--;
|
||||||
item->f = std::move(func);
|
item->void_callback = std::move(func);
|
||||||
item->remove = false;
|
item->remove = false;
|
||||||
this->push_(std::move(item));
|
this->push_(std::move(item));
|
||||||
}
|
}
|
||||||
bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
|
bool HOT Scheduler::cancel_interval(Component *component, const std::string &name) {
|
||||||
return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
|
return this->cancel_item_(component, name, SchedulerItem::INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time,
|
||||||
|
uint8_t max_attempts, std::function<RetryResult()> &&func,
|
||||||
|
float backoff_increase_factor) {
|
||||||
|
const uint32_t now = this->millis_();
|
||||||
|
|
||||||
|
if (!name.empty())
|
||||||
|
this->cancel_retry(component, name);
|
||||||
|
|
||||||
|
if (initial_wait_time == SCHEDULER_DONT_RUN)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u,max_attempts=%u, backoff_factor=%0.1f)", name.c_str(),
|
||||||
|
initial_wait_time, max_attempts, backoff_increase_factor);
|
||||||
|
|
||||||
|
auto item = make_unique<SchedulerItem>();
|
||||||
|
item->component = component;
|
||||||
|
item->name = name;
|
||||||
|
item->type = SchedulerItem::RETRY;
|
||||||
|
item->interval = initial_wait_time;
|
||||||
|
item->retry_countdown = max_attempts;
|
||||||
|
item->backoff_multiplier = backoff_increase_factor;
|
||||||
|
item->last_execution = now - initial_wait_time;
|
||||||
|
item->last_execution_major = this->millis_major_;
|
||||||
|
if (item->last_execution > now)
|
||||||
|
item->last_execution_major--;
|
||||||
|
item->retry_callback = std::move(func);
|
||||||
|
item->remove = false;
|
||||||
|
this->push_(std::move(item));
|
||||||
|
}
|
||||||
|
bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) {
|
||||||
|
return this->cancel_item_(component, name, SchedulerItem::RETRY);
|
||||||
|
}
|
||||||
|
|
||||||
optional<uint32_t> HOT Scheduler::next_schedule_in() {
|
optional<uint32_t> HOT Scheduler::next_schedule_in() {
|
||||||
if (this->empty_())
|
if (this->empty_())
|
||||||
return {};
|
return {};
|
||||||
|
@ -95,10 +129,9 @@ void IRAM_ATTR HOT Scheduler::call() {
|
||||||
ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now);
|
ESP_LOGVV(TAG, "Items: count=%u, now=%u", this->items_.size(), now);
|
||||||
while (!this->empty_()) {
|
while (!this->empty_()) {
|
||||||
auto item = std::move(this->items_[0]);
|
auto item = std::move(this->items_[0]);
|
||||||
const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout";
|
ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", item->get_type_str(),
|
||||||
ESP_LOGVV(TAG, " %s '%s' interval=%u last_execution=%u (%u) next=%u (%u)", type, item->name.c_str(),
|
item->name.c_str(), item->interval, item->last_execution, item->last_execution_major,
|
||||||
item->interval, item->last_execution, item->last_execution_major, item->next_execution(),
|
item->next_execution(), item->next_execution_major());
|
||||||
item->next_execution_major());
|
|
||||||
|
|
||||||
this->pop_raw_();
|
this->pop_raw_();
|
||||||
old_items.push_back(std::move(item));
|
old_items.push_back(std::move(item));
|
||||||
|
@ -129,6 +162,7 @@ void IRAM_ATTR HOT Scheduler::call() {
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!this->empty_()) {
|
while (!this->empty_()) {
|
||||||
|
RetryResult retry_result = RETRY;
|
||||||
// use scoping to indicate visibility of `item` variable
|
// use scoping to indicate visibility of `item` variable
|
||||||
{
|
{
|
||||||
// Don't copy-by value yet
|
// Don't copy-by value yet
|
||||||
|
@ -147,17 +181,19 @@ void IRAM_ATTR HOT Scheduler::call() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||||
const char *type = item->type == SchedulerItem::INTERVAL ? "interval" : "timeout";
|
ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", item->get_type_str(),
|
||||||
ESP_LOGVV(TAG, "Running %s '%s' with interval=%u last_execution=%u (now=%u)", type, item->name.c_str(),
|
item->name.c_str(), item->interval, item->last_execution, now);
|
||||||
item->interval, item->last_execution, now);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Warning: During f(), a lot of stuff can happen, including:
|
// Warning: During callback(), a lot of stuff can happen, including:
|
||||||
// - timeouts/intervals get added, potentially invalidating vector pointers
|
// - timeouts/intervals get added, potentially invalidating vector pointers
|
||||||
// - timeouts/intervals get cancelled
|
// - timeouts/intervals get cancelled
|
||||||
{
|
{
|
||||||
WarnIfComponentBlockingGuard guard{item->component};
|
WarnIfComponentBlockingGuard guard{item->component};
|
||||||
item->f();
|
if (item->type == SchedulerItem::RETRY)
|
||||||
|
retry_result = item->retry_callback();
|
||||||
|
else
|
||||||
|
item->void_callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,13 +211,16 @@ void IRAM_ATTR HOT Scheduler::call() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item->type == SchedulerItem::INTERVAL) {
|
if (item->type == SchedulerItem::INTERVAL ||
|
||||||
|
(item->type == SchedulerItem::RETRY && (--item->retry_countdown > 0 && retry_result != RetryResult::DONE))) {
|
||||||
if (item->interval != 0) {
|
if (item->interval != 0) {
|
||||||
const uint32_t before = item->last_execution;
|
const uint32_t before = item->last_execution;
|
||||||
const uint32_t amount = (now - item->last_execution) / item->interval;
|
const uint32_t amount = (now - item->last_execution) / item->interval;
|
||||||
item->last_execution += amount * item->interval;
|
item->last_execution += amount * item->interval;
|
||||||
if (item->last_execution < before)
|
if (item->last_execution < before)
|
||||||
item->last_execution_major++;
|
item->last_execution_major++;
|
||||||
|
if (item->type == SchedulerItem::RETRY)
|
||||||
|
item->interval *= item->backoff_multiplier;
|
||||||
}
|
}
|
||||||
this->push_(std::move(item));
|
this->push_(std::move(item));
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ class Scheduler {
|
||||||
void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> &&func);
|
void set_interval(Component *component, const std::string &name, uint32_t interval, std::function<void()> &&func);
|
||||||
bool cancel_interval(Component *component, const std::string &name);
|
bool cancel_interval(Component *component, const std::string &name);
|
||||||
|
|
||||||
|
void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
|
||||||
|
std::function<RetryResult()> &&func, float backoff_increase_factor = 1.0f);
|
||||||
|
bool cancel_retry(Component *component, const std::string &name);
|
||||||
|
|
||||||
optional<uint32_t> next_schedule_in();
|
optional<uint32_t> next_schedule_in();
|
||||||
|
|
||||||
void call();
|
void call();
|
||||||
|
@ -25,13 +29,20 @@ class Scheduler {
|
||||||
struct SchedulerItem {
|
struct SchedulerItem {
|
||||||
Component *component;
|
Component *component;
|
||||||
std::string name;
|
std::string name;
|
||||||
enum Type { TIMEOUT, INTERVAL } type;
|
enum Type { TIMEOUT, INTERVAL, RETRY } type;
|
||||||
union {
|
union {
|
||||||
uint32_t interval;
|
uint32_t interval;
|
||||||
uint32_t timeout;
|
uint32_t timeout;
|
||||||
};
|
};
|
||||||
uint32_t last_execution;
|
uint32_t last_execution;
|
||||||
std::function<void()> f;
|
// Ideally this should be a union or std::variant
|
||||||
|
// but unions don't work with object like std::function
|
||||||
|
// union CallBack_{
|
||||||
|
std::function<void()> void_callback;
|
||||||
|
std::function<RetryResult()> retry_callback;
|
||||||
|
// };
|
||||||
|
uint8_t retry_countdown{3};
|
||||||
|
float backoff_multiplier{1.0f};
|
||||||
bool remove;
|
bool remove;
|
||||||
uint8_t last_execution_major;
|
uint8_t last_execution_major;
|
||||||
|
|
||||||
|
@ -45,6 +56,18 @@ class Scheduler {
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
|
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
|
||||||
|
const char *get_type_str() {
|
||||||
|
switch (this->type) {
|
||||||
|
case SchedulerItem::INTERVAL:
|
||||||
|
return "interval";
|
||||||
|
case SchedulerItem::RETRY:
|
||||||
|
return "retry";
|
||||||
|
case SchedulerItem::TIMEOUT:
|
||||||
|
return "timeout";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
uint32_t millis_();
|
uint32_t millis_();
|
||||||
|
|
Loading…
Reference in a new issue