Skip to content

fix: prevent worker crashes from null ASTConsumer, invalid FileID, and missing PCH cache dir - #435

Merged
16bit-ykiko merged 2 commits into
mainfrom
fix/worker-crash-and-pch-robustness
Apr 23, 2026
Merged

fix: prevent worker crashes from null ASTConsumer, invalid FileID, and missing PCH cache dir#435
16bit-ykiko merged 2 commits into
mainfrom
fix/worker-crash-and-pch-robustness

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Apr 22, 2026

Copy link
Copy Markdown
Member

Summary

Three pre-existing bugs cause worker processes to crash with SEGV or SIGABRT. On the main branch these crashes are silent (workers die, requests fail fast with "transport closed", tests still pass because null responses are accepted). However when combined with #432's worker respawn mechanism, the crash-respawn-crash cycle on low-core CI machines causes request timeouts and smoke test hangs.

Fixes

  • compilation.cpp: ProxyAction::CreateASTConsumer now checks for null before passing to MultiplexConsumer. When the wrapped action's CreateASTConsumer fails (e.g. missing system headers during PCH generation), this previously caused a null pointer dereference, SEGV, ASAN kills the stateless worker.
  • compilation_unit.cpp: file_path() returns empty StringRef on invalid FileID instead of asserting. The assert fired when IncludeGraph::from() called file_path(interested_file()) on an AST compiled with synthesized default commands (no compile_commands.json, clang++ -std=c++20 fallback, no system headers, invalid main file ID), SIGABRT, stateful worker crash.
  • compiler.cpp: ensure_pch now creates the PCH cache directory before sending the build request. Previously, when load_workspace() exited early (no compile_commands.json), the cache subdirectories were never created, causing every PCH write to fail with "No such file or directory".
  • master_server.cpp/h: load_workspace() changed from kota::task<> to plain void -- it contains only synchronous filesystem operations and no co_await, so the coroutine wrapper was unnecessary. Called directly instead of via loop.schedule().

Test plan

  • Verified zero SEGV/SIGABRT/assertion crashes in worker stderr after fix
  • rapid_edit.jsonl smoke test passes 3/3 runs consistently (34s each)
  • Behavior matches main branch (both return 134 responses, 0 pending)
  • Debug build with ASAN (detect_leaks=0) -- clean run, no sanitizer reports

View in Codesmith
Codesmith can help with this PR — just tag @codesmith or enable autofix.

  • Autofix CI and bot reviews

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling for AST consumer creation with null checks and a clear failure path.
    • Safer file-path access that returns empty for invalid identifiers instead of asserting.
    • PCH cache handling now validates cache configuration, attempts directory creation, logs warnings, and aborts PCH builds on failure.
  • Refactor

    • Workspace loading changed from asynchronous to synchronous execution.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds defensive checks for AST consumer creation and FileID handling, makes workspace loading synchronous, prevents PCH builds when the project cache directory is unset or cannot be created (clearing in-flight state and signaling waiters), and updates several codec/header includes across server and test files.

Changes

