perf(authz): speed up RedactionService batch authorization

What does this MR do and why?

Authz::RedactionService runs one Ability.allowed? per node the Knowledge Graph (Orbit) redaction exchange returns, so a single graph query authorizes hundreds of resources in one batch. This MR makes that batch cheaper without changing any authorization result. Every change is confined to the redaction layer; nothing outside it changes behavior.

Three redaction-scoped optimizations:

  1. Run the batch inside a request store. #execute wraps in Gitlab::SafeRequestStore.ensure_request_store, so the ApplicationSetting read and the policy-preloader caches are memoized across the per-resource loop. It is a no-op on the Grape request path (a store is already active) and only helps callers without one (the gRPC stream redaction callback, Sidekiq, rake, specs), where each resource otherwise re-deserializes the whole ApplicationSetting through Marshal.

  2. Gated memo for the gem's per-call re-merges. DeclarativePolicy::Base rebuilds four static-per-class maps (conditions, ability_map, global_actions, delegations) via superclass.X.merge(own_X) on every lookup. A new initializer memoizes them only while a redaction batch is active, keyed off a SafeRequestStore flag that RedactionService sets. Every other policy caller evaluates exactly as before; the only global footprint is one presence check.

  3. Batch the root-ancestor SSO preload (EE). EE SSO and IP enforcement resolve root_ancestor.saml_provider once per resource. EE::Authz::RedactionService preseeds it for the projects' group namespaces before the policy preloaders run, restricted to Group namespaces (personal namespaces have no saml_provider). This collapses the largest per-resource query family: project redaction drops from 157 to 48 SQL queries, ci_pipeline from 137 to 24.

Performance

Round-trip count per batch (live data, authorization byte-identical). Fix 3 collapses the largest per-resource query family:

Entity type SQL queries before SQL queries after Reduction
project 157 48 -69%
ci_pipeline 137 24 -82%

Warm-path wall-clock per resource (request store active), from fixes 1 and 2:

Entity type Speedup Entity type Speedup
milestone ~5.3x project ~2.1-2.5x
merge_request ~3.2x ci_pipeline ~2.5x
ci_runner ~3.1x ci_build ~2.3x

Other types are already under 1 ms/resource. In the non-request (cold) regime the request-store wrap alone gives 2x or more for every type. issue/work_item/note improve but stay ~1.1-1.5x warm, bounded by deep stacked-policy delegation, which is outside this MR's redaction-only scope.

Production profiling against the gitlab.com main database on postgres.ai Database Lab confirmed every redaction query is an index lookup at production scale: 0 sequential scans on real tables, max plan cost 178.65, median 5.74. So redaction is not plan-bound; the cost is the per-batch round-trip count, which fix 3 reduces. Exact queries and plans are in the validation note below.

How to set up and validate locally

bundle exec rspec spec/services/authz/redaction_service_spec.rb \
  spec/initializers/authz/redaction_policy_conditions_cache_spec.rb \
  ee/spec/services/ee/authz/redaction_service_spec.rb

Redaction output is byte-identical to a direct Ability.allowed? ground truth, verified per entity type. The new specs cover the request-store wrap, the gated condition-map memo (memoization plus pass-through when no batch is active), and the EE preseed (group namespaces preloaded, personal namespaces skipped without raising).

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Edited by Michael Angelo Rivera

Merge request reports

Loading
Loading