Merge branch 'add-per-runner-job-timeout' into 'master'

Add per runner job timeout

Closes #43426

See merge request gitlab-org/gitlab-ce!17221
This commit is contained in:
Kamil Trzciński 2018-03-29 10:54:06 +00:00
commit 8b37ce6f7f
35 changed files with 645 additions and 30 deletions

View file

@ -11,11 +11,19 @@
type: String,
required: true,
},
helpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
hasTitle() {
return this.title.length > 0;
},
hasHelpURL() {
return this.helpUrl.length > 0;
},
},
};
</script>
@ -28,5 +36,21 @@
{{ title }}:
</span>
{{ value }}
<span
v-if="hasHelpURL"
class="help-button pull-right"
>
<a
:href="helpUrl"
target="_blank"
rel="noopener noreferrer nofollow"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
></i>
</a>
</span>
</p>
</template>

View file

@ -22,6 +22,11 @@
type: Boolean,
required: true,
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
shouldRenderContent() {
@ -39,6 +44,21 @@
runnerId() {
return `#${this.job.runner.id}`;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
return t;
},
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
@ -114,6 +134,13 @@
title="Queued"
:value="queued"
/>
<detail-row
class="js-job-timeout"
v-if="hasTimeout"
title="Timeout"
:help-url="runnerHelpUrl"
:value="timeout"
/>
<detail-row
class="js-job-runner"
v-if="job.runner"

View file

@ -51,6 +51,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
});
},

View file

@ -24,6 +24,10 @@ module Ci
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@persisted_environment ||= Environment.find_by(
@ -153,6 +157,14 @@ module Ci
before_transition any => [:running] do |build|
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end
before_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
end
def ensure_metadata
metadata || build_metadata(project: project)
end
def detailed_status(current_user)
@ -231,10 +243,6 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx)
end
def timeout
project.build_timeout
end
def triggered_by?(current_user)
user == current_user
end

View file

@ -0,0 +1,35 @@
module Ci
# The purpose of this class is to store Build related data that can be disposed.
# Data that should be persisted forever, should be stored with Ci::Build model.
class BuildMetadata < ActiveRecord::Base
extend Gitlab::Ci::Model
include Presentable
include ChronicDurationAttribute
self.table_name = 'ci_builds_metadata'
belongs_to :build, class_name: 'Ci::Build'
belongs_to :project
validates :build, presence: true
validates :project, presence: true
chronic_duration_attr_reader :timeout_human_readable, :timeout
enum timeout_source: {
unknown_timeout_source: 1,
project_timeout_source: 2,
runner_timeout_source: 3
}
def update_timeout_state
return unless build.runner.present?
project_timeout = project&.build_timeout
timeout = [project_timeout, build.runner.maximum_timeout].compact.min
timeout_source = timeout < project_timeout ? :runner_timeout_source : :project_timeout_source
update(timeout: timeout, timeout_source: timeout_source)
end
end
end

View file

@ -3,12 +3,13 @@ module Ci
extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern
include RedisCacheable
include ChronicDurationAttribute
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@ -51,6 +52,12 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
validates :maximum_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 600,
message: 'needs to be at least 10 minutes' }
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.

View file

@ -0,0 +1,39 @@
module ChronicDurationAttribute
extend ActiveSupport::Concern
class_methods do
def chronic_duration_attr_reader(virtual_attribute, source_attribute)
define_method(virtual_attribute) do
chronic_duration_attributes[virtual_attribute] || output_chronic_duration_attribute(source_attribute)
end
end
def chronic_duration_attr_writer(virtual_attribute, source_attribute)
chronic_duration_attr_reader(virtual_attribute, source_attribute)
define_method("#{virtual_attribute}=") do |value|
chronic_duration_attributes[virtual_attribute] = value.presence || ''
begin
new_value = ChronicDuration.parse(value).to_i if value.present?
assign_attributes(source_attribute => new_value)
rescue ChronicDuration::DurationParseError
# ignore error as it will be caught by validation
end
end
validates virtual_attribute, allow_nil: true, duration: true
end
alias_method :chronic_duration_attr, :chronic_duration_attr_writer
end
def chronic_duration_attributes
@chronic_duration_attributes ||= {}
end
def output_chronic_duration_attribute(source_attribute)
value = attributes[source_attribute.to_s]
ChronicDuration.output(value, format: :short) if value
end
end

