@@ -64,6 +64,7 @@ def initialize(testing_formulae, deleted_formulae, all_supported:, dependent_mat
6464 @dependent_matrix = dependent_matrix
6565 @compatible_testing_formulae = T . let ( { } , T ::Hash [ GitHubRunner , T ::Array [ TestRunnerFormula ] ] )
6666 @formulae_with_untested_dependents = T . let ( { } , T ::Hash [ GitHubRunner , T ::Array [ TestRunnerFormula ] ] )
67+ @compatible_untested_dependent_names = T . let ( { } , T ::Hash [ GitHubRunner , T ::Array [ String ] ] )
6768
6869 @runners = T . let ( [ ] , T ::Array [ GitHubRunner ] )
6970 generate_runners!
@@ -73,19 +74,41 @@ def initialize(testing_formulae, deleted_formulae, all_supported:, dependent_mat
7374
7475 sig { returns ( T ::Array [ RunnerSpecHash ] ) }
7576 def active_runner_specs_hash
76- runners . select ( &:active )
77- . map ( &:spec )
78- . map ( &:to_h )
77+ runners . select ( &:active ) . flat_map do |runner |
78+ selected_runner_count = selected_runner_count_for ( runner )
79+ runner_spec_hash = runner . spec . to_h
80+ name = runner_spec_hash . fetch ( :name )
81+
82+ Array . new ( selected_runner_count ) do |dependent_shard_index |
83+ runner_spec_hash . merge (
84+ name : (
85+ if selected_runner_count > 1
86+ "#{ name } (shard #{ dependent_shard_index + 1 } /#{ selected_runner_count } )"
87+ else
88+ name
89+ end
90+ ) ,
91+ dependent_shard_index : dependent_shard_index ,
92+ dependent_shard_total : selected_runner_count ,
93+ )
94+ end
95+ end
7996 end
8097
8198 private
8299
83100 SELF_HOSTED_LINUX_RUNNER = "linux-self-hosted-1"
101+ DEPS_SHARDING_ENV = "HOMEBREW_DEPS_SHARDING"
102+ DEPS_SHARD_MAX_RUNNERS_ENV = "HOMEBREW_DEPS_SHARD_MAX_RUNNERS"
103+ DEPS_SHARD_BASE_THRESHOLD_ENV = "HOMEBREW_DEPS_SHARD_BASE_THRESHOLD"
104+ DEPS_SHARD_RUNNER_PENALTY_ENV = "HOMEBREW_DEPS_SHARD_RUNNER_PENALTY"
84105 # ARM macOS timeout, keep this under 1/2 of GitHub's job execution time limit for self-hosted runners.
85106 # https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#usage-limits
86107 GITHUB_ACTIONS_LONG_TIMEOUT = 2160 # 36 hours
87108 GITHUB_ACTIONS_SHORT_TIMEOUT = 60
88- private_constant :SELF_HOSTED_LINUX_RUNNER , :GITHUB_ACTIONS_LONG_TIMEOUT , :GITHUB_ACTIONS_SHORT_TIMEOUT
109+ private_constant :SELF_HOSTED_LINUX_RUNNER , :DEPS_SHARDING_ENV , :DEPS_SHARD_MAX_RUNNERS_ENV ,
110+ :DEPS_SHARD_BASE_THRESHOLD_ENV , :DEPS_SHARD_RUNNER_PENALTY_ENV ,
111+ :GITHUB_ACTIONS_LONG_TIMEOUT , :GITHUB_ACTIONS_SHORT_TIMEOUT
89112
90113 sig { params ( arch : Symbol ) . returns ( LinuxRunnerSpec ) }
91114 def linux_runner_spec ( arch )
@@ -261,6 +284,71 @@ def generate_runners!
261284 @runners . freeze
262285 end
263286
287+ sig { params ( runner : GitHubRunner ) . returns ( Integer ) }
288+ def selected_runner_count_for ( runner )
289+ return 1 unless @dependent_matrix
290+ return 1 unless %w[ 1 true ] . include? ( ENV . fetch ( DEPS_SHARDING_ENV , "false" ) . downcase )
291+
292+ max_available_runners = T . must ( sharding_integer_env ( DEPS_SHARD_MAX_RUNNERS_ENV , runner , 1 ) )
293+ raise ArgumentError , "#{ DEPS_SHARD_MAX_RUNNERS_ENV } must be positive" if max_available_runners <= 0
294+ return 1 if max_available_runners <= 1
295+
296+ base_spillover_threshold = sharding_integer_env ( DEPS_SHARD_BASE_THRESHOLD_ENV , runner , nil )
297+ additional_runner_reluctance = sharding_integer_env ( DEPS_SHARD_RUNNER_PENALTY_ENV , runner , nil )
298+ return 1 if base_spillover_threshold . nil? || additional_runner_reluctance . nil?
299+
300+ raise ArgumentError , "#{ DEPS_SHARD_BASE_THRESHOLD_ENV } must be positive" if base_spillover_threshold <= 0
301+ if additional_runner_reluctance . negative?
302+ raise ArgumentError , "#{ DEPS_SHARD_RUNNER_PENALTY_ENV } must be non-negative"
303+ end
304+
305+ dependent_count = compatible_untested_dependent_names ( runner ) . count
306+ return 1 if dependent_count . zero?
307+
308+ selected_runner_count = if additional_runner_reluctance . zero?
309+ ( dependent_count . to_f / base_spillover_threshold ) . ceil
310+ else
311+ quadratic_term = 4 * additional_runner_reluctance * dependent_count
312+ linear_term = ( base_spillover_threshold - additional_runner_reluctance ) **2
313+ numerator = -( base_spillover_threshold - additional_runner_reluctance ) +
314+ Math . sqrt ( ( linear_term + quadratic_term ) . to_f )
315+ denominator = 2 * additional_runner_reluctance
316+ ( numerator / denominator ) . ceil
317+ end
318+
319+ selected_runner_count . clamp ( 1 , max_available_runners )
320+ end
321+
322+ sig { params ( base_env_name : String , runner : GitHubRunner , default : T . nilable ( Integer ) ) . returns ( T . nilable ( Integer ) ) }
323+ def sharding_integer_env ( base_env_name , runner , default )
324+ platform = runner . platform . to_s . upcase
325+ arch = runner . arch . to_s . upcase
326+ version = runner . macos_version &.to_sym
327+ version = version . to_s . upcase if version
328+
329+ [
330+ ( "#{ base_env_name } _#{ platform } _#{ arch } _#{ version } " if version ) ,
331+ "#{ base_env_name } _#{ platform } _#{ arch } " ,
332+ "#{ base_env_name } _#{ platform } " ,
333+ base_env_name ,
334+ ] . compact . each do |env_name |
335+ env_value = integer_env ( env_name , nil )
336+ return env_value unless env_value . nil?
337+ end
338+
339+ default
340+ end
341+
342+ sig { params ( env_name : String , default : T . nilable ( Integer ) ) . returns ( T . nilable ( Integer ) ) }
343+ def integer_env ( env_name , default )
344+ env_value = ENV . fetch ( env_name , nil )
345+ return default if env_value . blank?
346+
347+ Integer ( env_value , 10 )
348+ rescue ArgumentError
349+ raise ArgumentError , "#{ env_name } must be an integer"
350+ end
351+
264352 sig { params ( runner : GitHubRunner ) . returns ( T ::Array [ String ] ) }
265353 def testable_formulae ( runner )
266354 formulae = if @dependent_matrix
@@ -301,26 +389,37 @@ def compatible_testing_formulae(runner)
301389
302390 sig { params ( runner : GitHubRunner ) . returns ( T ::Array [ TestRunnerFormula ] ) }
303391 def formulae_with_untested_dependents ( runner )
304- @formulae_with_untested_dependents [ runner ] ||= begin
305- platform = runner . platform
306- arch = runner . arch
307- macos_version = runner . macos_version
392+ @formulae_with_untested_dependents [ runner ] ||= compatible_testing_formulae ( runner ) . select do | formula |
393+ compatible_untested_dependent_names_for_formula ( formula , runner ) . present?
394+ end
395+ end
308396
309- compatible_testing_formulae ( runner ) . select do | formula |
310- compatible_dependents = formula . dependents ( platform : , arch : , macos_version : macos_version &. to_sym )
311- . select do |dependent_f |
312- Homebrew :: SimulateSystem . with ( os : platform , arch : Homebrew :: SimulateSystem . arch_symbols . fetch ( arch ) ) do
313- simulated_dependent_f = TestRunnerFormula . new ( Formulary . factory ( dependent_f . name ) )
314- next false if macos_version && ! simulated_dependent_f . compatible_with? ( macos_version )
397+ sig { params ( runner : GitHubRunner ) . returns ( T :: Array [ String ] ) }
398+ def compatible_untested_dependent_names ( runner )
399+ @compatible_untested_dependent_names [ runner ] ||= compatible_testing_formulae ( runner ) . flat_map do |formula |
400+ compatible_untested_dependent_names_for_formula ( formula , runner )
401+ end . uniq . sort
402+ end
315403
316- simulated_dependent_f . public_send ( :"#{ platform } _compatible?" ) &&
317- simulated_dependent_f . public_send ( :"#{ arch } _compatible?" )
318- end
319- end
404+ sig { params ( formula : TestRunnerFormula , runner : GitHubRunner ) . returns ( T ::Array [ String ] ) }
405+ def compatible_untested_dependent_names_for_formula ( formula , runner )
406+ compatible_dependents_for_runner ( formula , runner ) . map ( &:name ) - @testing_formulae . map ( &:name )
407+ end
408+
409+ sig { params ( formula : TestRunnerFormula , runner : GitHubRunner ) . returns ( T ::Array [ TestRunnerFormula ] ) }
410+ def compatible_dependents_for_runner ( formula , runner )
411+ platform = runner . platform
412+ arch = runner . arch
413+ macos_version = runner . macos_version
414+
415+ formula . dependents ( platform :, arch :, macos_version : macos_version &.to_sym )
416+ . select do |dependent_formula |
417+ Homebrew ::SimulateSystem . with ( os : platform , arch : Homebrew ::SimulateSystem . arch_symbols . fetch ( arch ) ) do
418+ simulated_dependent_formula = TestRunnerFormula . new ( Formulary . factory ( dependent_formula . name ) )
419+ next false if macos_version && !simulated_dependent_formula . compatible_with? ( macos_version )
320420
321- # These arrays will generally have been generated by different Formulary caches,
322- # so we can only compare them by name and not directly.
323- ( compatible_dependents . map ( &:name ) - @testing_formulae . map ( &:name ) ) . present?
421+ simulated_dependent_formula . public_send ( :"#{ platform } _compatible?" ) &&
422+ simulated_dependent_formula . public_send ( :"#{ arch } _compatible?" )
324423 end
325424 end
326425 end
0 commit comments