feat(server,graphile): opt-in blueprint pooling integration — GRAPHILE_BLUEPRINT_POOLING (5/6)#1334
Open
yyyyaaa wants to merge 1 commit into
Open
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)svc_key; pooled services share abp:-keyed instance built fully qualified against the canonical tenant.pgSettingsvia the transaction-local GUCconstructive.pool_schemas, where the rewriting pool maps canonical→tenant identifiers.(preset).schema.constructivePooledmarks 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 invalidationA
schema:updateflush 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/flushroute still clears allbp:entries.Plugin pooling-compat (inert while the flag is off)
constructivePooledis set.databaseId.Measured end-state (from #1325, production-shaped rig: 48 tenant DBs, 61k
pg_classrows)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/server205/205 ·graphile-settings158/158 ·graphile-presigned-url-plugin25/25 (incl. new two-tier matching suite) ·graphile-search78/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