View file

@ -0,0 +1,18 @@
module Ci
class BuildMetadataPresenter < Gitlab::View::Presenter::Delegated
TIMEOUT_SOURCES = {
unknown_timeout_source: nil,
project_timeout_source: 'project',
runner_timeout_source: 'runner'
}.freeze
presents :metadata
def timeout_source
return unless metadata.timeout_source?
TIMEOUT_SOURCES[metadata.timeout_source.to_sym] ||
metadata.timeout_source
end
end
end

View file

@ -5,6 +5,8 @@ class BuildDetailsEntity < JobEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
expose :metadata, using: BuildMetadataEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)

View file

@ -0,0 +1,9 @@
class BuildMetadataEntity < Grape::Entity
expose :timeout_human_readable do |metadata|
metadata.timeout_human_readable unless metadata.timeout.nil?
end
expose :timeout_source do |metadata|
metadata.present.timeout_source
end
end

View file

@ -111,4 +111,4 @@
.js-build-options{ data: javascript_build_options }
#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json) } }
#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner') } }

View file

@ -39,6 +39,12 @@
Description
.col-sm-10
= f.text_field :description, class: 'form-control'
.form-group
= label_tag :maximum_timeout_human_readable, class: 'control-label' do
Maximum job timeout
.col-sm-10
= f.text_field :maximum_timeout_human_readable, class: 'form-control'
.help-block This timeout will take precedence when lower than Project-defined timeout
.form-group
= label_tag :tag_list, class: 'control-label' do
Tags

View file

@ -55,6 +55,9 @@
%tr
%td Description
%td= @runner.description
%tr
%td Maximum job timeout
%td= @runner.maximum_timeout_human_readable
%tr
%td Last contact
%td

View file

@ -0,0 +1,5 @@
---
title: Add per-runner configured job timeout
merge_request: 17221
author:
type: added

View file

@ -0,0 +1,9 @@
class AddMaximumTimeoutToCiRunners < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_runners, :maximum_timeout, :integer
end
end

View file

@ -0,0 +1,20 @@
class CreateCiBuildsMetadataTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :ci_builds_metadata do |t|
t.integer :build_id, null: false
t.integer :project_id, null: false
t.integer :timeout
t.integer :timeout_source, null: false, default: 1
t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
t.foreign_key :projects, column: :project_id, on_delete: :cascade
t.index :build_id, unique: true
t.index :project_id
end
end
end

View file

@ -329,6 +329,16 @@ ActiveRecord::Schema.define(version: 20180327101207) do
add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
create_table "ci_builds_metadata", force: :cascade do |t|
t.integer "build_id", null: false
t.integer "project_id", null: false
t.integer "timeout"
t.integer "timeout_source", default: 1, null: false
end
add_index "ci_builds_metadata", ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true, using: :btree
add_index "ci_builds_metadata", ["project_id"], name: "index_ci_builds_metadata_on_project_id", using: :btree
create_table "ci_group_variables", force: :cascade do |t|
t.string "key", null: false
t.text "value"
@ -459,6 +469,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.boolean "locked", default: false, null: false
t.integer "access_level", default: 0, null: false
t.string "ip_address"
t.integer "maximum_timeout"
end
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
@ -2027,6 +2038,8 @@ ActiveRecord::Schema.define(version: 20180327101207) do
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "projects", on_delete: :cascade
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade

View file

