Skip to content

feat(server): namespace-scoped introspection filter — 87× per-instance memory (3/6)#1332

Open
yyyyaaa wants to merge 1 commit into
feat/scale-s2-cache-hardeningfrom
feat/scale-s3-introspection-filter
Open

feat(server): namespace-scoped introspection filter — 87× per-instance memory (3/6)#1332
yyyyaaa wants to merge 1 commit into
feat/scale-s2-cache-hardeningfrom
feat/scale-s3-introspection-filter

Conversation

@yyyyaaa

@yyyyaaa yyyyaaa commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 3/6 of the #1325 split. Scopes each instance's catalog introspection to the namespaces it actually serves, opt-in via GRAPHILE_INTROSPECTION_FILTER=1. This turned out to be the memory wall — and it applies to dedicated (non-pooled) instances, so it delivers most of the per-instance win even if pooling (PRs 4–5) never merges.

Measured on the #1325 rig (48 tenant DBs, 61k pg_class rows in one database):

metric before after
retained heap per instance 1,305.6MB 14.7MB 87×
cold build 7.2s 0.42s 17×
PG-side introspection spike +1,283MiB +17MiB

Mechanism (introspection-filter.ts)

  • The stock pg-introspection query gates namespaces with a fixed textual predicate that appears exactly four times. Pre-parse, each occurrence is swapped for nspname = any (array[…]::text[]) over the keep set.
  • Keep set = served schemas ∪ public ∪ a bounded closure over cross-schema references (FK targets, attribute/procedure type owners), discovered with qualified catalog queries (MAX_CLOSURE_ROUNDS=5) and memoized per pool.
  • Fail-open on gate mismatch: if a pg-introspection upgrade changes the text, introspection runs unfiltered (correctness over savings) — and the pinning test fails loudly in CI instead of the filter silently disabling in production. Fail-closed on discovery errors.
  • pg-introspection@1.0.1 is pinned as a devDependency purely so the test suite can assert the installed package still carries the exact gate text.

Wiring

~10 lines in buildPreset: when the flag is on and the instance has a concrete schema list, the pgService pool gets a thin interceptor that touches only the introspection statement; every other query passes through byte-identical. Flag off ⇒ pool selection identical to today. Metrics sampler gains swap/discovery counters.

Testing

graphql/server 132/132 (14 suites) — includes the pinning test, gate-mismatch fail-open, literal escaping (quote doubling, pg_*/information_schema drops), closure-loop memoization, and the dedicated-wrapper path. (The filter-through-rewriting-pool composition tests land with PR 4, where that seam exists.)

Stack

Stacked on #1331 (feat/scale-s2-cache-hardening); merge after it. Full stack map in #1330. Extracted from #1325 (commit 7615a8c; kept open as the validated reference).

🤖 Generated with Claude Code

https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb

…PECTION_FILTER)

Every PostGraphile instance runs pg-introspection's full-catalog query and
retains the entire introspection graph — the single biggest per-instance cost
on production-shaped catalogs, and pure waste for an instance that serves 4
schemas out of 61k pg_class rows.

Measured on the #1325 rig (48 tenant DBs, 61k pg_class rows in one database):
- retained heap per instance:  1,305.6MB -> 14.7MB   (87x)
- cold build:                  7.2s      -> 0.42s
- PG-side introspection spike: +1,283MiB -> +17MiB

Mechanism (introspection-filter.ts):
- The stock pg-introspection query gates namespaces with a fixed textual
  predicate that appears EXACTLY four times. We swap each occurrence,
  pre-parse, for `nspname = any (array[...]::text[])` over the keep set.
- Keep set = served schemas ∪ public ∪ a bounded closure over cross-schema
  references (FK targets, attribute/procedure type owners), discovered with
  qualified catalog queries (MAX_CLOSURE_ROUNDS=5) and memoized per pool.
- Fail-open on gate-count mismatch (a pg-introspection upgrade that changes
  the text runs UNFILTERED — correctness over savings, and the pinning test
  below fails loudly). Fail-closed on discovery errors.
- pg-introspection@1.0.1 pinned as a devDependency: the test suite asserts the
  installed package still carries the exact gate text four times, so a
  dependency bump that drifts the text breaks CI here instead of silently
  disabling the filter in production.

Wiring: opt-in via GRAPHILE_INTROSPECTION_FILTER=1|true. When on (and the
instance has a concrete schema list), the pgService pool is wrapped with a
thin interceptor that only touches the introspection statement; every other
query passes through byte-identical. Flag off = pool selection identical to
today. The metrics sampler gains swap/discovery counters.

Extracted from #1325 (commit 7615a8c).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant