Skip to content

fix(index): flip shards back to disk blobs after save - #520

Merged
16bit-ykiko merged 2 commits into
mainfrom
fix/index-shard-flip-back
Jul 17, 2026
Merged

fix(index): flip shards back to disk blobs after save#520
16bit-ykiko merged 2 commits into
mainfrom
fix/index-shard-flip-back

Conversation

@16bit-ykiko

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

Copy link
Copy Markdown
Member

Background

Index shards are served from memory-mapped blobs, but any merge materializes a shard's in-memory form and drops the mapped buffer permanently. Two consequences:

  • Memory grows one-way for the server's lifetime: every shard a background round touches stays heap-resident until shutdown (~100KB per small file, MB-level for large translation units). Only a restart releases it.
  • The "needs rewrite" predicate keeps claiming every touched shard dirty forever, so each save rewrites the entire namespace: an incremental round costs seconds of serialization on large projects, and shutdown pays a full rewrite.

Changes

  • MergedIndex now carries a mutation stamp drawn from a process-wide monotonic source. Values never repeat across objects, so an entry erased and re-created at the same key while a save is mid-flight cannot alias a snapshot taken before the swap (a per-object counter restarting from zero could).
  • Indexer::save() reopens each committed blob inside the same thread-pool job as the commit — the mmap and the flatbuffer verification walk the whole file and stay off the event loop — and flips the shard back to the buffer-backed reload, but only when the shard still exists and its stamp is unchanged across the commit await. A merge that landed meanwhile keeps the shard dirty for the next save instead of being silently overwritten by the stale blob.
  • With flipped-back shards clean, the rewrite predicate is a true dirty set: an incremental round saves only the shards its merges touched, and a round with nothing to write commits zero blobs. The last save's committed count is exposed as a gauge.
  • New TEST-ONLY clice/internal/stats request (absent from capabilities, same test-hook family as clice/internal/poll) exposes ownership gauges: loaded preamble states and their mapped bytes, in-memory shards and their content bytes, the last save's committed shard count, in-flight cache tmp blobs, and trend counters. Memory-lifecycle regressions are pinned by deterministic counters instead of brittle RSS assertions.

