summaryrefslogtreecommitdiff
path: root/lib/gitlab/git/storage/circuit_breaker.rb
blob: ba56aa2baf7ccbcfd1c889319d804661da997683 (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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
module Gitlab
  module Git
    module Storage
      class CircuitBreaker
        FailureInfo = Struct.new(:last_failure, :failure_count)

        attr_reader :storage,
                    :hostname,
                    :storage_path,
                    :failure_count_threshold,
                    :failure_wait_time,
                    :failure_reset_time,
                    :storage_timeout

        delegate :last_failure, :failure_count, to: :failure_info

        def self.reset_all!
          pattern = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}*"

          Gitlab::Git::Storage.redis.with do |redis|
            all_storage_keys = redis.scan_each(match: pattern).to_a
            redis.del(*all_storage_keys) unless all_storage_keys.empty?
          end

          RequestStore.delete(:circuitbreaker_cache)
        end

        def self.for_storage(storage)
          cached_circuitbreakers = RequestStore.fetch(:circuitbreaker_cache) do
            Hash.new do |hash, storage_name|
              hash[storage_name] = build(storage_name)
            end
          end

          cached_circuitbreakers[storage]
        end

        def self.build(storage, hostname = Gitlab::Environment.hostname)
          config = Gitlab.config.repositories.storages[storage]

          if !config.present?
            NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Storage '#{storage}' is not configured"))
          elsif !config['path'].present?
            NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Path for storage '#{storage}' is not configured"))
          else
            new(storage, hostname)
          end
        end

        def initialize(storage, hostname)
          @storage = storage
          @hostname = hostname

          config = Gitlab.config.repositories.storages[@storage]
          @storage_path = config['path']
          @failure_count_threshold = config['failure_count_threshold']
          @failure_wait_time = config['failure_wait_time']
          @failure_reset_time = config['failure_reset_time']
          @storage_timeout = config['storage_timeout']
        end

        def perform
          return yield unless Feature.enabled?('git_storage_circuit_breaker')

          check_storage_accessible!

          yield
        end

        def circuit_broken?
          return false if no_failures?

          recent_failure = last_failure > failure_wait_time.seconds.ago
          too_many_failures = failure_count > failure_count_threshold

          recent_failure || too_many_failures
        end

        def failure_info
          @failure_info ||= get_failure_info
        end

        # Memoizing the `storage_available` call means we only do it once per
        # request when the storage is available.
        #
        # When the storage appears not available, and the memoized value is `false`
        # we might want to try again.
        def storage_available?
          return @storage_available if @storage_available

          if @storage_available = Gitlab::Git::Storage::ForkedStorageCheck
                                    .storage_available?(storage_path, storage_timeout)
            track_storage_accessible
          else
            track_storage_inaccessible
          end

          @storage_available
        end

        def check_storage_accessible!
          if circuit_broken?
            raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_wait_time)
          end

          unless storage_available?
            raise Gitlab::Git::Storage::Inaccessible.new("#{storage} not accessible", failure_wait_time)
          end
        end

        def no_failures?
          last_failure.blank? && failure_count == 0
        end

        def track_storage_inaccessible
          @failure_info = FailureInfo.new(Time.now, failure_count + 1)

          Gitlab::Git::Storage.redis.with do |redis|
            redis.pipelined do
              redis.hset(cache_key, :last_failure, last_failure.to_i)
              redis.hincrby(cache_key, :failure_count, 1)
              redis.expire(cache_key, failure_reset_time)
            end
          end
        end

        def track_storage_accessible
          return if no_failures?

          @failure_info = FailureInfo.new(nil, 0)

          Gitlab::Git::Storage.redis.with do |redis|
            redis.pipelined do
              redis.hset(cache_key, :last_failure, nil)
              redis.hset(cache_key, :failure_count, 0)
            end
          end
        end

        def cache_key
          @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
        end

        private

        def get_failure_info
          last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis|
            redis.hmget(cache_key, :last_failure, :failure_count)
          end

          last_failure = Time.at(last_failure.to_i) if last_failure.present?

          FailureInfo.new(last_failure, failure_count.to_i)
        end
      end
    end
  end
end