Class: Homebrew::DownloadQueue Private
- Includes:
- Utils::Output::Mixin
- Defined in:
- download_queue.rb
Overview
This class is part of a private API. This class may only be used in the Homebrew/brew repository. Third parties should avoid using this class if possible, as it may be removed or changed without warning.
Manages a queue of concurrent downloads with cooperative cancellation support.
Defined Under Namespace
Classes: Spinner
Instance Attribute Summary collapse
-
#failed_downloads ⇒ Array<Downloadable>
readonly
private
The downloadables that failed in the last
fetch, so callers can retry or skip only the packages that were actually affected.
Instance Method Summary collapse
- #downloads ⇒ Hash{Downloadable => Concurrent::Promises::Future} private
- #enqueue(downloadable, check_attestation: false, stage: pour) ⇒ void private
-
#fetch(only: nil, heading: nil, allow_failures: false) ⇒ void
private
Waits for and reports queued downloads.
- #initialize(retries: 1, force: false, pour: false) ⇒ void constructor private
- #shutdown ⇒ void private
- #stdout_print_and_flush(message) ⇒ void private
- #stdout_print_and_flush_if_tty(message) ⇒ void private
Methods included from Utils::Output::Mixin
#issue_reporting_message, #odebug, #odeprecated, #odie, #odisabled, #ofail, #oh1, #oh1_title, #ohai, #ohai_title, #onoe, #opoo, #opoo_outside_github_actions, #opoo_without_github_actions_annotation, #pretty_deprecated, #pretty_disabled, #pretty_duration, #pretty_install_status, #pretty_installed, #pretty_uninstalled, #pretty_unmarked, #pretty_upgradable, #pretty_warning
Constructor Details
#initialize(retries: 1, force: false, pour: false) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'download_queue.rb', line 23 def initialize(retries: 1, force: false, pour: false) @concurrency = T.let(EnvConfig.download_concurrency, Integer) @quiet = T.let(@concurrency > 1, T::Boolean) @tries = T.let(retries + 1, Integer) @force = force @pour = pour @pool = T.let(Concurrent::FixedThreadPool.new(concurrency), Concurrent::FixedThreadPool) @tty = T.let($stdout.tty?, T::Boolean) @dumb_tty = T.let(ENV["TERM"] == "dumb", T::Boolean) @spinner = T.let(nil, T.nilable(Spinner)) @symlink_targets = T.let({}, T::Hash[Pathname, T::Set[Downloadable]]) @downloads_by_location = T.let({}, T::Hash[Pathname, Concurrent::Promises::Future]) @staged_downloads_by_location = T.let({}, T::Hash[Pathname, Concurrent::Promises::Future]) @cancelled = T.let(Concurrent::AtomicBoolean.new(false), Concurrent::AtomicBoolean) @active_threads = T.let(Concurrent::Set.new, Concurrent::Set) @failed_downloads = T.let([], T::Array[Downloadable]) @deferred_failure_messages = T.let([], T::Array[T.proc.void]) end |
Instance Attribute Details
#failed_downloads ⇒ Array<Downloadable> (readonly)
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
The downloadables that failed in the last fetch, so callers can retry
or skip only the packages that were actually affected.
326 327 328 |
# File 'download_queue.rb', line 326 def failed_downloads @failed_downloads end |
Instance Method Details
#downloads ⇒ Hash{Downloadable => Concurrent::Promises::Future}
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
346 347 348 |
# File 'download_queue.rb', line 346 def downloads @downloads ||= T.let({}, T.nilable(T::Hash[Downloadable, Concurrent::Promises::Future])) end |
#enqueue(downloadable, check_attestation: false, stage: pour) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'download_queue.rb', line 49 def enqueue(downloadable, check_attestation: false, stage: pour) @cancelled.make_false cached_location = downloadable.cached_download @symlink_targets[cached_location] ||= Set.new targets = @symlink_targets.fetch(cached_location) targets << downloadable download = @downloads_by_location[cached_location] ||= Concurrent::Promises.future_on( pool, RetryableDownload.new(downloadable, tries:), @cancelled, force, quiet, check_attestation ) do |download, cancelled, force, quiet, check_attestation| with_active_thread do raise CancelledDownloadError if cancelled.true? download.clear_cache if force if !force && downloadable.downloaded_and_valid? check_bottle_attestation(downloadable, check_attestation:) create_symlinks_for_shared_download(cached_location) next cached_location end downloaded_path = download.fetch(quiet:) raise CancelledDownloadError if cancelled.true? check_bottle_attestation(downloadable, check_attestation:) if downloaded_path != cached_location @symlink_targets[downloaded_path] ||= Set.new @symlink_targets.fetch(downloaded_path).merge(@symlink_targets.fetch(cached_location, Set.new)) end create_symlinks_for_shared_download(downloaded_path) downloaded_path end end downloads[downloadable] = if stage stage_download = lambda do download.then_on( pool, downloadable, pour, @cancelled ) do |downloaded_path, queued_downloadable, queue_pour, cancelled| with_active_thread do raise CancelledDownloadError if cancelled.true? if queued_downloadable.stage_from_download_queue?(downloaded_path, pour: queue_pour) queued_downloadable.extracting! queued_downloadable.stage_from_download_queue(downloaded_path, pour: queue_pour) queued_downloadable.downloaded! end downloaded_path end end end if (staged_location = downloadable.staged_path_from_download_queue) @staged_downloads_by_location[staged_location] ||= stage_download.call else stage_download.call end else download end end |
#fetch(only: nil, heading: nil, allow_failures: false) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
Waits for and reports queued downloads. With only:, limits that to
downloadables of the given class, leaving the rest enqueued and
unreported for a later fetch, e.g. so dependency resolution can wait
on bottle manifests without reporting in-flight bottles before their
downloads heading has been printed. A heading: is printed only when
there is something to report, so every report gets a heading and empty
fetches stay silent. With allow_failures:, failures are still
reported with a ✘ line but neither raise nor mark the fetch or run
as failed, for metadata prefetches such as the bottle manifest of a
version whose bottle has not been published yet, where dependency
resolution just falls back to a full install; known-bad cached files
from checksum mismatches are still removed.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'download_queue.rb', line 128 def fetch(only: nil, heading: nil, allow_failures: false) @failed_downloads = [] @deferred_failure_messages = [] context_before_fetch = Context.current fetchable_downloads = if only downloads.select { |downloadable, _| downloadable.is_a?(only) } else downloads end return if fetchable_downloads.empty? if heading if tty oh1 heading, truncate: false $stdout.flush else # Keep the heading off parsed stdout (e.g. `brew info --json | jq`) # and on the same stream as the non-TTY report lines below. $stderr.puts oh1_title(heading, truncate: false) end end if concurrency == 1 fetchable_downloads.each do |downloadable, promise| promise.wait! rescue CancelledDownloadError next rescue ChecksumMismatchError => e if allow_failures report_tolerated_failure(downloadable) # Remove the known-bad download so it cannot be reused. unlink_cached_download(downloadable) next end @failed_downloads << downloadable ofail "#{downloadable.download_queue_type} reports different checksum: #{e.expected}" rescue raise unless allow_failures report_tolerated_failure(downloadable) end else = fetchable_downloads.keys.map do |download| download..length end.max || 0 remaining_downloads = fetchable_downloads.dup.to_a previous_pending_line_count = 0 max_lines = [concurrency, Tty.height].min resolution = Concurrent::Event.new fetchable_downloads.each_value { |future| future.on_resolution! { resolution.set } } begin stdout_print_and_flush_if_tty Tty.hide_cursor = lambda do |downloadable, future, last| status = status_from_future(future) exception = future.reason if future.rejected? next 1 if exception.is_a?(CancelledDownloadError) = downloadable. if tty_with_cursor_move_support? = (downloadable, future, , ) stdout_print_and_flush "#{status} #{}#{"\n" unless last}" elsif status $stderr.puts "#{status} #{}" end if future.rejected? && allow_failures # Remove known-bad downloads so they cannot be reused, while # staying non-fatal for tolerated metadata prefetches. unlink_cached_download(downloadable) if exception.is_a?(ChecksumMismatchError) elsif future.rejected? if exception.is_a?(ChecksumMismatchError) @failed_downloads << downloadable actual = Digest::SHA256.file(downloadable.cached_download).hexdigest , = (downloadable.download_queue_type) report_or_defer_failure do ofail "#{} #{exception.expected}" puts "#{} #{actual}" end elsif exception.is_a?(CannotInstallFormulaError) unlink_cached_download(downloadable) raise exception elsif bottle_manifest_error?(downloadable, exception) # Fatal: unlike a missing blob (which then fails to stage), a # stale blob would still pour without the manifest tab that # drives relocation, so abort rather than stage a broken keg. raise exception else = if exception.is_a?(DownloadError) && exception.cause.is_a?(ErrorDuringExecution) cause = T.cast(exception.cause, ErrorDuringExecution) if (stderr_output = cause.stderr.presence) "#{stderr_output}#{cause.}" else cause. end else future.reason.to_s end @failed_downloads << downloadable report_or_defer_failure { ofail } end end 1 end until remaining_downloads.empty? begin stdout_print_and_flush_if_tty Tty.begin_synchronized_update finished_states = [:fulfilled, :rejected] finished_downloads, remaining_downloads = remaining_downloads.partition do |_, future| finished_states.include?(future.state) end finished_downloads.each do |downloadable, future| previous_pending_line_count -= 1 .call(downloadable, future, false) stdout_print_and_flush_if_tty Tty.clear_to_end end previous_pending_line_count = 0 remaining_downloads.each_with_index do |(downloadable, future), i| break if previous_pending_line_count >= max_lines last = i == max_lines - 1 || i == remaining_downloads.count - 1 previous_pending_line_count += .call(downloadable, future, last) stdout_print_and_flush_if_tty Tty.clear_to_end end if previous_pending_line_count.positive? if (previous_pending_line_count - 1).zero? stdout_print_and_flush_if_tty Tty.move_cursor_beginning else stdout_print_and_flush_if_tty Tty.move_cursor_up_beginning(previous_pending_line_count - 1) end end stdout_print_and_flush_if_tty Tty.end_synchronized_update next if remaining_downloads.empty? resolution.reset # A download may resolve between the partition above and this # reset: re-check before waiting to avoid a lost wakeup. next if remaining_downloads.any? { |_, future| finished_states.include?(future.state) } # Wake as soon as any download resolves; the timeout only sets # the redraw cadence for spinner and progress bars on TTYs. resolution.wait(tty_with_cursor_move_support? ? 0.05 : 1) # `Interrupt` inherits from `Exception`, so rescue it to restore the TTY. rescue Exception # rubocop:disable Lint/RescueException if previous_pending_line_count.positive? stdout_print_and_flush_if_tty Tty.move_cursor_down(previous_pending_line_count - 1) end raise end end ensure stdout_print_and_flush_if_tty Tty.end_synchronized_update stdout_print_and_flush_if_tty Tty.show_cursor @deferred_failure_messages.each(&:call) end end # `Interrupt` inherits from `Exception`, so rescue it to cancel active workers # even when it arrives before fetch setup completes. rescue Exception # rubocop:disable Lint/RescueException cancel raise ensure # Restore the pre-parallel fetch context to avoid quiet state bleeding out # from threads, and clear queue state even when a fatal download error # aborts the fetch above. Context.current = context_before_fetch if context_before_fetch if only # Keep unfetched downloads (and their location dedup entries) queued # for the next fetch. fetchable_downloads.each_key { |downloadable| downloads.delete(downloadable) } fetched_staging = fetchable_downloads.each_value.to_set @staged_downloads_by_location.delete_if { |_, future| fetched_staging.include?(future) } else downloads.clear @downloads_by_location.clear @staged_downloads_by_location.clear @symlink_targets.clear end end |
#shutdown ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
340 341 342 343 |
# File 'download_queue.rb', line 340 def shutdown pool.shutdown pool.wait_for_termination end |
#stdout_print_and_flush(message) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
334 335 336 337 |
# File 'download_queue.rb', line 334 def stdout_print_and_flush() $stdout.print() $stdout.flush end |
#stdout_print_and_flush_if_tty(message) ⇒ void
This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.
This method returns an undefined value.
329 330 331 |
# File 'download_queue.rb', line 329 def stdout_print_and_flush_if_tty() stdout_print_and_flush() if tty_with_cursor_move_support? end |