workspaceAutocompleteUsersSearch ~18s CPU-bound: duoStatus flow-trigger preload fan-out for Duo-enabled nested groups
Summary
The GraphQL operation workspaceAutocompleteUsersSearch (used for @mention / assignee / reviewer autocomplete pickers) is extremely slow — ~18s average, mostly CPU-bound — for a GitLab Dedicated customer that recently enabled Duo across several groups with a large, deeply-nested group/project hierarchy. On gitlab.com the same operation is typically < 1s.
A sampled slow request showed 28.9s CPU, ~14M allocated objects, ~924MB allocated, but only ~4.9s DB time — consistent with Ruby materializing many rows rather than a single slow SQL query. Operation count stayed flat before/after Duo enablement, so this is a per-request cost amplification, not a traffic increase.
Root cause (confirmed against gitlab-org/gitlab master)
When the autocomplete query asks for a user's Duo status, the resolver preloads every flow trigger for each returned service account and then filters down to the current project in Ruby.
-
The field that triggers it —
workspaceAutocompleteUsersSearchrequestsduoStatus { flowTriggerEvents }(via theAutocompleteUserStatus→UserDisabledForDuoUsagefragment). -
Unscoped preload of all triggers —
ee/app/graphql/ee/resolvers/autocomplete_users_resolver.rbsetspreload_flow_triggers: truewheneverduo_statusis requested, andee/app/finders/ee/autocomplete/users_finder.rbthen preloadsai_flow_triggersandchild_item_consumers_flow_triggersfor each of the ~20 autocomplete users. Both associations are unscoped — they load every trigger for the service account across every project. -
Filtered to the current project in Ruby —
ee/app/graphql/types/users/duo_status_type.rb:triggers = (object.ai_flow_triggers + object.child_item_consumers_flow_triggers).uniq.select(&:active?) # ... when ::Project triggers.select { |trigger| trigger.project_id == parent_entity.id }It materializes the full set, then discards all but one project's triggers in memory.
-
Cost grows with the number of Duo-enabled projects —
child_item_consumers_flow_triggersgoesservice_account → ai_catalog_item_consumers → child_item_consumers → flow_trigger. Because each item consumer is sharded to exactly one project (sharding_keys = [:group, :project]), enabling a foundational flow per-project creates a separate consumer + trigger in every project. So a service account enabled across a large nested hierarchy accumulates one trigger per project — exactly matching the observed CPU/allocation blow-up.
This also explains why slowness persists after Namespaces::CascadeDuoSettingsWorker finishes (the fan-out is in the read path, not the cascade) and why the operation count didn't change.
Notes
- This query runs per-project (
namespace: project(fullPath:)), soparent_entityis aProject, not aNamespace— thegroup project_idsbranch ofduo_status_type.rbis not the driver here. - MR !257558 touches these same files but does not fix this. It adds group-inheritance lookups (
inherited_triggers) behind theai_catalog_group_inherited_consumersflag and leaves the slow path untouched —preload_flow_triggersstill preloads both full associations andfiltered_triggersstill does the same in-Rubyselectbyproject_id.
Suggested fix direction
Scope the preload before loading instead of load-then-filter-in-Ruby:
- Constrain
ai_flow_triggers/child_item_consumers_flow_triggersto the current project's triggers. - Limit to service accounts that actually need the Duo badge.
A quick confirmation experiment: replay the slow search with preload_flow_triggers: true vs false and compare allocations/CPU.
Impact
- GitLab Dedicated customer with a large nested hierarchy: ~18s average, CPU-bound, on a core collaboration UI (mentions / assignee / reviewer pickers).
- Any instance with Duo enabled across many projects could hit the same amplification.
/cc @jsoltani — flagged for prioritization per team request.
Raised from an internal Slack discussion; customer-specific details are in an internal note on this issue.