Loading
Update rate limit on issue and work item bulk updates
What does this MR do and why?
Adds a per-user rate limit to bulk updating issues and work items, behind the work_item_bulk_update_rate_limit feature flag.
Issue bulk updates reach the application through two paths, and both are covered by the same rule:
- The legacy issues list posts to
IssuableActions#bulk_update, which callsIssuable::BulkUpdateService. The same controller action and service also serve merge requests and epics. - The work items list calls the
workItemBulkUpdateGraphQL mutation, which callsWorkItems::BulkUpdateService.
File walkthrough:
lib/gitlab/application_rate_limiter/labkit_adapter/supported_rate_limits.rb: registers awork_item_bulk_updaterule with labkit namelimit_work_item_bulk_updates_by_user,characteristics: %i[user],period: 1.minute,action: :limit.app/services/issuable/bulk_update_service.rb: definesRATE_LIMIT_KEY = :work_item_bulk_updateandRATE_LIMITED_TYPES = %w[issue epic], and adds a privatecheck_rate_limit!(type)called as the first statement of#execute. It returns early for any type outsideRATE_LIMITED_TYPES, so merge request bulk updates are not limited. It then returns unless the feature flag is enabled forcurrent_user, and raises::RateLimitedService::RateLimitedErrorwhen::Gitlab::ApplicationRateLimiter.throttled?reports the user as throttled.app/services/work_items/bulk_update_service.rb: reuses the same key and adds the same flag-gated check as the first statement of#execute.config/feature_flags/gitlab_com_derisk/work_item_bulk_update_rate_limit.yml: newgitlab_com_deriskflag, milestone 19.6, groupgroup::portfolio planning,default_enabled: false.spec/services/issuable/bulk_update_service_spec.rb: covers the throttled case (raisesRateLimitedService::RateLimitedError, issues are left unchanged), the flag-disabled case (the limiter is never consulted for this key), and a merge request bulk update (the limiter is never consulted).spec/services/work_items/bulk_update_service_spec.rb: covers the throttled and flag-disabled cases for the GraphQL path's service.spec/requests/projects/issues_controller_spec.rb: addsPOST #bulk_updatecoverage using the existing shared example'rate limited endpoint', including the second-scope request that proves the limit is per user.spec/requests/api/graphql/mutations/work_items/bulk_update_spec.rb: uses the same shared example withgraphql: true.
Design notes:
- The check lives in the services rather than the controller or mutation, following
WorkItems::DeleteService, so every entry point is covered by one check. Both services share one key, so a user's budget is shared across the legacy list and the work items list. - Scoping
Issuable::BulkUpdateServiceto theissueandepictypes keeps merge request bulk updates out of scope. Epics are treated as work items here. - The check runs before any items are loaded or authorized per item, but after the controller's
authorize_admin_issuable!and the mutation's parent read check, so probing IDs the caller cannot update does not consume the budget. RateLimitedService::RateLimitedErroris already rescued byApplicationController, which renders HTTP429with the standard message and logs the request throughGitlab::AuthLogger, and byGraphqlControllerthrough itsERROR_STATUS_MAP, which maps it to429as well. The legacy list's bulk edit sidebar shows its generic failure alert on a non-2xx response, and the work items bulk edit sidebar surfaces the GraphQL error in an alert.- The throttle bypass header does not apply, because
Gitlab::ApplicationRateLimiter.throttled_request?needs the request object and the services do not have one. This matchesWorkItems::DeleteService. The feature flag is the kill switch during rollout.
References
- Feature issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/605436
- Rollout issue: #635005
- Audit parent: https://gitlab.com/gitlab-org/gitlab/-/work_items/605322
- Same pattern for work item deletion: !253621 (merged)
Screenshots or screen recordings
This is a backend-only change with no UI impact, so there is nothing to show.
How to set up and validate locally
- Enable the flag in a Rails console:
Feature.enable(:work_item_bulk_update_rate_limit). - To make the limit easy to trip, temporarily lower the
limit:value for thework_item_bulk_updateentry inlib/gitlab/application_rate_limiter/labkit_adapter/supported_rate_limits.rb. - GraphQL: in GraphiQL at
http://gdk.test:3000/-/graphql-explorer, repeatedly run the mutation against a project you can update, replacing the IDs:Confirm the response carries the errormutation { workItemBulkUpdate(input: { fullPath: "flightjs/Flight", ids: ["gid://gitlab/WorkItem/1"], stateEvent: REOPEN }) { updatedWorkItemCount errors } }This endpoint has been requested too many times. Try again later.once the limit trips. - Legacy list: on a project's issues list with the work items list disabled, select several issues, open bulk edit, and submit repeatedly. Confirm the request to
/-/issues/bulk_updatereturns429once the limit trips and the sidebar shows its failure alert. - Merge requests: repeat the bulk edit on a merge requests list and confirm it is never throttled.
- The specs are a safer way to validate the behavior:
bundle exec rspec spec/services/issuable/bulk_update_service_spec.rb spec/services/work_items/bulk_update_service_spec.rbbundle exec rspec spec/requests/projects/issues_controller_spec.rb spec/requests/api/graphql/mutations/work_items/bulk_update_spec.rb
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Edited by Matt D'Angelo