Skip to content

test(loading): kill the server on an unwinding panic, not on the last line - #714

Closed
TinDang97 wants to merge 1 commit into
mainfrom
fix/warm-segment-leak-546
Closed

test(loading): kill the server on an unwinding panic, not on the last line#714
TinDang97 wants to merge 1 commit into
mainfrom
fix/warm-segment-leak-546

Conversation

@TinDang97

Copy link
Copy Markdown
Collaborator

Fixes the leak reported in #713 for the one file that caused it.

What happened

tests/loading_state_476.rs killed its server on the last line of each test body:

let child = spawn(dir, port);
assert!(...);      // <- fires
kill(child);       // <- never runs; the unwind goes straight past it

After an afternoon of deliberately-failing mutation runs of this file, five servers were left reparented to init:

13469 171.1% 07:38:26   21085 180.0% 07:35:41   75746 160.1% 07:51:06
77551 153.6% 07:50:13   79340 169.2% 07:49:35

834% CPU combined, for 7.5 hours. They answered nothing on their ports and spent the time in syscalls — one thread showed 13:42 system against 0:09 user. Their data dirs still existed, so this is not the dirless spin of #369.

The wasted cores are the smaller half of the cost. Several ci-local runs and a stack of wall-clock-sensitive tests executed on a machine carrying a third of a load that nothing in their output disclosed.

The fix

The child is owned by a ServerGuard that kills and reaps on Drop, so an unwinding panic takes the server with it. kill_now() remains for the one site that needs the store closed rather than merely doomed, before it is reopened.

The regression test

lst476_a_panicking_test_does_not_orphan_its_server panics on purpose inside catch_unwind and then asserts on the PID, not the port. A dead server frees its port either way, so a connect-refused would prove nothing about whether anything is still running — that distinction is what makes the test non-vacuous.

Verified load-bearing rather than assumed: replacing the guard with mem::forget fails it with

server pid 8877 survived a panicking test — this is how orphans that spin at ~170% CPU for hours get created

Scope

This PR fixes one file. #713 tracks the other 21 files that call .kill() without a Drop guard, and suggests promoting a single guard into tests/common/mod.rs so the correct thing is also the default thing. (117 of 122 files already guard correctly.)

`register_warm_segments` attributes a recovered warm segment to its owning
index by reading exactly one file — mvcc.mpf, via `peek_key_hashes`. When
that file is missing there is no evidence, no owner can be chosen, and the
directory was left on disk with a warning. Nothing ever removed it, so every
subsequent restart re-read it and re-warned about it.

Reproduced by stripping mvcc.mpf from 6 of 12 warm segments and restarting
three times: 6 warnings each restart, 6/6 directories still present, DBSIZE
unchanged at 2400. The warning was permanent and the disk cost with it. On
the store that prompted moon#546 this was 897 directories.

Such a directory is what a crash mid-creation, or a GC pass that emptied a
directory without removing it, leaves behind. It can never be attached to an
index, and whatever keys it held are recovered by the keyspace rescan
regardless. It is now removed, counted, and reported as `retired as orphans
with no mvcc.mpf` in the startup summary. Once the directory is gone the
existing #546a pass retires its manifest entry on the next boot, so the
sequence converges instead of trading one permanent warning for another.

Retirement is gated strictly on ErrorKind::NotFound. Every other IO error
keeps the previous warn-and-leave behavior: a transient EIO or a permissions
problem can sit over perfectly good vectors, and deleting on those would turn
a recoverable blip into data loss. A present-but-unparseable mvcc.mpf —
truncated or corrupt — does not error at all; it mmaps and parses to zero ids
(measured), so it takes the existing no-owner path and is likewise kept.

Three tests, all mutation-checked. The retirement test was red before the
fix. The "must not delete on any other error" test uses a directory named
mvcc.mpf, because that is what actually produces a non-NotFound error —
an earlier draft used a zero-length file, and widening the gate to `if true`
still passed it, which is how the vacuity was caught. With the real fixture,
widening the gate fails it.

Refs: #546
author: Tin Dang
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 2 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b006371d-9c83-417d-8d44-7d48eeaa929c

📥 Commits

Reviewing files that changed from the base of the PR and between ec7bd4a and 5ded4c7.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/vector/persistence/recover_v2_warm_tests.rs
  • src/vector/store.rs

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.

@TinDang97

Copy link
Copy Markdown
Collaborator Author

Closing: this PR was opened from the wrong head branch. Its head is fix/warm-segment-leak-546, so the diff it shows is #712's warm-segment change — which is already merged — and not the ServerGuard work the title describes. Its green checks graded the wrong tree.

Reopened correctly as the PR from fix/test-server-leak-on-panic, which additionally fixes a vacuity bug found while re-reviewing: Check (Windows) runs this suite, ps does not exist there, and .unwrap_or(false) made the orphan assertion pass without checking anything.

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