Commit 0438e1c0 authored by Erick Bajao's avatar Erick Bajao

Refactor schema and use proper insert query

Use INSERT DO UPDATE for better handling of conflicts instead
of using with_lock.
parent e5168c8c
# frozen_string_literal: true
module Ci
class DailyCodeCoverage < ApplicationRecord
extend Gitlab::Ci::Model
def self.create_or_update_for_build(build)
ref = connection.quote(build.ref)
name = connection.quote(build.name)
date = connection.quote(build.created_at.to_date)
connection.execute <<-EOF.strip_heredoc
INSERT INTO #{table_name} (project_id, ref, name, date, last_build_id, coverage)
VALUES (#{build.project_id}, #{ref}, #{name}, #{date}, #{build.id}, #{build.coverage})
ON CONFLICT (project_id, ref, name, date)
DO UPDATE SET coverage = #{build.coverage}, last_build_id = #{build.id} WHERE #{table_name}.last_build_id < #{build.id};
EOF
end
end
end
...@@ -189,7 +189,10 @@ module Ci ...@@ -189,7 +189,10 @@ module Ci
end end
after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline| after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline|
pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) } # We want to wait a little bit to ensure that all BuildFinishedWorkers finish first
# because this is where code coverage is parsed and stored in CI build records which
# the daily code coverage worker relies on.
pipeline.run_after_commit { Ci::DailyCodeCoverageWorker.perform_in(5.minutes, pipeline.id) }
end end
after_transition do |pipeline, transition| after_transition do |pipeline, transition|
......
# frozen_string_literal: true
class DailyCodeCoverage < ApplicationRecord
validates :project_id, presence: true, uniqueness: { scope: [:ref, :name, :date], case_sensitive: false }
validates :last_pipeline_id, presence: true
validates :ref, presence: true
validates :name, presence: true
validates :coverage, presence: true
validates :date, presence: true
validate :newer_pipeline
private
def newer_pipeline
return if new_record?
return unless last_pipeline_id_changed?
old_pipeline_id, new_pipeline_id = last_pipeline_id_change
return if new_pipeline_id > old_pipeline_id
errors.add(:last_pipeline_id, 'new pipeline ID must be newer than the existing one')
end
end
...@@ -3,27 +3,11 @@ ...@@ -3,27 +3,11 @@
module Ci module Ci
class DailyCodeCoverageService class DailyCodeCoverageService
def execute(pipeline) def execute(pipeline)
return unless Feature.enabled?(:ci_daily_code_coverage, default_enabled: true)
pipeline.builds.with_coverage.each do |build| pipeline.builds.with_coverage.each do |build|
daily_coverage = daily_coverage_for(pipeline, build) DailyCodeCoverage.create_or_update_for_build(build)
daily_coverage.with_lock do
daily_coverage.coverage = build.coverage
daily_coverage.last_pipeline_id = pipeline.id
daily_coverage.save
end
end
end end
private
def daily_coverage_for(pipeline, build)
# rubocop: disable CodeReuse/ActiveRecord
DailyCodeCoverage.find_or_initialize_by(
project_id: pipeline.project_id,
ref: pipeline.ref,
name: build.name,
date: pipeline.created_at.to_date
)
# rubocop: enable CodeReuse/ActiveRecord
end end
end end
end end
...@@ -605,7 +605,7 @@ ...@@ -605,7 +605,7 @@
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: :idempotent:
- :name: pipeline_background:daily_code_coverage - :name: pipeline_background:ci_daily_code_coverage
:feature_category: :continuous_integration :feature_category: :continuous_integration
:has_external_dependencies: :has_external_dependencies:
:urgency: :default :urgency: :default
......
# frozen_string_literal: true
module Ci
class DailyCodeCoverageWorker
include ApplicationWorker
include PipelineBackgroundQueue
def perform(pipeline_id)
Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
Ci::DailyCodeCoverageService.new.execute(pipeline)
end
end
end
end
# frozen_string_literal: true
class DailyCodeCoverageWorker
include ApplicationWorker
include PipelineBackgroundQueue
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
Ci::DailyCodeCoverageService.new.execute(pipeline)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
...@@ -4,17 +4,18 @@ class CreateDailyCodeCoverages < ActiveRecord::Migration[6.0] ...@@ -4,17 +4,18 @@ class CreateDailyCodeCoverages < ActiveRecord::Migration[6.0]
DOWNTIME = false DOWNTIME = false
def change def change
create_table :daily_code_coverages do |t| create_table :ci_daily_code_coverages do |t|
t.date :date, null: false t.date :date, null: false
t.integer :project_id, null: false t.bigint :project_id, null: false
t.integer :last_pipeline_id, null: false t.bigint :last_build_id, null: false
t.float :coverage, null: false t.float :coverage, null: false
t.string :ref, null: false # rubocop:disable Migration/AddLimitToStringColumns t.string :ref, null: false # rubocop:disable Migration/AddLimitToStringColumns
t.string :name, null: false # rubocop:disable Migration/AddLimitToStringColumns t.string :name, null: false # rubocop:disable Migration/AddLimitToStringColumns
t.index :last_build_id
t.index [:project_id, :ref, :name, :date], name: 'index_daily_code_coverage_unique_columns', unique: true, order: { date: :desc } t.index [:project_id, :ref, :name, :date], name: 'index_daily_code_coverage_unique_columns', unique: true, order: { date: :desc }
t.foreign_key :projects, on_delete: :cascade t.foreign_key :projects, on_delete: :cascade
t.foreign_key :ci_pipelines, column: :last_pipeline_id, on_delete: :cascade t.foreign_key :ci_builds, column: :last_build_id, on_delete: :cascade
end end
end end
end end
...@@ -738,6 +738,17 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do ...@@ -738,6 +738,17 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
t.index ["build_id"], name: "index_ci_builds_runner_session_on_build_id", unique: true t.index ["build_id"], name: "index_ci_builds_runner_session_on_build_id", unique: true
end end
create_table "ci_daily_code_coverages", force: :cascade do |t|
t.date "date", null: false
t.bigint "project_id", null: false
t.bigint "last_build_id", null: false
t.float "coverage", null: false
t.string "ref", null: false
t.string "name", null: false
t.index ["last_build_id"], name: "index_ci_daily_code_coverages_on_last_build_id"
t.index ["project_id", "ref", "name", "date"], name: "index_daily_code_coverage_unique_columns", unique: true, order: { date: :desc }
end
create_table "ci_group_variables", id: :serial, force: :cascade do |t| create_table "ci_group_variables", id: :serial, force: :cascade do |t|
t.string "key", null: false t.string "key", null: false
t.text "value" t.text "value"
...@@ -1327,16 +1338,6 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do ...@@ -1327,16 +1338,6 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
t.float "percentage_service_desk_issues", default: 0.0, null: false t.float "percentage_service_desk_issues", default: 0.0, null: false
end end
create_table "daily_code_coverages", force: :cascade do |t|
t.date "date", null: false
t.integer "project_id", null: false
t.integer "last_pipeline_id", null: false
t.float "coverage", null: false
t.string "ref", null: false
t.string "name", null: false
t.index ["project_id", "ref", "name", "date"], name: "index_daily_code_coverage_unique_columns", unique: true, order: { date: :desc }
end
create_table "dependency_proxy_blobs", id: :serial, force: :cascade do |t| create_table "dependency_proxy_blobs", id: :serial, force: :cascade do |t|
t.integer "group_id", null: false t.integer "group_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
...@@ -4775,6 +4776,8 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do ...@@ -4775,6 +4776,8 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", 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_builds_metadata", "projects", on_delete: :cascade
add_foreign_key "ci_builds_runner_session", "ci_builds", column: "build_id", on_delete: :cascade add_foreign_key "ci_builds_runner_session", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_daily_code_coverages", "ci_builds", column: "last_build_id", on_delete: :cascade
add_foreign_key "ci_daily_code_coverages", "projects", on_delete: :cascade
add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", 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", "ci_builds", column: "job_id", on_delete: :cascade
add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade
...@@ -4842,8 +4845,6 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do ...@@ -4842,8 +4845,6 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
add_foreign_key "commit_user_mentions", "notes", on_delete: :cascade add_foreign_key "commit_user_mentions", "notes", on_delete: :cascade
add_foreign_key "container_expiration_policies", "projects", on_delete: :cascade add_foreign_key "container_expiration_policies", "projects", on_delete: :cascade
add_foreign_key "container_repositories", "projects" add_foreign_key "container_repositories", "projects"
add_foreign_key "daily_code_coverages", "ci_pipelines", column: "last_pipeline_id", on_delete: :cascade
add_foreign_key "daily_code_coverages", "projects", on_delete: :cascade
add_foreign_key "dependency_proxy_blobs", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "dependency_proxy_blobs", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "dependency_proxy_group_settings", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "dependency_proxy_group_settings", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
......
# frozen_string_literal: true # frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :daily_code_coverage do factory :ci_daily_code_coverage, class: 'Ci::DailyCodeCoverage' do
ref { 'test_branch' } ref { 'test_branch' }
name { 'test_coverage_job' } name { 'test_coverage_job' }
coverage { 77 } coverage { 77 }
date { Time.zone.now.to_date } date { Time.zone.now.to_date }
after(:build) do |dcc| after(:build) do |dcc|
pipeline = create(:ci_pipeline) build = create(:ci_build)
dcc.project_id = pipeline.project_id unless dcc.project_id dcc.project_id = build.project_id unless dcc.project_id
dcc.last_pipeline_id = pipeline.id unless dcc.last_pipeline_id dcc.last_build_id = build.id unless dcc.last_build_id
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::DailyCodeCoverage do
describe '::create_or_update_for_build' do
let!(:build) { create(:ci_build, created_at: '2020-02-06 00:01:10', name: 'rspec', coverage: 80) }
context 'when there is no existing record with matching project_id, ref, name, date' do
it 'creates a new record for the given build' do
described_class.create_or_update_for_build(build)
expect(described_class.last).to have_attributes(
project_id: build.project.id,
last_build_id: build.id,
ref: build.ref,
name: build.name,
coverage: build.coverage,
date: build.created_at.to_date
)
end
end
context 'when there is existing record with matching project_id, ref, name, date' do
let!(:new_build) { create(:ci_build, project: build.project, created_at: build.created_at, ref: build.ref, name: build.name, coverage: 99) }
let!(:existing) do
create(
:ci_daily_code_coverage,
project_id: existing_build.project.id,
last_build_id: existing_build.id,
ref: existing_build.ref,
name: existing_build.name,
coverage: existing_build.coverage,
date: existing_build.created_at.to_date
)
end
context 'and build ID is newer than last_build_id' do
let(:existing_build) { build }
it 'updates the last_build_id and coverage' do
described_class.create_or_update_for_build(new_build)
existing.reload
expect(existing).to have_attributes(
last_build_id: new_build.id,
coverage: new_build.coverage
)
end
end
context 'and build ID is not newer than last_build_id' do
let(:existing_build) { new_build }
it 'does not update the last_build_id and coverage' do
described_class.create_or_update_for_build(build)
existing.reload
expect(existing).to have_attributes(
last_build_id: new_build.id,
coverage: new_build.coverage
)
end
end
end
end
end
...@@ -1120,7 +1120,7 @@ describe Ci::Pipeline, :mailer do ...@@ -1120,7 +1120,7 @@ describe Ci::Pipeline, :mailer do
let(:from_status) { status } let(:from_status) { status }
it 'schedules pipeline success worker' do it 'schedules pipeline success worker' do
expect(PipelineSuccessWorker).to receive(:perform_async).with(pipeline.id) expect(Ci::DailyCodeCoverageWorker).to receive(:perform_in).with(5.minutes, pipeline.id)
pipeline.succeed pipeline.succeed
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DailyCodeCoverage do
describe 'validation' do
subject { described_class.new }
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:last_pipeline_id) }
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:coverage) }
it { is_expected.to validate_presence_of(:date) }
context 'uniqueness' do
before do
create(:daily_code_coverage)
end
it { is_expected.to validate_uniqueness_of(:project_id).scoped_to([:ref, :name, :date]) }
end
context 'ensuring newer pipeline' do
context 'on new records' do
subject { build(:daily_code_coverage, last_pipeline_id: 1) }
it { is_expected.to be_valid }
end
context 'on existing records' do
subject { create(:daily_code_coverage, last_pipeline_id: 12) }
context 'and new pipeline ID is older' do
before do
subject.last_pipeline_id = 10
end
it { is_expected.not_to be_valid }
end
context 'and new pipeline ID is newer' do
before do
subject.last_pipeline_id = 15
end
it { is_expected.to be_valid }
end
end
end
end
end
...@@ -3,37 +3,37 @@ ...@@ -3,37 +3,37 @@
require 'spec_helper' require 'spec_helper'
describe Ci::DailyCodeCoverageService, '#execute' do describe Ci::DailyCodeCoverageService, '#execute' do
let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') } let!(:pipeline) { create(:ci_pipeline) }
let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: 'rspec', coverage: 80) } let!(:rspec_job) { create(:ci_build, pipeline: pipeline, created_at: '2020-02-06 00:01:10', name: 'rspec', coverage: 80) }
let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: 'karma', coverage: 90) } let!(:karma_job) { create(:ci_build, pipeline: pipeline, created_at: '2020-02-06 00:01:12', name: 'karma', coverage: 90) }
let!(:extra_job) { create(:ci_build, pipeline: pipeline, name: 'extra', coverage: nil) } let!(:extra_job) { create(:ci_build, pipeline: pipeline, created_at: '2020-02-06 00:01:14', name: 'extra', coverage: nil) }
it 'creates daily code coverage record for each job in the pipeline that has coverage value' do it 'creates daily code coverage record for each job in the pipeline that has coverage value' do
described_class.new.execute(pipeline) described_class.new.execute(pipeline)
DailyCodeCoverage.find_by(name: 'rspec').tap do |coverage| Ci::DailyCodeCoverage.find_by(name: 'rspec').tap do |coverage|
expect(coverage).to have_attributes( expect(coverage).to have_attributes(
project_id: pipeline.project.id, project_id: pipeline.project.id,
last_pipeline_id: pipeline.id, last_build_id: rspec_job.id,
ref: pipeline.ref, ref: pipeline.ref,
name: rspec_job.name, name: rspec_job.name,
coverage: rspec_job.coverage, coverage: rspec_job.coverage,
date: pipeline.created_at.to_date date: rspec_job.created_at.to_date
) )
end end
DailyCodeCoverage.find_by(name: 'karma').tap do |coverage| Ci::DailyCodeCoverage.find_by(name: 'karma').tap do |coverage|
expect(coverage).to have_attributes( expect(coverage).to have_attributes(
project_id: pipeline.project.id, project_id: pipeline.project.id,
last_pipeline_id: pipeline.id, last_build_id: karma_job.id,
ref: pipeline.ref, ref: pipeline.ref,
name: karma_job.name, name: karma_job.name,
coverage: karma_job.coverage, coverage: karma_job.coverage,
date: pipeline.created_at.to_date date: karma_job.created_at.to_date
) )
end end
expect(DailyCodeCoverage.find_by(name: 'extra')).to be_nil expect(Ci::DailyCodeCoverage.find_by(name: 'extra')).to be_nil
end end
context 'when there is an existing daily code coverage for the matching date, project, ref, and name' do context 'when there is an existing daily code coverage for the matching date, project, ref, and name' do
...@@ -41,21 +41,20 @@ describe Ci::DailyCodeCoverageService, '#execute' do ...@@ -41,21 +41,20 @@ describe Ci::DailyCodeCoverageService, '#execute' do
create( create(
:ci_pipeline, :ci_pipeline,
project: pipeline.project, project: pipeline.project,
ref: pipeline.ref, ref: pipeline.ref
created_at: '2020-02-06 00:02:20'
) )
end end
let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: 'rspec', coverage: 84) } let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, created_at: '2020-02-06 00:02:20', name: 'rspec', coverage: 84) }
let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: 'karma', coverage: 92) } let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, created_at: '2020-02-06 00:02:22', name: 'karma', coverage: 92) }
before do before do
# Create the existing daily code coverage records # Create the existing daily code coverage records
described_class.new.execute(pipeline) described_class.new.execute(pipeline)
end end
it "updates the existing record's coverage value and last_pipeline_id" do it "updates the existing record's coverage value and last_build_id" do
rspec_coverage = DailyCodeCoverage.find_by(name: 'rspec') rspec_coverage = Ci::DailyCodeCoverage.find_by(name: 'rspec')
karma_coverage = DailyCodeCoverage.find_by(name: 'karma') karma_coverage = Ci::DailyCodeCoverage.find_by(name: 'karma')
# Bump up the coverage values # Bump up the coverage values
described_class.new.execute(new_pipeline) described_class.new.execute(new_pipeline)
...@@ -64,28 +63,27 @@ describe Ci::DailyCodeCoverageService, '#execute' do ...@@ -64,28 +63,27 @@ describe Ci::DailyCodeCoverageService, '#execute' do
karma_coverage.reload karma_coverage.reload
expect(rspec_coverage).to have_attributes( expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id, last_build_id: new_rspec_job.id,
coverage: new_rspec_job.coverage coverage: new_rspec_job.coverage
) )
expect(karma_coverage).to have_attributes( expect(karma_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id, last_build_id: new_karma_job.id,
coverage: new_karma_job.coverage coverage: new_karma_job.coverage
) )
end end
end end
context 'when the ID of the given pipeline is older than the last_pipeline_id' do context 'when the ID of the build is older than the last_build_id' do
let!(:new_pipeline) do let!(:new_pipeline) do
create( create(
:ci_pipeline, :ci_pipeline,
project: pipeline.project, project: pipeline.project,
ref: pipeline.ref, ref: pipeline.ref
created_at: '2020-02-06 00:02:20'
) )
end end
let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: 'rspec', coverage: 84) } let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, created_at: '2020-02-06 00:02:20', name: 'rspec', coverage: 84) }
let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: 'karma', coverage: 92) } let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, created_at: '2020-02-06 00:02:22', name: 'karma', coverage: 92) }
before do before do
# Create the existing daily code coverage records # Create the existing daily code coverage records
...@@ -94,8 +92,8 @@ describe Ci::DailyCodeCoverageService, '#execute' do ...@@ -94,8 +92,8 @@ describe Ci::DailyCodeCoverageService, '#execute' do
end end
it 'does not update the existing daily code coverage records' do it 'does not update the existing daily code coverage records' do
rspec_coverage = DailyCodeCoverage.find_by(name: 'rspec') rspec_coverage = Ci::DailyCodeCoverage.find_by(name: 'rspec')
karma_coverage = DailyCodeCoverage.find_by(name: 'karma') karma_coverage = Ci::DailyCodeCoverage.find_by(name: 'karma')
# Run another one but for the older pipeline. # Run another one but for the older pipeline.
# This simulates the scenario wherein the success worker # This simulates the scenario wherein the success worker
...@@ -108,12 +106,12 @@ describe Ci::DailyCodeCoverageService, '#execute' do ...@@ -108,12 +106,12 @@ describe Ci::DailyCodeCoverageService, '#execute' do
karma_coverage.reload karma_coverage.reload
expect(rspec_coverage).to have_attributes( expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id, last_build_id: new_rspec_job.id,
coverage: new_rspec_job.coverage coverage: new_rspec_job.coverage
) )
expect(karma_coverage).to have_attributes( expect(karma_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id, last_build_id: new_karma_job.id,
coverage: new_karma_job.coverage coverage: new_karma_job.coverage
) )
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe DailyCodeCoverageWorker do describe Ci::DailyCodeCoverageWorker do
describe '#perform' do describe '#perform' do
let!(:pipeline) { create(:ci_pipeline) } let!(:pipeline) { create(:ci_pipeline) }
......
# frozen_string_literal: true
require 'spec_helper'
describe PipelineSuccessWorker do
describe '#perform' do
context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline) }
it 'asynchronously executes the daily code coverage worker' do
expect(DailyCodeCoverageWorker)
.to receive(:perform_async).with(pipeline.id)
described_class.new.perform(pipeline.id)
end
end
context 'when pipeline does not exist' do
it 'does not raise exception' do
expect { described_class.new.perform(123) }
.not_to raise_error
end
end
end
end
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