Add a wrapper for pipeline statuses
This reads status from redis & stores it in redis if it was missing. If the project has no HEAD, the cache is deleted
This commit is contained in:
parent
56f6c6ac16
commit
e36f444930
2 changed files with 249 additions and 0 deletions
84
app/models/ci/pipeline_status.rb
Normal file
84
app/models/ci/pipeline_status.rb
Normal file
|
@ -0,0 +1,84 @@
|
|||
# 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
|
||||
# in Redis.
|
||||
module Ci
|
||||
class PipelineStatus
|
||||
attr_accessor :sha, :status, :project, :loaded
|
||||
|
||||
delegate :commit, to: :project
|
||||
|
||||
def self.load_for_project(project)
|
||||
new(project).tap do |status|
|
||||
status.load_status
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(project, sha: nil, status: nil)
|
||||
@project = project
|
||||
@sha = sha
|
||||
@status = status
|
||||
end
|
||||
|
||||
def has_status?
|
||||
loaded? && sha.present? && status.present?
|
||||
end
|
||||
|
||||
def load_status
|
||||
return if loaded?
|
||||
|
||||
if has_cache?
|
||||
load_from_cache
|
||||
else
|
||||
load_from_commit
|
||||
store_in_cache
|
||||
end
|
||||
|
||||
self.loaded = true
|
||||
end
|
||||
|
||||
def load_from_commit
|
||||
self.sha = commit.sha
|
||||
self.status = commit.status
|
||||
end
|
||||
|
||||
# We only cache the status for the HEAD commit of a project
|
||||
# This status is rendered in project lists
|
||||
def store_in_cache_if_needed
|
||||
return unless sha
|
||||
return delete_from_cache unless commit
|
||||
store_in_cache if commit.sha == self.sha
|
||||
end
|
||||
|
||||
def load_from_cache
|
||||
Gitlab::Redis.with do |redis|
|
||||
self.sha, self.status = redis.hmget(cache_key, :sha, :status)
|
||||
end
|
||||
end
|
||||
|
||||
def store_in_cache
|
||||
Gitlab::Redis.with do |redis|
|
||||
redis.mapped_hmset(cache_key, { sha: sha, status: status })
|
||||
end
|
||||
end
|
||||
|
||||
def delete_from_cache
|
||||
Gitlab::Redis.with do |redis|
|
||||
redis.del(cache_key)
|
||||
end
|
||||
end
|
||||
|
||||
def has_cache?
|
||||
Gitlab::Redis.with do |redis|
|
||||
redis.exists(cache_key)
|
||||
end
|
||||
end
|
||||
|
||||
def loaded?
|
||||
self.loaded
|
||||
end
|
||||
|
||||
def cache_key
|
||||
"projects/#{project.id}/build_status"
|
||||
end
|
||||
end
|
||||
end
|
165
spec/models/ci/pipeline_status_spec.rb
Normal file
165
spec/models/ci/pipeline_status_spec.rb
Normal file
|
@ -0,0 +1,165 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Ci::PipelineStatus do
|
||||
let(:project) { create(:project) }
|
||||
let(:pipeline_status) { described_class.new(project) }
|
||||
|
||||
describe '.load_for_project' do
|
||||
it "loads the status" do
|
||||
expect_any_instance_of(described_class).to receive(:load_status)
|
||||
|
||||
described_class.load_for_project(project)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_status?' do
|
||||
it "is false when the status wasn't loaded yet" do
|
||||
expect(pipeline_status.has_status?).to be_falsy
|
||||
end
|
||||
|
||||
it 'is true when all status information was loaded' do
|
||||
fake_commit = double
|
||||
allow(fake_commit).to receive(:status).and_return('failed')
|
||||
allow(fake_commit).to receive(:sha).and_return('failed424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6')
|
||||
allow(pipeline_status).to receive(:commit).and_return(fake_commit)
|
||||
allow(pipeline_status).to receive(:has_cache?).and_return(false)
|
||||
|
||||
pipeline_status.load_status
|
||||
|
||||
expect(pipeline_status.has_status?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#load_status' do
|
||||
it 'loads the status from the cache when there is one' do
|
||||
expect(pipeline_status).to receive(:has_cache?).and_return(true)
|
||||
expect(pipeline_status).to receive(:load_from_cache)
|
||||
|
||||
pipeline_status.load_status
|
||||
end
|
||||
|
||||
it 'loads the status from the project commit when there is no cache' do
|
||||
allow(pipeline_status).to receive(:has_cache?).and_return(false)
|
||||
|
||||
expect(pipeline_status).to receive(:load_from_commit)
|
||||
|
||||
pipeline_status.load_status
|
||||
end
|
||||
|
||||
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(:load_from_commit)
|
||||
|
||||
expect(pipeline_status).to receive(:store_in_cache)
|
||||
|
||||
pipeline_status.load_status
|
||||
end
|
||||
|
||||
it 'sets the state to loaded' do
|
||||
pipeline_status.load_status
|
||||
|
||||
expect(pipeline_status).to be_loaded
|
||||
end
|
||||
|
||||
it 'only loads the status once' do
|
||||
expect(pipeline_status).to receive(:has_cache?).and_return(true).exactly(1)
|
||||
expect(pipeline_status).to receive(:load_from_cache).exactly(1)
|
||||
|
||||
pipeline_status.load_status
|
||||
pipeline_status.load_status
|
||||
end
|
||||
end
|
||||
|
||||
describe "#load_from_commit" do
|
||||
let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
|
||||
|
||||
it 'reads the status from the pipeline for the commit' do
|
||||
pipeline_status.load_from_commit
|
||||
|
||||
expect(pipeline_status.status).to eq('success')
|
||||
expect(pipeline_status.sha).to eq(project.commit.sha)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#store_in_cache", :redis do
|
||||
it "sets the object in redis" do
|
||||
pipeline_status.sha = '123456'
|
||||
pipeline_status.status = 'failed'
|
||||
|
||||
pipeline_status.store_in_cache
|
||||
read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
|
||||
|
||||
expect(read_sha).to eq('123456')
|
||||
expect(read_status).to eq('failed')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#store_in_cache_if_needed', :redis do
|
||||
it 'stores the state in the cache when the sha is the HEAD of the project' do
|
||||
create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
|
||||
build_status = described_class.load_for_project(project)
|
||||
|
||||
build_status.store_in_cache_if_needed
|
||||
sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
|
||||
|
||||
expect(sha).not_to be_nil
|
||||
expect(status).not_to be_nil
|
||||
end
|
||||
|
||||
it "doesn't store the status in redis when the sha is not the head of the project" do
|
||||
other_status = described_class.new(project, sha: "123456", status: "failed")
|
||||
|
||||
other_status.store_in_cache_if_needed
|
||||
sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
|
||||
|
||||
expect(sha).to be_nil
|
||||
expect(status).to be_nil
|
||||
end
|
||||
|
||||
it "deletes the cache if the repository doesn't have a head commit" do
|
||||
empty_project = create(:empty_project)
|
||||
Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending" }) }
|
||||
other_status = described_class.new(empty_project, sha: "123456", status: "failed")
|
||||
|
||||
other_status.store_in_cache_if_needed
|
||||
sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status) }
|
||||
|
||||
expect(sha).to be_nil
|
||||
expect(status).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "with a status in redis", :redis do
|
||||
let(:status) { 'success' }
|
||||
let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
|
||||
|
||||
before do
|
||||
Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{project.id}/build_status", { sha: sha, status: status }) }
|
||||
end
|
||||
|
||||
describe '#load_from_cache' do
|
||||
it 'reads the status from redis' do
|
||||
pipeline_status.load_from_cache
|
||||
|
||||
expect(pipeline_status.sha).to eq(sha)
|
||||
expect(pipeline_status.status).to eq(status)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_cache?' do
|
||||
it 'knows the status is cached' do
|
||||
expect(pipeline_status.has_cache?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_from_cache' do
|
||||
it 'deletes values from redis' do
|
||||
pipeline_status.delete_from_cache
|
||||
|
||||
key_exists = Gitlab::Redis.with { |redis| redis.exists("projects/#{project.id}/build_status") }
|
||||
|
||||
expect(key_exists).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue