Skip to content

feat(server): end-to-end request cancellation - #511

Merged
16bit-ykiko merged 12 commits into
mainfrom
feat/e2e-cancellation
Jul 16, 2026
Merged

feat(server): end-to-end request cancellation#511
16bit-ykiko merged 12 commits into
mainfrom
feat/e2e-cancellation

Conversation

@16bit-ykiko

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

Copy link
Copy Markdown
Member

Background

clice's compilation is pull-based: feature requests pull the document's AST via a shared compile round, compile rounds pull shared PCH/PCM artifacts. Until now, cancellation stopped at the master's front door — kotatsu's peer layer tore down the handler frame and replied RequestCancelled, but the work already dispatched to worker processes always ran to completion. The expensive case is completion: editors cancel the in-flight completion on every keystroke, each one is a full stateless parse of the buffer snapshot, and — because those builds are keyed to a text snapshot rather than the document generation — the edit/supersede machinery never cancels them. The client's $/cancelRequest is the only death signal that path has. Zombie parses serialized on the stateless pool and the completion the user actually wanted queued behind them.

This PR makes cancellation reach the clang parse itself, on both paths, without weakening the crash-containment invariants from #502.

What changed

  1. Client cancel reaches the worker. The request's cancellation token threads from the transport handlers through FeatureRouter (12 methods) into the compiler forwards and is passed as request_options.token on the request's own worker sends. When the client cancels, the send resumes with RequestCancelled and emits a wire cancel; the worker-side handler cancellation flips the compile stop flag (CompilationParams::stop, polled after every top-level declaration), and the parse dies at the next declaration instead of running to completion.
  2. Supersede interrupts via an explicit CancelCompile notification. When an edit makes an in-flight compile stale — detected at the supersede point, or immediately on didChange (abandon_superseded, which also cancels the stale round's module-dependency waits since no replacement round follows) — the master notifies the worker, which sets the published stop flag for that document. The request is deliberately not wire-cancelled: it runs to a normal (incomplete) reply that the master discards at its generation gate.
  3. Include completion honors queued cancels. The synchronous include-path scan now yields once before reading any buffer state: a piped $/cancelRequest tears the frame before the directory walk starts, and a piped edit lands before the completion context is computed, so the scan serves one consistent snapshot.

Invariants

  • The shared compile is never client-cancelled. A request's token reaches only that request's own sends. The detached compile round serves every current and future puller; only buffer-identity change (supersede) cancels it. Same one level down: PCH/PCM builds are shared artifacts — client tokens never reach them, and a round's deps_scope releases only that round's interest in the module graph.
  • Crash accounting cannot be dodged. Supersede does not wire-cancel the compile request: kotatsu's with_token fuses resume-with-cancel, so a wire cancel racing a worker death would return RequestCancelled and the death would never reach the document's quarantine ledger. The notification interrupts the parse while the master still observes the request's real outcome — including a crash.
  • The notification can only hit the stale round. The master emits CancelCompile before the replacement compile can enter the pipe (spawn is eager, so this ordering is load-bearing), and the worker publishes each round's stop flag on request arrival, before its strand wait. Pipe FIFO makes a mistargeted cancel impossible; a cancel landing after the round finished sets a dead flag — a no-op.

Tests

Unit (7 new): CancelChain.HandlerCancelChainsThrough (the with_token resumption boundary emits the wire cancel that interrupts a 200k-declaration parse), StatefulWorker.CancelNotificationInterruptsCompile (notification path, pinned by reply content — an interrupted parse reports no deps and no index — not wall clock), CompilerGuards.ClientCancelSparesCompile (cancelling one waiter must not kill the other waiter's result), CompilerGuards.SupersededCompileCancelled / EditInterruptsStaleCompile (supersede and edit-path liveness), CompilerGuards.AbandonCancelsDepsScope (the two supersede entry points' deps_scope contract), plus tightened RequestCancelled-specific assertions.

Integration (4, real client over LSP with a 200k-declaration file blocking compilation): cancelled completion and signature help at end-of-file positions (clang truncates the parse at the completion point, so end-of-file forces the full parse — the cancel provably lands mid-parse), all ten forwarded features cancelled while the document compile is in flight with a closing hover proving the shared compile survived every cancel, and an edit-mid-compile test proving the superseded request unblocks promptly and the next request answers on the new content.

Known limits (documented, deliberate)

  • A shared PCH build is not cancellable: a round superseded during its PCH build waits for the build to reply. Detaching PCH builds with interest counting is the follow-up.
  • The stop flag polls per top-level declaration: a single giant declaration or end-of-TU template instantiation cannot be interrupted mid-flight.
  • clang-format exposes no cancellation hook: a format already executing runs to completion (queued formats are dequeued by the cancel).
  • Client-cancel racing a worker death on a query send can still skip per-kind crash attribution — pre-existing, bounded by pool-level slot containment and by the next un-cancelled request attributing normally; the precise fix (death-site attribution) belongs to the event-response follow-up.

Summary by CodeRabbit

  • New Features
    • Added request-scoped cancellation propagation to language feature requests (e.g., completion, hover, definitions, document links/symbols, formatting, and code actions).
  • Bug Fixes
    • Improved superseded-compile behavior by interrupting stale in-flight worker work more reliably and aligning dependency wait cancellation with the supersede/interrupt lifecycle.
    • When a document changes, any in-flight work for that session is abandoned sooner.
  • Tests
    • Expanded integration and unit tests to verify LSP cancellation handling, supersede/interrupt correctness, and continued server responsiveness after cancelled/edited requests.

@coderabbitai

coderabbitai Bot commented Jul 16, 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

The change propagates LSP request cancellation through feature routing and compiler worker dispatch, interrupts superseded compilations, preserves cancellation across stateless retries, removes dependency bookkeeping, and adds unit and integration coverage.

Changes

Cancellation and compile supersession

Layer / File(s) Summary
Worker compile cancellation contract
src/server/protocol/worker.h, src/server/worker/stateful_worker.cpp, tests/unit/server/stateful_worker_tests.cpp, tests/unit/server/cancel_chain_tests.cpp
Adds the cancelCompile notification, pre-published worker stop flags, and coverage for interrupting active or queued compilation.
Compile supersession lifecycle
src/server/compiler/*, src/server/state/session.h, tests/unit/server/compiler_tests.cpp, tests/integration/features/test_cancellation.py
Dependency preparation uses cancellation scopes, stale generations interrupt worker compilation, and superseded waiter behavior is tested.
Request cancellation propagation
src/server/transport/lsp_client.cpp, src/server/service/feature_router.*, src/server/compiler/*
LSP cancellation tokens flow through feature handlers, compiler forwarding APIs, stateful requests, and retryable stateless builds.

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

Sequence Diagram(s)

sequenceDiagram
  participant LSPClient
  participant FeatureRouter
  participant Compiler
  participant StatefulWorker
  LSPClient->>FeatureRouter: feature request with cancellation
  FeatureRouter->>Compiler: forward feature with token
  Compiler->>StatefulWorker: dispatch request with request_options
  LSPClient-->>FeatureRouter: cancel request
  StatefulWorker-->>Compiler: cancelled response
Loading

Possibly related PRs

  • clice-io/clice#454: Related compiler dependency-preparation cancellation and per-round cancellation behavior.
  • clice-io/clice#490: Overlapping compiler lifecycle changes around dependency preparation and superseded compilation.
  • clice-io/clice#509: Related cancellation-safe compiler and stateful-worker handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.00% 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 main change: end-to-end request cancellation in the server.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-cancellation

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.

@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: 5bad4a7c52

ℹ️ 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/compiler/compiler.cpp Outdated
Comment thread src/server/compiler/compiler.cpp

@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/unit/server/compiler_tests.cpp`:
- Around line 485-494: Update the cancellation assertions to verify the explicit
RequestCancelled result rather than merely completion or failure: in
tests/unit/server/compiler_tests.cpp lines 485-494, retain the kota::with_token
result in the cancelled_waiter flow and assert it is RequestCancelled; in
tests/unit/server/cancel_chain_tests.cpp lines 53-59 and 87-91, retain each
worker reply/error and require RequestCancelled instead of checking only
!result.has_value() or boolean flags.
🪄 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: 9bee3daa-35c8-4ab9-9f29-c44156c56ed5

📥 Commits

Reviewing files that changed from the base of the PR and between 1950fa2 and 5bad4a7.

📒 Files selected for processing (12)
  • src/server/compiler/compiler.cpp
  • src/server/compiler/compiler.h
  • src/server/protocol/worker.h
  • src/server/service/feature_router.cpp
  • src/server/service/feature_router.h
  • src/server/state/session.h
  • src/server/transport/lsp_client.cpp
  • src/server/worker/stateful_worker.cpp
  • tests/integration/features/test_cancellation.py
  • tests/unit/server/cancel_chain_tests.cpp
  • tests/unit/server/compiler_tests.cpp
  • tests/unit/server/stateful_worker_tests.cpp

Comment thread tests/unit/server/compiler_tests.cpp Outdated
codex review: the CancelCompile notification only fired when a later
ensure_compiled observed the stale round; an edit with no follow-up
request left the stale parse running and its waiters blocked. Single
emission point Compiler::interrupt_superseded, called from didChange
and the supersede point.

@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/unit/server/compiler_tests.cpp (1)

418-427: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Make supersession interruption observable rather than time-based.

Both tests allow 60 seconds and only assert eventual completion. If interrupt_superseded or the supersede notification becomes a no-op, the large parse can finish naturally and these tests still pass. Add deterministic notification/interruption instrumentation or a controlled blocked worker so completion requires CancelCompile.

Also applies to: 508-517

🤖 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/unit/server/compiler_tests.cpp` around lines 418 - 427, Update both
supersession tests around waiter_done and waiter_ok to remove the time-based
60-second polling and use deterministic instrumentation or a controlled blocked
worker. Ensure the worker cannot complete naturally, assert that supersede
notification/interruption occurs, and require CancelCompile to release it before
joining and validating completion.
🤖 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/unit/server/compiler_tests.cpp`:
- Around line 418-427: Update both supersession tests around waiter_done and
waiter_ok to remove the time-based 60-second polling and use deterministic
instrumentation or a controlled blocked worker. Ensure the worker cannot
complete naturally, assert that supersede notification/interruption occurs, and
require CancelCompile to release it before joining and validating completion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 771e7891-8e3e-4137-add2-ed45eab586d1

📥 Commits

Reviewing files that changed from the base of the PR and between 5bad4a7 and 17326b3.

📒 Files selected for processing (5)
  • src/server/compiler/compiler.cpp
  • src/server/compiler/compiler.h
  • src/server/transport/lsp_client.cpp
  • tests/unit/server/cancel_chain_tests.cpp
  • tests/unit/server/compiler_tests.cpp
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/unit/server/cancel_chain_tests.cpp
  • src/server/transport/lsp_client.cpp
  • src/server/compiler/compiler.cpp

@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: 17326b3ae7

ℹ️ 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/lsp_client.cpp Outdated
Comment thread src/server/compiler/compiler.cpp
Comment thread src/server/service/feature_router.cpp
codex round 2: an edit landing while the round is in dependency prep
only sent CancelCompile (a worker no-op pre-dispatch); the module-graph
waits kept the waiters blocked. abandon_superseded = interrupt + deps
scope cancel, for the edit path where no replacement round follows.
Also yield before the synchronous include-completion scan so a piped
$/cancelRequest tears the frame before the directory walk.

@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/unit/server/compiler_tests.cpp (1)

533-535: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider setting ast_dirty for consistency.

To fully simulate the state changes of a didChange event and maintain consistency with EditInterruptsStaleCompile, consider explicitly setting session->ast_dirty = true; here as well, even if it happens to still be true from the initial session state.

💡 Proposed change
         // The edit lands while the slow compile is in flight.
         session->text = "int fixed;\n";
         session->generation += 1;
+        session->ast_dirty = true;
🤖 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/unit/server/compiler_tests.cpp` around lines 533 - 535, Update the test
session state setup near the text and generation assignments to also set
session->ast_dirty = true, ensuring it fully simulates a didChange event and
remains consistent with EditInterruptsStaleCompile.
🤖 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/unit/server/compiler_tests.cpp`:
- Around line 533-535: Update the test session state setup near the text and
generation assignments to also set session->ast_dirty = true, ensuring it fully
simulates a didChange event and remains consistent with
EditInterruptsStaleCompile.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f4474b7e-b294-4163-9f54-2a58fa0dadc9

📥 Commits

Reviewing files that changed from the base of the PR and between 17326b3 and 913273b.

📒 Files selected for processing (5)
  • src/server/compiler/compiler.cpp
  • src/server/compiler/compiler.h
  • src/server/service/feature_router.cpp
  • src/server/transport/lsp_client.cpp
  • tests/unit/server/compiler_tests.cpp
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/server/transport/lsp_client.cpp
  • src/server/service/feature_router.cpp
  • src/server/compiler/compiler.cpp

@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: 913273b3a7

ℹ️ 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/service/feature_router.cpp Outdated
codex round 3: the yield left pctx/offset computed from the pre-edit
buffer; a didChange landing during the suspension made the include scan
serve candidates and TextEdit ranges for text that no longer exists.
Move the suspension before every buffer read: the synchronous remainder
serves one consistent snapshot, and every completion path gains the
early-cancel window.

@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/integration/features/test_cancellation.py (1)

110-172: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cumulative sequential cancellation delay risks flakiness as the shared compile finishes.

Each of the 10 requests is dispatched, slept on for 0.1s, then cancelled — sequentially, one after another (Line 165-166). By the time later iterations run, ~1s+ has elapsed since the shared compile started. If clang finishes parsing the 200k-decl file within that cumulative window (e.g. a fast release build or lightly-loaded CI runner), the tail requests would get real answers instead of RequestCancelled, since a completed shared compile answers instantly from the cached AST and is never itself client-cancelled per this PR's stated contract. This directly threatens the test's premise ("compile is still churning through the slow body") for the later entries in the loop.

Consider dispatching all 10 requests concurrently and cancelling them together shortly after, so every request's exposure window starts near the beginning of the compile rather than accumulating with loop position.

♻️ Suggested refactor to reduce cumulative timing risk
-        for method, params in requests:
-            await cancel_and_expect(client, method, params)
+        tasks = []
+        ids = []
+        for method, params in requests:
+            msg_id = str(uuid.uuid4())
+            ids.append(msg_id)
+            tasks.append(
+                asyncio.ensure_future(
+                    client.protocol.send_request_async(method, params, msg_id=msg_id)
+                )
+            )
+        await asyncio.sleep(0.1)
+        for msg_id in ids:
+            client.protocol.notify("$/cancelRequest", CancelParams(id=msg_id))
+        for task in tasks:
+            with pytest.raises(Exception) as exc:
+                await asyncio.wait_for(task, timeout=30)
+            assert getattr(exc.value, "code", None) == -32800
🤖 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/integration/features/test_cancellation.py` around lines 110 - 172,
Update test_cancelled_requests_while_compiling to dispatch all requests
concurrently and cancel them together after the initial delay, rather than
awaiting cancel_and_expect sequentially in the loop. Preserve the assertion that
every request returns RequestCancelled while the shared compile remains active,
then keep the final hover assertion verifying the completed AST is usable.
🤖 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/integration/features/test_cancellation.py`:
- Around line 110-172: Update test_cancelled_requests_while_compiling to
dispatch all requests concurrently and cancel them together after the initial
delay, rather than awaiting cancel_and_expect sequentially in the loop. Preserve
the assertion that every request returns RequestCancelled while the shared
compile remains active, then keep the final hover assertion verifying the
completed AST is usable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8bb50a66-6d53-41f4-848d-05cfbae5ec04

📥 Commits

Reviewing files that changed from the base of the PR and between daace12 and ee40a2b.

📒 Files selected for processing (1)
  • tests/integration/features/test_cancellation.py

macOS arm64 finished the shared 200k parse mid-sweep, so the seventh
cancel found a ready AST and got a normal reply. Each pulling request
now edits first and launches its own parse: the cancel window is per
round, not cumulative.
@16bit-ykiko
16bit-ykiko merged commit e308cd6 into main Jul 16, 2026
22 checks passed
@16bit-ykiko
16bit-ykiko deleted the feat/e2e-cancellation branch July 17, 2026 12:55
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