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 calls Issuable::BulkUpdateService. The same controller action and service also serve merge requests and epics.
  • The work items list calls the workItemBulkUpdate GraphQL mutation, which calls WorkItems::BulkUpdateService.

File walkthrough:

  • lib/gitlab/application_rate_limiter/labkit_adapter/supported_rate_limits.rb: registers a work_item_bulk_update rule with labkit name limit_work_item_bulk_updates_by_user, characteristics: %i[user], period: 1.minute, action: :limit.
  • app/services/issuable/bulk_update_service.rb: defines RATE_LIMIT_KEY = :work_item_bulk_update and RATE_LIMITED_TYPES = %w[issue epic], and adds a private check_rate_limit!(type) called as the first statement of #execute. It returns early for any type outside RATE_LIMITED_TYPES, so merge request bulk updates are not limited. It then returns unless the feature flag is enabled for current_user, and raises ::RateLimitedService::RateLimitedError when ::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: new gitlab_com_derisk flag, milestone 19.6, group group::portfolio planning, default_enabled: false.
  • spec/services/issuable/bulk_update_service_spec.rb: covers the throttled case (raises RateLimitedService::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: adds POST #bulk_update coverage 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 with graphql: 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::BulkUpdateService to the issue and epic types 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::RateLimitedError is already rescued by ApplicationController, which renders HTTP 429 with the standard message and logs the request through Gitlab::AuthLogger, and by GraphqlController through its ERROR_STATUS_MAP, which maps it to 429 as 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 matches WorkItems::DeleteService. The feature flag is the kill switch during rollout.

References

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

  1. Enable the flag in a Rails console: Feature.enable(:work_item_bulk_update_rate_limit).
  2. To make the limit easy to trip, temporarily lower the limit: value for the work_item_bulk_update entry in lib/gitlab/application_rate_limiter/labkit_adapter/supported_rate_limits.rb.
  3. GraphQL: in GraphiQL at http://gdk.test:3000/-/graphql-explorer, repeatedly run the mutation against a project you can update, replacing the IDs:
    mutation {
      workItemBulkUpdate(input: { fullPath: "flightjs/Flight", ids: ["gid://gitlab/WorkItem/1"], stateEvent: REOPEN }) {
        updatedWorkItemCount
        errors
      }
    }
    Confirm the response carries the error This endpoint has been requested too many times. Try again later. once the limit trips.
  4. 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_update returns 429 once the limit trips and the sidebar shows its failure alert.
  5. Merge requests: repeat the bulk edit on a merge requests list and confirm it is never throttled.
  6. 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.rb
    • bundle 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.

🤖 Generated with Claude Code

Edited by Matt D'Angelo

Merge request reports

Loading
Loading