# frozen_string_literal: true

# Copyright (C) 2014-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module Mongo
  module Srv
    # Periodically retrieves SRV records for the cluster's SRV URI, and
    # sets the cluster's server list to the SRV lookup result.
    #
    # If an error is encountered during SRV lookup or an SRV record is invalid
    # or disallowed for security reasons, a warning is logged and monitoring
    # continues.
    #
    # @api private
    class Monitor
      include Loggable
      include BackgroundThread

      MIN_SCAN_INTERVAL = 60

      DEFAULT_TIMEOUT = 10

      # Creates the SRV monitor.
      #
      # @param [ Cluster ] cluster The cluster.
      #
      # @option opts [ Float ] :timeout The timeout to use for DNS lookups.
      # @option opts [ URI::SRVProtocol ] :srv_uri The SRV URI to monitor.
      # @option opts [ Hash ] :resolv_options For internal driver use only.
      #   Options to pass through to Resolv::DNS constructor for SRV lookups.
      def initialize(cluster, **opts)
        @cluster = cluster
        raise ArgumentError, 'SRV URI is required' unless (@srv_uri = opts.delete(:srv_uri))

        @options = opts.freeze
        # Per the polling-srv-records spec, mismatched-domain records and empty
        # results MUST be logged and ignored, not raised. Force the polling
        # resolver into log-and-continue mode regardless of caller options.
        @resolver = Srv::Resolver.new(**opts, raise_on_invalid: false)
        @last_result = @srv_uri.srv_result
        @stop_semaphore = Semaphore.new
      end

      attr_reader :options, :cluster

      # @return [ Srv::Result ] Last known SRV lookup result. Used for
      #   determining intervals between SRV lookups, which depend on SRV DNS
      #   records' TTL values.
      attr_reader :last_result

      private

      def do_work
        scan!
        @stop_semaphore.wait(scan_interval)
      end

      def scan!
        begin
          last_result = Timeout.timeout(timeout) do
            @resolver.get_records(
              @srv_uri.query_hostname,
              @srv_uri.uri_options[:srv_service_name] || options[:srv_service_name],
              @srv_uri.uri_options[:srv_max_hosts] || @options[:srv_max_hosts]
            )
          end
        rescue Resolv::ResolvTimeout => e
          log_warn("SRV monitor: timed out trying to resolve hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
          return
        rescue ::Timeout::Error
          log_warn("SRV monitor: timed out trying to resolve hostname #{@srv_uri.query_hostname} (timeout=#{timeout})")
          return
        rescue Resolv::ResolvError => e
          log_warn("SRV monitor: unable to resolve hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
          return
        rescue Mongo::Error => e
          # Defense in depth: the polling resolver is configured to log-and-continue
          # for mismatched-domain and empty-result cases, but if any Mongo::Error
          # leaks out we MUST NOT let it terminate the SRV monitor thread.
          log_warn("SRV monitor: error resolving hostname #{@srv_uri.query_hostname}: #{e.class}: #{e}")
          return
        end

        if last_result.empty?
          log_warn("SRV monitor: hostname #{@srv_uri.query_hostname} resolved to zero records")
          return
        end

        @cluster.set_server_list(last_result.address_strs)
      end

      def scan_interval
        if last_result.empty?
          [ cluster.heartbeat_interval, MIN_SCAN_INTERVAL ].min
        elsif last_result.min_ttl.nil?
          MIN_SCAN_INTERVAL
        else
          [ last_result.min_ttl, MIN_SCAN_INTERVAL ].max
        end
      end

      def timeout
        options[:timeout] || DEFAULT_TIMEOUT
      end
    end
  end
end
