Module: Gitlab::ApplicationRateLimiter
- Extended by:
- Utils::StrongMemoize
- Defined in:
- lib/gitlab/application_rate_limiter.rb,
lib/gitlab/application_rate_limiter/base_strategy.rb,
lib/gitlab/application_rate_limiter/increment_per_action.rb,
lib/gitlab/application_rate_limiter/increment_per_actioned_resource.rb,
lib/gitlab/application_rate_limiter/increment_resource_usage_per_action.rb
Overview
This module implements a simple rate limiter that can be used to throttle certain actions. Unlike Rack Attack and Rack::Throttle, which operate at the middleware level, this can be used at the controller or API level. See CheckRateLimit concern for usage.
Defined Under Namespace
Classes: BaseStrategy, IncrementPerAction, IncrementPerActionedResource, IncrementResourceUsagePerAction
Constant Summary collapse
- InvalidKeyError =
Class.new(StandardError)
- InvalidScopeError =
Class.new(StandardError)
- LIMIT_USAGE_BUCKET =
[0.25, 0.5, 0.75, 1].freeze
Class Method Summary collapse
- .application_rate_limiter_histogram ⇒ Object
-
.interval(key) ⇒ Integer?
Returns the interval value for a given rate limit key.
-
.log_request(request, type, current_user, logger = Gitlab::AuthLogger) ⇒ Object
Logs request using provided logger.
-
.peek(key, scope:, threshold: nil, interval: nil, users_allowlist: nil) ⇒ Boolean
Returns the current rate limited state without incrementing the count.
-
.rate_limits ⇒ Object
Application rate limits.
- .report_metrics(key, value, threshold, peek) ⇒ Object
-
.resource_usage_throttled?(key, scope:, resource_key:, threshold:, interval:, peek: false) ⇒ Boolean
Increments the resource usage for a given key and returns true if the action should be throttled.
-
.throttled?(key, scope:, resource: nil, threshold: nil, interval: nil, users_allowlist: nil, peek: false) ⇒ Boolean
Increments the given key and returns true if the action should be throttled.
-
.throttled_request?(request, current_user, key, scope:, **options) ⇒ Boolean
Similar to #throttled? above but checks for the bypass header in the request and logs the request when it is over the rate limit.
Class Method Details
.application_rate_limiter_histogram ⇒ Object
232 233 234 235 236 237 238 239 |
# File 'lib/gitlab/application_rate_limiter.rb', line 232 def application_rate_limiter_histogram @application_rate_limiter_histogram ||= Gitlab::Metrics.histogram( :gitlab_application_rate_limiter_throttle_utilization_ratio, "The utilization-ratio of a throttle.", { peek: nil, throttle_key: nil, feature_category: nil }, LIMIT_USAGE_BUCKET ) end |
.interval(key) ⇒ Integer?
Returns the interval value for a given rate limit key
270 271 272 273 274 275 |
# File 'lib/gitlab/application_rate_limiter.rb', line 270 def interval(key) value = rate_limit_value_by_key(key, :interval) raise InvalidKeyError if value.nil? rate_limit_value(value) end |
.log_request(request, type, current_user, logger = Gitlab::AuthLogger) ⇒ Object
Logs request using provided logger
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/gitlab/application_rate_limiter.rb', line 247 def log_request(request, type, current_user, logger = Gitlab::AuthLogger) request_information = { message: 'Application_Rate_Limiter_Request', env: type, remote_ip: request.ip, method: request.request_method, path: request_path(request) } if current_user request_information.merge!( user_id: current_user.id, username: current_user.username ) end logger.error(request_information) end |
.peek(key, scope:, threshold: nil, interval: nil, users_allowlist: nil) ⇒ Boolean
Returns the current rate limited state without incrementing the count.
217 218 219 |
# File 'lib/gitlab/application_rate_limiter.rb', line 217 def peek(key, scope:, threshold: nil, interval: nil, users_allowlist: nil) throttled?(key, peek: true, scope: scope, threshold: threshold, interval: interval, users_allowlist: users_allowlist) end |
.rate_limits ⇒ Object
Application rate limits
Threshold value can be either an Integer or a Proc in order to not evaluate its value every time this method is called and only do that when it’s needed.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/gitlab/application_rate_limiter.rb', line 21 def rate_limits # rubocop:disable Metrics/AbcSize { ai_action: { threshold: -> { application_settings.ai_action_api_rate_limit }, interval: 8.hours }, auto_rollback_deployment: { threshold: 1, interval: 3.minutes }, autocomplete_users: { threshold: -> { application_settings.autocomplete_users_limit }, interval: 1.minute }, autocomplete_users_unauthenticated: { threshold: -> { application_settings.autocomplete_users_unauthenticated_limit }, interval: 1.minute }, bulk_delete_todos: { threshold: 6, interval: 1.minute }, bulk_import: { threshold: 6, interval: 1.minute }, ci_job_processed_subscription: { threshold: 50, interval: 1.minute }, ci_pipeline_statuses_subscription: { threshold: 50, interval: 1.minute }, code_suggestions_api_endpoint: { threshold: -> { application_settings.code_suggestions_api_rate_limit }, interval: 1.minute }, create_organization_api: { threshold: -> { application_settings.create_organization_api_limit }, interval: 1.minute }, delete_all_todos: { threshold: 1, interval: 5.minutes }, downstream_pipeline_trigger: { threshold: -> { application_settings.downstream_pipeline_trigger_limit_per_project_user_sha }, interval: 1.minute }, email_verification: { threshold: 10, interval: 10.minutes }, email_verification_code_send: { threshold: 10, interval: 1.hour }, expanded_diff_files: { threshold: 6, interval: 1.minute }, fetch_google_ip_list: { threshold: 10, interval: 1.minute }, github_import: { threshold: 6, interval: 1.minute }, fogbugz_import: { threshold: 1, interval: 1.minute }, gitea_import: { threshold: 6, interval: 1.minute }, gitlab_shell_operation: { threshold: application_settings.gitlab_shell_operation_limit, interval: 1.minute }, glql: { threshold: 1, interval: 15.minutes }, group_api: { threshold: -> { application_settings.group_api_limit }, interval: 1.minute }, group_archive_unarchive_api: { threshold: -> { application_settings.group_archive_unarchive_api_limit }, interval: 1.minute }, group_download_export: { threshold: -> { application_settings.group_download_export_limit }, interval: 1.minute }, group_export: { threshold: -> { application_settings.group_export_limit }, interval: 1.minute }, group_import: { threshold: -> { application_settings.group_import_limit }, interval: 1.minute }, group_invited_groups_api: { threshold: -> { application_settings.group_invited_groups_api_limit }, interval: 1.minute }, group_projects_api: { threshold: -> { application_settings.group_projects_api_limit }, interval: 1.minute }, group_shared_groups_api: { threshold: -> { application_settings.group_shared_groups_api_limit }, interval: 1.minute }, groups_api: { threshold: -> { application_settings.groups_api_limit }, interval: 1.minute }, import_source_user_notification: { threshold: 1, interval: 8.hours }, issues_create: { threshold: -> { application_settings.issues_create_limit }, interval: 1.minute }, jobs_index: { threshold: -> { application_settings.project_jobs_api_rate_limit }, interval: 1.minute }, large_blob_download: { threshold: 5, interval: 1.minute }, members_delete: { threshold: -> { application_settings.members_delete_limit }, interval: 1.minute }, namespace_exists: { threshold: 20, interval: 1.minute }, notes_create: { threshold: -> { application_settings.notes_create_limit }, interval: 1.minute }, notification_emails: { threshold: 1000, interval: 1.day }, oauth_dynamic_registration: { threshold: 5, interval: 1.hour }, offline_export: { threshold: 6, interval: 1.minute }, permanent_email_failure: { threshold: 5, interval: 1.day }, phone_verification_send_code: { threshold: 5, interval: 1.day }, phone_verification_verify_code: { threshold: 5, interval: 1.day }, pipelines_create: { threshold: -> { application_settings.pipeline_limit_per_project_user_sha }, interval: 1.minute }, pipelines_created_per_user: { threshold: -> { application_settings.pipeline_limit_per_user }, interval: 1.minute }, play_pipeline_schedule: { threshold: 1, interval: 1.minute }, profile_add_new_email: { threshold: 5, interval: 1.minute }, profile_resend_email_confirmation: { threshold: 5, interval: 1.minute }, profile_update_username: { threshold: 10, interval: 1.minute }, project_api: { threshold: -> { application_settings.project_api_limit }, interval: 1.minute }, project_members_api: { threshold: -> { application_settings.project_members_api_limit }, interval: 1.minute }, project_download_export: { threshold: -> { application_settings.project_download_export_limit }, interval: 1.minute }, project_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute }, project_fork_sync: { threshold: 10, interval: 30.minutes }, project_generate_new_export: { threshold: -> { application_settings.project_export_limit }, interval: 1.minute }, project_import: { threshold: -> { application_settings.project_import_limit }, interval: 1.minute }, project_invited_groups_api: { threshold: -> { application_settings.project_invited_groups_api_limit }, interval: 1.minute }, project_repositories_archive: { threshold: 5, interval: 1.minute }, project_repositories_changelog: { threshold: 5, interval: 1.minute }, project_repositories_health: { threshold: 5, interval: 1.hour }, project_testing_integration: { threshold: 5, interval: 1.minute }, projects_api: { threshold: -> { application_settings.projects_api_limit }, interval: 10.minutes }, projects_api_rate_limit_unauthenticated: { threshold: -> { application_settings.projects_api_rate_limit_unauthenticated }, interval: 10.minutes }, raw_blob: { threshold: -> { application_settings.raw_blob_request_limit }, interval: 1.minute }, runner_jobs_request_api: { threshold: -> { application_settings.runner_jobs_request_api_limit }, interval: 1.minute }, runner_jobs_patch_trace_api: { threshold: -> { application_settings.runner_jobs_patch_trace_api_limit }, interval: 1.minute }, runner_jobs_api: { threshold: -> { application_settings.runner_jobs_endpoints_api_limit }, interval: 1.minute }, search_rate_limit: { threshold: -> { application_settings.search_rate_limit }, interval: 1.minute }, search_rate_limit_unauthenticated: { threshold: -> { application_settings.search_rate_limit_unauthenticated }, interval: 1.minute }, temporary_email_failure: { threshold: 300, interval: 1.day }, update_environment_canary_ingress: { threshold: 1, interval: 1.minute }, update_namespace_name: { threshold: -> { application_settings.update_namespace_name_rate_limit }, interval: 1.hour }, user_large_commit_request: { threshold: 3, interval: 30.seconds }, user_contributed_projects_api: { threshold: -> { application_settings.user_contributed_projects_api_limit }, interval: 1.minute }, user_followers: { threshold: -> { application_settings.users_api_limit_followers }, interval: 1.minute }, user_following: { threshold: -> { application_settings.users_api_limit_following }, interval: 1.minute }, user_gpg_key: { threshold: -> { application_settings.users_api_limit_gpg_key }, interval: 1.minute }, user_gpg_keys: { threshold: -> { application_settings.users_api_limit_gpg_keys }, interval: 1.minute }, user_projects_api: { threshold: -> { application_settings.user_projects_api_limit }, interval: 1.minute }, user_sign_in: { threshold: 5, interval: 10.minutes }, user_sign_up: { threshold: 20, interval: 1.minute }, user_ssh_key: { threshold: -> { application_settings.users_api_limit_ssh_key }, interval: 1.minute }, user_ssh_keys: { threshold: -> { application_settings.users_api_limit_ssh_keys }, interval: 1.minute }, user_starred_projects_api: { threshold: -> { application_settings.user_starred_projects_api_limit }, interval: 1.minute }, user_status: { threshold: -> { application_settings.users_api_limit_status }, interval: 1.minute }, username_exists: { threshold: 20, interval: 1.minute }, users_get_by_id: { threshold: -> { application_settings.users_get_by_id_limit }, interval: 10.minutes }, vertex_embeddings_api: { threshold: 450, interval: 1.minute }, web_hook_calls: { interval: 1.minute }, web_hook_calls_low: { interval: 1.minute }, web_hook_calls_mid: { interval: 1.minute }, web_hook_event_resend: { threshold: 5, interval: 1.minute }, web_hook_test: { threshold: 5, interval: 1.minute } }.freeze end |
.report_metrics(key, value, threshold, peek) ⇒ Object
221 222 223 224 225 226 227 228 229 230 |
# File 'lib/gitlab/application_rate_limiter.rb', line 221 def report_metrics(key, value, threshold, peek) return if threshold == 0 # guard against div-by-zero label = { throttle_key: key, peek: peek, feature_category: Gitlab::ApplicationContext.current_context_attribute(:feature_category) } application_rate_limiter_histogram.observe(label, value / threshold.to_f) end |
.resource_usage_throttled?(key, scope:, resource_key:, threshold:, interval:, peek: false) ⇒ Boolean
Increments the resource usage for a given key and returns true if the action should be throttled.
168 169 170 171 172 173 174 |
# File 'lib/gitlab/application_rate_limiter.rb', line 168 def resource_usage_throttled?(key, scope:, resource_key:, threshold:, interval:, peek: false) validate_scope!(key, scope) strategy = IncrementResourceUsagePerAction.new(resource_key) _throttled?(key, scope: scope, strategy: strategy, threshold: threshold, interval: interval, peek: peek) end |
.throttled?(key, scope:, resource: nil, threshold: nil, interval: nil, users_allowlist: nil, peek: false) ⇒ Boolean
Increments the given key and returns true if the action should be throttled.
144 145 146 147 148 149 150 151 152 |
# File 'lib/gitlab/application_rate_limiter.rb', line 144 def throttled?(key, scope:, resource: nil, threshold: nil, interval: nil, users_allowlist: nil, peek: false) raise InvalidKeyError, key unless rate_limits[key] validate_scope!(key, scope) strategy = resource.present? ? IncrementPerActionedResource.new(resource.id) : IncrementPerAction.new _throttled?(key, scope: scope, strategy: strategy, threshold: threshold, interval: interval, users_allowlist: users_allowlist, peek: peek) end |
.throttled_request?(request, current_user, key, scope:, **options) ⇒ Boolean
Similar to #throttled? above but checks for the bypass header in the request and logs the request when it is over the rate limit
198 199 200 201 202 203 204 205 206 |
# File 'lib/gitlab/application_rate_limiter.rb', line 198 def throttled_request?(request, current_user, key, scope:, **) if ::Gitlab::Throttle.bypass_header.present? && request.get_header(Gitlab::Throttle.bypass_header) == '1' return false end throttled?(key, scope: scope, **).tap do |throttled| log_request(request, "#{key}_request_limit".to_sym, current_user) if throttled end end |