Testing

  • Unit: MergedIndex.RevisionAndFlipBack (stamp semantics, serialized twin is clean and equal), IndexerMerge.SaveFlipsShards (save flips a committed shard back and it still answers), IndexerMerge.MidSaveMergeKept (a merge interleaved into save's commit await is not lost: the shard keeps the newer content, stays dirty, and the next save writes it — the data-loss guard, exercised deterministically on the event loop).
  • Integration (tests/integration/server/test_memory_ownership.py): shards flip back after a settled round; an incremental change saves exactly one shard; a round with nothing to write commits zero; a 15-edit preamble supersession storm leaves zero in-flight tmp blobs; plus shape smoke for the gauges.
  • TDD: with the flip disabled, the flip-back and dirty-set tests fail; restored, all pass. Full suites green in RelWithDebInfo and Debug (1019 unit, 295 integration, 3/3 smoke).

Notes

  • The stats endpoint is read-only and intended for tests; pending_tmp_files scans the store's tmp directory under the store mutex, which is fine for a test hook but not meant for hot paths.
  • The gauge counts durably committed shard blobs, not serialize-time intent: failed commits do not inflate it.

Summary by CodeRabbit

  • New Features

    • Added internal stats endpoint for test/runtime monitoring (memory-lifecycle counters and gauges).
    • Exposed revision tracking on merged index and size/count helpers for mapped preamble state and pending temporary cache files.
  • Bug Fixes

    • Refined shard two-phase saving so committed/reloaded snapshots only replace in-memory data when revisions match.
    • Ensured concurrent or mid-save merges don’t get overwritten by stale committed snapshots.
    • Verified shard flip-back behavior and improved handling of cancelled/superseded processing cleanup.
  • Performance

    • After save, merged index shards return to their persistent form to reduce retained in-memory rewrite state.
  • Tests

    • Added integration and unit coverage for revision/flip-back, dirty-shard-only saves, and cancellation cleanup.

Any merge materialized a shard's heap Impl and dropped its mmap
buffer permanently: memory grew one-way until shutdown (~100KB per
synthetic file, MB-level on llvm-scale shards), and need_rewrite()
kept claiming every touched shard dirty forever, so each save rewrote
the whole namespace and shutdown paid a full serialize.

- MergedIndex carries a mutation stamp drawn from a process-wide
  monotonic source (values never repeat across objects, so an entry
  erased and re-created at the same key cannot alias a snapshot taken
  before the swap). Every merge/remove reassigns it.
- Indexer::save() reopens each committed blob inside the commit's
  thread-pool job (mmap + flatbuffer verification stay off the event
  loop) and flips the shard back to the buffer-backed reload — only
  when the shard still exists and its stamp is unchanged across the
  commit await; a merge that landed meanwhile keeps the shard dirty
  for the next save instead of being silently dropped.
- need_rewrite() is now a true dirty set: an incremental round saves
  only the shards its merges touched, and a round with nothing to
  write commits zero blobs.
- New TEST-ONLY clice/internal/stats endpoint exposes ownership
  gauges (loaded preamble states/bytes, in-memory shards/bytes, last
  save's committed shard count, in-flight tmp blobs, trend gauges) so
  memory-lifecycle regressions are pinned by deterministic counters
  instead of RSS assertions.

Tests: unit RevisionAndFlipBack + SaveFlipsShards + MidSaveMergeKept
(the mid-save merge interleaving pins the data-loss guard); integration
flip-back, dirty-set-only save, no-op save commits zero, cancel-storm
leaves no tmp blobs.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f4b45619-9e79-443f-84ea-f094b3558a13

📥 Commits

Reviewing files that changed from the base of the PR and between 366ef60 and 3cb9306.

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

📝 Walkthrough

Walkthrough

Changes

The change adds mutation revisions to MergedIndex, makes shard saves reload and conditionally flip committed data back into memory, and introduces an internal statistics endpoint with client and regression-test coverage for shard persistence and temporary-file cleanup.

Index persistence and lifecycle observability

Layer / File(s) Summary
MergedIndex revision tracking
src/index/merged_index.*, tests/unit/index/merged_index_tests.cpp
Mutating operations assign monotonic revisions, while serialized reloads reset revision state and preserve rewrite invariants.
Verified shard save and flip-back
src/server/compiler/indexer.*, tests/unit/server/indexer_tests.cpp
Shard saves record revisions, commit blobs, reload committed indices, and replace only shards unchanged during saving.
Lifecycle statistics and regression coverage
src/server/protocol/extension.h, src/server/transport/lsp_client.cpp, src/support/cache_store.*, src/index/preamble_state.h, tests/tools/client.py, tests/integration/server/test_memory_ownership.py
The internal stats request reports cache, index, temporary-file, context, and session gauges used by lifecycle regression tests.

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

Sequence Diagram(s)

sequenceDiagram
  participant Indexer
  participant MergedIndex
  participant CacheStore
  Indexer->>MergedIndex: serialize dirty shard and record revision
  Indexer->>CacheStore: commit pending shard blob
  CacheStore-->>Indexer: return committed shard path
  Indexer->>MergedIndex: reload committed shard
  Indexer->>MergedIndex: flip back only when revision matches
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% 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 matches the main change: flipping merged index shards back to disk-backed blobs after save.
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 fix/index-shard-flip-back

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/server/transport/lsp_client.cpp (1)

625-631: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Gate the test-only stats endpoint behind project.test_hooks.

This endpoint is registered in production despite being test-only and performs synchronous workspace/directory scans. Apply the same guard used by clice/internal/logFlood, then enable test_hooks in the integration-test initialization.

Proposed guard
         "clice/internal/stats",
         [this](RequestContext& ctx, const ext::StatsParams&) -> RawResult {
+            if(!this->server.workspace.config.project.test_hooks.value_or(false)) {
+                co_return kota::outcome_error(
+                    kota::ipc::Error{protocol::ErrorCode::InvalidRequest,
+                                     "test hooks are not enabled"});
+            }
             auto& srv = this->server;
🤖 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/transport/lsp_client.cpp` around lines 625 - 631, Guard
registration of the clice/internal/stats request in the same project.test_hooks
condition used by clice/internal/logFlood, so production projects do not expose
or execute this test-only endpoint. Update integration-test initialization to
enable project.test_hooks, preserving stats endpoint availability in tests.
🤖 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 `@src/server/compiler/indexer.cpp`:
- Around line 269-272: Move the saved_shards reset from the Phase 2 commit
section to the beginning of save(), before any serialization or other
early-return paths. Preserve the existing counter behavior while ensuring failed
ProjectIndex serialization cannot expose the previous successful save count.

---

Nitpick comments:
In `@src/server/transport/lsp_client.cpp`:
- Around line 625-631: Guard registration of the clice/internal/stats request in
the same project.test_hooks condition used by clice/internal/logFlood, so
production projects do not expose or execute this test-only endpoint. Update
integration-test initialization to enable project.test_hooks, preserving stats
endpoint availability in tests.
🪄 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: ccac8f6b-2f80-4d3d-98be-7ab046cb680f

📥 Commits

Reviewing files that changed from the base of the PR and between c16f4ac and 366ef60.

📒 Files selected for processing (13)
  • src/index/merged_index.cpp
  • src/index/merged_index.h
  • src/index/preamble_state.h
  • src/server/compiler/indexer.cpp
  • src/server/compiler/indexer.h
  • src/server/protocol/extension.h
  • src/server/transport/lsp_client.cpp
  • src/support/cache_store.cpp
  • src/support/cache_store.h
  • tests/integration/server/test_memory_ownership.py
  • tests/tools/client.py
  • tests/unit/index/merged_index_tests.cpp
  • tests/unit/server/indexer_tests.cpp

Comment thread src/server/compiler/indexer.cpp Outdated
A ProjectIndex serialization or store failure left the previous
round's committed count exposed as current; a failed save committed
nothing and must read back as zero.
@16bit-ykiko
16bit-ykiko merged commit ecd3043 into main Jul 17, 2026
22 checks passed
@16bit-ykiko
16bit-ykiko deleted the fix/index-shard-flip-back branch July 17, 2026 19:45
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