Cohort / File(s) Summary
Compilation & AST
src/compile/compilation.cpp, src/compile/compilation_unit.cpp
Null-check WrapperFrontendAction::CreateASTConsumer result and return nullptr on failure; guard CompilationUnitRef::file_path(clang::FileID) against invalid FileID by returning an empty path.
PCH Cache Initialization
src/server/compiler.cpp
Verify workspace.config.project.cache_dir is set and attempt to create <cache_dir>/cache/pch; on missing config or directory-creation failure clear the building event, signal waiters, log a warning, and return false to avoid dispatching BuildPCH.
Workspace Loading Sync
src/server/master_server.h, src/server/master_server.cpp
Change MasterServer::load_workspace() from kota::task<> coroutine to synchronous void and update caller to invoke it directly instead of scheduling the coroutine.
Codec / Include Updates
src/server/compiler.h, src/server/protocol.h, src/server/worker_common.h, benchmarks/scan_benchmark.cpp, src/server/config.cpp, tests/unit/server/*
Replace several kota/codec/* includes (e.g., raw_value.h, serializer.h, toml.h) with new header paths such as kota/codec/json/json.h and kota/codec/toml/toml.h; corresponding test includes updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 I hopped through headers, nibbled a bug,
Checked every pointer, gave nulls a hug.
I guarded the paths and hushed the build drum,
Now caches sleep until their homes come.
A tiny hop — the code runs calm.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the three main bugs being fixed: null ASTConsumer, invalid FileID handling, and missing PCH cache directory creation.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/worker-crash-and-pch-robustness

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 and usage tips.

@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

Caution

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

⚠️ Outside diff range comments (2)
src/server/master_server.cpp (1)

59-177: ⚠️ Potential issue | 🟠 Major

Synchronous load_workspace() now blocks the event loop during initialized.

Previously load_workspace() was a kota::task<> dispatched via loop.schedule(...), making workspace bootstrap a detached background operation (consistent with the pattern documented in src/server/compiler.cpp:643-678 and used in src/server/indexer.cpp:634-637 and src/server/worker_pool.cpp:100-112). After this change, the InitializedParams notification handler at line 334 calls it inline, so the entire bootstrap — create_directories, cleanup_cache/load_cache, compile_commands.json auto-scan via directory_iterator, workspace.cdb.load, scan_dependency_graph (which explicitly reports elapsed_ms), build_reverse_map, indexer.load, and the indexer enqueue loop over every CDB entry — runs on the event-loop thread before any other LSP message can be processed.

For small projects this is fine and matches the PR's smoke-test observations, but on large monorepos this can stall the loop for many seconds, delay worker-pool responsiveness, and make $/cancelRequest / shutdown / didOpen back up behind initialization. The PR rationale ("only synchronous filesystem operations") explains why co_await is unnecessary, but doesn't address the blocking-on-loop concern that originally motivated loop.schedule(...).

Consider keeping the function void (since it truly has no suspension points) but re-dispatching it as a detached task, e.g.:

♻️ Suggested pattern
-        load_workspace();
+        loop.schedule([this]() -> kota::task<> {
+            load_workspace();
+            co_return;
+        }());

Or, if the scheduler supports plain callables, schedule it directly without the coroutine wrapper.

If the intent is specifically to run this before the worker pool starts handling requests (note: pool.start(...) already happened earlier in the same handler), please add a short comment here documenting that trade-off so the deviation from the established loop.schedule() pattern isn't reverted later.

Also applies to: 334-334

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/master_server.cpp` around lines 59 - 177,
MasterServer::load_workspace is now invoked inline from the InitializedParams
handler and performs long blocking work (create_directories, directory_iterator
scan, workspace.cdb.load, scan_dependency_graph, indexer.enqueue, etc.), which
blocks the event loop; change the call site so load_workspace runs as a detached
background task instead of inline — e.g. dispatch a lambda that calls
MasterServer::load_workspace via loop.schedule(...) or wrap it as kota::task<>
and schedule it, leaving load_workspace as a void synchronous function; if the
blocking behavior was intentional (run-before-workers), add a short comment at
the InitializedParams handler noting the deliberate synchronous bootstrap and
its trade-offs so future reviewers don't revert it.
src/compile/compilation_unit.cpp (1)

83-114: ⚠️ Potential issue | 🟡 Minor

Downstream callers may silently propagate empty paths.

Returning an empty StringRef for invalid FileID fixes the SIGABRT, but several callers treat file_path's result as always-valid. Since the method can return empty in two scenarios—invalid FileID (line 85) or when getFileEntryRefForID fails for a valid FileID (line 92)—callers must guard accordingly:

  • src/index/include_graph.cpp:30path_table.try_emplace(path, …) inserts unvalidated path as a key; an empty path becomes a corrupt graph entry.
  • src/index/include_graph.cpp:54graph.paths.emplace_back(unit.file_path(unit.interested_file())) inserts unvalidated path unconditionally.
  • src/feature/diagnostics.cpp:57to_uri(unit.file_path(raw.fid)) produces a malformed URI in the LSP response if path is empty.
  • src/feature/document_links.cpp:39,45 — Guards on fid.isValid(), but this is insufficient; a valid FileID can still yield an empty path if the file entry lookup fails.

The deps() method (lines 248–259) already guards with !path.empty(). Apply the same guard at these call sites or audit them to prevent corrupt downstream data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compile/compilation_unit.cpp` around lines 83 - 114, Several callers
assume CompilationUnitRef::file_path(...) always returns a non-empty
llvm::StringRef; since file_path can return an empty path for invalid FileID or
missing FileEntry, update those call sites to guard against empty results (as
deps() already does using !path.empty()). Specifically: in
src/index/include_graph.cpp, check the returned path before calling
path_table.try_emplace(...) and before
graph.paths.emplace_back(unit.file_path(unit.interested_file())) and skip/handle
when empty; in src/feature/diagnostics.cpp ensure
to_uri(unit.file_path(raw.fid)) is only called when the path is non-empty; and
in src/feature/document_links.cpp add the same empty-path guard in addition to
the existing fid.isValid() checks — when empty, skip insertion or log/handle the
missing-path case to avoid corrupt graph entries or malformed URIs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/server/compiler.cpp`:
- Around line 493-500: The new unconditional create_directories call uses
workspace.config.project.cache_dir and can create a relative cache/pch in the
server CWD; restore the existing "empty-cache-dir" guard by checking if
workspace.config.project.cache_dir is empty (or otherwise indicates “do
nothing”) before computing pch_dir and calling
llvm::sys::fs::create_directories; if the cache_dir is empty, skip creating
pch_dir and avoid dispatching the PCH build (i.e., perform the same early exit
behavior you do elsewhere instead of proceeding to the create_directories,
preserving the existing resets on workspace.pch_cache[path_id].building,
completion->set(), and co_return false semantics where appropriate).

---

Outside diff comments:
In `@src/compile/compilation_unit.cpp`:
- Around line 83-114: Several callers assume CompilationUnitRef::file_path(...)
always returns a non-empty llvm::StringRef; since file_path can return an empty
path for invalid FileID or missing FileEntry, update those call sites to guard
against empty results (as deps() already does using !path.empty()).
Specifically: in src/index/include_graph.cpp, check the returned path before
calling path_table.try_emplace(...) and before
graph.paths.emplace_back(unit.file_path(unit.interested_file())) and skip/handle
when empty; in src/feature/diagnostics.cpp ensure
to_uri(unit.file_path(raw.fid)) is only called when the path is non-empty; and
in src/feature/document_links.cpp add the same empty-path guard in addition to
the existing fid.isValid() checks — when empty, skip insertion or log/handle the
missing-path case to avoid corrupt graph entries or malformed URIs.

In `@src/server/master_server.cpp`:
- Around line 59-177: MasterServer::load_workspace is now invoked inline from
the InitializedParams handler and performs long blocking work
(create_directories, directory_iterator scan, workspace.cdb.load,
scan_dependency_graph, indexer.enqueue, etc.), which blocks the event loop;
change the call site so load_workspace runs as a detached background task
instead of inline — e.g. dispatch a lambda that calls
MasterServer::load_workspace via loop.schedule(...) or wrap it as kota::task<>
and schedule it, leaving load_workspace as a void synchronous function; if the
blocking behavior was intentional (run-before-workers), add a short comment at
the InitializedParams handler noting the deliberate synchronous bootstrap and
its trade-offs so future reviewers don't revert it.
🪄 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: 05519fa7-f7a2-4bb3-b2ee-00acc27ff875

📥 Commits

Reviewing files that changed from the base of the PR and between 17e6801 and 2dfdcf4.

📒 Files selected for processing (5)
  • src/compile/compilation.cpp
  • src/compile/compilation_unit.cpp
  • src/server/compiler.cpp
  • src/server/master_server.cpp
  • src/server/master_server.h

Comment thread src/server/compiler.cpp
…d missing PCH cache dir

Three pre-existing bugs cause worker processes to crash (SEGV/SIGABRT),
which becomes a reliability issue when combined with the respawn mechanism
on low-core CI machines.

- compilation.cpp: Check for null before passing WrapperFrontendAction's
  ASTConsumer to MultiplexConsumer (prevents SEGV in stateless workers
  during PCH builds when CreateASTConsumer fails)
- compilation_unit.cpp: Return empty StringRef on invalid FileID instead
  of asserting (prevents SIGABRT in stateful workers when compilation
  produces an AST with no valid main file, e.g. synthesized default
  commands without system headers)
- compiler.cpp: Create PCH cache directory before sending build request
  to stateless worker (prevents "No such file or directory" when
  load_workspace didn't run due to missing compile_commands.json
- master_server: Make load_workspace a plain synchronous function since
  it contains no async operations
EOF
)

Made-with: Cursor
@16bit-ykiko
16bit-ykiko force-pushed the fix/worker-crash-and-pch-robustness branch from 2dfdcf4 to c9ac524 Compare April 22, 2026 19:36

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

59-177: Synchronous conversion is sound; be aware it still blocks the notification handler.

The body has no suspension points, so converting to void is behaviorally equivalent to the previous kota::task<> (which was already executing synchronously end‑to‑end once resumed). That said, load_workspace() is now invoked inline from the initialized notification callback (Line 334), so filesystem walks, workspace.cdb.load, scan_dependency_graph, build_module_map, and indexer.load all run on the event-loop thread before the handler returns. For large workspaces this can delay subsequent LSP messages. If this proves noticeable, consider re-scheduling the heavy portion onto a worker/background task (keeping it non-coroutine is fine — loop.schedule([this]{ load_workspace(); }) or similar).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/master_server.cpp` around lines 59 - 177, load_workspace() is now
synchronous and runs expensive filesystem and indexing work inline on the
event-loop (called from the initialized notification), which can block the
notification handler; split the heavy work (the filesystem walks,
workspace.cdb.load(...), scan_dependency_graph(...),
workspace.build_module_map(), indexer.load(...)/enqueue/schedule, and
compiler.init_compile_graph()) out of the inline callback and reschedule it onto
a background worker or the event loop's scheduler (e.g. capture this in a lambda
and call loop.schedule([this]{ /* heavy portion */ }); or dispatch to a thread
pool) while keeping the lightweight early checks and config handling in
load_workspace() so the notification handler returns quickly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/server/master_server.cpp`:
- Around line 59-177: load_workspace() is now synchronous and runs expensive
filesystem and indexing work inline on the event-loop (called from the
initialized notification), which can block the notification handler; split the
heavy work (the filesystem walks, workspace.cdb.load(...),
scan_dependency_graph(...), workspace.build_module_map(),
indexer.load(...)/enqueue/schedule, and compiler.init_compile_graph()) out of
the inline callback and reschedule it onto a background worker or the event
loop's scheduler (e.g. capture this in a lambda and call loop.schedule([this]{
/* heavy portion */ }); or dispatch to a thread pool) while keeping the
lightweight early checks and config handling in load_workspace() so the
notification handler returns quickly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dcaf9e76-62c4-455e-b423-383b71c82a78

📥 Commits

Reviewing files that changed from the base of the PR and between 2dfdcf4 and c9ac524.

📒 Files selected for processing (5)
  • src/compile/compilation.cpp
  • src/compile/compilation_unit.cpp
  • src/server/compiler.cpp
  • src/server/master_server.cpp
  • src/server/master_server.h
✅ Files skipped from review due to trivial changes (2)
  • src/compile/compilation_unit.cpp
  • src/compile/compilation.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server/compiler.cpp

kotatsu moved all codec headers into subdirectories; top-level files
like codec/raw_value.h and codec/toml.h no longer exist.

- codec/raw_value.h → removed (re-exported by json/json.h and
  bincode/bincode.h)
- codec/toml.h → codec/toml/toml.h
- codec/json/serializer.h → codec/json/json.h (use umbrella header)

Made-with: Cursor

@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)
src/server/master_server.h (1)

76-76: Avoid locking workspace loading into the event-loop path.

load_workspace() now has a synchronous API, but the implementation performs substantial filesystem/index work and is called directly from the initialized notification handler in src/server/master_server.cpp. On large workspaces this can stall LSP message processing during startup; consider moving the heavy scan/load work onto a background worker or keeping an async boundary for yielding/cancellation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/master_server.h` at line 76, The synchronous load_workspace() call
is doing heavy I/O on the event-loop path; change its usage so the expensive
scan/index work runs off the main thread by converting load_workspace() to an
asynchronous/background task or by introducing a worker thread/task queue and
invoking it from the initialized notification handler in master_server.cpp;
specifically, replace the direct call to MasterServer::load_workspace() in the
initialized handler with a dispatch to a background worker (or make
load_workspace_async() that returns a future/promised completion) and ensure any
state mutations are synchronized or marshalled back to the event loop when ready
so startup messages are not blocked and cancellation/yielding is possible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/server/master_server.h`:
- Line 76: The synchronous load_workspace() call is doing heavy I/O on the
event-loop path; change its usage so the expensive scan/index work runs off the
main thread by converting load_workspace() to an asynchronous/background task or
by introducing a worker thread/task queue and invoking it from the initialized
notification handler in master_server.cpp; specifically, replace the direct call
to MasterServer::load_workspace() in the initialized handler with a dispatch to
a background worker (or make load_workspace_async() that returns a
future/promised completion) and ensure any state mutations are synchronized or
marshalled back to the event loop when ready so startup messages are not blocked
and cancellation/yielding is possible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08ea113c-064a-4545-9a2e-bf6699b8cec2

📥 Commits

Reviewing files that changed from the base of the PR and between c9ac524 and 8009098.

📒 Files selected for processing (9)
  • benchmarks/scan_benchmark.cpp
  • src/server/compiler.h
  • src/server/config.cpp
  • src/server/master_server.h
  • src/server/protocol.h
  • src/server/worker_common.h
  • tests/unit/server/config_tests.cpp
  • tests/unit/server/stateful_worker_tests.cpp
  • tests/unit/server/stateless_worker_tests.cpp
💤 Files with no reviewable changes (1)
  • tests/unit/server/stateless_worker_tests.cpp
✅ Files skipped from review due to trivial changes (7)
  • tests/unit/server/stateful_worker_tests.cpp
  • src/server/worker_common.h
  • src/server/protocol.h
  • src/server/config.cpp
  • tests/unit/server/config_tests.cpp
  • benchmarks/scan_benchmark.cpp
  • src/server/compiler.h

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