module Ecoportal module API class V1 class Job class Awaiter include Common::Client::TimeOut DELAY_STATUS_CHECK = 4 MIN_STATUS_CHECK = 2 TIMEOUT_APPROACH = :conservative # adaptative timeout TIMEOUT_FALLBACK = :min attr_reader :job, :job_id, :total attr_accessor :timeout_approach def initialize(job, job_id:, total:) @job = job @job_id = job_id @total = total @checked = false self.timeout_approach = self.class::TIMEOUT_APPROACH end # Allows to preserve the learned throughput def new(**kargs) self.class.new(job, **kargs).tap do |out| out.throughput = throughput end end def await_completion! # rubocop:disable Metrics/AbcSize max_timeout = timeout_for(total, approach: timeout_approach) # timeout library is evil. So we make poor-man timeout. # https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/ before = Time.now delay_status_check = nil loop do status = job.status(job_id) waited = Time.now - before adapted = waited adapted = waited - (delay_status_check / 2) if delay_status_check ratio = throughput!(adapted, count: status.progress) break status if status.complete?(total) pending = status.pending(total) left = max_timeout - waited timeout!(status, timeout: max_timeout) unless left.positive? delay_status_check = status_check_in(pending, timeout_in: left) msg = " ... Awaiting #{delay_status_check} s. -- " msg << " TimeOut: #{left.round(2)} s. " msg << "(job '#{job_id}') " msg << "Done: #{status.progress} (est. #{ratio} rec/s) " msg << " \r" print msg $stdout.flush sleep(delay_status_check) end end private def timeout!(status, timeout:) self.timeout_approach = self.class::TIMEOUT_FALLBACK msg = "Job '#{job_id}' not complete (size: #{total}).\n" msg << " Timed out after #{timeout} seconds.\n" msg << " Current status: #{status}" raise API::Errors::TimeOut, msg end def checked? @checked end def status_check_in(pending, timeout_in:) unless checked? @checked = true return min_delay_status_check end return default_delay_status_check if around_min_throughput? eta = eta_for(pending, approach: :optimistic) check_in_max = [eta, timeout_in].min * 0.90 check_in_best = check_in_max / 2.0 default_5 = default_delay_status_check * 5 top_check = [default_5, check_in_best].min.ceil [top_check, min_delay_status_check].max end def default_delay_status_check self.class::DELAY_STATUS_CHECK end def min_delay_status_check self.class::MIN_STATUS_CHECK end end end end end end