Skip to content

fix(server): eviction recovery and conditional dirty-flag clearing - #490

Merged
16bit-ykiko merged 4 commits into
mainfrom
feat/invalidation-epoch-eviction
Jul 7, 2026
Merged

16bit-ykiko merged 4 commits into
mainfrom
feat/invalidation-epoch-eviction

Conversation

@16bit-ykiko

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

Copy link
Copy Markdown
Member

Motivation

Three related state-consistency holes around the same question — when may a compile result claim freshness?

  1. Evicted documents silently lose features. A stateful worker caps its compiled documents (LRU, 16) and notifies the master on eviction, but the master only dropped the owner-table entry. The session stayed "clean", so hover/semantic-tokens re-routed to a worker that no longer held the AST and silently returned null until the next edit. Deterministic: just open more than 16 files.
  2. In-flight compiles could swallow invalidation. When a dependency changed while a compile was in flight (header saved, worker crashed, document evicted), the compile's landing unconditionally cleared the dirty flag — the recompile the invalidation demanded never happened, and the stale AST could then self-certify as fresh. The prior mitigation bumped generation on some of these paths, which conflated "the buffer changed" with "the world got dirtier" and made still-valid in-flight results get discarded.
  3. Superseded compile continuations could write back a stale PCH reference after their suspension points, clobbering what a newer round or a context switch had just established.

Changes

  • New per-session dirty_epoch token. Every AST-invalidating dispatch effect bumps it; a compile snapshots it at takeoff and clears ast_dirty on landing only if it is unchanged (Session::settle_compile — now the only way to clear the flag). generation returns to pure buffer identity; the two dispatch-side generation bumps are reverted. Results that still match the buffer are published (bounded staleness) while the flag stays dirty, so the next request recompiles.
  • Eviction joins the event pipeline. Worker eviction notifications now dispatch a DocumentEvicted event whose effect is the same "AST lost, recompile" treatment as a worker crash. The worker's document cap is injectable via --max-documents (default 16, unchanged).
  • One reset primitive SessionStore::reset_compile_state(Session&, ResetDepth) replaces three hand-copied reset blocks (context switch, orphaned context choices, dispatch effect loops): Superseded bumps generation and drops PCH ref / deps snapshot / trial verdict; Lost bumps dirty_epoch only.
  • Lazy staleness detection unified with polling. The compile fast path's staleness check now dispatches a DiskChanged event (synchronously) instead of hand-rolling the same session reset, so it shares the file tracker's invalidation path. Behavior for open files is unchanged.
  • ContextChanged removed. The event had an empty handler and existed only as ceremony; the criterion for when logic may bypass the event pipeline is now documented in the invalidator header.
  • close_session no longer takes a peer. Diagnostics retraction goes through the session's output plus the on_output signal, like every other publish; the server core stays transport-free. Side effect: closing a file that was never opened no longer pushes an empty-diagnostics notification.
  • ensure_pch re-checks its generation snapshot before writing session.pch_ref after each suspension point, so a superseded round cannot write back a stale reference.

Tests

  • Integration: open 18 files against a single stateful worker; hover on the evicted first file must recover (regression test for the eviction bug). New assertion that didClose retracts diagnostics through the new signal path.
  • Unit: reset primitive semantics for both depths; conditional dirty-clear (epoch changed mid-flight keeps the flag); eviction event maps to the lost effect; worker LRU eviction driven directly with --max-documents 2.
  • Existing file-tracker polling tests pass unchanged, confirming the staleness-path unification is behavior-preserving.

Local verification: format, RelWithDebInfo build, full unit suite (848 passed), targeted integration subset (47 passed incl. file tracker, context switching, rapid edit, eviction), smoke tests 3/3.

Summary by CodeRabbit

  • New Features
    • Added --max-documents to configure the compiled-document eviction cap for stateful workers (default included).
  • Bug Fixes
    • Evicted documents are treated as “lost” to enable transparent recovery instead of returning empty results.
    • Improved handling of mid-compile invalidations and stale dependency changes to prevent outdated state from being published.
    • Closing a document now reliably clears published diagnostics.
  • Tests
    • Added/updated integration, stress, and unit tests for eviction recovery, diagnostics clearing, and compile-state reset/settlement behavior.

@coderabbitai

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

This PR adds a configurable --max-documents eviction cap for stateful workers, centralizes session compile-state resets, adds compiler generation and dirty-epoch staleness guards, and replaces context-change dispatch with document-eviction and stale-disk events through the server pipeline.

Changes

Eviction cap, epoch-based staleness, and centralized session reset

Layer / File(s) Summary
Worker eviction cap
src/clice.cc, src/server/worker/stateful_worker.{cpp,h}, tests/unit/server/stateful_worker_tests.cpp, tests/unit/server/worker_test_helpers.h, tests/integration/stress/test_eviction.py
Adds --max-documents CLI wiring and default_max_documents; the stateful worker uses max_documents for eviction, and tests/helpers pass and verify the limit.
Session reset depth and epochs
src/server/state/session.h, src/server/state/session_store.{cpp,h}, src/server/compiler/context_resolver.cpp, src/server/transport/master_server.cpp, tests/unit/server/session_store_tests.cpp
Introduces ResetDepth, dirty_epoch, settle_compile, and reset_compile_state; context switching and master-server invalidation now use the shared reset path.
Compiler generation and epoch guards
src/server/compiler/compiler.{cpp,h}, tests/unit/server/compiler_tests.cpp
ensure_pch and ensure_deps use a launch-generation snapshot; run_compile tracks dirty_epoch and settles conditionally; ensure_compiled routes stale disk changes through on_stale.
Document eviction and close wiring
src/server/state/invalidator.{cpp,h}, src/server/transport/master_server.{cpp,h}, src/server/transport/lsp_client.cpp, tests/unit/server/invalidator_tests.cpp, tests/integration/features/test_server.py
Replaces ContextChanged with DocumentEvicted, wires eviction and stale handling into dispatch, and updates close-session diagnostics clearing and tests.

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main server fixes around eviction recovery and conditional dirty-flag clearing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/invalidation-epoch-eviction

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/compiler/compiler.cpp (1)

823-835: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard on_stale before invoking it. Compiler::Compiler(...) doesn’t accept this callback, and there’s no wiring in this tree, so on_stale(path_id) can throw std::bad_function_call on the stale path. Either guard it like on_indexing_needed or make it a required constructor dependency.

