# frozen_string_literal: true class Pod < ApplicationRecord # a pod is active if it is online or was online less than 14 days ago ACTIVE_DAYS = 14.days enum status: %i( unchecked no_errors dns_failed net_failed ssl_failed http_failed version_failed unknown_error ) ERROR_MAP = { ConnectionTester::AddressFailure => :dns_failed, ConnectionTester::DNSFailure => :dns_failed, ConnectionTester::NetFailure => :net_failed, ConnectionTester::SSLFailure => :ssl_failed, ConnectionTester::HTTPFailure => :http_failed, ConnectionTester::NodeInfoFailure => :version_failed }.freeze # this are only the most common errors, the rest will be +unknown_error+ CURL_ERROR_MAP = { couldnt_resolve_host: :dns_failed, couldnt_connect: :net_failed, operation_timedout: :net_failed, ssl_cipher: :ssl_failed, ssl_cacert: :ssl_failed, redirected_to_other_hostname: :http_failed }.freeze # use -1 as port for default ports # we can't use the real default port (80/443) because we need to handle them # like both are the same and not both can exist at the same time. # we also can't use nil, because databases don't handle NULL in unique indexes # (except postgres >= 15 with "NULLS NOT DISTINCT"). DEFAULT_PORT = -1 DEFAULT_PORTS = [URI::HTTP::DEFAULT_PORT, URI::HTTPS::DEFAULT_PORT].freeze has_many :people scope :check_failed, lambda { where(arel_table[:status].gt(Pod.statuses[:no_errors])).where.not(status: Pod.statuses[:version_failed]) } scope :active, -> { where(["offline_since is null or offline_since > ?", DateTime.now.utc - ACTIVE_DAYS]) } validate :not_own_pod class << self def find_or_create_by(opts) # Rename this method to not override an AR method uri = URI.parse(opts.fetch(:url)) port = DEFAULT_PORTS.include?(uri.port) ? DEFAULT_PORT : uri.port find_or_initialize_by(host: uri.host.downcase, port: port).tap do |pod| pod.ssl ||= (uri.scheme == "https") pod.save end end # don't consider a failed version reading to be fatal def offline_statuses [Pod.statuses[:dns_failed], Pod.statuses[:net_failed], Pod.statuses[:ssl_failed], Pod.statuses[:http_failed], Pod.statuses[:unknown_error]] end def check_all! Pod.find_in_batches(batch_size: 20) {|batch| batch.each(&:test_connection!) } end def check_scheduled! Pod.where(scheduled_check: true).find_each(&:test_connection!) end end def offline? Pod.offline_statuses.include?(Pod.statuses[status]) end # a pod is active if it is online or was online recently def active? !offline? || offline_since.try {|date| date > DateTime.now.utc - ACTIVE_DAYS } end def to_s "#{id}:#{host}" end def schedule_check_if_needed update_column(:scheduled_check, true) if offline? && !scheduled_check end def test_connection! result = ConnectionTester.check uri.to_s logger.debug "tested pod: '#{uri}' - #{result.inspect}" transaction do update_from_result(result) end end # @param path [String] # @return [String] def url_to(path) uri.tap {|uri| uri.path = path }.to_s end def update_offline_since if offline? self.offline_since ||= DateTime.now.utc else self.offline_since = nil end end private def update_from_result(result) self.status = status_from_result(result) update_offline_since logger.warn "#{uri} OFFLINE: #{result.failure_message}" if offline? attributes_from_result(result) touch(:checked_at) self.scheduled_check = false save end def attributes_from_result(result) self.ssl ||= result.ssl self.error = result.error? ? result.failure_message[0..254] : nil self.software = result.software_version[0..254] if result.software_version.present? self.response_time = result.rt end def status_from_result(result) if result.error? ERROR_MAP.fetch(result.error.class, :unknown_error) else :no_errors end end # @return [URI] def uri @uri ||= (ssl ? URI::HTTPS : URI::HTTP).build(host: host, port: real_port) @uri.dup end def real_port if port == DEFAULT_PORT ssl ? URI::HTTPS::DEFAULT_PORT : URI::HTTP::DEFAULT_PORT else port end end def not_own_pod pod_uri = AppConfig.pod_uri pod_port = DEFAULT_PORTS.include?(pod_uri.port) ? DEFAULT_PORT : pod_uri.port errors.add(:base, "own pod not allowed") if pod_uri.host.downcase == host && pod_port == port end end