Commit 7b9dc100 authored by Kamil Trzcinski's avatar Kamil Trzcinski

Rough implementation of build minutes for shared runners [ci skip]

parent 62a4637d
......@@ -103,6 +103,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:user_oauth_applications,
:user_default_external,
:shared_runners_enabled,
:shared_runners_minutes,
:shared_runners_text,
:max_artifacts_size,
:max_pages_size,
......
......@@ -119,6 +119,9 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
validates :shared_runners_minutes,
numericality: { greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
......
......@@ -93,6 +93,12 @@ module Ci
end
end
after_transition any => [:success, :failed, :canceled] do |build|
build.run_after_commit do
UpdateBuildMinutesService.new(project, nil).execute(build)
end
end
after_transition any => [:success] do |build|
build.run_after_commit do
BuildSuccessWorker.perform_async(id)
......
......@@ -28,6 +28,8 @@ class Project < ActiveRecord::Base
:merge_requests_enabled?, :issues_enabled?, to: :project_feature,
allow_nil: true
delegate :shared_runners_minutes, to: :project_metrics, allow_nil: true
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
......@@ -150,6 +152,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy
has_one :project_metrics, dependent: :destroy
has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
......@@ -1232,7 +1235,9 @@ class Project < ActiveRecord::Base
return true
end
shared_runners_enabled? && Ci::Runner.shared.active.any?(&block)
shared_runners_enabled? &&
!shared_runners_minutes_used? &&
Ci::Runner.shared.active.any?(&block)
end
def valid_runners_token?(token)
......@@ -1541,6 +1546,20 @@ class Project < ActiveRecord::Base
end
end
def shared_runners_minutes_limit
read_attribute(:shared_runners_minutes_limit) || current_application_settings.shared_runners_minutes
end
def shared_runners_minutes_limit_enabled?
shared_runners_minutes_limit.nonzero?
end
def shared_runners_minutes_used?
shared_runners_enabled? &&
shared_runners_minutes_limit_enabled? &&
shared_runners_minutes.to_i < shared_runners_minutes_limit
end
private
# Check if a reference is being done cross-project
......
class ProjectMetrics < ActiveRecord::Base
belongs_to :project
validates :project, presence: true
end
......@@ -2,24 +2,16 @@ module Ci
# This class responsible for assigning
# proper pending build to runner on runner API request
class RegisterBuildService
include CurrentSettings
def execute(current_runner)
builds = Ci::Build.pending.unstarted
builds =
if current_runner.shared?
builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
builds_for_shared_runner_with_build_minutes
else
# do run projects which are only assigned to this runner (FIFO)
builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
builds_for_specific_runner
end
build = builds.find do |build|
......@@ -41,9 +33,36 @@ module Ci
private
def builds_for_shared_runner
new_builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
# select projects with allowed number of shared runner minutes
joins('LEFT JOIN project_metrics ON ci_builds.gl_project_id = project_metrics.project_id').
where('COALESCE(projects.shared_runner_minutes_limit, ?, 0) > 0 AND ' \
'COALESCE(project_metrics.shared_runner_minutes, 0) < COALESCE(projects.shared_runner_minutes_limit, ?, 0)',
current_application_settings.shared_runners_minutes)
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
def builds_for_specific_runner
new_builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC')
end
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared).
group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
end
def new_builds
Ci::Build.pending.unstarted
end
end
end
class UpdateBuildMinutesService < BaseService
def execute(build)
return unless build.runner
return unless build.runner.shared?
return unless build.duration
project.find_or_create_project_metrics.
update_all('shared_runners_minutes = shared_runners_minutes + ?', build.duration)
end
end
......@@ -215,6 +215,13 @@
= f.label :shared_runners_enabled do
= f.check_box :shared_runners_enabled
Enable shared runners for new projects
.form-group
= f.label :shared_runners_minutes, 'Shared runners minutes', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :shared_runners_minutes, class: 'form-control'
.help-block
Set the maximum amount of minutes that project can use shared runners in period of time
= link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "shared-runners-minutes")
.form-group
= f.label :shared_runners_text, class: 'control-label col-sm-2'
.col-sm-10
......
......@@ -90,6 +90,20 @@
%span.light archived:
%strong repository is read-only
- if @project.builds_enabled?
%li
%span.light Shared Runners:
%strong
- if @project.shared_runners_enabled?
Enabled
- if @project.shared_runner_minutes_limit.nonzero?
= @project.shared_runner_minutes_limit
total minutes
- elsif current_application_settings.shared_runners_minutes
Unlimited
- else
Disabled
%li
%span.light access:
%strong
......
......@@ -26,6 +26,19 @@
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
- if @build.project.shared_runners_minutes_used?
.bs-callout.bs-callout-warning
%p
You did use all your allowed shared runners minutes:
= @build.project.shared_runners_minutes.to_i
of
= @build.project.shared_runners_minutes_limit
.
Consider looking at
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
.
- if @build.starts_environment?
.prepend-top-default
.environment-information
......
class ClearSharedRunnerMinutesWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
def perform
ProjectMetrics.update_all(shared_runner_minutes: 0)
end
end
......@@ -403,6 +403,10 @@ Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.n
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
Settings.cron_jobs['clear_shared_runner_minutes_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['clear_shared_runner_minutes_worker']['cron'] ||= '0 0 0 * *'
Settings.cron_jobs['clear_shared_runner_minutes_worker']['job_class'] = 'ClearSharedRunnerMinutesWorker'
#
# GitLab Shell
#
......
class AddSharedRunnersMinutesToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :application_settings, :shared_runners_minutes, :integer, null: false, default: 0
end
end
class AddSharedRunnersMinutesLimitToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :projects, :shared_runners_minutes_limit, :integer
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreateTableProjectMetrics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
create_table :project_metrics do |t|
t.integer :project_id, null: false
t.integer :shared_runners_minutes, default: 0, null: false
end
add_foreign_key :project_metrics, :projects, column: :project_id, on_delete: :cascade
end
end
class AddIndexToProjectMetrics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_concurrent_index :project_metrics, [:project_id], { unique: true }
end
end
......@@ -108,6 +108,7 @@ module API
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
expose :approvals_before_merge
expose :shared_runners_minutes_limit
end
class Member < UserBasic
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment