BigW Consortium Gitlab

Commit 8631779b by Kamil Trzciński

Merge branch 'bvl-fix-project-ci-status-cache' into 'master'

Fix invalidating Project build status cache to often See merge request !10313
parents 46aadc5c 9eded57d
...@@ -31,7 +31,6 @@ module Ci ...@@ -31,7 +31,6 @@ module Ci
validate :valid_commit_sha, unless: :importing? validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing? after_create :keep_around_commits, unless: :importing?
after_create :refresh_build_status_cache
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
...@@ -351,7 +350,6 @@ module Ci ...@@ -351,7 +350,6 @@ module Ci
when 'manual' then block when 'manual' then block
end end
end end
refresh_build_status_cache
end end
def predefined_variables def predefined_variables
...@@ -393,10 +391,6 @@ module Ci ...@@ -393,10 +391,6 @@ module Ci
.fabricate! .fabricate!
end end
def refresh_build_status_cache
Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
end
private private
def pipeline_data def pipeline_data
......
...@@ -1182,7 +1182,7 @@ class Project < ActiveRecord::Base ...@@ -1182,7 +1182,7 @@ class Project < ActiveRecord::Base
end end
def pipeline_status def pipeline_status
@pipeline_status ||= Ci::PipelineStatus.load_for_project(self) @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end end
def mark_import_as_failed(error_message) def mark_import_as_failed(error_message)
......
...@@ -10,6 +10,8 @@ module Ci ...@@ -10,6 +10,8 @@ module Ci
store.touch(commit_pipelines_path) if pipeline.commit store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path) store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) } merge_requests_pipelines_paths.each { |path| store.touch(path) }
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline)
end end
private private
......
...@@ -251,7 +251,8 @@ module SharedProject ...@@ -251,7 +251,8 @@ module SharedProject
step 'project "Shop" has CI build' do step 'project "Shop" has CI build' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped' pipeline = create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master'
pipeline.skip
end end
step 'I should see last commit with CI status' do step 'I should see last commit with CI status' do
......
# This class is not backed by a table in the main database. # This class is not backed by a table in the main database.
# It loads the latest Pipeline for the HEAD of a repository, and caches that # It loads the latest Pipeline for the HEAD of a repository, and caches that
# in Redis. # in Redis.
module Ci module Gitlab
class PipelineStatus module Cache
attr_accessor :sha, :status, :project, :loaded module Ci
class ProjectPipelineStatus
attr_accessor :sha, :status, :ref, :project, :loaded
delegate :commit, to: :project delegate :commit, to: :project
...@@ -13,9 +15,17 @@ module Ci ...@@ -13,9 +15,17 @@ module Ci
end end
end end
def initialize(project, sha: nil, status: nil) def self.update_for_pipeline(pipeline)
new(pipeline.project,
sha: pipeline.sha,
status: pipeline.status,
ref: pipeline.ref).store_in_cache_if_needed
end
def initialize(project, sha: nil, status: nil, ref: nil)
@project = project @project = project
@sha = sha @sha = sha
@ref = ref
@status = status @status = status
end end
...@@ -29,37 +39,42 @@ module Ci ...@@ -29,37 +39,42 @@ module Ci
if has_cache? if has_cache?
load_from_cache load_from_cache
else else
load_from_commit load_from_project
store_in_cache store_in_cache
end end
self.loaded = true self.loaded = true
end end
def load_from_commit def load_from_project
return unless commit return unless commit
self.sha = commit.sha self.sha = commit.sha
self.status = commit.status self.status = commit.status
self.ref = project.default_branch
end end
# We only cache the status for the HEAD commit of a project # We only cache the status for the HEAD commit of a project
# This status is rendered in project lists # This status is rendered in project lists
def store_in_cache_if_needed def store_in_cache_if_needed
return unless sha
return delete_from_cache unless commit return delete_from_cache unless commit
store_in_cache if commit.sha == self.sha return unless sha
return unless ref
if commit.sha == sha && project.default_branch == ref
store_in_cache
end
end end
def load_from_cache def load_from_cache
Gitlab::Redis.with do |redis| Gitlab::Redis.with do |redis|
self.sha, self.status = redis.hmget(cache_key, :sha, :status) self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref)
end end
end end
def store_in_cache def store_in_cache
Gitlab::Redis.with do |redis| Gitlab::Redis.with do |redis|
redis.mapped_hmset(cache_key, { sha: sha, status: status }) redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
end end
end end
...@@ -83,4 +98,6 @@ module Ci ...@@ -83,4 +98,6 @@ module Ci
"projects/#{project.id}/build_status" "projects/#{project.id}/build_status"
end end
end end
end
end
end end
...@@ -7,7 +7,6 @@ RSpec.describe 'Dashboard Projects', feature: true do ...@@ -7,7 +7,6 @@ RSpec.describe 'Dashboard Projects', feature: true do
before do before do
project.team << [user, :developer] project.team << [user, :developer]
login_as user login_as user
visit dashboard_projects_path
end end
it 'shows the project the user in a member of in the list' do it 'shows the project the user in a member of in the list' do
...@@ -15,15 +14,19 @@ RSpec.describe 'Dashboard Projects', feature: true do ...@@ -15,15 +14,19 @@ RSpec.describe 'Dashboard Projects', feature: true do
expect(page).to have_content('awesome stuff') expect(page).to have_content('awesome stuff')
end end
describe "with a pipeline" do describe "with a pipeline", redis: true do
let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) } let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
before do before do
pipeline # Since the cache isn't updated when a new pipeline is created
# we need the pipeline to advance in the pipeline since the cache was created
# by visiting the login page.
pipeline.succeed
end end
it 'shows that the last pipeline passed' do it 'shows that the last pipeline passed' do
visit dashboard_projects_path visit dashboard_projects_path
expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']") expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']")
end end
end end
......
...@@ -19,7 +19,11 @@ describe CiStatusHelper do ...@@ -19,7 +19,11 @@ describe CiStatusHelper do
describe "#pipeline_status_cache_key" do describe "#pipeline_status_cache_key" do
it "builds a cache key for pipeline status" do it "builds a cache key for pipeline status" do
pipeline_status = Ci::PipelineStatus.new(build(:project), sha: "123abc", status: "success") pipeline_status = Gitlab::Cache::Ci::ProjectPipelineStatus.new(
build(:project),
sha: "123abc",
status: "success"
)
expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success") expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success")
end end
end end
......
...@@ -63,7 +63,7 @@ describe ProjectsHelper do ...@@ -63,7 +63,7 @@ describe ProjectsHelper do
end end
end end
describe "#project_list_cache_key" do describe "#project_list_cache_key", redis: true do
let(:project) { create(:project) } let(:project) { create(:project) }
it "includes the namespace" do it "includes the namespace" do
......
require 'spec_helper' require 'spec_helper'
describe Ci::PipelineStatus do describe Gitlab::Cache::Ci::ProjectPipelineStatus do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:pipeline_status) { described_class.new(project) } let(:pipeline_status) { described_class.new(project) }
...@@ -12,6 +12,20 @@ describe Ci::PipelineStatus do ...@@ -12,6 +12,20 @@ describe Ci::PipelineStatus do
end end
end end
describe '.update_for_pipeline' do
it 'refreshes the cache if nescessary' do
pipeline = build_stubbed(:ci_pipeline, sha: '123456', status: 'success')
fake_status = double
expect(described_class).to receive(:new).
with(pipeline.project, sha: '123456', status: 'success', ref: 'master').
and_return(fake_status)
expect(fake_status).to receive(:store_in_cache_if_needed)
described_class.update_for_pipeline(pipeline)
end
end
describe '#has_status?' do describe '#has_status?' do
it "is false when the status wasn't loaded yet" do it "is false when the status wasn't loaded yet" do
expect(pipeline_status.has_status?).to be_falsy expect(pipeline_status.has_status?).to be_falsy
...@@ -41,14 +55,14 @@ describe Ci::PipelineStatus do ...@@ -41,14 +55,14 @@ describe Ci::PipelineStatus do
it 'loads the status from the project commit when there is no cache' do it 'loads the status from the project commit when there is no cache' do
allow(pipeline_status).to receive(:has_cache?).and_return(false) allow(pipeline_status).to receive(:has_cache?).and_return(false)
expect(pipeline_status).to receive(:load_from_commit) expect(pipeline_status).to receive(:load_from_project)
pipeline_status.load_status pipeline_status.load_status
end end
it 'stores the status in the cache when it loading it from the project' do it 'stores the status in the cache when it loading it from the project' do
allow(pipeline_status).to receive(:has_cache?).and_return(false) allow(pipeline_status).to receive(:has_cache?).and_return(false)
allow(pipeline_status).to receive(:load_from_commit) allow(pipeline_status).to receive(:load_from_project)
expect(pipeline_status).to receive(:store_in_cache) expect(pipeline_status).to receive(:store_in_cache)
...@@ -70,14 +84,15 @@ describe Ci::PipelineStatus do ...@@ -70,14 +84,15 @@ describe Ci::PipelineStatus do
end end
end end
describe "#load_from_commit" do describe "#load_from_project" do
let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) } let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
it 'reads the status from the pipeline for the commit' do it 'reads the status from the pipeline for the commit' do
pipeline_status.load_from_commit pipeline_status.load_from_project
expect(pipeline_status.status).to eq('success') expect(pipeline_status.status).to eq('success')
expect(pipeline_status.sha).to eq(project.commit.sha) expect(pipeline_status.sha).to eq(project.commit.sha)
expect(pipeline_status.ref).to eq(project.default_branch)
end end
it "doesn't fail for an empty project" do it "doesn't fail for an empty project" do
...@@ -108,10 +123,11 @@ describe Ci::PipelineStatus do ...@@ -108,10 +123,11 @@ describe Ci::PipelineStatus do
build_status = described_class.load_for_project(project) build_status = described_class.load_for_project(project)
build_status.store_in_cache_if_needed build_status.store_in_cache_if_needed
sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) } sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status, :ref) }
expect(sha).not_to be_nil expect(sha).not_to be_nil
expect(status).not_to be_nil expect(status).not_to be_nil
expect(ref).not_to be_nil
end end
it "doesn't store the status in redis when the sha is not the head of the project" do it "doesn't store the status in redis when the sha is not the head of the project" do
...@@ -126,14 +142,15 @@ describe Ci::PipelineStatus do ...@@ -126,14 +142,15 @@ describe Ci::PipelineStatus do
it "deletes the cache if the repository doesn't have a head commit" do it "deletes the cache if the repository doesn't have a head commit" do
empty_project = create(:empty_project) empty_project = create(:empty_project)
Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending" }) } Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending", ref: 'master' }) }
other_status = described_class.new(empty_project, sha: "123456", status: "failed") other_status = described_class.new(empty_project, sha: "123456", status: "failed")
other_status.store_in_cache_if_needed other_status.store_in_cache_if_needed
sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status) } sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status, :ref) }
expect(sha).to be_nil expect(sha).to be_nil
expect(status).to be_nil expect(status).to be_nil
expect(ref).to be_nil
end end
end end
......
...@@ -375,7 +375,7 @@ describe Ci::Pipeline, models: true do ...@@ -375,7 +375,7 @@ describe Ci::Pipeline, models: true do
end end
end end
describe 'pipeline ETag caching' do describe 'pipeline caching' do
it 'executes ExpirePipelinesCacheService' do it 'executes ExpirePipelinesCacheService' do
expect_any_instance_of(Ci::ExpirePipelineCacheService).to receive(:execute).with(pipeline) expect_any_instance_of(Ci::ExpirePipelineCacheService).to receive(:execute).with(pipeline)
...@@ -1079,19 +1079,6 @@ describe Ci::Pipeline, models: true do ...@@ -1079,19 +1079,6 @@ describe Ci::Pipeline, models: true do
end end
end end
describe '#update_status' do
let(:pipeline) { create(:ci_pipeline, sha: '123456') }
it 'updates the cached status' do
fake_status = double
# after updating the status, the status is set to `skipped` for this pipeline's builds
expect(Ci::PipelineStatus).to receive(:new).with(pipeline.project, sha: '123456', status: 'skipped').and_return(fake_status)
expect(fake_status).to receive(:store_in_cache_if_needed)
pipeline.update_status
end
end
describe 'notifications when pipeline success or failed' do describe 'notifications when pipeline success or failed' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
......
...@@ -1874,7 +1874,7 @@ describe Project, models: true do ...@@ -1874,7 +1874,7 @@ describe Project, models: true do
describe '#pipeline_status' do describe '#pipeline_status' do
let(:project) { create(:project) } let(:project) { create(:project) }
it 'builds a pipeline status' do it 'builds a pipeline status' do
expect(project.pipeline_status).to be_a(Ci::PipelineStatus) expect(project.pipeline_status).to be_a(Gitlab::Cache::Ci::ProjectPipelineStatus)
end end
it 'hase a loaded pipeline status' do it 'hase a loaded pipeline status' do
......
...@@ -16,5 +16,12 @@ describe Ci::ExpirePipelineCacheService, services: true do ...@@ -16,5 +16,12 @@ describe Ci::ExpirePipelineCacheService, services: true do
subject.execute(pipeline) subject.execute(pipeline)
end end
it 'updates the cached status for a project' do
expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).
with(pipeline)
subject.execute(pipeline)
end
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