Seed-driven стресс-тест PG-машинерии (src/test/test_pg_chaos.cpp): ОДИН настоящий osd_t (primary) с настоящим хранилищем на заглушке диска (полностью в RAM, ringloop_mock) владеет одной PG (номер 1, пул 1, объекты — 4 шт по 128 К, гранула 4 К). Остальные члены PG — фейковые вторичные OSD внутри теста. PG: реплицированная x3 (members 2,3, spare 4) или EC 2+2 (members 2…4, spare 5, объект = один parity-stripe). Сид однозначно задаёт конфигурацию и всю последовательность событий; упавший прогон воспроизводится тем же --seed.

Проверяется в первую очередь PG-машинерия (subop-веер, peering/LIST/rollback/stabilize, repeer-демоны, stop_pg, INCOMPLETE, recovery) и частично хранилище первичного OSD; однако, проверка хранилища — не главная цель теста, его покрывает хаос-тест blockstore.

Конфигурации (10)

  • replicated/EC ×
  • immediate_commit none/all ×
  • csum_block нет/4096/16384

--recovery включает recovery/rebalance после set-change (по умолчанию выкл, чтобы гонять degraded-состояния).

Модель мира

Для каждого объекта — упорядоченная история версий; версия пишется в модель в момент SUBMIT (дельта = точные байты/длина/битмап гранул запроса), состояние:

  • V_SUBMITTED — операция отправлена, ответа ещё нет (выполняется). Ведёт себя как V_OPTIONAL во всём, кроме одного: параллельные чтения вправе не показывать результат этой записи, и это не будет маркировать её DEAD.
  • V_OPTIONAL — запись отправлена, но не подтверждена, и может быть либо применена (но тогда целиком), либо не применена вообще. Версии попадают в это состояние из V_SUBMITTED и, в режимах с явными fsync, V_ACKED при гибели клиентской сессии до запроса SYNC (это считается маркером, что запись не подтверждена). Наблюдение эффекта OPTIONAL записи в последующем чтении восстанавливает её в ACKED; наблюдение её отсутствия (когда она не видна в своём диапазоне) ⇒ DEAD.
  • V_ACKED — запись подтверждена; запоминается ack_live (живой набор на момент ack — используется для проверки превышения модели отказов). При immediate_commit=all сразу считается зафиксированной на диске. ACKED запись не должна откатываться, пока сессия клиента жива.
  • V_SYNCED — подтверждена и SYNC-нута ⇒ зафиксирована на диске и более не должна откатываться вообще.
  • V_PARTIAL — удалена, но, возможно, не со всех OSD (PG осталась в LEFT_ON_DEAD).
  • V_DEAD — версия либо точно отказана (-EAGAIN, возможно -ENOSPC в будущем), либо была OPTIONAL, но после этого стало точно известно, что она откачена, так как последующее чтение её уже НЕ наблюдало. Повторное появление DEAD-версии — провал теста.

Содержимое объектов самоидентифицируется версией: паттерн кодирует объект, версию и смещение — прочитанное можно декодировать обратно в «какая версия записала эти байты».

Верификация читает ТОЛЬКО через первичный OSD и проверяет, что ответ объясняется слиянием ОДНОГО согласованного подмножества записанных версий, где все зафиксированные точно присутствуют, все откаченные точно отсутствуют, а содержимое не врёт об авторстве. При неполном удалении разрешается INCOMPLETE в EC, а в репликах допускается воскрешение объекта.

Всё выводится из состояний версий и событий, без дополнительных флагов: ACKED не может исчезнуть, кроме как через событие, которое сначала ломает гарантию сессии — краш (уносит сессию вместе с процессом) или repeer/stop_pg (останавливают dirty-сессии ДО начала пиринга).

Проверки в тесте смотрят ТОЛЬКО на то, как процесс записи видит клиент, и не учитывают в модели, например, падения парных OSD сами по себе, так как клиент не может о них знать.

