Skip to content

perf(server): prompt shutdown and parallel integration tests - #498

Merged
16bit-ykiko merged 7 commits into
mainfrom
refactor/structured-shutdown
Jul 10, 2026
Merged

16bit-ykiko merged 7 commits into
mainfrom
refactor/structured-shutdown

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Jul 9, 2026 •

Copy link
Copy Markdown
Member

Summary

Started as a shutdown-latency fix and grew into a CI-speed overhaul. CI wall clock: 30–47 min → ~12.5 min; local integration suite: ~16 min → ~40 s; editor exit no longer stalls up to 3 s.

1. Shutdown expressed as cancellation (the core fix)

Every server shutdown idled up to 3 s: WorkerPool::stop() joined monitor_memory(), which was parked on a bare 3 s kota::sleep and only checked a flag after waking. That tax applied to every editor exit and every one of the 262 integration-test teardowns (~2.8 s × 262 per CI job).

  • WorkerPool: one stop_scope cancellation source; monitor_memory() is spawned via with_token and unwinds at its sleep the moment stop() fires. The monitor and IO groups merge into one worker_tasks group (same teardown verb: join, never cancel — worker exits observed, stderr drained to EOF for crash/sanitizer reports). Dead shutting_down guards removed.
  • MasterServer: the shutdown kota::event becomes a cancellation_source; run_serve_mode bounds transport tasks with with_token(..., shutdown_token()).
  • A wedged worker ignoring SIGTERM can no longer block shutdown: a watchdog escalates to SIGKILL after a 5 s grace period (cancelled promptly on the normal path).
  • New StopIsPrompt unit test guards the regression (stop must finish well under one poll interval).

