Update work item delete rate limit
What does this MR do and why?
File walkthrough:
app/services/work_items/delete_service.rb: definesRATE_LIMIT_KEY = :work_item_deleteand adds a privatecheck_rate_limit!method, called as the first statement of#execute. It returns early unless thework_item_delete_rate_limitfeature flag is enabled forcurrent_user, then calls::Gitlab::ApplicationRateLimiter.throttled?(RATE_LIMIT_KEY, scope: { user: current_user }). When throttled, it raises::RateLimitedService::RateLimitedErrorwith the message from::Gitlab::ApplicationRateLimiter.throttled_error_message.lib/gitlab/application_rate_limiter/labkit_adapter/supported_rate_limits.rb: registers awork_item_deleterule with labkit namelimit_work_item_deletes_by_user,characteristics: %i[user],limit: 300,period: 1.minute,action: :limit.config/feature_flags/gitlab_com_derisk/work_item_delete_rate_limit.yml: newgitlab_com_deriskflagwork_item_delete_rate_limit, milestone 19.4, groupgroup::portfolio planning,default_enabled: false.spec/services/work_items/delete_service_spec.rb: covers the throttled case (raisesRateLimitedService::RateLimitedError, work item survives) and the flag-disabled case (the limiter is never consulted for thework_item_deletekey).spec/requests/api/graphql/mutations/work_items/delete_spec.rb: uses the existing shared example'rate limited endpoint'withrate_limit_key: :work_item_delete, graphql: true, including the second-scope request that proves the limit is per user. Each request in the example targets a freshly created work item, because with the check inside the service, a reused (already deleted) work item would fail authorization in the mutation before the limiter runs.spec/requests/api/work_items/delete_spec.rb: asserts the REST endpoint returns429, returns the throttled error message, and leaves the work item in place.
Coverage, since the check now lives in the service:
- GraphQL
workItemDeletemutation, which callsWorkItems::DeleteService: covered. This is also the path the UI uses (app/assets/javascripts/work_items/graphql/delete_work_item.mutation.graphql), so it was already covered by the earlier mutation-level version. - REST
DELETE /namespaces/:id/-/work_items/:work_item_iid, and the project and group variants inlib/api/work_items/delete.rb, which also callWorkItems::DeleteService: newly covered. This is a hidden experimental endpoint, itself gated by thework_item_rest_apifeature flag. - Still not covered:
DELETE /projects/:id/issues/:issue_iidinlib/api/issues.rband the legacyProjects::IssuesController#destroy. Both call the parent classIssues::DestroyServicedirectly. Putting the limit onIssues::DestroyServicewould also catchBulkImports::Projects::Pipelines::IssuesPipeline, which calls that service during imports and must not be throttled, so it would need an explicit bypass. That is left as separate follow-up work.
Design notes and trade-offs:
Issues::CreateService sets a precedent for rate limiting inside a service by using prepend RateLimitedService. We did not use that concern here, for two reasons. It has no hook for a feature flag check, and this limit is flag gated. It also passes scope: as a positional array, which Gitlab::ApplicationRateLimiter documents as the legacy form being removed, whereas the registry entry here declares characteristics: %i[user], so we pass scope: { user: current_user } instead.
Moving the check to the service has two trade-offs. First, the check now runs after authorization, because both entry points authorize before calling the service, so probing IDs the caller cannot delete no longer consumes this limiter's budget (the generic front-door throttle still applies to that case). Second, the throttle bypass header no longer applies, because Gitlab::ApplicationRateLimiter.throttled_request? needs the request object and the service does not have one, so we call plain throttled? instead. This matches Issues::CreateService. The feature flag remains the kill switch.
The 300 requests per minute threshold matches the limit already applied to work item creation: WorkItems::CreateService inherits from Issues::CreateService, which rate limits on the issues_create key, and the issues_create_limit application setting defaults to 300. Observed production traffic peaks at roughly 94 requests per minute across all users combined, so 300 leaves headroom.
Moving the check also changes the error path: RateLimitedService::RateLimitedError is already rescued by GraphqlController (via its ERROR_STATUS_MAP), by API::API, and by ApplicationController, all mapping to HTTP 429. The previous mutation-level check used raise_resource_not_available_error!, which returned HTTP 200 with a GraphQL error, so this is an incidental improvement. Request logging is preserved: GraphqlController#handle_exception calls exception.try(:log_request, request, current_user), so throttled requests still reach Gitlab::AuthLogger with env: :work_item_delete_request_limit.
References
- Feature issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/605439
- Rollout issue: #628006
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_delete_rate_limit). - To make the limit easy to trip, temporarily lower the
limit:value for thework_item_deleteentry inlib/gitlab/application_rate_limiter/labkit_adapter/supported_rate_limits.rb. - GraphQL: in GraphiQL at
http://gdk.test:3000/-/graphql-explorer, repeatedly run theworkItemDeletemutation:
mutation {
workItemDelete(input: { id: "gid://gitlab/WorkItem/1" }) {
errors
}
}- Confirm the response carries the error
This endpoint has been requested too many times. Try again later. - REST: the endpoint also needs
Feature.enable(:work_item_rest_api). Repeatedly callDELETE /api/v4/projects/:id/-/work_items/:work_item_iidand confirm a429once the limit trips. - Each successful call really deletes a work item, so use throwaway records when testing manually. The specs are a safer way to validate the behavior:
bundle exec rspec spec/services/work_items/delete_service_spec.rbbundle exec rspec spec/requests/api/work_items/delete_spec.rb spec/requests/api/graphql/mutations/work_items/delete_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.