Improve bulk closing work items performance on group work items list
What does this MR do and why?
This MR adds :project, :author and :dates_source to the existing includes call in app/services/work_items/bulk_update_service.rb, which previously only preloaded namespace: [:route, :namespace_settings_with_ancestors_inherited_settings].
Each addition avoids a per-item query during bulk update:
:project, becauseIssue#resource_parentreturnsproject || namespace, so every project-level work item was re-fetching its project and everything hanging off it.:author, becauseIssue#hidden?callsauthor&.banned?once per item.:dates_source, becauseWorkItem#due_dateandWorkItem#start_dateread through it once per item.
Measured on GDK, closing 25 work items in a group: roughly 1255 queries and 1.70s before, roughly 955 queries and 1.36s after (about 25% fewer queries, 20% less wall time; medians of three runs).
This MR started out addressing issue #606918 (closed), where bulk closing work items from a group-level Work Items list shows "Something went wrong while bulk editing." The leading theory was a GraphQL timeout: the WorkItemBulkUpdate mutation can exceed the 30 second GraphQL timeout, and GraphQL::Schema::Timeout only checks elapsed time between field resolutions, so it never interrupts a running resolver. The mutation runs to completion and closes every item, and only then does the updatedWorkItemCount field raise on the timeout check. The frontend catches that error and shows the generic failure alert, telling users the operation failed when it actually succeeded. An earlier report, issue #557179 (closed), captured the exact error text "Timeout on WorkItemBulkUpdate.updatedWorkItemCount" and confirmed the items had in fact been closed after a refresh.
An earlier revision of this MR also lowered BULK_EDIT_CHUNK_SIZE in app/assets/javascripts/work_items/list/components/work_item_bulk_edit_sidebar.vue from 25 to 10, to keep each chunked mutation inside the timeout budget. That has now been reverted, because the reporter pointed out that bulk closing the same number of work items succeeds from the project list and fails only from the group list. That does not rule a timeout out (the group path can cost considerably more per item, since the selection can span many projects and the per-item permission check does not reuse a warm cache), but it does mean the root cause is not confirmed. Lowering the chunk size only helps if the mutation really is running out of time, and it costs an extra round trip per selection, so it is not worth shipping on an unconfirmed theory. We are waiting on more detail from the reporter.
The preload is worth keeping either way, so this MR is now scoped to just that.
Two follow-ups are out of scope here: distinguishing a timeout from the generic "Something went wrong while bulk editing" alert so users get an actionable message, and moving bulk updates to a background job, tracked in issue #572140.
References
Screenshots or screen recordings
This is a backend-only change with no UI change. The effect is fewer queries per bulk update request.
How to set up and validate locally
- On a group with several projects, create or find more than 10 open work items.
- Go to the group's Plan > Work items list.
- Enable Bulk edit.
- Select all.
- Set State to Closed.
- Click Update Selected.
- Confirm the update succeeds.
To sanity check the query reduction, you can run WorkItems::BulkUpdateService in a rails console wrapped in Gitlab::SafeRequestStore.ensure_request_store, counting sql.active_record notifications. Without the request store, ApplicationSetting caching is disabled and the query counts are badly distorted.
Testing done
RSpec: 62 examples, 0 failures across spec/services/work_items/bulk_update_service_spec.rb (20), spec/requests/api/graphql/mutations/work_items/bulk_update_spec.rb and its EE counterpart ee/spec/requests/api/graphql/mutations/work_items/bulk_update_spec.rb (42 between them). RuboCop is clean.
No Jest run is listed because the frontend change was reverted, so work_item_bulk_edit_sidebar.vue and its specs are unchanged from master.
No QueryRecorder regression guard was added for the preload. One was attempted, but in a sparse spec environment the preload only removes about one project query per item (38 down to 33 at 6 items), because the remaining project lookups come from Event#project and Integrations::Loggable, which do not go through the work item's association. A fixed count assertion would be brittle. Happy to add one if a reviewer wants it anyway.
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.