Improve Redaction Service / Auth Scalability in Monolith Components
### Problem
The Knowledge Graph's `RedactionService` fires hundreds of SQL queries per batch authorization request due to per-resource authorization lookups (N+1 on `project_authorizations`, `members`, `saml_providers`, `License.current`). Additionally, GKG query execution blocks Puma worker threads for 500ms-8s per request while the bidirectional gRPC stream runs.
### Solution
Five MRs across two repos addressing query performance and thread utilization.
| MR | What it does |
|---|---|
| https://gitlab.com/gitlab-org/gitlab/-/merge_requests/229374+ | Eager-load associations in `PRELOAD_ASSOCIATIONS` (72% query reduction) |
| https://gitlab.com/gitlab-org/gitlab/-/merge_requests/229378+ | Batch pre-seed authorization caches + `License.current` SafeRequestStore |
| https://gitlab.com/gitlab-org/gitlab/-/merge_requests/229394+ | Move GKG queries from Puma to Workhorse via SendData/Injecter pattern |
| https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/merge_requests/732+ | Go protobuf stubs for Workhorse integration |
| https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/merge_requests/730+ | ADR 008: Workhorse query acceleration design doc |
Related issues:
- https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/work_items/349+ Move GKG queries to Workhorse (tracking issue)
- https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/work_items/330+ Tune tonic gRPC HTTP/2 settings
- https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/work_items/404+ Correlation ID tokio::spawn bug
---
### [!229374](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/229374): Eager-load associations
Added missing associations to `PRELOAD_ASSOCIATIONS` so they are batch-loaded upfront in the existing `includes()` call. DeclarativePolicy conditions access `:organization`, `:saml_provider`, `:namespace`, `:assignees`, `:author`, and `:system_note_metadata` during `Ability.allowed?` checks, but these were not being eagerly loaded, causing N+1 lazy-load queries per resource.
**Results (215 resources, cold caches):**
| Metric | Before | After | Reduction |
|---|---|---|---|
| Mixed batch queries | 430 | 118 | 72% |
| system_note_metadata | 50 (N+1) | 1 (batch) | 98% |
| licenses | 217 | 55 | 75% |
---
### [!229378](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/229378): Batch authorization cache pre-seeding
Before policy evaluation, `RedactionService` now collects all projects and groups from loaded resources in a single pass, then runs `ProjectPolicyPreloader` and `GroupPolicyPreloader` to batch-load `max_access_level` into `RequestStore`. This replaces per-resource N+1 queries on `project_authorizations` and `members` tables.
Also wraps `License.current` in `SafeRequestStore` to avoid repeated JSON deserialization and AR object allocation on every call (each call parses JSON and allocates two License AR objects, ~800 unnecessary allocations per 50-project batch).
**Postgres.ai results (session webui-150381, production clone):**
| Query | Batch (1 query) | N+1 per call | N+1 x50 total | Improvement |
|---|---|---|---|---|
| `project_authorizations` | 23.9ms | 1.6ms | 95ms | 2.5x, 49 fewer round trips |
| `saml_providers` | 0.07ms | 0.9ms | 26ms | 371x, 28 fewer round trips |
| group member access | 85.7ms | 167ms | 3,340ms | 39x, 19 fewer round trips |
| `License.current` | 0ms (cached) | 1.4ms | 122ms | 86 JSON parses eliminated |
---
### [!229394](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/229394): Workhorse query acceleration
Moves the bidirectional gRPC stream between Rails and GKG from Puma worker threads to Workhorse goroutines using the SendData/Injecter pattern (same pattern used by Gitaly for blob serving).
**How it works:**
1. Rails receives the query request, authenticates the user, builds a JWT
2. Rails responds with a `Gitlab-Workhorse-Send-Data` header containing the GKG connection details
3. Workhorse intercepts the header, opens a bidirectional gRPC stream to GKG
4. During the stream, GKG sends `RedactionRequired` messages back to Workhorse
5. Workhorse calls a Rails internal API (`POST /api/v4/internal/orbit/redaction`) to check permissions
6. Workhorse sends authorized resource IDs back to GKG, which filters results
7. Workhorse writes the final JSON response to the client
**Impact:** Puma worker blocking drops from 500ms-8s (full query duration) to two short calls (~10-50ms each: initial auth + redaction callback). The query execution and streaming happen entirely in Workhorse goroutines.
**Implementation:**
- `workhorse/internal/orbit/`: 3 Go files (client.go, sendquery.go, redaction.go) + generated proto stubs
- `ee/lib/gitlab/workhorse.rb`: `send_orbit_query` method
- `ee/lib/api/internal/orbit.rb`: `POST /api/v4/internal/orbit/redaction` endpoint
- `ee/app/controllers/dashboard/orbit/data.rb`: `ORBIT_WORKHORSE_ENABLED` env var gate
- All 5 query types pass E2E: search, traversal, aggregation, path_finding, neighbors
- MCP `query_graph` tool also routed through Workhorse via `McpId` param
**Supporting MRs:**
- https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/merge_requests/732+ Go protobuf stubs published as sub-module at `proto/go/`
- https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/merge_requests/730+ ADR 008 design document with PlantUML diagrams
issue