Files
rust-xmpp/docs/DURABLE-MESSAGE-IO.md
T
wt 821e1c3e2f
Test, build and publish / All tests and Linux binary (push) Failing after 19s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
Версия 0.8.3 удерживала глобальный Store.slots mutex на весь durable
commit
(включая fsync/append). fsync одного канала задерживал SM-коммиты всех
остальных
сессий; при медленном диске и нескольких пользователях это выглядело как
«сервер
перестал отвечать на запросы» и секундная доставка сообщений.

- Глобальная блокировка разделена: короткий lock `slots` теперь только
  валидирует token/limits и резервирует слот; сам append+fsync идёт под
  per-session `wal` lock. Один медленный канал больше не блокирует
  остальных.
- Ротация/compaction/GC сериализуются отдельным `maintenance` lock вне
  любого
  per-session lock — очереди append и compaction для одного WAL не
  гонки,
  дедлоков нет, инварианты durable и crash-тесты сохранены.
- Все существующие crash/latency/recovery regressions зелёные без
  изменений.

Для genuinely медленного диска (VPS с медленным fsync) добавлена опция
как в
Prosody, где исходящие SM-stanza живого клиента идут в in-memory smqueue
без
fsync, а durable пишутся счётчик/h и переход в hibernation.

- `smacks_online_durable = false` (strict — default): live-сессии не
  fsync на
  каждый outbound/ack, а помечают `dirty`; фоновый проход
  `smacks_flush_interval` (default1с) и offline-переход фиксируют
  durable.
- Offline/hibernating цели в любом режиме фиксируются durable ДО
  доставки —
  сохраняются критичные инварианты («Alice отправила, Bob ещё не
  авторизован»)
  и crash-регрессии; отзыв токенов/expiry остаётся durable.
- Pre-ACK durable гарантию даёт строгий режим; deferred — компромисс
  Prosody
  «низкая задержка» в обмен на ограниченное окно потери для
  online-пиров.

- `cargo test --locked --release`: 54 binary, 336 passed, 11 ignored (0
  fail).
- Strict crash (`slow_storage_crash_network`), latency
  (`slow_storage_latency`),
  `sm_restart`, `journal_recovery_sync`, личная доставка — все зелёные.
- Новые регрессии: `tests/deferred_sm_online_network.py` (wire, deferred
  режим:
  offline durable до ACK, online доставка + flush persist), два Rust
  unit-теста
  в `tests/sm_journal.rs` (deferred online commit/flush, offline
  durable).
- `push_latency_network.py` в этой среде падает на TLS CA `keyUsage`
  из-за более
  строгого TLS в Python 3.14 (до SM-кода, не связано с этим изменением).

Документация: [docs/DURABLE-MESSAGE-IO.md](docs/DURABLE-MESSAGE-IO.md),
[docs/COMPATIBILITY.md](docs/COMPATIBILITY.md).
2026-10-21 14:43:59 +07:00

11 KiB
Raw Blame History

Личные сообщения и durable IO в 0.8.3

В присланном журнале сервера 0.8.2 есть три message-processing-slow с 4792–5549 мс, route=local. Этот таймер начинается после приёма XML и заканчивается после mark_handled: ожидание происходит внутри обработчика. Старые события не содержат разбивки по этапам и сами по себе не доказывают причину медленного IO. Исходный пользовательский журнал не включён в архив проекта.

Изменение записи

До 0.8.3 append в MAM требовал 4 последовательных sync: rollback marker file+dir, native list, удаление marker+dir. Обычное сохранение SM требовало 2: JSON file+dir. В TLS fixture1:1 с двумя аккаунтами, четырьмя SM-ресурсами и carbons это обычно 16 sync на сообщение. Увеличение задержки каждого настоящего fsync/fdatasync внутри изолированного data_path воспроизводит задержку в несколько секунд. Interposer не пропускает sync и не меняет системные настройки/production.

В 0.8.3 принятые данные сначала фиксируются в журнале восстановления с checksum:

  • MAM .rredo: native .list сохраняет формат Prosody; обычный append требует одного sync журнала. Replay восстанавливает недостающий native suffix и отбрасывает незавершённый хвост. Полностью записанная повреждённая запись вызывает ошибку. Ротация —128 frames/1 MiB плюс максимум одна 16 MiB запись.
  • SM .wal: authoritative полные snapshots и tombstones; readable JSON — восстанавливаемый экспорт. Обычное сохранение требует одного sync_data. Ротация —16 MiB плюс максимум один snapshot; aggregate WAL cap 512 MiB, logical slots по-прежнему 256 MiB/8 MiB на slot/4096 stanzas и 4096 sessions. Tombstone GC сначала синхронизирует удаление JSON, затем удаляет WAL.

Recovery синхронизирует полностью проверенный журнал перед публикацией восстановленного состояния: полный frame после SIGKILL может остаться только в page cache. Checksum подтверждает целостность, но не запись на диск. Ошибка этого sync запрещает startup/handoff.

