CartoDB/cartodb20

View on GitHub
lib/carto/named_maps/api.rb

Summary

Maintainability
A
1 hr
Test Coverage
require_relative 'template'
require_relative '../http/client'
require_dependency 'carto/named_maps/template'

module Carto
  module NamedMaps
    class Api

      include ::LoggerHelper

      HTTP_CONNECT_TIMEOUT_SECONDS = 45
      HTTP_REQUEST_TIMEOUT_SECONDS = 60
      RETRY_TIME_SECONDS = 2
      MAX_RETRY_ATTEMPTS = 3
      MAX_NAMED_MAPS_TO_CLEAN = 100

      attr_accessor :user, :visualization

      def initialize(visualization)
        @visualization = visualization
        @user = visualization.user
        @named_map_template = Carto::NamedMaps::Template.new(visualization)
      end

      def index
        stats_aggregator.timing('carto-named-maps-api.index') do
          response = stats_aggregator.timing('call') do
            http_client.get(url, request_params)
          end

          if response.code.to_s.match?(/^2/)
            ::JSON.parse(response.response_body).deep_symbolize_keys
          else
            log_response(response, 'index')
          end
        end
      end

      def create
        stats_aggregator.timing('carto-named-maps-api.create') do
          params = request_params
          params[:body] = @named_map_template.to_json

          response = http_client.post(url, params)

          response_code = response.code
          if response_code.to_s.match?(/^2/)
            response_body = ::JSON.parse(response.response_body).deep_symbolize_keys
            if response_body.key?(:limit_message)
              # Send message to support and clean some named_maps
              ReporterMailer.named_maps_near_the_limit(response_body[:limit_message]).deliver_now
              tables = Carto::UserTable.where(user_id: user.id, privacy: 0)
                                       .limit(MAX_NAMED_MAPS_TO_CLEAN)
                                       .order('updated_at')
              named_maps_ids = tables.map { |t| "tpl_#{t.visualization.id.tr('-', '_')}" }
              urls = named_maps_ids.map { |id| url(template_name: id) }
              ::Resque.enqueue(
                ::Resque::UserJobs::NamedMapsLimitsJobs::CleanNamedMaps,
                { urls: urls, request_params: request_params }
              )
            end
            response_body
          elsif response_code != 409 # Ignore when max number of templates is reached
            log_response(response, 'create')
          end
        end
      end

      def show
        stats_aggregator.timing('carto-named-maps-api.show') do
          response = stats_aggregator.timing('call') do
            url = url(template_name: @named_map_template.name)

            response = http_client.get(url, request_params)
          end

          response_code = response.code
          if response_code.to_s.match?(/^2/)
            ::JSON.parse(response.response_body).deep_symbolize_keys
          else
            log_response(response, 'show') unless response_code == 404
          end
        end
      end

      def update(retries: 0)
        stats_aggregator.timing('carto-named-maps-api.update') do
          params = request_params
          params[:body] = @named_map_template.to_json

          response = http_client.put(url(template_name: @named_map_template.name), params)

          response_code_string = response.code.to_s
          if response_code_string.match?(/^2/)
            ::JSON.parse(response.response_body).deep_symbolize_keys
          elsif (response_code_string.match?(/^5/) || response.code == 429) && retries < MAX_RETRY_ATTEMPTS
            sleep(RETRY_TIME_SECONDS**retries)
            update(retries: retries + 1)
          else
            log_response(response, 'update')
          end
        end
      end

      def destroy(retries: 0)
        stats_aggregator.timing('carto-named-maps-api.destroy') do
          url = url(template_name: @named_map_template.name)
          response = http_client.delete(url, request_params)

          response_code_string = response.code.to_s
          if response_code_string.match?(/^2/)
            response.response_body
          elsif (response_code_string.match?(/^5/) || response.code == 429) && retries < MAX_RETRY_ATTEMPTS
            sleep(RETRY_TIME_SECONDS**retries)
            destroy(retries: retries + 1)
          else
            log_response(response, 'destroy') unless response.code == 404
          end
        end
      end

      def upsert
        show ? update : create
      end

      private

      def stats_aggregator
        @@stats_aggregator_instance ||= CartoDB::Stats::EditorAPIs.instance
      end

      def url(template_name: '')
        username = user.username
        user_url = CartoDB.subdomainless_urls? ? "/user/#{username}" : ''

        "#{protocol}://#{host(username)}:#{port}#{user_url}/api/v1/map/named/#{template_name}?api_key=#{user.api_key}"
      end

      def request_params
        {
          headers: headers(user.username),
          ssl_verifypeer: ssl_verifypeer,
          ssl_verifyhost: ssl_verifyhost,
          followlocation: true,
          connecttimeout: HTTP_CONNECT_TIMEOUT_SECONDS,
          timeout: HTTP_REQUEST_TIMEOUT_SECONDS
        }
      end

      def headers(username)
        @headers ||= {
          'content-type': 'application/json',
          'host': domain(username)
        }
      end

      def domain(username)
        return @domain if @domain

        config_domain = Cartodb.get_config(:tiler, 'internal', 'domain')

        CartoDB.subdomainless_urls? ? config_domain : "#{username}.#{config_domain}"
      end

      def port
        @port ||= Cartodb.get_config(:tiler, 'internal', 'port')
      end

      def protocol
        @protocol ||= Cartodb.get_config(:tiler, 'internal', 'protocol')
      end

      def ssl_verifypeer
        @verifycert ||= Cartodb.get_config(:tiler, 'internal', 'verifycert')
      end

      def ssl_verifyhost
        ssl_verifypeer ? 2 : 0
      end

      def host(username)
        return @host if @host

        config_host = Cartodb.get_config(:tiler, 'internal', 'host')

        @host ||= config_host.blank? ? domain(username) : config_host
      end

      def http_client
        @http_client ||= Carto::Http::Client.get('named_maps')
      end

      def log_response(response, action)
        # Suppressed for now, we have to work on this trace, it's only
        # generating noise without any relevant information.
        #   log_warning(
        #     message: 'Error in named maps API',
        #     current_user: user,
        #     visualization_id: visualization.id,
        #     action: action,
        #     request_url: response.request.url,
        #     status: response.code,
        #     response_body: response.body
        #   )
        # rescue Encoding::UndefinedConversionError => e
        #   # Hotfix for preventing https://rollbar.com/carto/CartoDB/items/41457 until we find the root cause
        #   # https://cartoteam.slack.com/archives/CEQLWTW9Z/p1599134417001900
        #   # https://app.clubhouse.io/cartoteam/story/101908/fix-encoding-error-while-logging-request
        #   # Rollbar.error(e)
      end

      def log_context
        super.merge(request_id: Carto::Common::CurrentRequest.request_id, component: 'cartodb.named-maps-client')
      end
    end
  end
end