Cache GLQL dashboard results behind one shared Apollo client

What does this MR do and why?

Every GLQL request created its own ApolloClient with its own empty cache, and createDefaultClient() registered each one in apolloClients without ever removing it. So every panel, trend comparison and "next page" leaked a client, identical queries on different panels were fetched separately, and switching dashboard tabs or changing a filter refetched everything.

This MR adds a shared execution context per request queue. Executor.shared(queueName) returns one { client, results } per queue and endpoint path, so group and project namespaces stay separate. The client is a single ApolloClient with fetchPolicy: 'no-cache'; results is a new ResultCache keyed by compiled query text plus variables JSON.

Caching details:

  • The cache stores the pending request, not just the result, so an identical in-flight request is joined instead of duplicated. Either way, one network request.
  • A cached key (pending or finished) does not take a queue slot, and only the task that actually sends the request holds one.
  • Failed requests are removed from the cache, so failures are never cached.
  • At most 200 entries, LRU eviction, and entries expire 5 minutes after their request started so a long-open dashboard doesn't serve stale numbers.
  • Apollo's queryDeduplication is disabled for dashboard requests, because forget() on reload drops entries for still-running requests and Apollo would otherwise return the pre-reload result. Embedded blocks keep deduplication.

Why not just one shared client with Apollo's InMemoryCache: GLQL analytics queries put metrics and GROUP BY dimensions inside nodes { ... } rather than in arguments, so panels sharing a date range write to the same normalized entry and the last writer wins. Worse, a panel whose selection set is a subset of another's can be served the other's rows as a silent cache hit and display a wrong number. Measured on the DAP Impact dashboard, that approach served only 2 of 12 Overview queries from cache.

Two related changes:

  • GlqlVisualization#reload() calls forget(...) to drop entries tagged with the panel's source, then emits reload; the panel wrapper refetches and remounts the resolver, so no key bump is needed. retry() still bumps the key (nothing else changes on retry) after calling forget(...).
  • extractGroupOrProject now strips the query string and hash, so explore dashboard URLs (no /-/) no longer derive a different endpoint path, and therefore a different context, on every tab switch.
  • Subquery variables (milestone/iteration lookups) now go through #enqueue, so they are queued, cancelled and cached like the query they feed.

Measurements

api/glql requests counted in the browser network tab on the DAP Impact dashboard in GDK:

Action Before After
First load of the Overview tab 12 12
Switch to Adoption tab 12 12
Switch back to Overview 12 0
Reload one panel 2 2

References

Screenshots or screen recordings

Not applicable, no visible UI change, only which network requests are sent.

How to set up and validate locally

  1. Enable the dap_impact_v1 feature flag.
  2. Open /explore/analytics_dashboards, open the DAP Impact dashboard, pick a group, and filter the network tab to api/glql.
  3. Switch to another dashboard tab and back: no new requests.
  4. Change the date range: new requests. Change it back: no new requests.
  5. Panel actions menu > Reload: only that panel's queries are re-requested (two if it has a trend comparison).
  6. Leave the dashboard open for over five minutes, then switch tabs and back: panels request again.
  7. For contrast, an embedded GLQL block in an issue description still requests on every render.

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.

🤖 Generated with Claude Code

Edited by Brandon Labuschagne

Merge request reports

Loading
Loading