2015-08-25 21:42:46 -04:00
|
|
|
module Ci
|
|
|
|
# This class responsible for assigning
|
|
|
|
# proper pending build to runner on runner API request
|
2017-02-15 19:05:44 -05:00
|
|
|
class RegisterJobService
|
2017-01-20 06:25:53 -05:00
|
|
|
attr_reader :runner
|
|
|
|
|
2017-01-25 08:23:47 -05:00
|
|
|
Result = Struct.new(:build, :valid?)
|
|
|
|
|
2017-01-20 06:25:53 -05:00
|
|
|
def initialize(runner)
|
|
|
|
@runner = runner
|
|
|
|
end
|
|
|
|
|
|
|
|
def execute
|
2015-08-25 21:42:46 -04:00
|
|
|
builds =
|
2017-01-20 06:25:53 -05:00
|
|
|
if runner.shared?
|
|
|
|
builds_for_shared_runner
|
2015-08-25 21:42:46 -04:00
|
|
|
else
|
2017-01-20 06:25:53 -05:00
|
|
|
builds_for_specific_runner
|
2015-08-25 21:42:46 -04:00
|
|
|
end
|
|
|
|
|
2017-02-28 13:23:35 -05:00
|
|
|
valid = true
|
|
|
|
|
2017-11-30 17:17:41 -05:00
|
|
|
if Feature.enabled?('ci_job_request_with_tags_matcher')
|
|
|
|
# pick builds that does not have other tags than runner's one
|
|
|
|
builds = builds.matches_tag_ids(runner.tags.ids)
|
|
|
|
|
|
|
|
# pick builds that have at least one tag
|
|
|
|
unless runner.run_untagged?
|
|
|
|
builds = builds.with_any_tags
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-28 13:23:35 -05:00
|
|
|
builds.find do |build|
|
|
|
|
next unless runner.can_pick?(build)
|
|
|
|
|
|
|
|
begin
|
|
|
|
# In case when 2 runners try to assign the same build, second runner will be declined
|
|
|
|
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
|
2017-12-12 02:17:45 -05:00
|
|
|
begin
|
|
|
|
build.runner_id = runner.id
|
|
|
|
build.run!
|
|
|
|
register_success(build)
|
2017-02-28 13:23:35 -05:00
|
|
|
|
2017-12-12 02:17:45 -05:00
|
|
|
return Result.new(build, true)
|
|
|
|
rescue Ci::Build::MissingDependenciesError
|
|
|
|
build.drop!(:missing_dependency_failure)
|
|
|
|
end
|
2017-02-28 13:23:35 -05:00
|
|
|
rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
|
|
|
|
# We are looping to find another build that is not conflicting
|
|
|
|
# It also indicates that this build can be picked and passed to runner.
|
|
|
|
# If we don't do it, basically a bunch of runners would be competing for a build
|
|
|
|
# and thus we will generate a lot of 409. This will increase
|
|
|
|
# the number of generated requests, also will reduce significantly
|
|
|
|
# how many builds can be picked by runner in a unit of time.
|
|
|
|
# In case we hit the concurrency-access lock,
|
|
|
|
# we still have to return 409 in the end,
|
|
|
|
# to make sure that this is properly handled by runner.
|
|
|
|
valid = false
|
|
|
|
end
|
2015-08-25 21:42:46 -04:00
|
|
|
end
|
|
|
|
|
2017-07-24 06:44:33 -04:00
|
|
|
register_failure
|
2017-02-28 13:23:35 -05:00
|
|
|
Result.new(nil, valid)
|
2015-08-25 21:42:46 -04:00
|
|
|
end
|
2016-06-13 15:52:41 -04:00
|
|
|
|
|
|
|
private
|
|
|
|
|
2017-01-20 06:25:53 -05:00
|
|
|
def builds_for_shared_runner
|
2017-08-01 04:08:14 -04:00
|
|
|
new_builds.
|
2017-01-20 06:25:53 -05:00
|
|
|
# don't run projects which have not enabled shared runners and builds
|
2017-06-19 13:24:14 -04:00
|
|
|
joins(:project).where(projects: { shared_runners_enabled: true, pending_delete: false })
|
|
|
|
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
|
2017-08-01 04:08:14 -04:00
|
|
|
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
|
2017-01-20 06:25:53 -05:00
|
|
|
|
|
|
|
# Implement fair scheduling
|
|
|
|
# this returns builds that are ordered by number of running builds
|
|
|
|
# we prefer projects that don't use shared runners at all
|
2017-08-01 04:08:14 -04:00
|
|
|
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
|
|
|
|
.order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
|
2017-01-20 06:25:53 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def builds_for_specific_runner
|
2017-08-01 04:08:14 -04:00
|
|
|
new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC')
|
2017-01-20 06:25:53 -05:00
|
|
|
end
|
|
|
|
|
2016-06-13 18:14:30 -04:00
|
|
|
def running_builds_for_shared_runners
|
2017-06-21 09:48:12 -04:00
|
|
|
Ci::Build.running.where(runner: Ci::Runner.shared)
|
|
|
|
.group(:project_id).select(:project_id, 'count(*) AS running_builds')
|
2016-06-13 15:52:41 -04:00
|
|
|
end
|
2017-01-20 06:25:53 -05:00
|
|
|
|
|
|
|
def new_builds
|
2017-08-01 04:08:14 -04:00
|
|
|
builds = Ci::Build.pending.unstarted
|
2017-08-29 02:56:03 -04:00
|
|
|
builds = builds.ref_protected if runner.ref_protected?
|
2017-08-01 04:08:14 -04:00
|
|
|
builds
|
2017-01-20 06:25:53 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def shared_runner_build_limits_feature_enabled?
|
|
|
|
ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
|
|
|
|
end
|
2017-07-24 06:44:33 -04:00
|
|
|
|
|
|
|
def register_failure
|
2017-08-10 14:05:44 -04:00
|
|
|
failed_attempt_counter.increment
|
|
|
|
attempt_counter.increment
|
2017-07-24 06:44:33 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def register_success(job)
|
|
|
|
job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
|
2017-08-10 14:05:44 -04:00
|
|
|
attempt_counter.increment
|
2017-07-24 06:44:33 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def failed_attempt_counter
|
|
|
|
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
|
|
|
|
end
|
|
|
|
|
|
|
|
def attempt_counter
|
|
|
|
@attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_total, "Counts the times a runner tries to register a job")
|
|
|
|
end
|
|
|
|
|
|
|
|
def job_queue_duration_seconds
|
|
|
|
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time')
|
|
|
|
end
|
2015-08-25 21:42:46 -04:00
|
|
|
end
|
|
|
|
end
|