src/derails/passive_aggressive/middleware/database_selector/resolver.rb
# frozen_string_literal: true

require "passive_aggressive/middleware/database_selector/resolver/session"
require "active_support/core_ext/numeric/time"

module PassiveAggressive
  module Middleware
    class DatabaseSelector
      # The Resolver class is used by the DatabaseSelector middleware to
      # determine which database the request should use.
      #
      # To change the behavior of the Resolver class in your application,
      # create a custom resolver class that inherits from
      # DatabaseSelector::Resolver and implements the methods that need to
      # be changed.
      #
      # By default the Resolver class will send read traffic to the replica
      # if it's been 2 seconds since the last write.
      class Resolver # :nodoc:
        SEND_TO_REPLICA_DELAY = 2.seconds

        def self.call(context, options = {})
          new(context, options)
        end

        def initialize(context, options = {})
          @context = context
          @options = options
          @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
          @instrumenter = ActiveSupport::Notifications.instrumenter
        end

        attr_reader :context, :delay, :instrumenter

        def read(&blk)
          if read_from_primary?
            read_from_primary(&blk)
          else
            read_from_replica(&blk)
          end
        end

        def write(&blk)
          write_to_primary(&blk)
        end

        def update_context(response)
          context.save(response)
        end

        def reading_request?(request)
          request.get? || request.head?
        end

        private
          def read_from_primary(&blk)
            PassiveAggressive::Base.connected_to(role: PassiveAggressive.writing_role, prevent_writes: true) do
              instrumenter.instrument("database_selector.passive_aggressive.read_from_primary", &blk)
            end
          end

          def read_from_replica(&blk)
            PassiveAggressive::Base.connected_to(role: PassiveAggressive.reading_role, prevent_writes: true) do
              instrumenter.instrument("database_selector.passive_aggressive.read_from_replica", &blk)
            end
          end

          def write_to_primary
            PassiveAggressive::Base.connected_to(role: PassiveAggressive.writing_role, prevent_writes: false) do
              instrumenter.instrument("database_selector.passive_aggressive.wrote_to_primary") do
                yield
              ensure
                context.update_last_write_timestamp
              end
            end
          end

          def read_from_primary?
            !time_since_last_write_ok?
          end

          def send_to_replica_delay
            delay
          end

          def time_since_last_write_ok?
            Time.now - context.last_write_timestamp >= send_to_replica_delay
          end
      end
    end
  end
end
