1# frozen_string_literal: true
2
3module Gitlab
4  module Database
5    module LoadBalancing
6      # Configuration settings for a single LoadBalancer instance.
7      class Configuration
8        attr_accessor :hosts, :max_replication_difference,
9                      :max_replication_lag_time, :replica_check_interval,
10                      :service_discovery
11
12        # Creates a configuration object for the given ActiveRecord model.
13        def self.for_model(model)
14          cfg = model.connection_db_config.configuration_hash.deep_symbolize_keys
15          lb_cfg = cfg[:load_balancing] || {}
16          config = new(model)
17
18          if (diff = lb_cfg[:max_replication_difference])
19            config.max_replication_difference = diff
20          end
21
22          if (lag = lb_cfg[:max_replication_lag_time])
23            config.max_replication_lag_time = lag.to_f
24          end
25
26          if (interval = lb_cfg[:replica_check_interval])
27            config.replica_check_interval = interval.to_f
28          end
29
30          if (hosts = lb_cfg[:hosts])
31            config.hosts = hosts
32          end
33
34          discover = lb_cfg[:discover] || {}
35
36          # We iterate over the known/default keys so we don't end up with
37          # random keys in our configuration hash.
38          config.service_discovery.each do |key, _|
39            if (value = discover[key])
40              config.service_discovery[key] = value
41            end
42          end
43
44          config.reuse_primary_connection!
45
46          config
47        end
48
49        def initialize(model, hosts = [])
50          @max_replication_difference = 8.megabytes
51          @max_replication_lag_time = 60.0
52          @replica_check_interval = 60.0
53          @model = model
54          @hosts = hosts
55          @service_discovery = {
56            nameserver: 'localhost',
57            port: 8600,
58            record: nil,
59            record_type: 'A',
60            interval: 60,
61            disconnect_timeout: 120,
62            use_tcp: false
63          }
64
65          # Temporary model for GITLAB_LOAD_BALANCING_REUSE_PRIMARY_
66          # To be removed with FF
67          @primary_model = nil
68        end
69
70        def db_config_name
71          @model.connection_db_config.name.to_sym
72        end
73
74        # With connection re-use the primary connection can be overwritten
75        # to be used from different model
76        def primary_connection_specification_name
77          (@primary_model || @model).connection_specification_name
78        end
79
80        def primary_db_config
81          (@primary_model || @model).connection_db_config
82        end
83
84        def replica_db_config
85          @model.connection_db_config
86        end
87
88        def pool_size
89          # The pool size may change when booting up GitLab, as GitLab enforces
90          # a certain number of threads. If a Configuration is memoized, this
91          # can lead to incorrect pool sizes.
92          #
93          # To support this scenario, we always attempt to read the pool size
94          # from the model's configuration.
95          @model.connection_db_config.configuration_hash[:pool] ||
96            Database.default_pool_size
97        end
98
99        # Returns `true` if the use of load balancing replicas should be
100        # enabled.
101        #
102        # This is disabled for Rake tasks to ensure e.g. database migrations
103        # always produce consistent results.
104        def load_balancing_enabled?
105          return false if Gitlab::Runtime.rake?
106
107          hosts.any? || service_discovery_enabled?
108        end
109
110        # This is disabled for Rake tasks to ensure e.g. database migrations
111        # always produce consistent results.
112        def service_discovery_enabled?
113          return false if Gitlab::Runtime.rake?
114
115          service_discovery[:record].present?
116        end
117
118        # TODO: This is temporary code to allow re-use of primary connection
119        # if the two connections are pointing to the same host. This is needed
120        # to properly support transaction visibility.
121        #
122        # This behavior is required to support [Phase 3](https://gitlab.com/groups/gitlab-org/-/epics/6160#progress).
123        # This method is meant to be removed as soon as it is finished.
124        #
125        # The remapping is done as-is:
126        #   export GITLAB_LOAD_BALANCING_REUSE_PRIMARY_<name-of-connection>=<new-name-of-connection>
127        #
128        # Ex.:
129        #   export GITLAB_LOAD_BALANCING_REUSE_PRIMARY_ci=main
130        #
131        def reuse_primary_connection!
132          new_connection = ENV["GITLAB_LOAD_BALANCING_REUSE_PRIMARY_#{db_config_name}"]
133          return unless new_connection.present?
134
135          @primary_model = Gitlab::Database.database_base_models[new_connection.to_sym]
136
137          unless @primary_model
138            raise "Invalid value for 'GITLAB_LOAD_BALANCING_REUSE_PRIMARY_#{db_config_name}=#{new_connection}'"
139          end
140        end
141      end
142    end
143  end
144end
145