Skip to content

feat(server,graphile): opt-in blueprint pooling integration — GRAPHILE_BLUEPRINT_POOLING (5/6)#1334

Open
yyyyaaa wants to merge 1 commit into
feat/scale-s4-pooling-corefrom
feat/scale-s5-pooling-integration
Open

feat(server,graphile): opt-in blueprint pooling integration — GRAPHILE_BLUEPRINT_POOLING (5/6)#1334
yyyyaaa wants to merge 1 commit into
feat/scale-s4-pooling-corefrom
feat/scale-s5-pooling-integration

Conversation

@yyyyaaa

@yyyyaaa yyyyaaa commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 5/6 of the #1325 split. Turns the seam from PR 4 on, opt-in via GRAPHILE_BLUEPRINT_POOLING=1. Default off — with the flag unset the dispatcher takes exactly the pre-existing path: key = svc_key, no decision, no extra queries, no pool wrapping.

This is deliberately the smallest code PR of the stack (+425/−24): all mechanism was reviewed in PRs 2–4; this is just the wiring that connects them.

Dispatcher (graphile.ts)

  • Resolves the memoized pooling decision per svc_key; pooled services share a bp:-keyed instance built fully qualified against the canonical tenant.
  • Per-request routing: the requesting tenant's physical schema list travels in pgSettings via the transaction-local GUC constructive.pool_schemas, where the rewriting pool maps canonical→tenant identifiers.
  • Role names de-closured: read from the request's resolved API so a shared instance uses the requesting tenant's roles, not whichever tenant happened to build it.
  • (preset).schema.constructivePooled marks shared instances so plugins report logical (hash-stripped) schema names instead of leaking the canonical tenant's physical names through _meta.

Flush (flush.ts) — per-database invalidation

A schema:update flush clears only the changed database's decisions and the blueprint it was attached to. The fleet-wide alternative turned every tenant provision into a cold restart of every pooled instance — and the immediate rebuilds raced the evicted instances' drain (their ~GB stays live until in-flight requests finish), which was the measured 24h-soak OOM in #1325. New tenants have no memoized decisions, so provisioning is a no-op here (instances are shape-generic; a same-shape tenant attaches with no rebuild). The admin /flush route still clears all bp: entries.

Plugin pooling-compat (inert while the flag is off)

  • meta-schema: report logical schema names when constructivePooled is set.
  • presigned-url: two-tier storage-module matching — exact physical schema first (existing behavior, always wins when not pooled), logical-suffix match only as the pooled fallback; safe because configs are already filtered to one databaseId.
  • bm25-codec: documents the store's concurrency contract (serialized by PR 2's build semaphore) and why bm25 shapes are excluded from pooling (bind-value index name — see PR 4).

Measured end-state (from #1325, production-shaped rig: 48 tenant DBs, 61k pg_class rows)

47 tenants pooled onto ONE 2048MB node — 30min authenticated zipf @50rps: 104,818/104,818 OK, error rate 0, p99 21ms, heap peak 195.7MB (9.6% of budget), zero cross-tenant bleed, fail-closed counter 0. Same-tenant SDL byte-identical (MD5) pooled vs dedicated. Capacity/ops guidance (heap floor 1024MB, sizing law) lands with PR 6's docs.

Testing

graphql/server 205/205 · graphile-settings 158/158 · graphile-presigned-url-plugin 25/25 (incl. new two-tier matching suite) · graphile-search 78/78.

Stack

Stacked on #1333 (feat/scale-s4-pooling-core); merge after it. Full stack map in #1330. Extracted from #1325 (kept open as the validated reference).

🤖 Generated with Claude Code

https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb

… per schema-shape (GRAPHILE_BLUEPRINT_POOLING)

Turns the seam from the previous PR on. Default OFF: with the flag unset the
dispatcher takes exactly the pre-existing path (key = svc_key, no decision, no
extra queries, no pool wrapping).

Dispatcher (graphile.ts):
- Resolve the memoized pooling decision per svc_key; pooled services share a
  `bp:`-keyed instance built against the canonical tenant, fully qualified.
- Pooled presets route per request: the requesting tenant's physical schema
  list travels in pgSettings via the transaction-local GUC
  constructive.pool_schemas, where the rewriting pool maps canonical→tenant
  identifiers. Role names de-closured (read from the request's resolved API)
  so a shared instance uses the REQUESTING tenant's roles.
- `(preset).schema.constructivePooled` marks shared instances so plugins
  report logical (hash-stripped) schema names instead of leaking the
  canonical tenant's physical names through _meta.

Flush (flush.ts): per-database invalidation — a schema:update flush clears
ONLY the changed database's decisions and the blueprint it was attached to.
(The fleet-wide alternative turned every tenant provision into a cold restart
of every pooled instance, and the rebuild raced the evicted instances' drain —
the measured 24h-soak OOM.) The admin /flush route clears all `bp:` entries.

Plugin pooling-compat (inert when the flag is off):
- meta-schema: report logical schema names when constructivePooled is set.
- presigned-url: two-tier storage-module matching — exact physical schema
  first (existing behavior), logical-suffix match only as the pooled
  fallback; safe because configs are already filtered to one databaseId.
- bm25-codec: document the store's concurrency contract and why bm25 shapes
  are excluded from pooling (bind-value index name; see pooling-decision.ts).

api.ts precomputes logicalSchemas for the keying.

Measured end-state on the #1325 rig (48 tenant DBs, 61k pg_class rows): 47
tenants pooled onto ONE 2048MB node — 30min authenticated zipf @50rps,
104,818/104,818 OK, p99 21ms, heap peak 195.7MB (9.6%), zero cross-tenant
bleed, fail-closed counter 0. Same-tenant SDL byte-identical (MD5) pooled vs
dedicated.

Extracted from #1325 (commits 0d1496e, 445d4c3, b3bad9e, b468b15,
f62fb69).

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