116 lines
3.8 KiB
Ruby
116 lines
3.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Glql
|
|
class BaseController < GraphqlController
|
|
before_action :check_rate_limit, only: [:execute]
|
|
before_action :set_namespace_context, only: [:execute]
|
|
|
|
GlqlQueryLockedError = Class.new(StandardError)
|
|
|
|
# Overrides GraphqlController#execute to add rate limiting for GLQL queries.
|
|
# When a query times out (raising ActiveRecord::QueryAborted), we increment the
|
|
# Gitlab::ApplicationRateLimiter counter. If a second failure occurs within a
|
|
# 15-minute window (configured in lib/gitlab/application_rate_limiter.rb),
|
|
# the request is throttled. One failure is allowed, but consecutive
|
|
# failures within the time window trigger throttling.
|
|
def execute
|
|
start_time = Gitlab::Metrics::System.monotonic_time
|
|
|
|
::Gitlab::Database::LoadBalancing::SessionMap.use_replica_if_available do
|
|
super
|
|
end
|
|
rescue StandardError => error
|
|
# We catch all errors here so they are tracked by SLIs.
|
|
# But we only increment the rate limiter failure count for ActiveRecord::QueryAborted.
|
|
increment_rate_limit_counter if error.is_a?(ActiveRecord::QueryAborted)
|
|
|
|
raise error
|
|
ensure
|
|
increment_glql_sli(
|
|
duration_s: Gitlab::Metrics::System.monotonic_time - start_time,
|
|
error_type: error_type_from(error)
|
|
)
|
|
end
|
|
|
|
rescue_from GlqlQueryLockedError do |exception|
|
|
log_exception(exception)
|
|
|
|
render_error(exception.message, status: :forbidden)
|
|
end
|
|
|
|
private
|
|
|
|
# When `set_current_context` in app/controllers/application_controller.rb calls
|
|
# `to_lazy_hash` on Gitlab::ApplicationContext, the meta fields (meta.project and
|
|
# meta.root_namespace) will be populated using @group or @project variables.
|
|
def set_namespace_context
|
|
@project ||= Project.find_by_full_path(permitted_params[:project]) if permitted_params[:project].present?
|
|
@group ||= Group.find_by_full_path(permitted_params[:group]) if permitted_params[:group].present?
|
|
end
|
|
|
|
# Overrides GraphqlController#permitted_params to permit project and group params
|
|
def permitted_standalone_query_params
|
|
params.permit(:query, :operationName, :remove_deprecated, :group, :project, variables: {})
|
|
end
|
|
|
|
def logs
|
|
graphql_logs = super.presence || [{}]
|
|
|
|
graphql_logs.map do |log|
|
|
log.merge(
|
|
glql_referer: request.headers["Referer"],
|
|
glql_query_sha: query_sha
|
|
)
|
|
end
|
|
end
|
|
|
|
def check_rate_limit
|
|
return unless Gitlab::ApplicationRateLimiter.peek(:glql, scope: query_sha)
|
|
|
|
raise GlqlQueryLockedError,
|
|
'Query temporarily blocked due to repeated timeouts. Please try again later or narrow your search scope.'
|
|
end
|
|
|
|
def increment_rate_limit_counter
|
|
Gitlab::ApplicationRateLimiter.throttled?(:glql, scope: query_sha)
|
|
end
|
|
|
|
def query_sha
|
|
@query_sha ||= Digest::SHA256.hexdigest(permitted_params[:query].to_s)
|
|
end
|
|
|
|
def increment_glql_sli(duration_s:, error_type:)
|
|
query_urgency = Gitlab::EndpointAttributes::Config::REQUEST_URGENCIES.fetch(:low)
|
|
|
|
labels = {
|
|
endpoint_id: ::Gitlab::ApplicationContext.current_context_attribute(:caller_id),
|
|
feature_category: ::Gitlab::ApplicationContext.current_context_attribute(:feature_category),
|
|
query_urgency: query_urgency.name
|
|
}
|
|
|
|
Gitlab::Metrics::GlqlSlis.record_error(
|
|
labels: labels.merge(error_type: error_type),
|
|
error: error_type.present?
|
|
)
|
|
|
|
return if error_type
|
|
|
|
Gitlab::Metrics::GlqlSlis.record_apdex(
|
|
labels: labels.merge(error_type: nil),
|
|
success: duration_s <= query_urgency.duration
|
|
)
|
|
end
|
|
|
|
def error_type_from(exception)
|
|
return unless exception
|
|
|
|
case exception
|
|
when ActiveRecord::QueryAborted
|
|
:query_aborted
|
|
else
|
|
:other
|
|
end
|
|
end
|
|
end
|
|
end
|