1# frozen_string_literal: true 2 3# This class is not backed by a table in the main database. 4# It loads the latest Pipeline for the HEAD of a repository, and caches that 5# in Redis. 6module Gitlab 7 module Cache 8 module Ci 9 class ProjectPipelineStatus 10 include Gitlab::Utils::StrongMemoize 11 12 attr_accessor :sha, :status, :ref, :project, :loaded 13 14 def self.load_for_project(project) 15 new(project).tap do |status| 16 status.load_status 17 end 18 end 19 20 def self.load_in_batch_for_projects(projects) 21 projects.each do |project| 22 project.pipeline_status = new(project) 23 project.pipeline_status.load_status 24 end 25 end 26 27 def self.update_for_pipeline(pipeline) 28 pipeline_info = { 29 sha: pipeline.sha, 30 status: pipeline.status, 31 ref: pipeline.ref 32 } 33 34 new(pipeline.project, pipeline_info: pipeline_info) 35 .store_in_cache_if_needed 36 end 37 38 def initialize(project, pipeline_info: {}, loaded_from_cache: nil) 39 @project = project 40 @sha = pipeline_info[:sha] 41 @ref = pipeline_info[:ref] 42 @status = pipeline_info[:status] 43 @loaded = loaded_from_cache 44 end 45 46 def has_status? 47 loaded? && sha.present? && status.present? 48 end 49 50 def load_status 51 return if loaded? 52 53 if has_cache? 54 load_from_cache 55 else 56 load_from_project 57 store_in_cache 58 end 59 60 self.loaded = true 61 rescue GRPC::Unavailable, GRPC::DeadlineExceeded => e 62 # Handle Gitaly connection issues gracefully 63 Gitlab::ErrorTracking 64 .track_exception(e, project_id: project.id) 65 end 66 67 def load_from_project 68 return unless commit 69 70 self.sha = commit.sha 71 self.status = commit.status 72 self.ref = project.repository.root_ref 73 end 74 75 # We only cache the status for the HEAD commit of a project 76 # This status is rendered in project lists 77 def store_in_cache_if_needed 78 return delete_from_cache unless commit 79 return unless sha 80 return unless ref 81 82 if commit.sha == sha && project.repository.root_ref == ref 83 store_in_cache 84 end 85 end 86 87 def load_from_cache 88 Gitlab::Redis::Cache.with do |redis| 89 self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref) 90 91 self.status = nil if self.status.empty? 92 end 93 end 94 95 def store_in_cache 96 Gitlab::Redis::Cache.with do |redis| 97 redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref }) 98 end 99 end 100 101 def delete_from_cache 102 Gitlab::Redis::Cache.with do |redis| 103 redis.del(cache_key) 104 end 105 end 106 107 def has_cache? 108 return self.loaded unless self.loaded.nil? 109 110 Gitlab::Redis::Cache.with do |redis| 111 redis.exists(cache_key) 112 end 113 end 114 115 def loaded? 116 self.loaded 117 end 118 119 def cache_key 120 "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status" 121 end 122 123 def commit 124 strong_memoize(:commit) do 125 project.commit 126 end 127 end 128 end 129 end 130 end 131end 132