Skip to content

fix: bound raw MCP query worker lifecycle - #256

Closed
lzehrung wants to merge 3 commits into
fix/mcp-session-safety-basefrom
fix/mcp-stream-and-sqlite-deadline
Closed

fix: bound raw MCP query worker lifecycle#256
lzehrung wants to merge 3 commits into
fix/mcp-session-safety-basefrom
fix/mcp-stream-and-sqlite-deadline

Conversation

@lzehrung

Copy link
Copy Markdown
Owner

Summary

  • Package raw SQLite workers with the core distribution.
  • Propagate request cancellation through raw query workers and impact streams.
  • Bound concurrent and cleaning-up workers with observable lifecycle cleanup.

Verification

  • Focused MCP lifecycle, cancellation, bundle, package-surface, and worker-path regressions passed.

Impact streaming's internal chunk queue buffered every produced item in
an unbounded array whenever the consumer was not actively reading, and
the background analyzeImpact() producer kept running to completion even
after a consumer abandoned the stream (broke out of iteration, or the
generator was otherwise returned early), retaining index snapshots,
sets, and closures for work nobody would ever read.

Add an internal AbortController to analyzeImpactStreaming(); the
generator's finally block aborts it on every exit path, including the
async-generator return protocol triggered by early consumer
cancellation. The onImpactItem producer callback checks the signal and
throws once it fires, unwinding analyzeImpact's in-progress work so no
further symbol batches or transitive passes start. This needs no public
API change: yield* delegation already forwards a caller's early return
into the inner generator, so session.ts's analyzeImpactStream benefits
without any code change there.

Cap the internal queue at a bounded number of unread chunks
(DEFAULT_MAX_IMPACT_STREAM_QUEUED_CHUNKS, overridable via the internal
ImpactStreamingContext.maxQueuedChunks test seam). True backpressure
would require making the onImpactItem emission callback awaitable at
every synchronous call site in direct.ts/transitive.ts, which is outside
this module. On overflow the stream surfaces an explicit
ImpactStreamOverflowError as a terminal error chunk instead of silently
dropping items or completing as if nothing were missed.
Raw query_sqlite reads ran a synchronous DatabaseSync iteration with
row and byte caps but no time budget or cancellation: a non-recursive
but expensive statement (a large join, ORDER BY random() with no
matching index, ...) could hold the host event loop for as long as
SQLite took to produce a row, and a client disconnect did not stop it.

Run the query in a dedicated Piscina worker thread (same pattern as the
existing query-index worker pool) with a hard deadline. On expiry the
worker thread is force-terminated and the call rejects with
SqliteQueryDeadlineExceededError immediately; the host event loop is
never blocked regardless of how long the underlying query actually
runs, and a subsequent query against the same file succeeds right away
since concurrent read-only SQLite connections do not block each other.
Pool teardown is fire-and-forget on deadline expiry rather than
awaited, so the caller is never delayed by an orphaned worker thread
still finishing a single already-in-flight synchronous native call
(worker termination cannot preempt one in-progress call the same way it
can prevent further JS from running) -- documented in
rawQueryWorkerPool.ts, verified directly against a 200M-row recursive
CTE.

If the compiled worker asset cannot be located (a corrupted or partial
install), the query falls back to running in-process under a
per-row elapsed-time budget instead of refusing outright. That fallback
is strictly weaker and is documented as such: the budget is only
checked between rows the native iterator has already produced, so a
statement that is slow to produce its very first row is not bounded by
it.

Preserves the existing row/byte cap contracts and
normalizeSqliteRowLimit reuse in this file.
@lzehrung

Copy link
Copy Markdown
Owner Author

Superseded by #264, which consolidates the MCP session, transport, SQLite, and streaming fixes.

@lzehrung lzehrung closed this Aug 16, 2026
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