@@ -64,28 +64,40 @@ 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-
67+ @compatible_untested_dependent_names = T . let ( { } , T :: Hash [ GitHubRunner , T :: Array [ String ] ] )
6868 @runners = T . let ( [ ] , T ::Array [ GitHubRunner ] )
6969 generate_runners!
7070
7171 freeze
7272 end
7373
74- sig { returns ( T ::Array [ RunnerSpecHash ] ) }
74+ sig { returns ( T ::Array [ T :: Hash [ Symbol , T . untyped ] ] ) }
7575 def active_runner_specs_hash
76- runners . select ( &:active )
77- . map ( &:spec )
78- . map ( &:to_h )
76+ runners . filter ( &:active ) . flat_map do |r |
77+ Array . new ( shard_count = selected_runner_count_for ( r ) ) do |i |
78+ ( spec = r . spec . to_h ) . merge (
79+ name : ( shard_count > 1 ) ? "#{ spec [ :name ] } (shard #{ i + 1 } /#{ shard_count } )" : spec [ :name ] ,
80+ shard_index : i ,
81+ shard_total : shard_count ,
82+ )
83+ end
84+ end
7985 end
8086
8187 private
8288
8389 SELF_HOSTED_LINUX_RUNNER = "linux-self-hosted-1"
90+ DEPS_SHARDING_ENV = "HOMEBREW_DEPS_SHARDING"
91+ DEPS_SHARD_MAX_RUNNERS_ENV = "HOMEBREW_DEPS_SHARD_MAX_RUNNERS"
92+ DEPS_SHARD_BASE_THRESHOLD_ENV = "HOMEBREW_DEPS_SHARD_BASE_THRESHOLD"
93+ DEPS_SHARD_RUNNER_PENALTY_ENV = "HOMEBREW_DEPS_SHARD_RUNNER_PENALTY"
8494 # ARM macOS timeout, keep this under 1/2 of GitHub's job execution time limit for self-hosted runners.
8595 # https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#usage-limits
8696 GITHUB_ACTIONS_LONG_TIMEOUT = 2160 # 36 hours
8797 GITHUB_ACTIONS_SHORT_TIMEOUT = 60
88- private_constant :SELF_HOSTED_LINUX_RUNNER , :GITHUB_ACTIONS_LONG_TIMEOUT , :GITHUB_ACTIONS_SHORT_TIMEOUT
98+ private_constant :SELF_HOSTED_LINUX_RUNNER , :DEPS_SHARDING_ENV , :DEPS_SHARD_MAX_RUNNERS_ENV ,
99+ :DEPS_SHARD_BASE_THRESHOLD_ENV , :DEPS_SHARD_RUNNER_PENALTY_ENV ,
100+ :GITHUB_ACTIONS_LONG_TIMEOUT , :GITHUB_ACTIONS_SHORT_TIMEOUT
89101
90102 sig { params ( arch : Symbol ) . returns ( LinuxRunnerSpec ) }
91103 def linux_runner_spec ( arch )
@@ -261,6 +273,62 @@ def generate_runners!
261273 @runners . freeze
262274 end
263275
276+ sig { params ( runner : GitHubRunner ) . returns ( Integer ) }
277+ def selected_runner_count_for ( runner )
278+ return 1 if !@dependent_matrix || %w[ 1 true ] . exclude? ( ENV . fetch ( DEPS_SHARDING_ENV , "false" ) . downcase )
279+
280+ max_available_runners = T . must ( sharding_integer_env ( DEPS_SHARD_MAX_RUNNERS_ENV , runner , 1 ) )
281+ base_spillover_threshold = sharding_integer_env ( DEPS_SHARD_BASE_THRESHOLD_ENV , runner , nil )
282+ additional_runner_reluctance = sharding_integer_env ( DEPS_SHARD_RUNNER_PENALTY_ENV , runner , nil )
283+ raise ArgumentError , "#{ DEPS_SHARD_MAX_RUNNERS_ENV } must be positive" unless max_available_runners . positive?
284+ raise ArgumentError , "#{ DEPS_SHARD_BASE_THRESHOLD_ENV } must be positive" if base_spillover_threshold &.<= 0
285+
286+ if additional_runner_reluctance &.negative?
287+ raise ArgumentError ,
288+ "#{ DEPS_SHARD_RUNNER_PENALTY_ENV } must be non-negative"
289+ end
290+ return 1 if max_available_runners <= 1 || base_spillover_threshold . nil? || additional_runner_reluctance . nil?
291+
292+ dependent_names = @compatible_untested_dependent_names [ runner ] ||= compatible_testing_formulae ( runner )
293+ . flat_map do |formula |
294+ compatible_untested_dependent_names_for_formula (
295+ formula , runner
296+ )
297+ end . uniq . sort
298+ dependent_count = dependent_names . count
299+ return 1 if dependent_count . zero?
300+
301+ runner_count = if additional_runner_reluctance . zero?
302+ dependent_count . to_f / base_spillover_threshold
303+ else
304+ threshold_difference = base_spillover_threshold - additional_runner_reluctance
305+ discriminant = ( threshold_difference **2 ) + ( 4 * additional_runner_reluctance * dependent_count )
306+ ( Math . sqrt ( discriminant . to_f ) - threshold_difference ) / ( 2 * additional_runner_reluctance )
307+ end
308+
309+ runner_count . ceil . clamp ( 1 , max_available_runners )
310+ end
311+
312+ sig { params ( base_env_name : String , runner : GitHubRunner , default : T . nilable ( Integer ) ) . returns ( T . nilable ( Integer ) ) }
313+ def sharding_integer_env ( base_env_name , runner , default )
314+ platform = runner . platform . to_s . upcase
315+ arch = runner . arch . to_s . upcase
316+ version = runner . macos_version &.to_sym
317+ version = version . to_s . upcase if version
318+
319+ [
320+ ( "#{ base_env_name } _#{ platform } _#{ arch } _#{ version } " if version ) ,
321+ "#{ base_env_name } _#{ platform } _#{ arch } " ,
322+ "#{ base_env_name } _#{ platform } " ,
323+ base_env_name ,
324+ ] . compact . each do |env_name |
325+ env_value = ENV . fetch ( env_name , nil )
326+ return Integer ( env_value , 10 ) if env_value . present?
327+ end
328+
329+ default
330+ end
331+
264332 sig { params ( runner : GitHubRunner ) . returns ( T ::Array [ String ] ) }
265333 def testable_formulae ( runner )
266334 formulae = if @dependent_matrix
@@ -301,27 +369,30 @@ def compatible_testing_formulae(runner)
301369
302370 sig { params ( runner : GitHubRunner ) . returns ( T ::Array [ TestRunnerFormula ] ) }
303371 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
372+ @formulae_with_untested_dependents [ runner ] ||= compatible_testing_formulae ( runner ) . select do | formula |
373+ compatible_untested_dependent_names_for_formula ( formula , runner ) . present?
374+ end
375+ end
308376
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 )
377+ sig { params ( formula : TestRunnerFormula , runner : GitHubRunner ) . returns ( T ::Array [ String ] ) }
378+ def compatible_untested_dependent_names_for_formula ( formula , runner )
379+ platform = runner . platform
380+ arch = runner . arch
381+ macos_version = runner . macos_version
315382
316- simulated_dependent_f . public_send ( :"#{ platform } _compatible?" ) &&
317- simulated_dependent_f . public_send ( :"#{ arch } _compatible?" )
318- end
319- end
383+ compatible_dependents = formula . dependents ( platform :, arch :, macos_version : macos_version &.to_sym )
384+ . select do |dependent_f |
385+ Homebrew ::SimulateSystem . with ( os : platform , arch : Homebrew ::SimulateSystem . arch_symbols . fetch ( arch ) ) do
386+ simulated_dependent_f = TestRunnerFormula . new ( Formulary . factory ( dependent_f . name ) )
387+ next false if macos_version && !simulated_dependent_f . compatible_with? ( macos_version )
320388
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?
389+ simulated_dependent_f . public_send ( :"#{ platform } _compatible?" ) &&
390+ simulated_dependent_f . public_send ( :"#{ arch } _compatible?" )
324391 end
325392 end
393+
394+ # These arrays will generally have been generated by different Formulary caches,
395+ # so we can only compare them by name and not directly.
396+ compatible_dependents . map ( &:name ) - @testing_formulae . map ( &:name )
326397 end
327398end
0 commit comments