@ -153,7 +153,8 @@ Example response:
"mysql"
],
"version": null,
"access_level": "ref_protected"
"access_level": "ref_protected",
"maximum_timeout": 3600
}
```
@ -174,6 +175,7 @@ PUT /runners/:id
| `run_untagged` | boolean | no | Flag indicating the runner can execute untagged jobs |
| `locked` | boolean | no | Flag indicating the runner is locked |
| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
```
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
@ -211,7 +213,8 @@ Example response:
"tag2"
],
"version": null,
"access_level": "ref_protected"
"access_level": "ref_protected",
"maximum_timeout": null
}
```

View file

@ -231,6 +231,38 @@ To make a Runner pick tagged/untagged jobs:
1. Check the **Run untagged jobs** option
1. Click **Save changes** for the changes to take effect
### Setting maximum job timeout for a Runner
For each Runner you can specify a _maximum job timeout_. Such timeout,
if smaller than [project defined timeout], will take the precedence. This
feature can be used to prevent Shared Runner from being appropriated
by a project by setting a ridiculous big timeout (e.g. one week).
When not configured, Runner will not override project timeout.
How this feature will work:
**Example 1 - Runner timeout bigger than project timeout**
1. You set the _maximum job timeout_ for a Runner to 24 hours
1. You set the _CI/CD Timeout_ for a project to **2 hours**
1. You start a job
1. The job, if running longer, will be timeouted after **2 hours**
**Example 2 - Runner timeout not configured**
1. You remove the _maximum job timeout_ configuration from a Runner
1. You set the _CI/CD Timeout_ for a project to **2 hours**
1. You start a job
1. The job, if running longer, will be timeouted after **2 hours**
**Example 3 - Runner timeout smaller than project timeout**
1. You set the _maximum job timeout_ for a Runner to **30 minutes**
1. You set the _CI/CD Timeout_ for a project to 2 hours
1. You start a job
1. The job, if running longer, will be timeouted after **30 minutes**
### Be careful with sensitive information
With some [Runner Executors](https://docs.gitlab.com/runner/executors/README.html),
@ -259,12 +291,6 @@ Mentioned briefly earlier, but the following things of Runners can be exploited.
We're always looking for contributions that can mitigate these
[Security Considerations](https://docs.gitlab.com/runner/security/).
[install]: http://docs.gitlab.com/runner/install/
[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
[register]: http://docs.gitlab.com/runner/register/
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
## Determining the IP address of a Runner
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6.
@ -297,3 +323,10 @@ You can find the IP address of a Runner for a specific project by:
1. On the details page you should see a row for "IP Address"
![specific Runner IP address](img/specific_runner_ip_address.png)
[install]: http://docs.gitlab.com/runner/install/
[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
[register]: http://docs.gitlab.com/runner/register/
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
[project defined timeout]: ../../user/project/pipelines/settings.html#timeout

View file

@ -27,6 +27,13 @@ The default value is 60 minutes. Decrease the time limit if you want to impose
a hard limit on your jobs' running time or increase it otherwise. In any case,
if the job surpasses the threshold, it is marked as failed.
### Timeout overriding on Runner level
> - [Introduced][ce-17221] in GitLab 10.6.
Project defined timeout (either specific timeout set by user or the default
60 minutes timeout) may be [overridden on Runner level][timeout overriding].
## Custom CI config path
> - [Introduced][ce-12509] in GitLab 9.4.
@ -152,5 +159,7 @@ into your `README.md`:
[var]: ../../../ci/yaml/README.md#git-strategy
[coverage report]: #test-coverage-parsing
[timeout overriding]: ../../../ci/runners/README.html#setting-maximum-job-timeout-for-a-runner
[ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362
[ce-12509]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509
[ce-17221]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17221

View file

@ -951,6 +951,7 @@ module API
expose :tag_list
expose :run_untagged
expose :locked
expose :maximum_timeout
expose :access_level
expose :version, :revision, :platform, :architecture
expose :contacted_at
@ -1119,7 +1120,7 @@ module API
end
class RunnerInfo < Grape::Entity
expose :timeout
expose :metadata_timeout, as: :timeout
end
class Step < Grape::Entity

View file

@ -14,9 +14,10 @@ module API
optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
end
post '/' do
attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list])
attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list, :maximum_timeout])
.merge(get_runner_details_from_request)
runner =

View file

@ -57,6 +57,7 @@ module API
optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
optional :access_level, type: String, values: Ci::Runner.access_levels.keys,
desc: 'The access_level of the runner'
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
at_least_one_of :description, :active, :tag_list, :run_untagged, :locked, :access_level
end
put ':id' do

View file

@ -14,7 +14,7 @@ module Gitlab
self.new(:script).tap do |step|
step.script = job.options[:before_script].to_a + job.options[:script].to_a
step.script = job.commands.split("\n") if step.script.empty?
step.timeout = job.timeout
step.timeout = job.metadata_timeout
step.when = WHEN_ON_SUCCESS
end
end
@ -25,7 +25,7 @@ module Gitlab
self.new(:after_script).tap do |step|
step.script = after_script
step.timeout = job.timeout
step.timeout = job.metadata_timeout
step.when = WHEN_ALWAYS
step.allow_failure = true
end

View file

@ -0,0 +1,9 @@
FactoryBot.define do
factory :ci_build_metadata, class: Ci::BuildMetadata do
build factory: :ci_build
after(:build) do |build_metadata, _|
build_metadata.project ||= build_metadata.build.project
end
end
end

View file

@ -115,6 +115,10 @@ export default {
commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
},
},
metadata: {
timeout_human_readable: '1m 40s',
timeout_source: 'runner',
},
merge_request: {
iid: 2,
path: '/root/ci-mock/merge_requests/2',

View file

@ -37,4 +37,25 @@ describe('Sidebar detail row', () => {
vm.$el.textContent.replace(/\s+/g, ' ').trim(),
).toEqual('this is the title: this is the value');
});
describe('when helpUrl not provided', () => {
it('should not render help', () => {
expect(vm.$el.querySelector('.help-button')).toBeNull();
});
});
describe('when helpUrl provided', () => {
beforeEach(() => {
vm = new SidebarDetailRow({
propsData: {
helpUrl: 'help url',
value: 'foo',
},
}).$mount();
});
it('should render help', () => {
expect(vm.$el.querySelector('.help-button a').getAttribute('href')).toEqual('help url');
});
});
});

View file

@ -96,6 +96,12 @@ describe('Sidebar details block', () => {
).toEqual('Runner: #1');
});
it('should render timeout information', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-timeout')),
).toEqual('Timeout: 1m 40s (from runner)');
});
it('should render coverage', () => {
expect(
trimWhitespace(vm.$el.querySelector('.js-job-coverage')),

View file

@ -5,10 +5,14 @@ describe Gitlab::Ci::Build::Step do
shared_examples 'has correct script' do
subject { described_class.from_commands(job) }
before do
job.run!
end
it 'fabricates an object' do
expect(subject.name).to eq(:script)
expect(subject.script).to eq(script)
expect(subject.timeout).to eq(job.timeout)
expect(subject.timeout).to eq(job.metadata_timeout)
expect(subject.when).to eq('on_success')
expect(subject.allow_failure).to be_falsey
end
@ -47,6 +51,10 @@ describe Gitlab::Ci::Build::Step do
subject { described_class.from_after_script(job) }
before do
job.run!
end
context 'when after_script is empty' do
it 'doesn not fabricate an object' do
is_expected.to be_nil
@ -59,7 +67,7 @@ describe Gitlab::Ci::Build::Step do
it 'fabricates an object' do
expect(subject.name).to eq(:after_script)
expect(subject.script).to eq(['ls -la', 'date'])
expect(subject.timeout).to eq(job.timeout)
expect(subject.timeout).to eq(job.metadata_timeout)
expect(subject.when).to eq('always')
expect(subject.allow_failure).to be_truthy
end

View file

@ -0,0 +1,61 @@
require 'spec_helper'
describe Ci::BuildMetadata do
set(:user) { create(:user) }
set(:group) { create(:group, :access_requestable) }
set(:project) { create(:project, :repository, group: group, build_timeout: 2000) }
set(:pipeline) do
create(:ci_pipeline, project: project,
sha: project.commit.id,
ref: project.default_branch,
status: 'success')
end
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:build_metadata) { create(:ci_build_metadata, build: build) }
describe '#update_timeout_state' do
subject { build_metadata }
context 'when runner is not assigned to the job' do
it "doesn't change timeout value" do
expect { subject.update_timeout_state }.not_to change { subject.reload.timeout }
end
it "doesn't change timeout_source value" do
expect { subject.update_timeout_state }.not_to change { subject.reload.timeout_source }
end
end
context 'when runner is assigned to the job' do
before do
build.update_attributes(runner: runner)
end
context 'when runner timeout is lower than project timeout' do
let(:runner) { create(:ci_runner, maximum_timeout: 1900) }
it 'sets runner timeout' do
expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(1900)
end
it 'sets runner_timeout_source' do
expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to('runner_timeout_source')
end
end
context 'when runner timeout is higher than project timeout' do
let(:runner) { create(:ci_runner, maximum_timeout: 2100) }
it 'sets project timeout' do
expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(2000)
end
it 'sets project_timeout_source' do
expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to('project_timeout_source')
end
end
end
end
end

View file

@ -1271,12 +1271,6 @@ describe Ci::Build do
end
describe 'project settings' do
describe '#timeout' do
it 'returns project timeout configuration' do
expect(build.timeout).to eq(project.build_timeout)
end
end
describe '#allow_git_fetch' do
it 'return project allow_git_fetch configuration' do
expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch)
@ -2011,6 +2005,70 @@ describe Ci::Build do
end
end
describe 'state transition: pending: :running' do
let(:runner) { create(:ci_runner) }
let(:job) { create(:ci_build, :pending, runner: runner) }
before do
job.project.update_attribute(:build_timeout, 1800)
end
def run_job_without_exception
job.run!
rescue StateMachines::InvalidTransition
end
shared_examples 'saves data on transition' do
it 'saves timeout' do
expect { job.run! }.to change { job.reload.ensure_metadata.timeout }.from(nil).to(expected_timeout)
end
it 'saves timeout_source' do
expect { job.run! }.to change { job.reload.ensure_metadata.timeout_source }.from('unknown_timeout_source').to(expected_timeout_source)
end
context 'when Ci::BuildMetadata#update_timeout_state fails update' do
before do
allow_any_instance_of(Ci::BuildMetadata).to receive(:update_timeout_state).and_return(false)
end
it "doesn't save timeout" do
expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
end
it "doesn't save timeout_source" do
expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
end
it 'raises an exception' do
expect { job.run! }.to raise_error(StateMachines::InvalidTransition)
end
end
end
context 'when runner timeout overrides project timeout' do
let(:expected_timeout) { 900 }
let(:expected_timeout_source) { 'runner_timeout_source' }
before do
runner.update_attribute(:maximum_timeout, 900)
end
it_behaves_like 'saves data on transition'
end
context "when runner timeout doesn't override project timeout" do
let(:expected_timeout) { 1800 }
let(:expected_timeout_source) { 'project_timeout_source' }
before do
runner.update_attribute(:maximum_timeout, 3600)
end
it_behaves_like 'saves data on transition'
end
end
describe 'state transition: any => [:running]' do
shared_examples 'validation is active' do
context 'when depended job has not been completed yet' do

View file

@ -0,0 +1,115 @@
require 'spec_helper'
shared_examples 'ChronicDurationAttribute reader' do
it 'contains dynamically created reader method' do
expect(subject.class).to be_public_method_defined(virtual_field)
end
it 'outputs chronic duration formatted value' do
subject.send("#{source_field}=", 120)
expect(subject.send(virtual_field)).to eq('2m')
end
context 'when value is set to nil' do
it 'outputs nil' do
subject.send("#{source_field}=", nil)
expect(subject.send(virtual_field)).to be_nil
end
end
end
shared_examples 'ChronicDurationAttribute writer' do
it 'contains dynamically created writer method' do
expect(subject.class).to be_public_method_defined("#{virtual_field}=")
end
before do
subject.send("#{virtual_field}=", '10m')
end
it 'parses chronic duration input' do
expect(subject.send(source_field)).to eq(600)
end
it 'passes validation' do
expect(subject.valid?).to be_truthy
end
context 'when negative input is used' do
before do
subject.send("#{source_field}=", 3600)
end
it "doesn't raise exception" do
expect { subject.send("#{virtual_field}=", '-10m') }.not_to raise_error(ChronicDuration::DurationParseError)
end
it "doesn't change value" do
expect { subject.send("#{virtual_field}=", '-10m') }.not_to change { subject.send(source_field) }
end
it "doesn't pass validation" do
subject.send("#{virtual_field}=", '-10m')
expect(subject.valid?).to be_falsey
expect(subject.errors&.messages).to include(virtual_field => ['is not a correct duration'])
end
end
context 'when empty input is used' do
before do
subject.send("#{virtual_field}=", '')
end
it 'writes nil' do
expect(subject.send(source_field)).to be_nil
end
it 'passes validation' do
expect(subject.valid?).to be_truthy
end
end
context 'when nil input is used' do
before do
subject.send("#{virtual_field}=", nil)
end
it 'writes nil' do
expect(subject.send(source_field)).to be_nil
end
it 'passes validation' do
expect(subject.valid?).to be_truthy
end
it "doesn't raise exception" do
expect { subject.send("#{virtual_field}=", nil) }.not_to raise_error(NoMethodError)
end
end
end
describe 'ChronicDurationAttribute' do
let(:source_field) {:maximum_timeout}
let(:virtual_field) {:maximum_timeout_human_readable}
subject { Ci::Runner.new }
it_behaves_like 'ChronicDurationAttribute reader'
it_behaves_like 'ChronicDurationAttribute writer'
end
describe 'ChronicDurationAttribute - reader' do
let(:source_field) {:timeout}
let(:virtual_field) {:timeout_human_readable}
subject {Ci::BuildMetadata.new}
it "doesn't contain dynamically created writer method" do
expect(subject.class).not_to be_public_method_defined("#{virtual_field}=")
end
it_behaves_like 'ChronicDurationAttribute reader'
end

View file

@ -109,6 +109,26 @@ describe API::Runner do
end
end
context 'when maximum job timeout is specified' do
it 'creates runner' do
post api('/runners'), token: registration_token,
maximum_timeout: 9000
expect(response).to have_gitlab_http_status 201
expect(Ci::Runner.first.maximum_timeout).to eq(9000)
end
context 'when maximum job timeout is empty' do
it 'creates runner' do
post api('/runners'), token: registration_token,
maximum_timeout: ''
expect(response).to have_gitlab_http_status 201
expect(Ci::Runner.first.maximum_timeout).to be_nil
end
end
end
%w(name version revision platform architecture).each do |param|
context "when info parameter '#{param}' info is present" do
let(:value) { "#{param}_value" }
@ -340,12 +360,12 @@ describe API::Runner do
let(:expected_steps) do
[{ 'name' => 'script',
'script' => %w(ls date),
'timeout' => job.timeout,
'timeout' => job.metadata_timeout,
'when' => 'on_success',
'allow_failure' => false },
{ 'name' => 'after_script',
'script' => %w(ls date),
'timeout' => job.timeout,
'timeout' => job.metadata_timeout,
'when' => 'always',
'allow_failure' => true }]
end
@ -648,6 +668,41 @@ describe API::Runner do
end
end
end
describe 'timeout support' do
context 'when project specifies job timeout' do
let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) }
it 'contains info about timeout taken from project' do
request_job
expect(response).to have_gitlab_http_status(201)
expect(json_response['runner_info']).to include({ 'timeout' => 1234 })
end
context 'when runner specifies lower timeout' do
let(:runner) { create(:ci_runner, maximum_timeout: 1000) }
it 'contains info about timeout overridden by runner' do
request_job
expect(response).to have_gitlab_http_status(201)
expect(json_response['runner_info']).to include({ 'timeout' => 1000 })
end
end
context 'when runner specifies bigger timeout' do
let(:runner) { create(:ci_runner, maximum_timeout: 2000) }
it 'contains info about timeout not overridden by runner' do
request_job
expect(response).to have_gitlab_http_status(201)
expect(json_response['runner_info']).to include({ 'timeout' => 1234 })
end
end
end
end
end
def request_job(token = runner.token, **params)

View file

@ -123,6 +123,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(json_response['description']).to eq(shared_runner.description)
expect(json_response['maximum_timeout']).to be_nil
end
end
@ -192,7 +193,8 @@ describe API::Runners do
tag_list: ['ruby2.1', 'pgsql', 'mysql'],
run_untagged: 'false',
locked: 'true',
access_level: 'ref_protected')
access_level: 'ref_protected',
maximum_timeout: 1234)
shared_runner.reload
expect(response).to have_gitlab_http_status(200)
@ -204,6 +206,7 @@ describe API::Runners do
expect(shared_runner.ref_protected?).to be_truthy
expect(shared_runner.ensure_runner_queue_value)
.not_to eq(runner_queue_value)
expect(shared_runner.maximum_timeout).to eq(1234)
end
end

View file

@ -29,7 +29,8 @@ describe Ci::RetryBuildService do
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
artifacts_file_store artifacts_metadata_store].freeze
artifacts_file_store artifacts_metadata_store
metadata].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }