Skip to content

test(mcp-server): ожидания тестов не зависят от нагрузки процессора - #428

Merged
itrous merged 16 commits into
developfrom
fix/mcp-tests-load-independence
Oct 14, 2026
Merged

itrous merged 16 commits into
developfrom
fix/mcp-tests-load-independence

Conversation

@itrous

@itrous itrous commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Что сделано

Тесты mcp-server и test-utils решали успех по часам: дедлайны в секундах, пороги в 500 мс, паузы «для прогрева». Здесь ожидания переведены на счётчики опросов и синхронизацию по событию, а проверки скорости оставлены явными контрактами. Продуктовое поведение не меняется; единственное добавление в публичный API — McpServer::shutdown_within(budget), shutdown вызывает его с прежними пятью секундами.

Какие тесты зависели от нагрузки и почему

  • tests/diagnostics_baseline.rs::baseline_parity_partial_stale_error_recovery_and_cancellation — ждал первого ответа со stale: true. Этот ответ приходит, когда правка файла уже видна, а нечитаемость файла (дыра, state: partial) ещё нет. Ожидание переведено на оба факта.
  • graph::build::tests::a_failed_reload_whose_retry_is_held_stays_owed, graph::state::tests::a_refused_reload_spawn_gives_its_claim_back, graph::state::tests::a_walk_that_outlived_its_episode_is_still_the_only_walk, graph::state::tests::every_admission_carries_the_healings_measured_before_it — читали состояние сразу после Ready, пока фон держал слот сборки, проход публикации или обход. Переведены на wait_settled.
  • tests/tasks.rs, tests/task_deadline.rs — фикстура стенда удалялась, пока фоновый владелец bsl-overlay-backlog её ещё читал. Теперь сервер останавливается и владельцы уходят до удаления стенда (Retired).
  • state::embed::tests::embedding_fence_distinguishes_retry_from_supersession, state::embed::tests::an_embed_pass_in_its_retry_pause_is_not_live_work — держатель блокировки отпускался по часам.
  • tests::all_status_requests_are_cached_under_held_lease — фильтр фенсов видел только рабочие потоки tokio и не видел вызовов на потоке теста; порог 500 мс у статуса заменён счётом фенсов по потоку теста.
  • diagnostics_state::session::tests::a_cancelled_call_frees_the_resident_slot — абсолютный порог «ждать дольше 1 с» оставлен контролем, что живой обход действительно держит слот; проверка «отменённый ждёт в 3 раза меньше» сравнивает два замера под одной нагрузкой.
  • tests/tasks.rs::cancelling_a_working_handle_stops_the_read_and_frees_the_resident — абсолютный порог «не дольше 3 с» после отмены заменён сравнением с тем же свипом, оставленным работать.

Что заменено

  • Ожидания состояний: eventually(Duration, f) → eventually(f) (6000 опросов по 10 мс; 20-минутный предел по часам остаётся страховкой от зависания). Рукописные циклы по дедлайну → Patience (счётчик опросов).
  • Позитивные recv_timeout / timeout на события с пределами 5–30 с → общий STATE_CEILING (60 с). Он защищает от зависания, а не меряет скорость.
  • Проверки «не ждёт» доказываются удержанием ресурса до ответа или журналом дисковых проверок lease (disk_checks_on_this_thread); исход доказывает сам результат (Pending, Failed, Stopping). Оставшиеся верхние границы — порядка величины и названы в тестах (например, 500 мс у отмены поиска).
  • Владельцы фикстур: Retired в tests/common (остановка сервера и ожидание владельцев до удаления стенда).
  • graph/test_support::wait_settled вместо wait_ready в четырёх тестах graph::*.
  • Гейт WALK_GATE в трёх тестах lib.rs, которые двигали общий счётчик обходов.
  • test-utils::walk_probe::await_walk_start: счётчик опросов вместо 60 с по часам.

Изменения в продакшен-коде

  • state/mod.rs: shutdown вызывает shutdown_within(owners_budget) с 5 с. Тело shutdown_within совпадает с прежним shutdown шаг в шаг. OwnerStop::wait_empty вынесен в wait_empty_in_slices(timeout, slice): бюджет, которого нет в Instant, теперь ждёт уходом владельцев, а не возвращает false после первого часового среза. Рабочие бюджеты не меняются; продакшен передаёт 5 с.
  • lib.rs: публичный McpServer::shutdown_within.
  • Остальное в src/ — #[cfg(test)]-хелперы (walk_out, disk_checks_on_this_thread, hold_file_lock_for_test и др.) и тесты. В graph/state.rs и graph/debt.rs новые функции под cfg(test).

Результаты под нагрузкой

Нагрузка: 2 × число ядер (64) процессов yes > /dev/null на 32 ядрах.

Полные проходы всех бинарников mcp-server (cargo test -p mcp-server, --no-fail-fast, 3 прохода):

  • До PR (база 4467ba9a, load average 146–174): проход 1 — 1 падение (diagnostics_baseline…); проход 2 — без падений; проход 3 — 4 падения (графовые тесты). Итого: 2 из 3 проходов с падениями, 5 падений.
  • После PR (cf8263f9, load average 107–131): в каждом проходе по одному падению, все в графовом семействе: graph::state::tests::a_chronically_unreadable_subtree_is_probed_but_never_rebuilt, tools::graph::tests::graph_data_requests_are_pool_only_under_held_lease, graph::state::tests::a_healed_subtree_is_found_by_the_probe_and_rebuilt. Итого: 3 из 3 проходов с падениями, 3 падения.

Load average в проходах «после» был ниже, поэтому прямое сравнение неточное.

Повторы каждого из 32 затронутых тестов по 20 раз под той же нагрузкой (изолированно, один тест на запуск):

тест до PR после PR
diagnostics_baseline::baseline_parity_partial_stale_error_recovery_and_cancellation 12/20 20/20
tasks::cancelling_a_working_handle_stops_the_read_and_frees_the_resident 19/20 20/20

У остальных 30 тестов из списка 20/20 и до, и после: в изоляции под этой нагрузкой они не воспроизводили падений, поэтому по ним эффект не измерен.

Графовые тесты, падавшие в полных проходах (повторы по 100 раз, изолированно, под нагрузкой):

тест до PR после PR
graph::state::tests::a_chronically_unreadable_subtree_is_probed_but_never_rebuilt 97/100 94/100
graph::state::tests::a_healed_subtree_is_found_by_the_probe_and_rebuilt 98/100 99/100
tools::graph::tests::graph_data_requests_are_pool_only_under_held_lease 99/100 97/100

Частоты до и после в пределах шума: PR этот флейк не лечит.

Без нагрузки: cargo test -p mcp-server (все 25 бинарников): 1815 passed, 0 failed, 23 ignored. Все бинарники завершились с кодом 0.

Что не сделано и что остаётся

  • Графовое семейство (graph::state::tests::*, tools::graph::tests::*) под полной нагрузкой падает примерно в 1–6% прогонов каждого теста — до и после одинаково. Причина в фоновой перестройке графа, которую тест ждёт счётчиком опросов. Это отдельная работа, в этом PR не закрыта.
  • Четыре графовых теста из списка «зависели от нагрузки» в изоляции под нагрузкой не падали ни до, ни после (20/20); одно падение до PR было в полном проходе. Поэтому не утверждаю, что их причина устранена; подтверждено только отсутствие их падений в трёх полных проходах после PR.
  • tests/tasks.rs::cancelling_a_working_handle…: порог «отменённый в 3 раза быстрее живого» на грани под нагрузкой (в одной из серий отношение 0,38). Ослабить до 1/2 или оставить — решение автора; сейчас оставлено как есть.
  • wait_until в diagnostics_state/test_support.rs: при медленной пробе провал растягивается до 20-минутного предела. Замечание ревью (low) принято, но не исправлено: любая правка либо снова вводит часовой срез (укорачивает успешные ожидания под нагрузкой), либо меняет предел.
  • references.rs: фикстура Толпа* ещё без Retired.
  • Оставшиеся sleep в тестовом коде: шаги опроса внутри счётчиков и фиксированные паузы перед записью на диск (наблюдатели файлов).
  • McpServer::shutdown_within не ограничивает join референсного воркера, baseline и sweeper — они остаются безграничными, как и в shutdown.
  • Часть падений из исходного отчёта (включая лог с 734 паниками) в нашей среде не воспроизвелась.