Создание журналов, восстановление и ротация дополнительно синхронизируют файлы и каталог. Экономия относится к обычной записи, не каждому возможному запросу. Отключения durability или периодической записи вместо commit-before-ACK нет. При неопределённой ошибке SM IO store блокирует дальнейшие изменения до восстановления. После успешного WAL commit ошибка экспорта не откатывает память. Queues/archives не образуют общей транзакции с сетью: гарантия replay остаётся at least once, а не exactly once между всеми хранилищами.

Передача данных Prosody и откат версии

После SIGKILL/OS crash сначала используйте 0.8.3 для восстановления redo. Не удаляйте .wal/.rredo вручную и не редактируйте JSON export как источник истины. Для смены writer или отката на старую версию остановите сервер и выполните:

rust-xmpp storage-checkpoint /etc/prosody/prosody.cfg.lua

Команда берёт data-directory lock и SM store lock, восстанавливает журналы, синхронизирует native archives и актуальные JSON, затем durably удаляет redo. При ошибке передача владения не завершена; разберите ошибку, не запускайте другой writer. Перед работой с production данными сохраните обычную резервную копию всего data_path. Native Prosody не понимает resumable SM snapshots Rust; checkpoint SM обеспечивает совместимый JSON для старого Rust, не переносит TLS/TCP/SM-сессию в Prosody. Простой graceful stop не заменяет эту явную команду. Нельзя одновременно изменять .list средствами Prosody и Rust. Неjournaled in-place append при активном redo считается незавершённым и может быть отброшен. При восстановлении согласованной копии всего data_path журнал остаётся авторитетным независимо от смены inode файла. Любая внешняя модификация, включая atomic replacement, требует handoff. Внутренний rewrite сначала фиксирует старый native snapshot и удаление redo, затем атомарно заменяет native файл: авария в промежутке сохраняет старый snapshot, после замены — старый/новый. Редактирование baseline in place без handoff не поддержано. Anchor проверяет последние 4 KiB baseline, не является checksum всего старого архива.

Диагностика

message-processing-slow по-прежнему имеет threshold 250 мс, duration_ms, route и connection. Новые поля в integer milliseconds:

Поле Измеряемый этап
pre_routing_ms Работа до личного MAM; для MUC/ранних выходов — обработка до handled
sender_mam_ms Политика и архивирование отправителя
recipient_mam_ms Проверка аккаунта, политика и архивирование получателя
delivery_ms Маршрутизация, targets, mutex и transmit
carbons_ms Выбор ресурсов и отправка carbon
offline_push_ms Offline commit и подготовка/enqueue push
handled_ms Session mutex и durable mark_handled

Неисполненные этапы равны 0. target_count — выбранные локальные ресурсы, carbon_count — успешные carbon transmit. Это серверные этапы, не время отображения клиентом. Тексты сообщений, SASL и токены не логируются.

Проверки и границы

tests/slow_storage_latency_network.py — реальный STARTTLS, MAM/carbons/PEP, контролируемый sync delay и счётчик операций. tests/slow_storage_crash_network.py — sender SM ACK, немедленный SIGKILL, проверка committed journals/обоих архивов до повторной аутентификации и replay всех ресурсов. Rust regressions проверяют partial tails, corruption, ротацию, tombstones, migration и offline handoff. Отдельный journal_recovery_sync_network.py вводит scoped EIO для WAL/redo и проверяет отказ startup/handoff без потери journal и успешный повтор. Точные измерения и результаты этой сборки — VALIDATION.md.

Общий mutex SM store, обязательное durable IO, большие pending snapshots, полные MAM queries/maintenance, PEP storage и federation всё ещё имеют стоимость. Медленный диск не становится быстрым от сокращения числа sync. Проверка на loopback не устанавливает причину каждого production warning; новые phase fields позволяют проверить это на целевой системе.

0.8.4: разблокировка и deferred online durability

Глобальный Store.slots mutex ранее удерживался на весь commit (включая fsync), так что fsync одной сессии блокировал durable-коммиты всех остальных — при медленном диске и нескольких пользователях это выглядело как «сервер перестал отвечать на запросы». Теперь речь накладывает короткий lock slots только на валидацию/резервирование, а сам append+fsync идёт под per-session wal lock. Ротация/compaction/GC сериализуются отдельным maintenance lock вне любого sessions wal lock — дедлоков нет и инварианты durable сохранены.

Для genuinely медленного диска добавлена Prosody-совместимая опция smacks_online_durable = false (strict — default). В режиме live-сессии с открытым transport не fsync на каждый outbound/ack, а помечают dirty; фоновый проход smacks_flush_interval (default1с) и offline-переход фиксируют состояние durable. Offline/hibernating цели фиксируются durable ДО доставки в любом режиме (сохранены crash-инварианты и регрессия Bob-online-не-авторизован). Резюме wire-проверки: strict crash/latency тесты зелёные; deferred прошел tests/deferred_sm_online_network.py; семантику store проверяют два новых Rust unit-теста в tests/sm_journal.rs. Полную durable-гарантию пред-ACK даёт строгий режим; deferred — это компромисс «максимальная совместимость с Prosody и низкая задержка» в обмен на ограниченное окно потери для online-пиров.