# frozen_string_literal: true require 'dynflow/action/timeouts' module Dynflow module Action::Polling def self.included(base) base.send :include, Action::Timeouts end Poll = Algebrick.atom def run(event = nil) case event when nil if external_task resume_external_action else initiate_external_action end when Poll poll_external_task_with_rescue when Action::Timeouts::Timeout process_timeout suspend else raise "unrecognized event #{event}" end done? ? on_finish : suspend_and_ping end def done? raise NotImplementedError end def invoke_external_task raise NotImplementedError end def poll_external_task raise NotImplementedError end def on_finish end # External task data. It should return nil when the task has not # been triggered yet. def external_task output[:task] end def external_task=(external_task_data) output[:task] = external_task_data end # What is the trend in waiting for next polling event. It allows # to strart with frequent polling, but slow down once it's clear this # task will take some time: the idea is we don't care that much in finishing # few seconds sooner, when the task takes orders of minutes/hours. It allows # not overwhelming the backend-servers with useless requests. # By default, it switches to next interval after +attempts_before_next_interval+ number # of attempts def poll_intervals [0.5, 1, 2, 4, 8, 16] end def attempts_before_next_interval 5 end # Returns the time to wait between two polling intervals. def poll_interval interval_level = poll_attempts[:total]/attempts_before_next_interval poll_intervals[interval_level] || poll_intervals.last end # How man times in row should we retry the polling before giving up def poll_max_retries 3 end def initiate_external_action self.external_task = invoke_external_task end def resume_external_action poll_external_task_with_rescue rescue initiate_external_action end def suspend_and_ping plan_event(Poll, poll_interval) suspend end def poll_external_task_with_rescue poll_attempts[:total] += 1 self.external_task = poll_external_task poll_attempts[:failed] = 0 rescue => error poll_attempts[:failed] += 1 rescue_external_task(error) end def poll_attempts output[:poll_attempts] ||= { total: 0, failed: 0 } end def rescue_external_task(error) if poll_attempts[:failed] < poll_max_retries action_logger.warn("Polling failed, attempt no. #{poll_attempts[:failed]}, retrying in #{poll_interval}") action_logger.warn(error) else raise error end end end end