Commit 40397f35 authored by Kamil Trzciński's avatar Kamil Trzciński

Allow to make builds soft-archived.

The soft-archived builds cannot be run after some deadline time.
The intent is to aggressively recycle old builds after sometime.
parent 90473e06
...@@ -115,6 +115,7 @@ module ApplicationSettingsHelper ...@@ -115,6 +115,7 @@ module ApplicationSettingsHelper
:akismet_api_key, :akismet_api_key,
:akismet_enabled, :akismet_enabled,
:allow_local_requests_from_hooks_and_services, :allow_local_requests_from_hooks_and_services,
:archive_builds_in_human_readable,
:authorized_keys_enabled, :authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:auto_devops_domain, :auto_devops_domain,
......
...@@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class ApplicationSetting < ActiveRecord::Base
include CacheMarkdownField include CacheMarkdownField
include TokenAuthenticatable include TokenAuthenticatable
include IgnorableColumn include IgnorableColumn
include ChronicDurationAttribute
add_authentication_token_field :runners_registration_token add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token add_authentication_token_field :health_check_access_token
...@@ -45,6 +46,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -45,6 +46,8 @@ class ApplicationSetting < ActiveRecord::Base
default_value_for :id, 1 default_value_for :id, 1
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
validates :uuid, presence: true validates :uuid, presence: true
validates :session_expire_delay, validates :session_expire_delay,
...@@ -184,6 +187,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -184,6 +187,10 @@ class ApplicationSetting < ActiveRecord::Base
validates :user_default_internal_regex, js_regex: true, allow_nil: true validates :user_default_internal_regex, js_regex: true, allow_nil: true
validates :archive_builds_in_seconds,
allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
...@@ -441,6 +448,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -441,6 +448,10 @@ class ApplicationSetting < ActiveRecord::Base
latest_terms latest_terms
end end
def archive_builds_older_than
archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds
end
private private
def ensure_uuid! def ensure_uuid!
......
...@@ -258,8 +258,24 @@ module Ci ...@@ -258,8 +258,24 @@ module Ci
self.name == 'pages' self.name == 'pages'
end end
# degenerated build is one that cannot be run by Runner
def degenerated?
self.options.nil?
end
def degenerate!
self.update!(options: nil, yaml_variables: nil, commands: nil)
end
def archived?
return true if degenerated?
archive_builds_older_than = Gitlab::CurrentSettings.current_application_settings.archive_builds_older_than
archive_builds_older_than.present? && created_at < archive_builds_older_than
end
def playable? def playable?
action? && (manual? || scheduled? || retryable?) action? && !archived? && (manual? || scheduled? || retryable?)
end end
def schedulable? def schedulable?
...@@ -287,7 +303,7 @@ module Ci ...@@ -287,7 +303,7 @@ module Ci
end end
def retryable? def retryable?
success? || failed? || canceled? !archived? && (success? || failed? || canceled?)
end end
def retries_count def retries_count
...@@ -295,7 +311,7 @@ module Ci ...@@ -295,7 +311,7 @@ module Ci
end end
def retries_max def retries_max
self.options.fetch(:retry, 0).to_i self.options.to_h.fetch(:retry, 0).to_i
end end
def latest? def latest?
......
...@@ -51,7 +51,8 @@ class CommitStatus < ActiveRecord::Base ...@@ -51,7 +51,8 @@ class CommitStatus < ActiveRecord::Base
missing_dependency_failure: 5, missing_dependency_failure: 5,
runner_unsupported: 6, runner_unsupported: 6,
stale_schedule: 7, stale_schedule: 7,
job_execution_timeout: 8 job_execution_timeout: 8,
archived_failure: 9
} }
## ##
...@@ -167,16 +168,18 @@ class CommitStatus < ActiveRecord::Base ...@@ -167,16 +168,18 @@ class CommitStatus < ActiveRecord::Base
false false
end end
# To be overridden when inherrited from
def retryable? def retryable?
false false
end end
# To be overridden when inherrited from
def cancelable? def cancelable?
false false
end end
def archived?
false
end
def stuck? def stuck?
false false
end end
......
...@@ -20,12 +20,17 @@ module Ci ...@@ -20,12 +20,17 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref) @subject.project.branch_allows_collaboration?(@user, @subject.ref)
end end
condition(:archived, scope: :subject) do
@subject.archived?
end
condition(:terminal, scope: :subject) do condition(:terminal, scope: :subject) do
@subject.has_terminal? @subject.has_terminal?
end end
rule { protected_ref }.policy do rule { protected_ref | archived }.policy do
prevent :update_build prevent :update_build
prevent :update_commit_status
prevent :erase_build prevent :erase_build
end end
......
...@@ -2,4 +2,13 @@ ...@@ -2,4 +2,13 @@
class DeploymentPolicy < BasePolicy class DeploymentPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
condition(:can_retry_deployable) do
can?(:update_build, @subject.deployable)
end
rule { ~can_retry_deployable }.policy do
prevent :create_deployment
prevent :update_deployment
end
end end
...@@ -10,7 +10,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -10,7 +10,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
missing_dependency_failure: 'There has been a missing dependency failure', missing_dependency_failure: 'There has been a missing dependency failure',
runner_unsupported: 'Your runner is outdated, please upgrade your runner', runner_unsupported: 'Your runner is outdated, please upgrade your runner',
stale_schedule: 'Delayed job could not be executed by some reason, please try again', stale_schedule: 'Delayed job could not be executed by some reason, please try again',
job_execution_timeout: 'The script exceeded the maximum execution time set for the job' job_execution_timeout: 'The script exceeded the maximum execution time set for the job',
archived_failure: 'The job is archived and cannot be run'
}.freeze }.freeze
private_constant :CALLOUT_FAILURE_MESSAGES private_constant :CALLOUT_FAILURE_MESSAGES
...@@ -30,6 +31,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated ...@@ -30,6 +31,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
end end
def unrecoverable? def unrecoverable?
script_failure? || missing_dependency_failure? script_failure? || missing_dependency_failure? || archived_failure?
end end
end end
...@@ -7,6 +7,7 @@ class JobEntity < Grape::Entity ...@@ -7,6 +7,7 @@ class JobEntity < Grape::Entity
expose :name expose :name
expose :started?, as: :started expose :started?, as: :started
expose :archived?, as: :archived
expose :build_path do |build| expose :build_path do |build|
build_path(build) build_path(build)
......
...@@ -82,6 +82,11 @@ module Ci ...@@ -82,6 +82,11 @@ module Ci
return false return false
end end
if build.archived?
build.drop!(:archived_failure)
return false
end
build.run! build.run!
true true
end end
......
...@@ -41,5 +41,13 @@ ...@@ -41,5 +41,13 @@
The default unit is in seconds, but you can define an alternative. For example: The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>. <code>4 mins 2 sec</code>, <code>2h42min</code>.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
.form-group
= f.label :archive_builds_in_human_readable, 'Archive builds in', class: 'label-bold'
= f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never'
.form-text.text-muted
Set the duration when build gonna be considered old. Archived builds cannot be retried.
Make it empty to never expire builds. It has to be larger than 1 day.
The default unit is in seconds, but you can define an alternative. For example:
<code>4 mins 2 sec</code>, <code>2h42min</code>.
= f.submit 'Save changes', class: "btn btn-success" = f.submit 'Save changes', class: "btn btn-success"
- if can?(current_user, :create_deployment, deployment) && deployment.deployable - if can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment') - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do
- if deployment.last? - if deployment.last?
......
...@@ -61,8 +61,10 @@ ...@@ -61,8 +61,10 @@
%td.responsive-table-cell.build-failure{ data: { column: _("Failure")} } %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} }
= build.present.callout_failure_message = build.present.callout_failure_message
%td.responsive-table-cell.build-actions %td.responsive-table-cell.build-actions
- if can?(current_user, :update_build, job)
= link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
= icon('repeat') = icon('repeat')
- if can?(current_user, :read_build, job)
%tr.build-trace-row.responsive-table-border-end %tr.build-trace-row.responsive-table-border-end
%td %td
%td.responsive-table-cell.build-trace-container{ colspan: 4 } %td.responsive-table-cell.build-trace-container{ colspan: 4 }
......
---
title: Soft-archive old jobs
merge_request:
author:
type: added
# frozen_string_literal: true
class AddArchiveBuildsDurationToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column(:application_settings, :archive_builds_in_seconds, :integer, allow_null: true)
end
end
...@@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do ...@@ -165,6 +165,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do
t.integer "usage_stats_set_by_user_id" t.integer "usage_stats_set_by_user_id"
t.integer "receive_max_input_size" t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false t.integer "diff_max_patch_bytes", default: 102400, null: false
t.integer "archive_builds_in_seconds"
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
......
...@@ -14,7 +14,8 @@ module Gitlab ...@@ -14,7 +14,8 @@ module Gitlab
missing_dependency_failure: 'missing dependency failure', missing_dependency_failure: 'missing dependency failure',
runner_unsupported: 'unsupported runner', runner_unsupported: 'unsupported runner',
stale_schedule: 'stale schedule', stale_schedule: 'stale schedule',
job_execution_timeout: 'job execution timeout' job_execution_timeout: 'job execution timeout',
archived_failure: 'archived failure'
}.freeze }.freeze
private_constant :REASONS private_constant :REASONS
......
...@@ -27,6 +27,12 @@ FactoryBot.define do ...@@ -27,6 +27,12 @@ FactoryBot.define do
pipeline factory: :ci_pipeline pipeline factory: :ci_pipeline
trait :degenerated do
commands nil
options nil
yaml_variables nil
end
trait :started do trait :started do
started_at 'Di 29. Okt 09:51:28 CET 2013' started_at 'Di 29. Okt 09:51:28 CET 2013'
end end
......
...@@ -53,10 +53,21 @@ describe 'Environment' do ...@@ -53,10 +53,21 @@ describe 'Environment' do
it 'does show build name' do it 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})") expect(page).to have_link("#{build.name} (##{build.id})")
expect(page).to have_link('Re-deploy') expect(page).not_to have_link('Re-deploy')
expect(page).not_to have_terminal_button expect(page).not_to have_terminal_button
end end
context 'when user has ability to re-deploy' do
let(:permissions) do
create(:protected_branch, :developers_can_merge,
name: build.ref, project: project)
end
it 'does show re-deploy' do
expect(page).to have_link('Re-deploy')
end
end
context 'with manual action' do context 'with manual action' do
let(:action) do let(:action) do
create(:ci_build, :manual, pipeline: pipeline, create(:ci_build, :manual, pipeline: pipeline,
......
...@@ -388,54 +388,83 @@ describe 'Pipeline', :js do ...@@ -388,54 +388,83 @@ describe 'Pipeline', :js do
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) } let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) } let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
subject { visit pipeline_failures_page }
context 'with failed build' do context 'with failed build' do
before do before do
failed_build.trace.set('4 examples, 1 failure') failed_build.trace.set('4 examples, 1 failure')
visit pipeline_failures_page
end end
it 'shows jobs tab pane as active' do it 'shows jobs tab pane as active' do
subject
expect(page).to have_content('Failed Jobs') expect(page).to have_content('Failed Jobs')
expect(page).to have_css('#js-tab-failures.active') expect(page).to have_css('#js-tab-failures.active')
end end
it 'lists failed builds' do it 'lists failed builds' do
subject
expect(page).to have_content(failed_build.name) expect(page).to have_content(failed_build.name)
expect(page).to have_content(failed_build.stage) expect(page).to have_content(failed_build.stage)
end end
it 'shows build failure logs' do it 'shows build failure logs' do
subject
expect(page).to have_content('4 examples, 1 failure') expect(page).to have_content('4 examples, 1 failure')
end end
it 'shows the failure reason' do it 'shows the failure reason' do
subject
expect(page).to have_content('There is an unknown failure, please try again') expect(page).to have_content('There is an unknown failure, please try again')
end end
context 'when user does not have permission to retry build' do
it 'shows retry button for failed build' do it 'shows retry button for failed build' do
subject
page.within(find('.build-failures', match: :first)) do page.within(find('.build-failures', match: :first)) do
expect(page).to have_link('Retry') expect(page).not_to have_link('Retry')
end end
end end
end end
context 'when missing build logs' do context 'when user does have permission to retry build' do
before do before do
visit pipeline_failures_page create(:protected_branch, :developers_can_merge,
name: pipeline.ref, project: project)
end
it 'shows retry button for failed build' do
subject
page.within(find('.build-failures', match: :first)) do
expect(page).to have_link('Retry')
end
end
end
end end
context 'when missing build logs' do
it 'shows jobs tab pane as active' do it 'shows jobs tab pane as active' do
subject
expect(page).to have_content('Failed Jobs') expect(page).to have_content('Failed Jobs')
expect(page).to have_css('#js-tab-failures.active') expect(page).to have_css('#js-tab-failures.active')
end end
it 'lists failed builds' do it 'lists failed builds' do
subject
expect(page).to have_content(failed_build.name) expect(page).to have_content(failed_build.name)
expect(page).to have_content(failed_build.stage) expect(page).to have_content(failed_build.stage)
end end
it 'does not show trace' do it 'does not show trace' do
subject
expect(page).to have_content('No job trace') expect(page).to have_content('No job trace')
end end
end end
...@@ -448,11 +477,9 @@ describe 'Pipeline', :js do ...@@ -448,11 +477,9 @@ describe 'Pipeline', :js do
end end
context 'when accessing failed jobs page' do context 'when accessing failed jobs page' do
before do
visit pipeline_failures_page
end
it 'fails to access the page' do it 'fails to access the page' do
subject
expect(page).to have_title('Access Denied') expect(page).to have_title('Access Denied')
end end
end end
...@@ -461,11 +488,11 @@ describe 'Pipeline', :js do ...@@ -461,11 +488,11 @@ describe 'Pipeline', :js do
context 'without failures' do context 'without failures' do
before do before do
failed_build.update!(status: :success) failed_build.update!(status: :success)
visit pipeline_failures_page
end end
it 'displays the pipeline graph' do it 'displays the pipeline graph' do
subject
expect(current_path).to eq(pipeline_path(pipeline)) expect(current_path).to eq(pipeline_path(pipeline))
expect(page).not_to have_content('Failed Jobs') expect(page).not_to have_content('Failed Jobs')
expect(page).to have_selector('.pipeline-visualization') expect(page).to have_selector('.pipeline-visualization')
......
...@@ -9,7 +9,8 @@ ...@@ -9,7 +9,8 @@
"playable", "playable",
"created_at", "created_at",
"updated_at", "updated_at",
"status" "status",
"archived"
], ],
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
...@@ -27,7 +28,8 @@ ...@@ -27,7 +28,8 @@
"updated_at": { "type": "string" }, "updated_at": { "type": "string" },
"status": { "$ref": "../status/ci_detailed_status.json" }, "status": { "$ref": "../status/ci_detailed_status.json" },
"callout_message": { "type": "string" }, "callout_message": { "type": "string" },
"recoverable": { "type": "boolean" } "recoverable": { "type": "boolean" },
"archived": { "type": "boolean" }
}, },
"additionalProperties": true "additionalProperties": true
} }
...@@ -594,4 +594,24 @@ describe ApplicationSetting do ...@@ -594,4 +594,24 @@ describe ApplicationSetting do
end end
end end
end end
describe '#archive_builds_older_than' do
subject { setting.archive_builds_older_than }
context 'when the archive_builds_in_seconds is set' do
before do
setting.archive_builds_in_seconds = 3600
end
it { is_expected.to be_within(1.minute).of(1.hour.ago) }
end
context 'when the archive_builds_in_seconds is set' do
before do
setting.archive_builds_in_seconds = nil
end
it { is_expected.to be_nil }
end
end
end end
...@@ -1314,6 +1314,14 @@ describe Ci::Build do ...@@ -1314,6 +1314,14 @@ describe Ci::Build do
it { is_expected.not_to be_retryable } it { is_expected.not_to be_retryable }
end end
context 'when build is degenerated' do
before do
build.degenerate!
end
it { is_expected.not_to be_retryable }
end
end end
end end
...@@ -1396,6 +1404,14 @@ describe Ci::Build do ...@@ -1396,6 +1404,14 @@ describe Ci::Build do
expect(subject.retries_max).to eq 0 expect(subject.retries_max).to eq 0
end end
end end
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated) }
it 'returns zero' do
expect(subject.retries_max).to eq 0
end
end
end end
end end
...@@ -1659,6 +1675,12 @@ describe Ci::Build do ...@@ -1659,6 +1675,12 @@ describe Ci::Build do
it { is_expected.to be_playable } it { is_expected.to be_playable }
end end
context 'when build is a manual and degenerated' do
subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual) }
it { is_expected.not_to be_playable }
end
end end
context 'when build is scheduled' do context 'when build is scheduled' do
...@@ -3227,4 +3249,54 @@ describe Ci::Build do ...@@ -3227,4 +3249,54 @@ describe Ci::Build do
it { expect(build.deployment_status).to eq(:creating) } it { expect(build.deployment_status).to eq(:creating) }
end end
end end
describe '#degenerated?' do
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated) }
it { is_expected.to be_degenerated }
end
context 'when build is valid' do
subject { create(:ci_build) }
it { is_expected.not_to be_degenerated }
context 'and becomes degenerated' do
before do
subject.degenerate!
end
it { is_expected.to be_degenerated }
end
end
end
describe '#archived?' do
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated) }
it { is_expected.to be_archived }
end
context 'for old build' do
subject { create(:ci_build, created_at: 1.day.ago) }
context 'when archive_builds_in is set' do
before do
stub_application_setting(archive_builds_in_seconds: 3600)
end
it { is_expected.to be_archived }
end
context 'when archive_builds_in is not set' do
before do
stub_application_setting(archive_builds_in_seconds: nil)
end
it { is_expected.not_to be_archived }
end
end
end
end end
...@@ -267,7 +267,7 @@ describe Ci::BuildPresenter do ...@@ -267,7 +267,7 @@ describe Ci::BuildPresenter do
let(:build) { create(:ci_build, :failed, :script_failure) } let(:build) { create(:ci_build, :failed, :script_failure) }
context 'when is a script or missing dependency failure' do context 'when is a script or missing dependency failure' do
let(:failure_reasons) { %w(script_failure missing_dependency_failure) } let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) }
it 'should return false' do it 'should return false' do
failure_reasons.each do |failure_reason| failure_reasons.each do |failure_reason|
......
...@@ -478,6 +478,20 @@ module Ci ...@@ -478,6 +478,20 @@ module Ci
it_behaves_like 'validation is not active' it_behaves_like 'validation is not active'
end end
end end
context 'when build is degenerated' do
let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) }
subject { execute(specific_runner, {}) }
it 'does not pick the build and drops the build' do
expect(subject).to be_nil
pending_job.reload
expect(pending_job).to be_failed
expect(pending_job).to be_archived_failure
end
end
end end
describe '#register_success' do describe '#register_success' do
......
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