mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 06:33:51 +00: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:
		| @@ -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_(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user