2. Parallel integration tests

  • pytest-xdist with -n auto --dist loadgroup; tests sharing a @workspace directory are pinned to one worker via an auto-assigned xdist_group mark (tryfirst hook). CDB generation moved to controller-side pytest_configure.
  • Tests default to one worker of each kind (5 → 3 processes per test) — per-test process spawn is the dominant cost on macOS Debug (~2.3 s/test dyld+codesign constant for large Debug binaries).
  • find_free_port now draws from disjoint per-xdist-worker port ranges (bind(0)'s shared pool races across workers).
  • Fixed a racy assertion xdist exposed: test_cli_status asserted status.total > 0, but total is the transient in-flight index queue that legitimately drains to zero.

3. Regression guards

Step timeouts tightened so a reintroduced stall fails CI: native integration 25→20 min, cross integration 25→15 min, smoke 15/10→5 min.

Integration-test step times (CI)

Job before after
ubuntu RelWithDebInfo 952 s 58–61 s
ubuntu Debug 1042 s 164 s
windows-2025 1004 s 104–110 s
macos-15 RelWithDebInfo 983 s 91–115 s
macos-15 Debug (ASAN) 1411 s 405 s
cross linux-aarch64 / windows-arm64 / macos-x64 — 63–193 s

All jobs ≤ ~10 min; total wall clock ~12.5 min.

Test plan

  • Unit 857 / integration 262 (multiple consecutive runs, serial and -n 4 Debug+ASAN) / smoke 3/3 — all green locally
  • Full CI green across 3 validation rounds
  • 3-agent pre-PR review (correctness / style / tests) + external review findings (CodeRabbit, Codex) addressed

WorkerPool::stop() joined monitor_memory()'s bare 3s kota::sleep, so
every shutdown (and each of the 262 integration-test teardowns) idled
for up to 3 seconds. Express shutdown as cancellation instead:

- WorkerPool: one stop_scope cancellation_source; monitor_memory is
  spawned via with_token and unwinds at its sleep. Merge the monitor
  and io groups into worker_tasks (same teardown verb: join, never
  cancel, so worker exits are observed and stderr drained to EOF).
- MasterServer: replace the shutdown kota::event with a
  cancellation_source; run_serve_mode bounds its transport tasks with
  with_token(..., shutdown_token()).
- Drop the dead shutting_down guards only reachable from the monitor.

Integration suite: 950s -> 200s. Editor exit no longer stalls 3s.
@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

MasterServer now uses cancellation tokens for serve-mode shutdown. WorkerPool unifies coroutine ownership under a cancellable task group with straggler escalation, while tests gain prompt-stop coverage and xdist-safe parallel integration infrastructure.

Changes

Cancellation-based shutdown refactor

Layer / File(s) Summary
MasterServer shutdown token contract
src/server/transport/master_server.h
Replaces the shutdown event member and accessor with shutdown_source and shutdown_token().
MasterServer serve-mode cancellation
src/server/transport/master_server.cpp
schedule_shutdown() cancels the source; pipe and socket modes use token-bound execution, with serve_peer() coordinating peer and acceptor tasks.
WorkerPool cancellation ownership
src/server/worker/worker_pool.h
Replaces the shutdown flag and separate task groups with stop_scope, unified worker_tasks, and kill_stragglers().
WorkerPool task scheduling and stop sequencing
src/server/worker/worker_pool.cpp
Worker and monitoring coroutines use worker_tasks; stopping cancels stop_scope, joins tasks, and escalates remaining workers.
Prompt stop validation and CI limits
tests/unit/server/worker_pool_tests.cpp, .github/workflows/*.yml, tests/integration/agentic/test_cli.py
Adds a sub-1500ms stop test, updates workflow timeouts, and permits an empty indexing queue in CLI status validation.
Parallel integration test harness
pixi.toml, tests/conftest.py
Adds pytest-xdist execution, controller-only CDB generation, workspace worker grouping, centralized initialization options, and per-worker port ranges.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant MasterServer
  participant run_serve_mode
  participant JsonPeer
  participant Acceptor

  Caller->>MasterServer: schedule_shutdown()
  MasterServer->>MasterServer: shutdown_source.cancel()
  run_serve_mode->>MasterServer: shutdown_token()
  run_serve_mode->>JsonPeer: run under cancellation token
  run_serve_mode->>Acceptor: accept_connections under cancellation token
  MasterServer-->>JsonPeer: cancellation
  MasterServer-->>Acceptor: cancellation
Loading
sequenceDiagram
  participant Caller
  participant WorkerPool
  participant Monitor
  participant worker_tasks

  Caller->>WorkerPool: stop()
  WorkerPool->>WorkerPool: stop_scope.cancel()
  WorkerPool-->>Monitor: cancellation
  WorkerPool->>worker_tasks: join()
  WorkerPool->>WorkerPool: kill_stragglers()
  worker_tasks-->>WorkerPool: all coroutines complete
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main themes of the PR: faster server shutdown and parallelized integration tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/structured-shutdown

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/worker/worker_pool.cpp (1)

188-195: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Add a hard-stop fallback in stop()
stop() sends only SIGTERM and then waits on worker_tasks.join(). If a worker ignores SIGTERM or wedges before exit, shutdown can block forever. Add a bounded grace period with SIGKILL escalation or timeout the join.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/worker/worker_pool.cpp` around lines 188 - 195,
`WorkerPool::stop()` only sends SIGTERM and then waits indefinitely on
`worker_tasks.join()`, so add a bounded shutdown path here. After signaling
`stateless_workers` and `stateful_workers`, wait for a short grace period, then
escalate any still-alive workers with SIGKILL before completing the join;
alternatively, make the join time out and force termination. Keep the fix
localized to `WorkerPool::stop()` and use the existing `w.alive`,
`w.proc.kill(...)`, and `worker_tasks.join()` flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/server/worker/worker_pool.cpp`:
- Around line 188-195: `WorkerPool::stop()` only sends SIGTERM and then waits
indefinitely on `worker_tasks.join()`, so add a bounded shutdown path here.
After signaling `stateless_workers` and `stateful_workers`, wait for a short
grace period, then escalate any still-alive workers with SIGKILL before
completing the join; alternatively, make the join time out and force
termination. Keep the fix localized to `WorkerPool::stop()` and use the existing
`w.alive`, `w.proc.kill(...)`, and `worker_tasks.join()` flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 044c7709-4d2a-415c-b9c5-6beda66138f8

📥 Commits

Reviewing files that changed from the base of the PR and between 87b1726 and cf7c5b9.

📒 Files selected for processing (5)
  • src/server/transport/master_server.cpp
  • src/server/transport/master_server.h
  • src/server/worker/worker_pool.cpp
  • src/server/worker/worker_pool.h
  • tests/unit/server/worker_pool_tests.cpp

Integration suites now finish in 215-970s (was 950-1400s), so lower
the step timeouts to catch a reintroduced per-test shutdown stall:
native integration 25->20min (macos Debug still needs 16min — its
residual slowness is a follow-up), cross integration 25->15min,
smoke 15/10->5min (actual ~50s).
Tests each spawn their own server, so they parallelize cleanly:
-n auto with --dist loadgroup, pinning tests that share a @workspace
directory (which they mutate) to one worker via an auto-assigned
xdist_group mark. CDB generation moves to the controller-side
pytest_configure so workers don't race writing compile_commands.json.

Also default tests to one worker of each kind (5 -> 3 processes per
test) — per-test process spawn is the dominant integration-suite cost
on macOS Debug (~2.3s/test constant, dyld + code-sign of large Debug
binaries).

Local suite: 196s serial -> 40s (-n auto), Debug+ASAN at -n 4: 127s.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/conftest.py (1)

181-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared init_options builder to avoid duplication.

The client and agentic fixtures duplicate the same init_options/project defaulting logic (cache_dir, worker counts). The client fixture includes an explanatory comment for the worker count rationale, but the agentic fixture omits it, risking divergence if one is updated without the other.

♻️ Proposed helper extraction
+def _build_init_options(request: pytest.FixtureRequest, workspace: Path) -> dict:
+    """Build initialization options with test-appropriate defaults."""
+    init_options_marker = request.node.get_closest_marker("init_options")
+    init_options = dict(init_options_marker.args[0]) if init_options_marker else {}
+    project = dict(init_options.get("project", {}))
+    # Force cache_dir into the workspace so .clice/ cleanup prevents stale PCH.
+    project.setdefault("cache_dir", str(workspace / ".clice"))
+    # One worker of each kind is enough for tests and halves the
+    # per-test process-spawn cost (5 -> 3 processes), which dominates
+    # suite time on macOS Debug. Tests needing more override via
+    # `@pytest.mark.init_options`.
+    project.setdefault("stateless_worker_count", 1)
+    project.setdefault("stateful_worker_count", 1)
+    init_options["project"] = project
+    return init_options

Then in both fixtures:

-        init_options_marker = request.node.get_closest_marker("init_options")
-        init_options = dict(init_options_marker.args[0]) if init_options_marker else {}
-        # Force cache_dir into the workspace so .clice/ cleanup prevents stale PCH.
-        project = dict(init_options.get("project", {}))
-        project.setdefault("cache_dir", str(workspace / ".clice"))
-        # One worker of each kind is enough for tests and halves the
-        # per-test process-spawn cost (5 -> 3 processes), which dominates
-        # suite time on macOS Debug. Tests needing more override via
-        # `@pytest.mark.init_options`.
-        project.setdefault("stateless_worker_count", 1)
-        project.setdefault("stateful_worker_count", 1)
-        init_options["project"] = project
+        init_options = _build_init_options(request, workspace)
         await c.initialize(workspace, initialization_options=init_options)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/conftest.py` around lines 181 - 182, Extract the duplicated
init_options and project-default setup from the client and agentic fixtures into
a shared helper in conftest.py, preserving the client fixture’s explanatory
worker-count comment and current defaults for cache_dir, stateless_worker_count,
and stateful_worker_count. Update both fixtures to call the helper so future
changes remain centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/conftest.py`:
- Around line 181-182: Extract the duplicated init_options and project-default
setup from the client and agentic fixtures into a shared helper in conftest.py,
preserving the client fixture’s explanatory worker-count comment and current
defaults for cache_dir, stateless_worker_count, and stateful_worker_count.
Update both fixtures to call the helper so future changes remain centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8b8d7e1f-5bbe-4cf0-8014-6747dff568f7

📥 Commits

Reviewing files that changed from the base of the PR and between 58673a6 and b8dfc18.

⛔ Files ignored due to path filters (1)
  • pixi.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • pixi.toml
  • tests/conftest.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8dfc18a89

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pixi.toml
status.total reports the in-flight round queue, which drains to zero
when indexing completes — and the indexed_server fixture waits for
exactly that. The 'total > 0' assertion only passed by racing into
the middle of a round; on slow runners (macOS Debug + ASAN) the race
is always lost.
stop() joins worker exit observers and pipe drains; a wedged worker
that ignores SIGTERM would block that join forever. A watchdog task
SIGKILLs still-alive workers after a 5s grace period; the normal path
cancels it right after the join completes.
find_free_port drew from the kernel's shared bind(0) pool, so two
concurrent xdist workers could grab the same port in the close-then-
rebind gap; carve disjoint per-worker ranges instead. Also fold the
duplicated client/agentic init_options defaulting into one helper.
@16bit-ykiko

Copy link
Copy Markdown
Member Author

Addressed all three review findings:

  • CodeRabbit (SIGKILL escalation): valid — stop() could block forever on a worker that ignores SIGTERM (pre-existing, but worth fixing here). Added a watchdog task that SIGKILLs still-alive workers after a 5s grace period; the normal path cancels it immediately after the join (ea66346).
  • Codex (find_free_port race under xdist): valid TOCTOU — replaced bind(0)'s shared kernel pool with disjoint per-xdist-worker port ranges via PYTEST_XDIST_WORKER, no caller changes (fc4796d).
  • CodeRabbit (init_options duplication): extracted a shared build_init_options helper used by both fixtures (fc4796d).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/conftest.py`:
- Around line 107-122: Ensure build_init_options always enforces a
workspace-scoped cache directory by replacing the project cache_dir setdefault
with an unconditional assignment to workspace / ".clice"; retain or update the
comment to match this enforced behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b00246ec-2e2d-4e14-980b-e2dbb0744732

📥 Commits

Reviewing files that changed from the base of the PR and between 35c27e7 and fc4796d.

📒 Files selected for processing (3)
  • src/server/worker/worker_pool.cpp
  • src/server/worker/worker_pool.h
  • tests/conftest.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/server/worker/worker_pool.h
  • src/server/worker/worker_pool.cpp

Comment thread tests/conftest.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc4796d0da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/transport/master_server.cpp
Comment thread src/server/worker/worker_pool.cpp
@16bit-ykiko 16bit-ykiko changed the title fix(server): shutdown without waiting out monitor poll sleep perf(server): prompt shutdown and parallel integration tests Jul 10, 2026
setdefault let a test-supplied cache_dir escape the workspace and
survive the .clice cleanup; every existing override passes the same
workspace path anyway, so assign unconditionally to match the comment.
@16bit-ykiko
16bit-ykiko merged commit 2bafe72 into main Jul 10, 2026
21 checks passed
@16bit-ykiko
16bit-ykiko deleted the refactor/structured-shutdown branch July 10, 2026 04:22
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