Симулируемая реальность

  • Симулятор диска: запросы с задержками; power_loss() = потеря питания — незафиксированные записи теряются, зафиксированные остаются. Неизвестный FD/opcode → падение теста (защита заглушки).
  • Симулятор etcd: ключ/значение + lease; revoke/TTL эмулируются при крахе (ключи OSD’а удаляются, lease отзывается).
  • Симулятор парных OSD: отвечает в соответствии с контрактом вторичного OSD / blockstore. Чтение/запись/удаление, SYNC, LIST. При падении парного OSD не-SYNC-нутые записи гибнут с вероятностью peer_cache_loss% (по умолчанию 50 — моделирует волатильный кэш диска), SYNC-нутые выживают всегда. Модель мира на краш пира сама НЕ реагирует: она знает только то, что видит клиент. Краш реперит PG, репер убивает dirty-сессии клиентов (volatile ACKED — это и есть dirty-клиент), сессия гибнет — вот это клиент наблюдает (PEER_STOPPED/EPIPE), и только session_died() делает demote. Если краш откатит что-то при живой сессии — это ненаблюдаемый клиентом откат, проверка 5 его ловит.
  • Симулятор сети: service_peers, каждый раунд. Задержка на этапе запроса — применяются случайные --p-wire% (по умолчанию 70) очередей subop’ов пира; задержка на этапе ответа — доставляются случайные p_reply% применённых ответов. Ответ принадлежит соединению: соединение умерло — недоставленные ответы гибнут с -EPIPE (применение остаётся). Однако, при гибели экземпляра OSD мутирующие операции в sent_ops копируются в отдельную очередь «сети»; применяются позже (--p-wire%) БЕЗ ответа (соединение упало). Немутирующие операции (READ/LIST) отбрасываются (отвечать некому), очередь собранных операций ограничивается 64.

Операции (step)

На каждом шаге (веса --p-*; 0 выключает действие):

  • wait (500) — ничего не слать, прокрутить симуляцию (событийные окна).
  • write (60) — запись случайного диапазона случайного объекта; увеличение версии; дельта запоминается как OPTIONAL; ≤ max_inflight (8) одновременно.
  • delete (6) — удаление случайного объекта; версия запоминается как OPTIONAL с нулевой длиной (полное удаление); LEFT_ON_DEAD/ack отслеживаются в account().
  • read (40) — чтение объекта, полное с вероятностью full_read%, иначе произвольный диапазон; при полном чтении объект проверяется.
  • sync (30) — синхронизация кэша (fsync); при подтверждении версии помечаются SYNCED (зафиксированными).
  • restart (1) — ЧИСТАЯ перезагрузка OSD: очистка очереди (все inflight закончены), ожидание простоя диска, уничтожение, пересоздание, ожидание подъёма OSD и, с шансом --p-post, чтение и проверка содержимого всех объектов («чистый перезапуск, который ничего не потерял»).
  • takeover (1) — передача PG: сначала остановка PG через смену первичного OSD на фейковый в заглушке etcd → stop_pg останавливает PG; далее сразу после удаления ключа /pg/state/1/1 (означающего, что OSD остановил PG) → пересоздание OSD и его переустановка в качестве первичного → перепроверка объектов, как в restart.
  • crash (2) — падение первичного OSD: потеря питания диска (потеря не-синкнутых записей), отмена лизы + удаление ключей OSD без отмены операций в полёте к парным OSD, запуск нового экземпляра, перепроверка объектов.
  • peer_down (3) — крах парного OSD: выбирается жертва, не ломающая кворум ни одного объекта (проверяются все последние OPTIONAL версии до первой SYNCED), парный OSD останавливается, соединение с ним разрывается.
  • peer_up (3) — подъём парного OSD (соединение — если PG или её история его ещё касается); после — settle(): ждать подъёма PG (ACTIVE|INCOMPLETE + покой), затем прогнать recover_pending — отложенные проверки объектов, чьи цепи могли перегласоваться.
  • set_change (1) — переезд PG на другой OSD-набор через etcd; с --recovery — восстановительные чтения; без — деградация легальна, мир «выживших копий» защищён проверками.

В конце — ожидание завершения: очистка до «PG в порядке и все операции завершены», максимум до MAX_STEPS=1e6 шагов → если превышается, то падение с ошибкой «did not settle».

