Skip to content

perf(query): retire excess workers after idle bursts - #2862

Closed
doomsday1906 wants to merge 1 commit into
colbymchenry:mainfrom
doomsday1906:upstream/query-worker-idle-W9cb1df
Closed

doomsday1906 wants to merge 1 commit into
colbymchenry:mainfrom
doomsday1906:upstream/query-worker-idle-W9cb1df

Conversation

@doomsday1906

@doomsday1906 doomsday1906 commented Oct 23, 2026 •

Copy link
Copy Markdown

A session grows its query pool for concurrent reads but retains every worker's heap and SQLite connection until shutdown. After a burst, an otherwise idle MCP server therefore keeps paying for the peak worker count.

Retire excess workers after 60 seconds idle, retain one warm worker, and regrow through the existing dispatch path. Active/starting workers are preserved, intentional retirement does not consume the crash budget, and shutdown awaits retirements already underway. No new public setting or dependency.

Validation on Linux/WSL, based on main 44adef8: all 28 query-pool tests passed, including real compiled workers, multi-project dispatch, failed-open recovery and teardown. The three added regressions cover repeated shrink/regrow, busy/starting workers, and shutdown during retirement. An installed MCP comparison on the maintained fork previously dropped idle RSS from about 673 MiB to 426 MiB while preserving source queries.

Windows x64 and macOS arm64 native builds and focused regressions also passed (39 tests on each platform, including the native scaffold). Cross-platform validation checks the exact PR head commit. This PR excludes the maintained-fork installer, runtime identity and workstation configuration.

@doomsday1906
doomsday1906 marked this pull request as ready for review October 23, 2026 21:04
@doomsday1906 doomsday1906 changed the title Release excess query workers after idle bursts perf(query): retire excess workers after idle bursts Oct 23, 2026
colbymchenry pushed a commit that referenced this pull request Oct 31, 2026
…en back

A session grows its query pool for concurrent reads, and every worker kept its
graph caches, SQLite pages and heap until shutdown, so a server that had served
one burst held that memory for the rest of its life.

After five minutes with no work the pool now retires every worker that has been
used and starts one fresh one, which is ready for the next call. A retired
worker is asked to close: it closes its projects, runs a full collection and
exits by itself, the path worker-teardown.ts measured as safe on Windows. Only
one that hasn't exited after 5 seconds is terminated, through the pool's own
start-aware path. Calls that arrive while the fresh worker starts are served in
process, the same way as before the pool's first worker is up.

Changed on top of the PR:
- The idle delay is five minutes, not the PR's 60 seconds, so main doesn't get
  slower than 1.6.2. Agents routinely pause over a minute to edit or build, and
  the first explore on a fresh worker takes about 200 ms more. The memory still
  comes back when a session really goes quiet.
- The recycle also arms when a burst's last worker reports ready after the
  last result. Before, that pool stayed at full size until the next call.
- A warm worker that has served nothing is never recycled, so that readiness
  arming can't replace a clean worker over and over.
- destroy() waits for retirements underway, so a worker is not left to end with
  the process. PR #2862 had this; the PR here did not.

PR #2862 by @doomsday1906 made the same idle-worker change and is closed in
favor of this one. On its own fork it measured idle RSS going from about
673 MiB to 426 MiB.

Validation:
- Red on main: six pool tests fail, including a real worker that retires with
  exit code 0 and is never terminated. Each added case was also proven red by
  undoing its line.
- Windows 11 x64, on a busy machine: 480 child processes, each with four real
  workers on an excalidraw index and retiring right after a burst, all exited 0
  (1,920 worker exits). The AGENTS.md crash did not reproduce here even with an
  owner terminate in place of the close (0 of 480), so this shows the path
  works, not that the crash can't happen.
- excalidraw, four workers after a burst of explores: 611 MB RSS idle on main,
  113 MB here. The first explore after a recycle took 299 ms against 93 ms on a
  warm worker, and the second was back to 92 ms.

Original PR #1583 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry pushed a commit that referenced this pull request Oct 31, 2026
…en back

A session grows its query pool for concurrent reads, and every worker kept its
graph caches, SQLite pages and heap until shutdown, so a server that had served
one burst held that memory for the rest of its life.

After five minutes with no work the pool now retires every worker that has been
used and starts one fresh one, which is ready for the next call. A retired
worker is asked to close: it closes its projects, runs a full collection and
exits by itself, the path worker-teardown.ts measured as safe on Windows. Only
one that hasn't exited after 5 seconds is terminated, through the pool's own
start-aware path. Calls that arrive while the fresh worker starts are served in
process, the same way as before the pool's first worker is up.

Changed on top of the PR:
- The idle delay is five minutes, not the PR's 60 seconds, so main doesn't get
  slower than 1.6.2. Agents routinely pause over a minute to edit or build, and
  the first explore on a fresh worker takes about 200 ms more. The memory still
  comes back when a session really goes quiet.
- The recycle also arms when a burst's last worker reports ready after the
  last result. Before, that pool stayed at full size until the next call.
- A warm worker that has served nothing is never recycled, so that readiness
  arming can't replace a clean worker over and over.
- destroy() waits for retirements underway, so a worker is not left to end with
  the process. PR #2862 had this; the PR here did not.

PR #2862 by @doomsday1906 made the same idle-worker change and is closed in
favor of this one. On its own fork it measured idle RSS going from about
673 MiB to 426 MiB.

Validation:
- Red on main: six pool tests fail, including a real worker that retires with
  exit code 0 and is never terminated. Each added case was also proven red by
  undoing its line.
- Windows 11 x64, on a busy machine: 480 child processes, each with four real
  workers on an excalidraw index and retiring right after a burst, all exited 0
  (1,920 worker exits). The AGENTS.md crash did not reproduce here even with an
  owner terminate in place of the close (0 of 480), so this shows the path
  works, not that the crash can't happen.
- excalidraw, four workers after a burst of explores: 611 MB RSS idle on main,
  113 MB here. The first explore after a recycle took 299 ms against 93 ms on a
  warm worker, and the second was back to 92 ms.

Original PR #1583 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry

Copy link
Copy Markdown
Owner

Thanks for retiring excess query workers after idle bursts, @doomsday1906! The same idle-worker change has landed as #3126, from the worker commit in #1583, which came first. Your measured gain, idle RSS down from about 673 MiB to 426 MiB, is cited in that commit, destroy() waiting for workers on their way out comes from your PR, and the changelog credits you. Closing this one.

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.

2 participants