# 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