Учёт результатов операций (account)

  • Завершена с ожидаемым кодом возврата → V_ACKED. Удаление без LEFT_ON_DEAD закрывает окно частичного удаления. Удаление с LEFT_ON_DEAD требует повтора после подъёма пиров, а с EC допускает будущий INCOMPLETE, если объект не перезаписан вновь.
  • Завершена с EAGAIN → V_DEAD: версия точно не применена; в тесте сейчас недостижимо, оставлено как заготовка на будущее, также применимо, например, к ENOSPC.
  • Завершена с EPIPE → версия остаётся OPTIONAL: отказ — не ack, но операция могла примениться (а могла и нет). Опциональное удаление также требует повтора после подъёма пиров и тоже разрешает EC INCOMPLETE до следующей перезаписи.
  • Завершена с другой ошибкой → падение теста («failed with retval»).

Проверки (все точки abort)

Согласованность данных (world_check):

  1. world_check — ответ обязан объясняться подмножеством версий: все обязательные (зафиксированные) присутствуют; байты отвечают ровно своей версией («record obj/seq/off mismatch», битмап-гранула «carries data from wrong version»).
  2. «shows %s but the highest…» — прочитана старая версия объекта, последние записи потеряны (монотонность нарушена).
  3. «read shows seq over acked v’s granule» — содержимое гранулы (obj/seq/off в record) противоречит версии, которой гранула принадлежит по модели: сборное/порванное содержимое вместо подтверждённых байт.
  4. «DEAD version showed again» — откаченные версии не должны возвращаться.
  5. «committed version read fully absent over floor» / «established version read absent over its granule» — подтверждённые операции не должны исчезать.
  6. read -EIO «OSD deems unrecoverable» — легален ровно в одном окне: незавершённое EC-удаление. Вне его -EIO — падение теста, в т.ч. «чистый» ack удаления + INCOMPLETE после revival (OSD соврал о полноте) и откат цепей при живой сессии без окна удаления; прочие retval ≠ len → падение теста.
  7. bitmap_len ≠ ожидаемому в ответах чтения → падение теста.
  8. «came back INCOMPLETE» после set_change/crash — тот же критерий: окно незавершённого удаления ⇒ INCOMPLETE легален и удаление повторяется; в ином случае → падение теста.
  9. «came back … which no world of the recorded versions explains» — финальная проверка корректности содержимого объектов после завершения теста.
  10. «changed across a clean restart that lost nothing» — неизменность объектов после перезапуска.

Живость/тайминг:

  1. «did not settle in MAX_STEPS» — операции не завершились за большое число шагов (возможна самоблокировка);
  2. «operations did not complete» — залипшие операции, печатаются в дампе; «a read of object N did not complete in MAX_STEPS … deadlock?»; «OSD did not load PGs from etcd in MAX_STEPS» / «OSD did not finish peering in MAX_STEPS» — проверка на старте OSD.
  3. «did not release the PG» — OSD не отпустил PG по команде;
  4. «disk did not go idle» — проверка против use-after-free, что все операции диска завершены перед уничтожением OSD;
  5. «PG did not become healthy after reviving every peer» — PG не поднялась после подъёма всех OSD;
  6. «pending recovery/repeated deletion could not complete on a healthy PG» — удаление/восстановление не завершено на живой PG.

Защиты тестовых заглушек:

  1. Неожиданный SQE (fd/opcode) на заглушке диска; «unexpected secondary opcode» у пира; аргументы --p-reply ≥ 1, --max-inflight ≥ 1 и т.п.

Проверки покрытия:

  1. peer_cache_loss<100 + volatile-конфиг + ≥8 смертей пира ⇒ проверка, что хотя бы раз выполнен вариант, в котором парный OSD не потерял журнал.
  2. ≥10 грязных смертей OSD/передач PG ⇒ wire_harvested > 0: хотя бы одна операция первичного OSD, бывшая в полёте, обязана была собраться в «сеть» при остановке — путь «доставки от мёртвого OSD» выполнен.

Отчёт и воспроизводимость

OK-строка: writes(refused)/deletes/syncs/crashes/restarts/takeovers/peer deaths (kept journal)/recoveries/set changes/verified reads/stragglers (applied/captured/lost)/disk ops.

Детерминизм: один seed ⇒ та же конфигурация и то же расписание; -v — трасса операций диска и парных OSD, захватов операций при падении пиров и изменений etcd.