summaryrefslogtreecommitdiff
path: root/lib/gitlab/git/storage/checker.rb
blob: 391f0d70583555f62dc08aff6d72ce8dad37a31e (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
module Gitlab
  module Git
    module Storage
      class Checker
        include CircuitBreakerSettings

        attr_reader :storage_path, :storage, :hostname, :logger
        METRICS_MUTEX = Mutex.new
        STORAGE_TIMING_BUCKETS = [0.1, 0.15, 0.25, 0.33, 0.5, 1, 1.5, 2.5, 5, 10, 15].freeze

        def self.check_all(logger = Rails.logger)
          threads = Gitlab.config.repositories.storages.keys.map do |storage_name|
            Thread.new do
              Thread.current[:result] = new(storage_name, logger).check_with_lease
            end
          end

          threads.map do |thread|
            thread.join
            thread[:result]
          end
        end

        def self.check_histogram
          @check_histogram ||=
            METRICS_MUTEX.synchronize do
              @check_histogram || Gitlab::Metrics.histogram(:circuitbreaker_storage_check_duration_seconds,
                                                            'Storage check time in seconds',
                                                            {},
                                                            STORAGE_TIMING_BUCKETS
                                                           )
            end
        end

        def initialize(storage, logger = Rails.logger)
          @storage = storage
          config = Gitlab.config.repositories.storages[@storage]
          @storage_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { config.legacy_disk_path }
          @logger = logger

          @hostname = Gitlab::Environment.hostname
        end

        def check_with_lease
          lease_key = "storage_check:#{cache_key}"
          lease = Gitlab::ExclusiveLease.new(lease_key, timeout: storage_timeout)
          result = { storage: storage, success: nil }

          if uuid = lease.try_obtain
            result[:success] = check

            Gitlab::ExclusiveLease.cancel(lease_key, uuid)
          else
            logger.warn("#{hostname}: #{storage}: Skipping check, previous check still running")
          end

          result
        end

        def check
          if perform_access_check
            track_storage_accessible
            true
          else
            track_storage_inaccessible
            logger.error("#{hostname}: #{storage}: Not accessible.")
            false
          end
        end

        private

        def perform_access_check
          start_time = Gitlab::Metrics::System.monotonic_time

          Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout, access_retries)
        ensure
          execution_time = Gitlab::Metrics::System.monotonic_time - start_time
          self.class.check_histogram.observe({ storage: storage }, execution_time)
        end

        def track_storage_inaccessible
          first_failure = current_failure_info.first_failure || Time.now
          last_failure = Time.now

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

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

        def maintain_known_keys(redis)
          expire_time = Time.now.to_i + failure_reset_time
          redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, expire_time, cache_key)
          redis.zremrangebyscore(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, '-inf', Time.now.to_i)
        end

        def current_failure_info
          FailureInfo.load(cache_key)
        end
      end
    end
  end
end