diff options
Diffstat (limited to 'app/services/ci')
19 files changed, 199 insertions, 135 deletions
diff --git a/app/services/ci/after_requeue_job_service.rb b/app/services/ci/after_requeue_job_service.rb index b422e57baad..f717dd0862c 100644 --- a/app/services/ci/after_requeue_job_service.rb +++ b/app/services/ci/after_requeue_job_service.rb @@ -38,7 +38,7 @@ module Ci end def stage_dependent_jobs(processable) - skipped_jobs(processable).scheduling_type_stage.after_stage(processable.stage_idx) + skipped_jobs(processable).after_stage(processable.stage_idx) end def needs_dependent_jobs(processable) diff --git a/app/services/ci/append_build_trace_service.rb b/app/services/ci/append_build_trace_service.rb index 8200f9790ee..0eef0ff0e61 100644 --- a/app/services/ci/append_build_trace_service.rb +++ b/app/services/ci/append_build_trace_service.rb @@ -71,8 +71,7 @@ module Ci end def trace_size_exceeded?(size) - Feature.enabled?(:ci_jobs_trace_size_limit, project, default_enabled: :yaml) && - project.actual_limits.exceeded?(:ci_jobs_trace_size_limit, size / 1.megabyte) + project.actual_limits.exceeded?(:ci_jobs_trace_size_limit, size / 1.megabyte) end end end diff --git a/app/services/ci/build_cancel_service.rb b/app/services/ci/build_cancel_service.rb new file mode 100644 index 00000000000..a23418ed738 --- /dev/null +++ b/app/services/ci/build_cancel_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Ci + class BuildCancelService + def initialize(build, user) + @build = build + @user = user + end + + def execute + return forbidden unless allowed? + return unprocessable_entity unless build.cancelable? + + build.cancel + + ServiceResponse.success(payload: build) + end + + private + + attr_reader :build, :user + + def allowed? + user.can?(:update_build, build) + end + + def forbidden + ServiceResponse.error(message: 'Forbidden', http_status: :forbidden) + end + + def unprocessable_entity + ServiceResponse.error(message: 'Unprocessable entity', http_status: :unprocessable_entity) + end + end +end diff --git a/app/services/ci/build_unschedule_service.rb b/app/services/ci/build_unschedule_service.rb new file mode 100644 index 00000000000..3b367ecc4c4 --- /dev/null +++ b/app/services/ci/build_unschedule_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Ci + class BuildUnscheduleService + def initialize(build, user) + @build = build + @user = user + end + + def execute + return forbidden unless allowed? + return unprocessable_entity unless build.scheduled? + + build.unschedule! + + ServiceResponse.success(payload: build) + end + + private + + attr_reader :build, :user + + def allowed? + user.can?(:update_build, build) + end + + def forbidden + ServiceResponse.error(message: 'Forbidden', http_status: :forbidden) + end + + def unprocessable_entity + ServiceResponse.error(message: 'Unprocessable entity', http_status: :unprocessable_entity) + end + end +end diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb index e9ec2338171..a65c22e273c 100644 --- a/app/services/ci/create_downstream_pipeline_service.rb +++ b/app/services/ci/create_downstream_pipeline_service.rb @@ -33,8 +33,9 @@ module Ci current_user, pipeline_params.fetch(:target_revision)) - downstream_pipeline = service.execute( - pipeline_params.fetch(:source), **pipeline_params[:execute_params]) + downstream_pipeline = service + .execute(pipeline_params.fetch(:source), **pipeline_params[:execute_params]) + .payload downstream_pipeline.tap do |pipeline| update_bridge_status!(@bridge, pipeline) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index c039f31aafc..ba9665555cc 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -11,11 +11,11 @@ module Ci Gitlab::Ci::Pipeline::Chain::Validate::Abilities, Gitlab::Ci::Pipeline::Chain::Validate::Repository, Gitlab::Ci::Pipeline::Chain::Validate::SecurityOrchestrationPolicy, + Gitlab::Ci::Pipeline::Chain::Skip, Gitlab::Ci::Pipeline::Chain::Config::Content, Gitlab::Ci::Pipeline::Chain::Config::Process, Gitlab::Ci::Pipeline::Chain::Validate::AfterConfig, Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs, - Gitlab::Ci::Pipeline::Chain::Skip, Gitlab::Ci::Pipeline::Chain::SeedBlock, Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules, Gitlab::Ci::Pipeline::Chain::Seed, @@ -87,12 +87,16 @@ module Ci if pipeline.persisted? schedule_head_pipeline_update create_namespace_onboarding_action + else + # If pipeline is not persisted, try to recover IID + pipeline.reset_project_iid end - # If pipeline is not persisted, try to recover IID - pipeline.reset_project_iid unless pipeline.persisted? - - pipeline + if error_message = pipeline.full_error_messages.presence || pipeline.failure_reason.presence + ServiceResponse.error(message: error_message, payload: pipeline) + else + ServiceResponse.success(payload: pipeline) + end end # rubocop: enable Metrics/ParameterLists @@ -100,8 +104,8 @@ module Ci source = args[0] params = Hash(args[1]) - execute(source, **params, &block).tap do |pipeline| - unless pipeline.persisted? + execute(source, **params, &block).tap do |response| + unless response.payload.persisted? raise CreateError, pipeline.full_error_messages end end diff --git a/app/services/ci/daily_build_group_report_result_service.rb b/app/services/ci/daily_build_group_report_result_service.rb index 820e6e08fc5..25c6d57d961 100644 --- a/app/services/ci/daily_build_group_report_result_service.rb +++ b/app/services/ci/daily_build_group_report_result_service.rb @@ -3,7 +3,13 @@ module Ci class DailyBuildGroupReportResultService def execute(pipeline) - DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline)) + if DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline)) + Projects::CiFeatureUsage.insert_usage( + project_id: pipeline.project_id, + feature: :code_coverage, + default_branch: pipeline.default_branch? + ) + end end private diff --git a/app/services/ci/delete_unit_tests_service.rb b/app/services/ci/delete_unit_tests_service.rb index 28f96351175..230661a107d 100644 --- a/app/services/ci/delete_unit_tests_service.rb +++ b/app/services/ci/delete_unit_tests_service.rb @@ -23,7 +23,7 @@ module Ci def delete_batch!(klass) deleted = 0 - ActiveRecord::Base.transaction do + klass.transaction do ids = klass.deletable.lock('FOR UPDATE SKIP LOCKED').limit(BATCH_SIZE).pluck(:id) break if ids.empty? diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb index 494fcb23a06..dd5c8e0379f 100644 --- a/app/services/ci/destroy_pipeline_service.rb +++ b/app/services/ci/destroy_pipeline_service.rb @@ -7,7 +7,7 @@ module Ci Ci::ExpirePipelineCacheService.new.execute(pipeline, delete: true) - pipeline.cancel_running if pipeline.cancelable? && ::Feature.enabled?(:cancel_pipelines_prior_to_destroy, default_enabled: :yaml) + pipeline.cancel_running if pipeline.cancelable? pipeline.reset.destroy! diff --git a/app/services/ci/drop_pipeline_service.rb b/app/services/ci/drop_pipeline_service.rb index f510943251b..16d3abcbfa0 100644 --- a/app/services/ci/drop_pipeline_service.rb +++ b/app/services/ci/drop_pipeline_service.rb @@ -2,6 +2,9 @@ module Ci class DropPipelineService + PRELOADED_COMMIT_STATUS_RELATIONS = [:project, :pipeline].freeze + PRELOADED_CI_BUILD_RELATIONS = [:metadata, :deployment, :taggings].freeze + # execute service asynchronously for each cancelable pipeline def execute_async_for_all(pipelines, failure_reason, context_user) pipelines.cancelable.select(:id).find_in_batches do |pipelines_batch| @@ -27,11 +30,11 @@ module Ci private - def preload_associations_for_drop(builds_batch) - ActiveRecord::Associations::Preloader.new.preload( # rubocop: disable CodeReuse/ActiveRecord - builds_batch, - [:project, :pipeline, :metadata, :deployment, :taggings] - ) + # rubocop: disable CodeReuse/ActiveRecord + def preload_associations_for_drop(commit_status_batch) + ActiveRecord::Associations::Preloader.new.preload(commit_status_batch, PRELOADED_COMMIT_STATUS_RELATIONS) + ActiveRecord::Associations::Preloader.new.preload(commit_status_batch.select { |job| job.is_a?(Ci::Build) }, PRELOADED_CI_BUILD_RELATIONS) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/ci/external_pull_requests/create_pipeline_service.rb b/app/services/ci/external_pull_requests/create_pipeline_service.rb index 78be94bfb41..83499524a8e 100644 --- a/app/services/ci/external_pull_requests/create_pipeline_service.rb +++ b/app/services/ci/external_pull_requests/create_pipeline_service.rb @@ -7,7 +7,8 @@ module Ci module ExternalPullRequests class CreatePipelineService < BaseService def execute(pull_request) - return unless pull_request.open? && pull_request.actual_branch_head? + return pull_request_not_open_error unless pull_request.open? + return pull_request_branch_error unless pull_request.actual_branch_head? create_pipeline_for(pull_request) end @@ -26,6 +27,14 @@ module Ci target_sha: pull_request.target_sha } end + + def pull_request_not_open_error + ServiceResponse.error(message: 'The pull request is not opened', payload: nil) + end + + def pull_request_branch_error + ServiceResponse.error(message: 'The source sha is not the head of the source branch', payload: nil) + end end end end diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb deleted file mode 100644 index c756e376901..00000000000 --- a/app/services/ci/extract_sections_from_build_trace_service.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Ci - class ExtractSectionsFromBuildTraceService < BaseService - def execute(build) - return false unless build.trace_sections.empty? - - Gitlab::Database.bulk_insert(BuildTraceSection.table_name, extract_sections(build)) # rubocop:disable Gitlab/BulkInsert - true - end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def find_or_create_name(name) - project.build_trace_section_names.find_or_create_by!(name: name) - rescue ActiveRecord::RecordInvalid - project.build_trace_section_names.find_by!(name: name) - end - # rubocop: enable CodeReuse/ActiveRecord - - def extract_sections(build) - build.trace.extract_sections.map do |attr| - name = attr.delete(:name) - name_record = find_or_create_name(name) - - attr.merge( - build_id: build.id, - project_id: project.id, - section_name_id: name_record.id) - end - end - end -end diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb index 62c4d6b4599..7746382b845 100644 --- a/app/services/ci/pipeline_trigger_service.rb +++ b/app/services/ci/pipeline_trigger_service.rb @@ -27,13 +27,13 @@ module Ci # this check is to not leak the presence of the project if user cannot read it return unless trigger.project == project - pipeline = Ci::CreatePipelineService + response = Ci::CreatePipelineService .new(project, trigger.owner, ref: params[:ref], variables_attributes: variables) .execute(:trigger, ignore_skip_ci: true) do |pipeline| pipeline.trigger_requests.build(trigger: trigger) end - pipeline_service_response(pipeline) + pipeline_service_response(response.payload) end def pipeline_service_response(pipeline) @@ -57,7 +57,7 @@ module Ci # this check is to not leak the presence of the project if user cannot read it return unless can?(job.user, :read_project, project) - pipeline = Ci::CreatePipelineService + response = Ci::CreatePipelineService .new(project, job.user, ref: params[:ref], variables_attributes: variables) .execute(:pipeline, ignore_skip_ci: true) do |pipeline| source = job.sourced_pipelines.build( @@ -69,7 +69,7 @@ module Ci pipeline.source_pipeline = source end - pipeline_service_response(pipeline) + pipeline_service_response(response.payload) end def job_from_token diff --git a/app/services/ci/pipelines/add_job_service.rb b/app/services/ci/pipelines/add_job_service.rb index 03bdb491200..41f9903e48c 100644 --- a/app/services/ci/pipelines/add_job_service.rb +++ b/app/services/ci/pipelines/add_job_service.rb @@ -3,21 +3,33 @@ module Ci module Pipelines class AddJobService + include ::Gitlab::ExclusiveLeaseHelpers + attr_reader :pipeline def initialize(pipeline) @pipeline = pipeline - raise ArgumentError, "Pipeline must be persisted for this service to be used" unless @pipeline.persisted? + raise ArgumentError, "Pipeline must be persisted for this service to be used" unless pipeline.persisted? end def execute!(job, &block) assign_pipeline_attributes(job) - Ci::Pipeline.transaction do - yield(job) + if Feature.enabled?(:ci_pipeline_add_job_with_lock, pipeline.project, default_enabled: :yaml) + in_lock("ci:pipelines:#{pipeline.id}:add-job", ttl: LOCK_TIMEOUT, sleep_sec: LOCK_SLEEP, retries: LOCK_RETRIES) do + Ci::Pipeline.transaction do + yield(job) + + job.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, pipeline.project, default_enabled: :yaml) + end + end + else + Ci::Pipeline.transaction do + yield(job) - job.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, @pipeline.project, default_enabled: :yaml) + job.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, pipeline.project, default_enabled: :yaml) + end end ServiceResponse.success(payload: { job: job }) @@ -27,10 +39,14 @@ module Ci private + LOCK_TIMEOUT = 1.minute + LOCK_SLEEP = 0.5.seconds + LOCK_RETRIES = 20 + def assign_pipeline_attributes(job) - job.pipeline = @pipeline - job.project = @pipeline.project - job.ref = @pipeline.ref + job.pipeline = pipeline + job.project = pipeline.project + job.ref = pipeline.ref end end end diff --git a/app/services/ci/queue/builds_table_strategy.rb b/app/services/ci/queue/builds_table_strategy.rb index c941734ac40..d0a343cb9d4 100644 --- a/app/services/ci/queue/builds_table_strategy.rb +++ b/app/services/ci/queue/builds_table_strategy.rb @@ -53,6 +53,14 @@ module Ci relation.pluck(:id) end + def use_denormalized_shared_runners_data? + false + end + + def use_denormalized_minutes_data? + false + end + private def running_builds_for_shared_runners diff --git a/app/services/ci/queue/pending_builds_strategy.rb b/app/services/ci/queue/pending_builds_strategy.rb index 55d5cb96a0a..efe3a981d3a 100644 --- a/app/services/ci/queue/pending_builds_strategy.rb +++ b/app/services/ci/queue/pending_builds_strategy.rb @@ -11,25 +11,9 @@ module Ci # rubocop:disable CodeReuse/ActiveRecord def builds_for_shared_runner - relation = new_builds - # don't run projects which have not enabled shared runners and builds - .joins('INNER JOIN projects ON ci_pending_builds.project_id = projects.id') - .where(projects: { shared_runners_enabled: true, pending_delete: false }) - .joins('LEFT JOIN project_features ON ci_pending_builds.project_id = project_features.project_id') - .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') + shared_builds = builds_available_for_shared_runners - if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops, default_enabled: :yaml) - # if disaster recovery is enabled, we fallback to FIFO scheduling - relation.order('ci_pending_builds.build_id ASC') - else - # 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 - relation - .with(running_builds_for_shared_runners_cte.to_arel) - .joins("LEFT JOIN project_builds ON ci_pending_builds.project_id = project_builds.project_id") - .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_pending_builds.build_id ASC') - end + builds_ordered_for_shared_runners(shared_builds) end def builds_matching_tag_ids(relation, ids) @@ -52,8 +36,44 @@ module Ci relation.pluck(:build_id) end + def use_denormalized_shared_runners_data? + ::Feature.enabled?(:ci_queueing_denormalize_shared_runners_information, runner, type: :development, default_enabled: :yaml) + end + + def use_denormalized_minutes_data? + ::Feature.enabled?(:ci_queueing_denormalize_ci_minutes_information, runner, type: :development, default_enabled: :yaml) + end + private + def builds_available_for_shared_runners + if use_denormalized_shared_runners_data? + new_builds.with_instance_runners + else + new_builds + # don't run projects which have not enabled shared runners and builds + .joins('INNER JOIN projects ON ci_pending_builds.project_id = projects.id') + .where(projects: { shared_runners_enabled: true, pending_delete: false }) + .joins('LEFT JOIN project_features ON ci_pending_builds.project_id = project_features.project_id') + .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') + end + end + + def builds_ordered_for_shared_runners(relation) + if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops, default_enabled: :yaml) + # if disaster recovery is enabled, we fallback to FIFO scheduling + relation.order('ci_pending_builds.build_id ASC') + else + # 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 + relation + .with(running_builds_for_shared_runners_cte.to_arel) + .joins("LEFT JOIN project_builds ON ci_pending_builds.project_id = project_builds.project_id") + .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_pending_builds.build_id ASC') + end + end + def running_builds_for_shared_runners_cte running_builds = ::Ci::RunningBuild .instance_type @@ -67,3 +87,5 @@ module Ci end end end + +Ci::Queue::PendingBuildsStrategy.prepend_mod_with('Ci::Queue::PendingBuildsStrategy') diff --git a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb index 9e3e6de3928..1d329fe7b53 100644 --- a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb +++ b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb @@ -5,6 +5,8 @@ module Ci class AssignResourceFromResourceGroupService < ::BaseService # rubocop: disable CodeReuse/ActiveRecord def execute(resource_group) + release_resource_from_stale_jobs(resource_group) + free_resources = resource_group.resources.free.count resource_group.processables.waiting_for_resource.take(free_resources).each do |processable| @@ -12,6 +14,14 @@ module Ci end end # rubocop: enable CodeReuse/ActiveRecord + + private + + def release_resource_from_stale_jobs(resource_group) + resource_group.resources.stale_processables.find_each do |processable| + resource_group.release_resource_from(processable) + end + end end end end diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb deleted file mode 100644 index 7c9fc44e7f4..00000000000 --- a/app/services/ci/stop_environments_service.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module Ci - class StopEnvironmentsService < BaseService - attr_reader :ref - - def execute(branch_name) - @ref = branch_name - - return unless @ref.present? - - environments.each { |environment| stop(environment) } - end - - def execute_for_merge_request(merge_request) - merge_request.environments.each { |environment| stop(environment) } - end - - ## - # This method is for stopping multiple environments in a batch style. - # The maximum acceptable count of environments is roughly 5000. Please - # apply acceptable `LIMIT` clause to the `environments` relation. - def self.execute_in_batch(environments) - stop_actions = environments.stop_actions.load - - environments.update_all(auto_stop_at: nil, state: 'stopped') - - stop_actions.each do |stop_action| - stop_action.play(stop_action.user) - rescue StandardError => e - Gitlab::ErrorTracking.track_exception(e, deployable_id: stop_action.id) - end - end - - private - - def environments - @environments ||= Environments::EnvironmentsByDeploymentsFinder - .new(project, current_user, ref: @ref, recently_updated: true) - .execute - end - - def stop(environment) - return unless environment.stop_action_available? - return unless can?(current_user, :stop_environment, environment) - - environment.stop_with_action!(current_user) - end - end -end diff --git a/app/services/ci/unlock_artifacts_service.rb b/app/services/ci/unlock_artifacts_service.rb index 07faf90dd6d..7c169cb8395 100644 --- a/app/services/ci/unlock_artifacts_service.rb +++ b/app/services/ci/unlock_artifacts_service.rb @@ -17,7 +17,7 @@ module Ci SQL loop do - break if ActiveRecord::Base.connection.exec_query(query).empty? + break if Ci::Pipeline.connection.exec_query(query).empty? end end |