diff --git a/src/server/compiler/indexer.cpp b/src/server/compiler/indexer.cpp index dcbda26b4..699729040 100644 --- a/src/server/compiler/indexer.cpp +++ b/src/server/compiler/indexer.cpp @@ -1005,12 +1005,16 @@ kota::task<> Indexer::run_background_indexing() { // running round, but staleness is re-judged per round anyway. fv_verdicts.clear(); + // Freeze this round's boundary before announcing it. Progress callbacks + // and failed tasks may enqueue more work; those entries belong to the + // next idle-delayed round, not the one currently being drained. + const auto round_end = index_queue.size(); std::stable_partition( index_queue.begin() + index_queue_pos, - index_queue.end(), + index_queue.begin() + round_end, [this](std::uint32_t id) { return workspace.path_to_module.contains(id); }); - auto total = index_queue.size() - index_queue_pos; + auto total = round_end - index_queue_pos; std::size_t dispatched = 0; std::size_t completed = 0; @@ -1025,7 +1029,7 @@ kota::task<> Indexer::run_background_indexing() { ScopedTimer timer; kota::task_group<> workers(loop); - while(index_queue_pos < index_queue.size()) { + while(index_queue_pos < round_end) { if(pause_depth > 0) co_await resume_event.wait(); diff --git a/tests/unit/server/indexer_tests.cpp b/tests/unit/server/indexer_tests.cpp index e7a78e2ad..c42bb7e1e 100644 --- a/tests/unit/server/indexer_tests.cpp +++ b/tests/unit/server/indexer_tests.cpp @@ -2,6 +2,7 @@ #include #include +#include "test/cdb_helper.h" #include "test/temp_dir.h" #include "test/test.h" #include "command/argument_parser.h" @@ -1224,6 +1225,60 @@ TEST_CASE(DropIndexEvictsPersisted) { TEST_SUITE(IndexerRequeue) { +TEST_CASE(UnavailableWorkerDoesNotSpinInCurrentRound) { + TempDir tmp; + IndexerFixture f; + + tmp.touch("retry.cpp", "int retry_me;\n"); + auto path = tmp.path("retry.cpp"); + auto cdb = build_cdb_json({ + {tmp.root, path, {}} + }); + write_cdb(tmp, f.workspace.cdb, cdb); + + // A started but empty pool returns worker_unavailable while still + // treating the outage as retryable -- the state that triggered the loop. + ASSERT_TRUE(f.pool.start({.stateless_count = 0, .stateful_count = 0})); + + f.workspace.config.project.idle_timeout_ms = 0; + + constexpr std::size_t runaway_threshold = 64; + std::size_t rounds = 0; + Indexer::Progress last_report; + auto progress_connection = f.indexer.on_progress_changed.connect([&] { + const auto& state = f.indexer.progress(); + if(state.stage == Indexer::Progress::Stage::Begin) { + ++rounds; + // A legitimate retry belongs to a later, idle-delayed round. + f.workspace.config.project.idle_timeout_ms = 60'000; + return; + } + if(state.stage != Indexer::Progress::Stage::Report) + return; + + last_report = state; + if(state.completed == runaway_threshold) + f.indexer.pause_indexing(); + }); + + auto id = f.workspace.path_pool.intern(path); + f.indexer.enqueue(id, ReindexReason::ContentChanged); + f.indexer.schedule(); + + auto stop_after_sample = [&]() -> kota::task<> { + co_await kota::sleep(25); + co_await f.indexer.stop(); + co_await f.pool.stop(); + }; + f.loop.schedule(stop_after_sample()); + f.loop.run(); + + ASSERT_TRUE(f.indexer.pending_reason(id).has_value()); + ASSERT_EQ(rounds, 1u); + ASSERT_EQ(last_report.total, 1u); + ASSERT_EQ(last_report.completed, last_report.total); +} + TEST_CASE(PreemptionKeepsBudget) { IndexerFixture f; auto id = f.workspace.path_pool.intern("/proj/a.cpp");