Ревью

Круги pio по коммитам: 487 (a47d6367) закрыт линзами; две находки mid исправлены в 3b95770b и записаны как подтверждённые. 488 (3b95770b) закрыт; одна находка low (wait_until, см. выше) подтверждена и не исправлена. 491 (cf8263f9) закрыт линзами, находок нет. Формальное закрытие pio недоступно: выбранный коммит не совпадает с HEAD корня, поэтому closureEligibility — false. Часть линз в отдельных кругах не отработала.

🤖 Generated with Claude Code

Елишев Кирилл and others added 16 commits October 14, 2026 20:57
`await_walk_start` ждал первый файл обхода не дольше 60 секунд по часам. Теперь ожидание
отсчитывает опросы: под нагрузкой каждый опрос растягивается, а бюджет по-прежнему истекает
только тогда, когда обход не начался вообще.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…рки порядка вместо часов

Тесты падали под нагрузкой процессора, потому что решали успех по часам: дедлайн в
секундах у ожидания состояния, порог в 500 мс у статуса, которому нужен лок, пауза
«для прогрева» перед гонкой. Продуктовое поведение не меняется; `McpServer::shutdown`
сохраняет пятисекундное ожидание владельцев, добавлен только `shutdown_within`.

Ожидания состояний:
- `eventually` считает опросы (6000 по 10 мс), а не время; `eventually_within` для ожидания,
  которое задаётся расписанием повторов (бутстрап, 30 с + 60 с).
- Рукописные циклы с дедлайном в тестовом коде переведены на `Patience` (счётчик опросов).
  В тестовом коде таких циклов не осталось.
- `wait_until_watching`, `poll_now`, `tick_now` больше не принимают таймаут: ожидание
  ограничено общим `STATE_CEILING` (60 с), он защищает от зависания, а не меряет скорость.
- Позитивные `recv_timeout`/`timeout` на события с пределами 5–30 с подняты до общего предела.
  Окна отсутствия (`!recv_timeout(100 мс)`) оставлены: они могут скрыть баг, но не вызвать
  ложный отказ.
- `graph/test_support`: `wait_until_within` и `hold_every_handle` считают опросы.

Владельцы и фикстуры:
- `tests/tasks.rs`, `tests/task_deadline.rs`: сервер останавливается и ждёт выхода фоновых
  владельцев до удаления стенда (`Retired`, `shutdown_within`). Раньше фоновая очистка
  `bsl-overlay-backlog` читала уже удалённые `Толпа*`.
- `tests/task_deadline.rs`: перед гейтируемой задачей индекс прогревается, чтобы срок
  попадал в фазу чтения, а не в фазу построения.
- `tests/diagnostics_baseline.rs`: `partial` проверяется после того, как рабочее пространство
  сообщило, что видит оба модуля (`files_total == 2`).

Гонки:
- lease: `hold_file_lock_for_test` ждёт освобождения держателя, а не считает, что lock свободен.
- `all_status_requests_are_cached_under_held_lease`: вместо порога в 500 мс проверяется
  журнал дисковых проверок (`disk_checks_on_this_thread`): статус не ходил за lease.
- `embedding_fence_*`, `an_embed_pass_in_its_retry_pause_is_not_live_work`: выход пасса
  ждётся по счётчику `OwnerStop::live`, держатель отпускается после второй попытки fence.
