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