Bound provider calls, cache quota policies, and unblock queue consumers - #237
Merged
Merged
Conversation
Five load-path fixes from a capacity review of the console and its enterprise overlay. Each one addressed a place where the system had no ceiling under load rather than a wrong one. - Provider calls now honour GATEWAY_REQUEST_TIMEOUT_MS. The setting was defined and documented but never read, and Fastify's own request timeouts are unset, so a stalled provider held its socket, its request context and its stream indefinitely. withResilience applies the budget per attempt and hands the operation an AbortSignal, so a timeout aborts the upstream call instead of abandoning it; the chat paths pass that signal to LangChain. - Quota limit resolution caches the tenant license and policy set for QUOTA_POLICY_CACHE_TTL_SECONDS (default 30). An inference request resolved limits three times before reaching the model, each one an uncached tenant read plus a policy read, against a ten-connection pool. Only the inputs are cached: scope and domain filtering still runs per request, so one caller can never receive another's limits. Policy writes invalidate the entry. - Evicted provider runtimes now release their connections. Eviction was a bare map delete, which dropped the reference while the pg pool or Elasticsearch client kept every socket open — an unbounded FD leak across tenant/provider combinations. Runtimes opt in with close(); the close is deferred by PROVIDER_RUNTIME_DISPOSE_GRACE_SECONDS so a request still holding the runtime is never cut off, and is immediate at shutdown. - Agent, MCP, browser and crawler consumers take a concurrency setting. All four were fixed at one, so a cross-node job head-of-line blocked every job behind it until the queue's 60s invoke timeout failed the ones still waiting. Crawler defaults lower than the rest because each job already fans out to as many as sixteen fetches. - Knowledge Engine ingest can run off the request path. Chunking, embedding and upserting a document ran inside the HTTP call with no timeout to bound it. Clients opt in with `async: true` and get 202 with a pending document, which a queue consumer indexes. The source is committed before the job is published, and a boot sweep re-publishes pending documents, so a restart on the memory queue driver costs the indexing rather than the content. The default stays synchronous: the 201-with-indexed-document contract is what existing clients read chunkCount from and query immediately afterwards. File conversion still runs in-request on the file ingest path; only chunking and embedding are deferred. Adds 28 tests covering the timeout, the cache (including that filtering stays per-caller on a hit), deferred disposal, and both halves of the split ingest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ltao83ZEHPKrpAQZv3n5z1
8 tasks
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
Five load-path fixes from a capacity review of the console and its enterprise overlay. Each one addresses a place where the system had no ceiling under load rather than a wrong one: a provider call that could hang forever, a hot path that re-read the same two documents on every request, connections that were dropped without being closed, queue consumers pinned at one job, and document ingestion that occupied an HTTP worker for its whole run.
Behaviour is unchanged for existing clients. The one API-visible addition — deferred ingest — is opt-in, because the current
201-with-indexed-document contract is what callers readchunkCountfrom and query immediately afterwards.Changes
Provider calls honour
GATEWAY_REQUEST_TIMEOUT_MS. The setting was defined and documented but never read, and Fastify's ownrequestTimeout/connectionTimeoutare unset — so a stalled provider held its socket, its request context and (when streaming) its generation indefinitely.withResiliencenow applies the budget per attempt and hands the operation anAbortSignal, so a timeout aborts the upstream call rather than abandoning it. The chat paths pass that signal into LangChain; the streaming path chains it into the existing client-disconnect controller.timeoutMs: 0opts out. All 29withResiliencecall sites inherit this.Quota resolution caches the tenant license + policy set (
QUOTA_POLICY_CACHE_TTL_SECONDS, default 30,0disables)./client/v1/chat/completionsresolves limits three times before the model is reached, each one an uncachedfindTenantByIdpluslistQuotaPolicies, against a ten-connection pool. Only the inputs are cached — scope/domain filtering still runs per request, so a cache hit can never hand one caller another's limits (covered by a test). Policy create/update/delete invalidates the entry; the TTL is the backstop.Evicted provider runtimes release their connections. Eviction was a bare
pool.delete(), which dropped the reference while the pg pool or Elasticsearch client kept every socket open — an unbounded FD leak across tenant×provider combinations. Runtimes opt in structurally viaclose()(implemented for pgvector and the three Elasticsearch drivers). The close is deferred byPROVIDER_RUNTIME_DISPOSE_GRACE_SECONDS(default 300) so a request still holding the runtime is never cut off, and runs immediately on shutdown.Agent / MCP / browser / crawler consumers take a concurrency setting. All four were fixed at
1, so a cross-node job head-of-line blocked everything behind it until the queue's 60sinvoketimeout failed the ones still waiting. Defaults: agent 4, MCP 4, browser 4, crawler 2 — crawler lower because a single job already fans out to as many as sixteen fetches and may drive a Chromium instance.Knowledge Engine ingest can run off the request path. Chunking, embedding and vector upsert ran inside the HTTP call with no timeout to bound them. Clients opt in with
async: trueand get202with apendingdocument that a queue consumer indexes. The source is committed before the job is published, and a boot sweep re-publishespendingdocuments, so a restart on the memory queue driver costs the indexing, never the content.indexPendingRagDocumentreturns early for an already-indexed document, so a duplicate delivery cannot double-embed.Scope note: on the file-ingest path, file→text conversion still runs in-request — the bytes have to become text before the document can be persisted. Only chunking and embedding are deferred. Worth a follow-up.
New settings are documented in
.env.example; theasyncflag and the202response are inopenapi.yaml.Validation
npm run lint— 0 errors (164 pre-existing warnings)npm run test— 3663 passed, 0 failures. 5 test files fail to start becausemongodb-memory-servercannot download its binary in this sandbox (403/502 fromfastdl.mongodb.org); confirmed identical on a clean checkout ofmain.npm run buildnpm run docs:buildnpm run test:endpoints— 217 uncovered / 137 new, byte-identical tomain; this change adds no uncovered routes.Adds 28 tests: the timeout (firing, abort propagation, per-attempt budget, opt-out), the policy cache (including that filtering stays per-caller on a hit), deferred disposal (grace period, prefix invalidate, throwing
close()), and both halves of the split ingest.Release Notes
.env.example,openapi.yamlGenerated by Claude Code