- `graph::*` (четыре теста из прогона под нагрузкой): `wait_settled` ждёт не только статус
  `Ready`, но и проход публикации, пустой слот сборки и отсутствие обхода (`walk_out`).
- Три теста без `WALK_GATE` читали общий счётчик обходов: теперь держат гейт.
- `a_cancelled_call_frees_the_resident_slot`, `cancelling_a_working_handle_...`: порог
  «не дольше 3 с» заменён сравнением с тем же ожиданием за незавершённым обходом.

Проверки скорости (было 41 `elapsed`-порог в тестовом коде):
- Неблокировка доказывается удержанием ресурса до ответа или журналом дисковых проверок
  (`snapshot`, `hybrid`, `state/mod`).
- Ответ сам доказывает результат: `Pending`/`Failed` вместо «быстрее 500 мс».
- Остались нижние границы (`>= floor`, `>= 200 мс`), они не нарушаются нагрузкой, и
  порядка величины 2× (15/30 с, 30/60 с, 300/600 с) — указано в тестах.

Проверено: `cargo fmt --check`, `cargo clippy -p mcp-server --all-targets --all-features
-D warnings`, `cargo test -p mcp-server --lib` и все интеграционные тесты без нагрузки
проходят; с нагрузкой результаты — в описании PR.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
- Patience считает шаг в наносекундах: шаг в микросекунды больше не усекается до миллисекунды.
- Циклы, которые крутились на `yield_now`, теперь делают шаг по времени: бюджет опросов
  больше не тратится за миллисекунды (сборка графа, тикер тестов сессии, перенос подписки,
  `yield_until` в tasks).
- Смоук-тест индексации снова ограничен своим бюджетом (150 с), а не общим пределом в 60 с.
- Тест shutdown базового сервиса ограничивает возврат десятью производственными
  ack-таймаутами: блокировку по-прежнему доказывает шлюз, а задержку — верхняя граница.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Бюджет ограничивает только ожидание владельцев. Остановка reference worker, baseline и
sweeper выполняется до него без изменений, как и в `shutdown`; документация теперь говорит
это прямо.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
- Watchdog изолированного фикстурного процесса снова отмеряет часами: это бюджет приёмки
  (150 с и 30 с), а не ожидание состояния, и сообщение называет секунды.
- `walk_out` перенесён выше документа `reserve_probe`, чтобы документация не съехала.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
- Проверки задержки ответа на отмену возвращены: `CANCEL_BOUND` (500 мс) как явный контракт
  скорости в `cancel_tests`, его проверят повторные прогоны под нагрузкой.
- Потолки 120 с (resident) и 90 с (владельцы графа) возвращены к прежним значениям:
  сокращение до 60 с ужесточало тесты там, где ожидание самое длинное.
- Контроль `a_cancelled_call_frees_the_resident_slot` меряет холостой вызов на свежем
  резиденте, который обход не трогал.
- Условные ожидания в гейтах `PollGate` и `AnnounceBarrier` списывают бюджет только на таймаут
  (общий шаг `wait_step`), а пробуждения ограничены отдельным пределом.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…аний)

- Ожидание занятости lifecycle в lease-тесте спит между опросами, а не крутится на yield.
- Шаг опроса в metadata равен реальной паузе цикла (100 мс): бюджет 60 с, а не 600 с.
- Отрицательный контроль владельца снова ждёт 2 с: окно отсутствия не сжимается.
- `connect_within` использует свой бюджет (раньше параметр игнорировался).
- Бюджеты ожидания из базы (180/120/90 с) возвращены в name_dictionary, outline,
  references, location_contract и query_validate.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…онде

Проверка текста не ловит чистую задержку: статус, который ждёт зонда, но возвращает тот же
текст, прошёл бы тест. Возвращена проверка «не дольше 500 мс» как явный контракт скорости
(так же, как для отмены поиска).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Ожидания, чьим единственным детектором задержки была граница по времени, снова ограничены
исходными значениями: явные проверки `elapsed` (200/500 мс, 1/2/3/5 с, 100 мс) и короткие
ожидания «быстро» как счётчики опросов с исходным номиналом (100 и 200 опросов по 10 мс).
Контроль `a_cancelled_call_frees_the_resident_slot` снова абсолютный (сосед за живым обходом
ждёт больше 1 с): нагрузка только удлиняет такое ожидание, ложного падения не будет.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
- `disk_checks_on_this_thread` на безымянном потоке сравнивает с той же меткой
  `"<unnamed>"`, что и запись: проверка «на этом потоке» не становится пустой.
- Ожидания по опросам имеют стенной предел-страховку 20 минут: дорогой опрос не растягивает
  путь падения на часы. Успешный путь он не касается.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
- Тест «владелец, которому велено уйти, отвечает Stopping при занятом движке» запускает
  вызовы в отдельном потоке и ждёт ответ не дольше секунды: регрессия даёт падение, а не
  зависание.
- Матрица отмены снова проверяет задержку ответа (500 мс) у каждой ячейки; предел 60 с
  остаётся как страховка от зависания.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
- Бюджет, который не помещается на часах (`shutdown_within(Duration::MAX)`), ждёт срезами по
  часу, а не паникует при сложении `Instant`.
- Комментарий в тесте shutdown называет предел 200 мс тем, чем он является: исходным
  контрактом задержки (100 мс ack-таймаут в тестовой сборке, предел вдвое больше).
- Паника 20-минутного предохранителя в `wait_until` называет предохранитель, а не число опросов.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
… теста

Тело теста с #[tokio::test] выполняется на его собственном потоке, а ветка status отвечает
инлайн, поэтому фенс статуса записан под именем потока теста. Фильтр по рантайм-воркерам его
пропускал: регрессия не ловилась. Теперь считаются фенсы текущего потока. Мутация (фенс в ветке
metadata status) валит тест с «a status call fenced the lease».

Также: предел shutdown-теста 1 с (десятикратный ack-таймаут, как написано в комментарии), а
сообщение probe-теста снова называет фактическую длительность.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…ниченное тело опроса

Ревью круга 487 (линзы перебор) дало два mid-замечания, оба подтверждены по коду.

- OwnerStop::wait_empty с бюджетом, который не помещается в Instant, ждал часовыми срезами,
  но после первого истёкшего среза при живом владельце возвращал false. Теперь такой бюджет
  заканчивается только уходом владельцев; ограниченный бюджет ведёт себя как прежде.
  Цикл вынесен в wait_empty_in_slices(timeout, slice), чтобы тест мог взять срез в 10 мс.
- Тело eventually в state/sync.rs спит 200 мс на попытку, а бюджет считал попытки по 10 мс:
  провал растягивался до 20 минут (HANG_GUARD). Теперь eventually_within(100, …) — около 21 с.

Продуктового поведения это не меняет: shutdown по-прежнему ждёт владельцев пять секунд.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
… первой устарелости

Тест baseline_parity_partial_stale_error_recovery_and_cancellation под нагрузкой падал в 8 из 20
прогонов базы и в 10 из 20 после прошлых правок: ответ уже был stale, но baseline ещё `full`.

Правка файла (drift) наблюдается раньше, чем нечитаемость файла (дыра). Хелпер ждал только
stale и отдавал первый такой ответ, а утверждение требовало state `partial`. Теперь хелпер
(workspace_until_partial_stale) ждёт обоих фактов по счётчику опросов. Дыра, однажды
возникнув, держит partial до лечения, так что ожидание сходится в одну сторону.

В сообщение проверки добавлено тело ответа: падение теперь показывает, что именно наблюдалось.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
@itrous
itrous merged commit 34a3fba into develop Oct 14, 2026
4 checks passed
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