summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/runner_releases.rb
blob: dab24bfd5016508a8ea03bbf9793cf51f0a789c8 (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
# frozen_string_literal: true

module Gitlab
  module Ci
    class RunnerReleases
      include Singleton

      RELEASES_VALIDITY_PERIOD = 1.day

      INITIAL_BACKOFF = 5.seconds
      MAX_BACKOFF = 1.hour
      BACKOFF_GROWTH_FACTOR = 2.0

      def initialize
        reset_backoff!
      end

      # Returns a sorted list of the publicly available GitLab Runner releases
      #
      def releases
        return if backoff_active?

        Rails.cache.fetch(
          cache_key,
          skip_nil: true,
          expires_in: RELEASES_VALIDITY_PERIOD,
          race_condition_ttl: 10.seconds
        ) do
          response = Gitlab::HTTP.try_get(runner_releases_url)
          @releases_by_minor = nil

          unless response&.success?
            @backoff_expire_time = next_backoff.from_now
            break nil
          end

          reset_backoff!
          extract_releases(response)
        rescue Errno::ETIMEDOUT
          @backoff_expire_time = next_backoff.from_now
          break nil
        end
      end

      # Returns a hash with the latest runner version per minor release
      #
      def releases_by_minor
        return unless releases

        @releases_by_minor ||= releases.group_by(&:without_patch).transform_values(&:max)
      end

      def reset_backoff!
        @backoff_expire_time = nil
        @backoff_count = 0
      end

      private

      def runner_releases_url
        @runner_releases_url ||= ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url
      end

      def cache_key
        runner_releases_url
      end

      def backoff_active?
        return false unless @backoff_expire_time

        Time.now.utc < @backoff_expire_time
      end

      def extract_releases(response)
        return unless response.parsed_response.is_a?(Array)

        releases = response.parsed_response
          .map { |release| parse_runner_release(release) }
          .select(&:valid?)
          .sort

        return if releases.empty? && response.parsed_response.present?

        releases
      end

      def parse_runner_release(release)
        ::Gitlab::VersionInfo.parse(release['name'], parse_suffix: true)
      end

      def next_backoff
        return MAX_BACKOFF if @backoff_count >= 11 # optimization to prevent expensive exponentiation and possible overflows

        backoff = (INITIAL_BACKOFF * (BACKOFF_GROWTH_FACTOR**@backoff_count))
          .clamp(INITIAL_BACKOFF, MAX_BACKOFF)
          .seconds
        @backoff_count += 1

        backoff
      end
    end
  end
end