brew/Library/Homebrew/github_runner_matrix.rb
Carlo Cabrera a1853fc975
github_runner_matrix: extend timeout for dependent tests on arm64
We give our ARM runners half the timeout of the Intel runners because
the ARM runners are faster. However, this is no longer true for
dependent testing because we test recursive dependents on ARM but skip
them on Intel.

This means that we can often hit the timeout on ARM but have all jobs
finish on Intel. We can re-run these with the long build label, but
that's a bit wasteful of our limited long build slots.

Instead, let's just use the same timeout value across ARM and Intel
runners when testing dependents: 2 hours.
2024-11-01 12:26:50 +08:00

267 lines
9.4 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "test_runner_formula"
require "github_runner"
class GitHubRunnerMatrix
RunnerSpec = T.type_alias { T.any(LinuxRunnerSpec, MacOSRunnerSpec) }
private_constant :RunnerSpec
MacOSRunnerSpecHash = T.type_alias do
{
name: String,
runner: String,
timeout: Integer,
cleanup: T::Boolean,
testing_formulae: String,
}
end
private_constant :MacOSRunnerSpecHash
LinuxRunnerSpecHash = T.type_alias do
{
name: String,
runner: String,
container: T::Hash[Symbol, String],
workdir: String,
timeout: Integer,
cleanup: T::Boolean,
testing_formulae: String,
}
end
private_constant :LinuxRunnerSpecHash
RunnerSpecHash = T.type_alias { T.any(LinuxRunnerSpecHash, MacOSRunnerSpecHash) }
private_constant :RunnerSpecHash
sig { returns(T::Array[GitHubRunner]) }
attr_reader :runners
sig {
params(
testing_formulae: T::Array[TestRunnerFormula],
deleted_formulae: T::Array[String],
all_supported: T::Boolean,
dependent_matrix: T::Boolean,
).void
}
def initialize(testing_formulae, deleted_formulae, all_supported:, dependent_matrix:)
if all_supported && (testing_formulae.present? || deleted_formulae.present? || dependent_matrix)
raise ArgumentError, "all_supported is mutually exclusive to other arguments"
end
@testing_formulae = T.let(testing_formulae, T::Array[TestRunnerFormula])
@deleted_formulae = T.let(deleted_formulae, T::Array[String])
@all_supported = T.let(all_supported, T::Boolean)
@dependent_matrix = T.let(dependent_matrix, T::Boolean)
@compatible_testing_formulae = T.let({}, T::Hash[GitHubRunner, T::Array[TestRunnerFormula]])
@formulae_with_untested_dependents = T.let({}, T::Hash[GitHubRunner, T::Array[TestRunnerFormula]])
@runners = T.let([], T::Array[GitHubRunner])
generate_runners!
freeze
end
sig { returns(T::Array[RunnerSpecHash]) }
def active_runner_specs_hash
runners.select(&:active)
.map(&:spec)
.map(&:to_h)
end
private
SELF_HOSTED_LINUX_RUNNER = "linux-self-hosted-1"
# ARM macOS timeout, keep this under 1/2 of GitHub's job execution time limit for self-hosted runners.
# https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#usage-limits
GITHUB_ACTIONS_LONG_TIMEOUT = 2160 # 36 hours
GITHUB_ACTIONS_SHORT_TIMEOUT = 60
sig { returns(LinuxRunnerSpec) }
def linux_runner_spec
linux_runner = ENV.fetch("HOMEBREW_LINUX_RUNNER")
LinuxRunnerSpec.new(
name: "Linux",
runner: linux_runner,
container: {
image: "ghcr.io/homebrew/ubuntu22.04:master",
options: "--user=linuxbrew -e GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED",
},
workdir: "/github/home",
timeout: GITHUB_ACTIONS_LONG_TIMEOUT,
cleanup: linux_runner == SELF_HOSTED_LINUX_RUNNER,
)
end
VALID_PLATFORMS = T.let([:macos, :linux].freeze, T::Array[Symbol])
VALID_ARCHES = T.let([:arm64, :x86_64].freeze, T::Array[Symbol])
sig {
params(
platform: Symbol,
arch: Symbol,
spec: RunnerSpec,
macos_version: T.nilable(MacOSVersion),
).returns(GitHubRunner)
}
def create_runner(platform, arch, spec, macos_version = nil)
raise "Unexpected platform: #{platform}" if VALID_PLATFORMS.exclude?(platform)
raise "Unexpected arch: #{arch}" if VALID_ARCHES.exclude?(arch)
runner = GitHubRunner.new(platform:, arch:, spec:, macos_version:)
runner.spec.testing_formulae += testable_formulae(runner)
runner.active = active_runner?(runner)
runner.freeze
end
NEWEST_HOMEBREW_CORE_MACOS_RUNNER = :sequoia
OLDEST_HOMEBREW_CORE_MACOS_RUNNER = :ventura
NEWEST_HOMEBREW_CORE_INTEL_MACOS_RUNNER = :sonoma
sig { params(macos_version: MacOSVersion).returns(T::Boolean) }
def runner_enabled?(macos_version)
macos_version <= NEWEST_HOMEBREW_CORE_MACOS_RUNNER && macos_version >= OLDEST_HOMEBREW_CORE_MACOS_RUNNER
end
NEWEST_GITHUB_ACTIONS_INTEL_MACOS_RUNNER = :ventura
OLDEST_GITHUB_ACTIONS_INTEL_MACOS_RUNNER = :monterey
NEWEST_GITHUB_ACTIONS_ARM_MACOS_RUNNER = :sequoia
OLDEST_GITHUB_ACTIONS_ARM_MACOS_RUNNER = :sonoma
GITHUB_ACTIONS_RUNNER_TIMEOUT = 360
sig { void }
def generate_runners!
return if @runners.present?
if !@all_supported || ENV.key?("HOMEBREW_LINUX_RUNNER")
@runners << create_runner(:linux, :x86_64, linux_runner_spec)
end
github_run_id = ENV.fetch("GITHUB_RUN_ID")
long_timeout = ENV.fetch("HOMEBREW_MACOS_LONG_TIMEOUT", "false") == "true"
use_github_runner = ENV.fetch("HOMEBREW_MACOS_BUILD_ON_GITHUB_RUNNER", "false") == "true"
runner_timeout = long_timeout ? GITHUB_ACTIONS_LONG_TIMEOUT : GITHUB_ACTIONS_SHORT_TIMEOUT
# Use GitHub Actions macOS Runner for testing dependents if compatible with timeout.
use_github_runner ||= @dependent_matrix
use_github_runner &&= runner_timeout <= GITHUB_ACTIONS_RUNNER_TIMEOUT
ephemeral_suffix = "-#{github_run_id}"
ephemeral_suffix << "-deps" if @dependent_matrix
ephemeral_suffix << "-long" if runner_timeout == GITHUB_ACTIONS_LONG_TIMEOUT
ephemeral_suffix.freeze
MacOSVersion::SYMBOLS.each_value do |version|
macos_version = MacOSVersion.new(version)
next unless runner_enabled?(macos_version)
github_runner_available = macos_version <= NEWEST_GITHUB_ACTIONS_ARM_MACOS_RUNNER &&
macos_version >= OLDEST_GITHUB_ACTIONS_ARM_MACOS_RUNNER
runner, timeout = if use_github_runner && github_runner_available
["macos-#{version}", GITHUB_ACTIONS_RUNNER_TIMEOUT]
elsif macos_version >= :monterey
["#{version}-arm64#{ephemeral_suffix}", runner_timeout]
else
["#{version}-arm64", runner_timeout]
end
# We test recursive dependents on ARM macOS, so they can be slower than our Intel runners.
timeout *= 2 if @dependent_matrix && timeout < GITHUB_ACTIONS_RUNNER_TIMEOUT
spec = MacOSRunnerSpec.new(
name: "macOS #{version}-arm64",
runner:,
timeout:,
cleanup: !runner.end_with?(ephemeral_suffix),
)
@runners << create_runner(:macos, :arm64, spec, macos_version)
next if !@all_supported && macos_version > NEWEST_HOMEBREW_CORE_INTEL_MACOS_RUNNER
github_runner_available = macos_version <= NEWEST_GITHUB_ACTIONS_INTEL_MACOS_RUNNER &&
macos_version >= OLDEST_GITHUB_ACTIONS_INTEL_MACOS_RUNNER
runner, timeout = if use_github_runner && github_runner_available
["macos-#{version}", GITHUB_ACTIONS_RUNNER_TIMEOUT]
else
["#{version}-x86_64#{ephemeral_suffix}", runner_timeout]
end
# macOS 12-x86_64 is usually slower.
timeout += 30 if macos_version <= :monterey
# The ARM runners are typically over twice as fast as the Intel runners.
timeout *= 2 if !(use_github_runner && github_runner_available) && timeout < GITHUB_ACTIONS_LONG_TIMEOUT
spec = MacOSRunnerSpec.new(
name: "macOS #{version}-x86_64",
runner:,
timeout:,
cleanup: !runner.end_with?(ephemeral_suffix),
)
@runners << create_runner(:macos, :x86_64, spec, macos_version)
end
@runners.freeze
end
sig { params(runner: GitHubRunner).returns(T::Array[String]) }
def testable_formulae(runner)
formulae = if @dependent_matrix
formulae_with_untested_dependents(runner)
else
compatible_testing_formulae(runner)
end
formulae.map(&:name)
end
sig { params(runner: GitHubRunner).returns(T::Boolean) }
def active_runner?(runner)
return true if @all_supported
return true if @deleted_formulae.present? && !@dependent_matrix
runner.spec.testing_formulae.present?
end
sig { params(runner: GitHubRunner).returns(T::Array[TestRunnerFormula]) }
def compatible_testing_formulae(runner)
@compatible_testing_formulae[runner] ||= begin
platform = runner.platform
arch = runner.arch
macos_version = runner.macos_version
@testing_formulae.select do |formula|
next false if macos_version && !formula.compatible_with?(macos_version)
formula.public_send(:"#{platform}_compatible?") &&
formula.public_send(:"#{arch}_compatible?")
end
end
end
sig { params(runner: GitHubRunner).returns(T::Array[TestRunnerFormula]) }
def formulae_with_untested_dependents(runner)
@formulae_with_untested_dependents[runner] ||= begin
platform = runner.platform
arch = runner.arch
macos_version = runner.macos_version
compatible_testing_formulae(runner).select do |formula|
compatible_dependents = formula.dependents(platform:, arch:, macos_version: macos_version&.to_sym)
.select do |dependent_f|
next false if macos_version && !dependent_f.compatible_with?(macos_version)
dependent_f.public_send(:"#{platform}_compatible?") &&
dependent_f.public_send(:"#{arch}_compatible?")
end
# These arrays will generally have been generated by different Formulary caches,
# so we can only compare them by name and not directly.
(compatible_dependents.map(&:name) - @testing_formulae.map(&:name)).present?
end
end
end
end