summaryrefslogtreecommitdiff
path: root/lib/gitlab/database/load_balancing/session.rb
blob: 3682c9265c22127611db67ca05ea1679986fd0ee (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
# frozen_string_literal: true

module Gitlab
  module Database
    module LoadBalancing
      # Tracking of load balancing state per user session.
      #
      # A session starts at the beginning of a request and ends once the request
      # has been completed. Sessions can be used to keep track of what hosts
      # should be used for queries.
      class Session
        CACHE_KEY = :gitlab_load_balancer_session

        def self.current
          RequestStore[CACHE_KEY] ||= new
        end

        def self.clear_session
          RequestStore.delete(CACHE_KEY)
        end

        def self.without_sticky_writes(&block)
          current.ignore_writes(&block)
        end

        def initialize
          @use_primary = false
          @performed_write = false
          @ignore_writes = false
          @fallback_to_replicas_for_ambiguous_queries = false
          @use_replicas_for_read_queries = false
        end

        def use_primary?
          @use_primary
        end

        alias_method :using_primary?, :use_primary?

        def use_primary!
          @use_primary = true
        end

        def use_primary(&blk)
          used_primary = @use_primary
          @use_primary = true
          yield
        ensure
          @use_primary = used_primary || @performed_write
        end

        def ignore_writes(&block)
          @ignore_writes = true

          yield
        ensure
          @ignore_writes = false
        end

        # Indicates that the read SQL statements from anywhere inside this
        # blocks should use a replica, regardless of the current primary
        # stickiness or whether a write query is already performed in the
        # current session. This interface is reserved mostly for performance
        # purpose. This is a good tool to push expensive queries, which can
        # tolerate the replica lags, to the replicas.
        #
        # Write and ambiguous queries inside this block are still handled by
        # the primary.
        def use_replicas_for_read_queries(&blk)
          previous_flag = @use_replicas_for_read_queries
          @use_replicas_for_read_queries = true
          yield
        ensure
          @use_replicas_for_read_queries = previous_flag
        end

        def use_replicas_for_read_queries?
          @use_replicas_for_read_queries == true
        end

        # Indicate that the ambiguous SQL statements from anywhere inside this
        # block should use a replica. The ambiguous statements include:
        # - Transactions.
        # - Custom queries (via exec_query, execute, etc.)
        # - In-flight connection configuration change (SET LOCAL statement_timeout = 5000)
        #
        # This is a weak enforcement. This helper incorporates well with
        # primary stickiness:
        # - If the queries are about to write
        # - The current session already performed writes
        # - It prefers to use primary, aka, use_primary or use_primary! were called
        def fallback_to_replicas_for_ambiguous_queries(&blk)
          previous_flag = @fallback_to_replicas_for_ambiguous_queries
          @fallback_to_replicas_for_ambiguous_queries = true
          yield
        ensure
          @fallback_to_replicas_for_ambiguous_queries = previous_flag
        end

        def fallback_to_replicas_for_ambiguous_queries?
          @fallback_to_replicas_for_ambiguous_queries == true && !use_primary? && !performed_write?
        end

        def write!
          @performed_write = true

          return if @ignore_writes

          use_primary!
        end

        def performed_write?
          @performed_write
        end
      end
    end
  end
end