🤖 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/compiler/compiler.cpp` around lines 823 - 835, The stale-path
callback in Compiler::needs_refresh (or the surrounding stale-check block) can
throw because on_stale is invoked without verifying it is set. Add the same kind
of guard used for on_indexing_needed before calling on_stale(path_id), or make
on_stale an обязательный constructor dependency in Compiler::Compiler(...) and
wire it through the class so the callback is always valid.
🧹 Nitpick comments (1)
tests/integration/stress/test_eviction.py (1)

1-7: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test implicitly coupled to default_max_documents.

FILE_COUNT = 18 and the "16" in the comment hardcode the worker's default cap. Consider explicitly setting stateful_worker_count's companion --max-documents (if exposed via config) or documenting the coupling more strongly, so a future change to default_max_documents doesn't produce a confusing failure here.

🤖 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/stress/test_eviction.py` around lines 1 - 7, The stress
test is implicitly tied to the worker document cap, so update test_eviction to
avoid hardcoding the default limit. In the test setup around FILE_COUNT and the
LRU-cap comment, either pass an explicit max-documents setting alongside
stateful_worker_count or strengthen the test description to clearly state the
dependency on default_max_documents. Use the FILE_COUNT constant and the worker
configuration used by this test to keep the expectation stable if the default
changes.
🤖 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/compiler/compiler.cpp`:
- Around line 823-835: The stale-path callback in Compiler::needs_refresh (or
the surrounding stale-check block) can throw because on_stale is invoked without
verifying it is set. Add the same kind of guard used for on_indexing_needed
before calling on_stale(path_id), or make on_stale an обязательный constructor
dependency in Compiler::Compiler(...) and wire it through the class so the
callback is always valid.

---

Nitpick comments:
In `@tests/integration/stress/test_eviction.py`:
- Around line 1-7: The stress test is implicitly tied to the worker document
cap, so update test_eviction to avoid hardcoding the default limit. In the test
setup around FILE_COUNT and the LRU-cap comment, either pass an explicit
max-documents setting alongside stateful_worker_count or strengthen the test
description to clearly state the dependency on default_max_documents. Use the
FILE_COUNT constant and the worker configuration used by this test to keep the
expectation stable if the default changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08667415-ef3d-4e5a-90be-40c05c5ad237

📥 Commits

Reviewing files that changed from the base of the PR and between 8f5614b and a024822.

📒 Files selected for processing (20)
  • src/clice.cc
  • src/server/compiler/compiler.cpp
  • src/server/compiler/compiler.h
  • src/server/compiler/context_resolver.cpp
  • src/server/state/invalidator.cpp
  • src/server/state/invalidator.h
  • src/server/state/session.h
  • src/server/state/session_store.cpp
  • src/server/state/session_store.h
  • src/server/transport/lsp_client.cpp
  • src/server/transport/master_server.cpp
  • src/server/transport/master_server.h
  • src/server/worker/stateful_worker.cpp
  • src/server/worker/stateful_worker.h
  • tests/integration/features/test_server.py
  • tests/integration/stress/test_eviction.py
  • tests/unit/server/invalidator_tests.cpp
  • tests/unit/server/session_store_tests.cpp
  • tests/unit/server/stateful_worker_tests.cpp
  • tests/unit/server/worker_test_helpers.h

@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: a024822655

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

@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: bd666390d7

ℹ️ 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 Outdated
@16bit-ykiko
16bit-ykiko force-pushed the feat/invalidation-epoch-eviction branch from bfdba30 to 6998113 Compare July 7, 2026 02:41

@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: 6998113bb8

ℹ️ 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

@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/compiler/compiler.cpp (1)

869-876: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard on_stale before invoking it. Compiler::on_stale is a plain std::function, so a direct Compiler instance can leave it empty; calling it here will fail with std::bad_function_call if the stale path is hit. Add a no-op default or an if(on_stale) check first.

🤖 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/compiler/compiler.cpp` around lines 869 - 876, The stale-path
handler in Compiler::dispatch is invoked unconditionally even though on_stale is
a plain std::function and may be empty on a direct Compiler instance. Add a
guard before calling on_stale(path_id) or initialize on_stale with a no-op
default so the stale-path flow in Compiler::on_stale cannot throw
std::bad_function_call when unset.
🧹 Nitpick comments (1)
tests/unit/server/compiler_tests.cpp (1)

26-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add coverage for a post-suspension epoch guard.

This test bumps dirty_epoch before ensure_pch() starts, so it only exercises the entry guard. Please add a case where dirty_epoch changes while waiting on an in-flight PCH or after BuildPCH returns, before pch_ref assignment.

🤖 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 26 - 64, This test only
covers the early dirty_epoch check in CompilerFixture::ensure_pch, so extend it
to verify the post-suspension epoch guard as well. Add a scenario where
Session::dirty_epoch changes after the coroutine has already started waiting on
the in-flight PCH or immediately after BuildPCH returns but before pch_ref is
written, and assert the continuation exits without updating session.pch_ref. Use
the existing ensure_pch/CompilerFixture path and keep the stale-session
expectations explicit.
🤖 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/compiler/compiler.cpp`:
- Around line 869-876: The stale-path handler in Compiler::dispatch is invoked
unconditionally even though on_stale is a plain std::function and may be empty
on a direct Compiler instance. Add a guard before calling on_stale(path_id) or
initialize on_stale with a no-op default so the stale-path flow in
Compiler::on_stale cannot throw std::bad_function_call when unset.

---

Nitpick comments:
In `@tests/unit/server/compiler_tests.cpp`:
- Around line 26-64: This test only covers the early dirty_epoch check in
CompilerFixture::ensure_pch, so extend it to verify the post-suspension epoch
guard as well. Add a scenario where Session::dirty_epoch changes after the
coroutine has already started waiting on the in-flight PCH or immediately after
BuildPCH returns but before pch_ref is written, and assert the continuation
exits without updating session.pch_ref. Use the existing
ensure_pch/CompilerFixture path and keep the stale-session expectations
explicit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 229bbe12-e43c-4e4b-b5dc-8136f1aa321f

📥 Commits

Reviewing files that changed from the base of the PR and between 6998113 and 162262c.

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

@16bit-ykiko
16bit-ykiko merged commit 00f0e53 into main Jul 7, 2026
22 checks passed
@16bit-ykiko
16bit-ykiko deleted the feat/invalidation-epoch-eviction 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