summaryrefslogtreecommitdiff
path: root/lib/gitlab/cache/ci/project_pipeline_status.rb
blob: 9209c9b4927760f6fe0148d9e0c9000f368110c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# frozen_string_literal: true

# 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 Gitlab
  module Cache
    module Ci
      class ProjectPipelineStatus
        include Gitlab::Utils::StrongMemoize

        attr_accessor :sha, :status, :ref, :project, :loaded

        def self.load_for_project(project)
          new(project).tap do |status|
            status.load_status
          end
        end

        def self.load_in_batch_for_projects(projects)
          projects.each do |project|
            project.pipeline_status = new(project)
            project.pipeline_status.load_status
          end
        end

        def self.update_for_pipeline(pipeline)
          pipeline_info = {
            sha: pipeline.sha,
            status: pipeline.status,
            ref: pipeline.ref
          }

          new(pipeline.project, pipeline_info: pipeline_info)
            .store_in_cache_if_needed
        end

        def initialize(project, pipeline_info: {}, loaded_from_cache: nil)
          @project = project
          @sha = pipeline_info[:sha]
          @ref = pipeline_info[:ref]
          @status = pipeline_info[:status]
          @loaded = loaded_from_cache
        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_project
            store_in_cache
          end

          self.loaded = true
        rescue GRPC::Unavailable, GRPC::DeadlineExceeded => e
          # Handle Gitaly connection issues gracefully
          Gitlab::ErrorTracking
            .track_exception(e, project_id: project.id)
        end

        def load_from_project
          return unless commit

          self.sha = commit.sha
          self.status = commit.status
          self.ref = project.repository.root_ref
        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 delete_from_cache unless commit
          return unless sha
          return unless ref

          if commit.sha == sha && project.repository.root_ref == ref
            store_in_cache
          end
        end

        def load_from_cache
          Gitlab::Redis::Cache.with do |redis|
            self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref)

            self.status = nil if self.status.empty?
          end
        end

        def store_in_cache
          Gitlab::Redis::Cache.with do |redis|
            redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
          end
        end

        def delete_from_cache
          Gitlab::Redis::Cache.with do |redis|
            redis.del(cache_key)
          end
        end

        def has_cache?
          return self.loaded unless self.loaded.nil?

          Gitlab::Redis::Cache.with do |redis|
            redis.exists?(cache_key) # rubocop:disable CodeReuse/ActiveRecord
          end
        end

        def loaded?
          self.loaded
        end

        def cache_key
          "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status"
        end

        def commit
          strong_memoize(:commit) do
            project.commit
          end
        end
      end
    end
  end
end