Compare commits

...
13 Commits
Author SHA1 Message Date
wt e17ce5504e some
Test, build and publish / All tests and Linux binary (push) Failing after 22s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
2026-10-21 20:03:10 +07:00
wt d1492f3b44 test
Test, build and publish / All tests and Linux binary (push) Failing after 22s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
2026-10-21 19:49:17 +07:00
wt 29176eb90b ci: run mam_broken_baseline_network in verify-local
Test, build and publish / All tests and Linux binary (push) Failing after 27s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
2026-10-21 18:59:20 +07:00
wt 75bb730d3c docs: MAM baseline 0.8.4 — best-effort archival and offline repair
Document the 'archive redo baseline changed in place' failure, the runtime
session isolation, the offline storage-checkpoint repair, and the Prosody
native-format comparison (list_append in-place vs list_store atomic rename).
2026-10-21 18:57:51 +07:00
wt 100005379b MAM archive storage fault must not drop live message delivery
Test, build and publish / All tests and Linux binary (push) Failing after 26s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
Monal-iOS reconnect-looped with connection-closed reason=
protocol-or-transport-error on every message. The cause was a damaged
MAM archive redo baseline ('archive redo baseline changed in place'): the
native archive at data/<host>/archive/<user>.list no longer matched the
anchor recorded in its .rredo journal, so every archive_for_user() call
returned Err and the '?' propagated up through authenticated_transport,
turning a storage fault into a dropped session.

Delivery is an attribute of MAM, not a prerequisite. Make archival
best-effort at the three inbound/outbound message sites (sender,
recipient, incoming federation): on storage failure the original stanza
is still routed, both sessions stay alive, and a sanitized
'mam-archive-skipped' log is emitted instead of a connection-closed.

Offline storage-checkpoint now repairs the damaged journal: prepare_rewrite
detects an untrusted baseline (changed in place, shorter, missing, or
journal overflows bound), treats the durably synced native as authoritative
and retires the untrusted journal, so MAM history reads and future appends
work again after handoff. Live appends remain fail-closed (the journal is
preserved, never silently dropped).

Tests: redo_rewrite_tests::runtime_append_fails_closed... and
offline_checkpoint_retires_untrusted_redo_baseline; new network regression
mam_broken_baseline_network.py proves message delivery and both sessions
survive a broken baseline and that storage-checkpoint restores MAM reads.
2026-10-21 18:52:53 +07:00
wt c71aca637c Update server.rs
Test, build and publish / All tests and Linux binary (push) Failing after 17s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
2026-10-21 17:54:18 +07:00
wt 5b8ee4f4ad Revert "Версия 0.8.3 удерживала глобальный Store.slots mutex на весь durable"
Test, build and publish / All tests and Linux binary (push) Failing after 14s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
This reverts commit 821e1c3e2f.
2026-10-21 15:23:34 +07:00
wt 821e1c3e2f Версия 0.8.3 удерживала глобальный Store.slots mutex на весь durable
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
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
wt bff609097b - Уменьшено число последовательных синхронизаций до доставки: ordinary
Test, build and publish / All tests and Linux binary (push) Failing after 30s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
MAM
  append использует один sync вместо четырёх, SM checkpoint — один
  вместо двух.
  Checksummed redo journals сохраняют commit-before-ACK и
  восстанавливают
  native archive/SM JSON exports после аварии.
- Ограничены рост журналов, ротация и tombstone GC. Старый `.rappend` и
  legacy
  JSON импортируются. Retain/save/purge не возвращают удалённые записи.
- Recovery синхронизирует валидный journal перед публикацией состояния;
  проверка
  checksum не заменяет durability barrier. EIO при adoption блокирует
  startup
  или offline handoff. Новые fault fixtures проверяют сохранность
  журнала и retry.
- Replay SM проверяет checksum/JSON всех frames, но создаёт и валидирует
  полный
  Record только для последнего snapshot, сокращая работу с superseded
  queues.
- `message-processing-slow` содержит времена MAM отправителя/получателя,
  доставки, carbons, offline/push и handled, а также число
  ресурсов/копий.
  Payload и секреты не добавляются в логи.
- Добавлена offline команда `storage-checkpoint CONFIG`: durable экспорт
  и
  передача archive writer/подготовка отката на legacy Rust. Не
  запускайте другого
  writer и не удаляйте journals до успешного завершения команды.
- Новые crash, partial-write, corruption, EIO, recovery, handoff и
  latency
  регрессии включены в GitHub Actions через verify-local.sh; отчёты
  сохраняются.

Измерения, ограничения и эксплуатация:
[docs/DURABLE-MESSAGE-IO.md](docs/DURABLE-MESSAGE-IO.md),
[docs/VALIDATION-0.8.3.md](docs/VALIDATION-0.8.3.md).
2026-10-21 13:14:19 +07:00
wt e9329ccbb2 - Исправлена задержка доставки MUC с растущим архивом: после каждого
Test, build and publish / All tests and Linux binary (push) Failing after 23s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
сообщения
  больше не выполняется полный scan/rewrite архива для expiry. Очистка
  остаётся
  в maintenance и перед MAM-запросами; append и crash checkpoint до
  fanout сохранены.
- Неизменённые native config/persistent index не перезаписываются при
  каждом
  сообщении. Удалённые файлы восстанавливаются, ошибки экспорта
  повторяются.
- CSI больше не сохраняет SM snapshot при отсутствии изменения durable
  очереди.
  Изменённая очередь сохраняется до отправки; при ошибке записи очередь
  откатывается.
- Личные сообщения, IQ и входящая федерация к аккаунтам больше не
  захватывают
  общий MUC mutex, ожидая несвязанную работу с комнатными архивами.
- Добавлены регрессии native exports/archive scanning/CSI rollback и
  реальный
  TLS latency check в verify-local.sh, автоматически включённый и в CI.

Измерения и границы подтверждения:
[docs/MESSAGE-LATENCY.md](docs/MESSAGE-LATENCY.md).
Результаты итогового прогона:
[docs/VALIDATION-0.8.1.md](docs/VALIDATION-0.8.1.md).

- Личные сообщения и SM ACK больше не ждут сетевую отправку push.
  Уведомления
  отправляются через очередь1024 с максимум8параллельных маршрутов;
  worker
  и его задачи отменяются при завершении serve. Ошибки очереди/маршрута
  логируются.
- Offline сообщение сохраняется до публикации push, чтобы проснувшийся
  клиент
  мог сразу получить его. Подтверждена очередь при первом TLS stream
  push gateway.
- По умолчанию CSI считает важными bodyless OMEMO legacy/2, delivery
  receipts
  и chat markers. Явный список `csi_important_payloads` заменяет
  расширенные defaults.
  Низкоприоритетный headline по-прежнему буферизуется. Policy
  обновляется и при SM resume.
- Выбор адресатов и carbons фильтрует владельцев до захвата session
  mutex;
  зависшая сессия чужого аккаунта не задерживает этот путь. Duplicate SM
  ACK
  без изменения durable counters/queue не переписывает snapshot.
- Bind2 и MAM metadata IQ читают только первый/последний native record
  вместо
  сканирования всей истории. Индекс не требуется; recovery/лимиты записи
  сохранены.
- Добавлен `message-processing-slow`: обработка message дольше250мс,
  поля
  duration_ms/route, без payload/токенов. Это время обработки сервером,
  не peer receipt.
- Новые сетевые регрессии включены в verify-local.sh/GitHub CI.

Причины, изменение CSI policy, измерения и ограничения:
[docs/PERSONAL-MESSAGE-LATENCY.md](docs/PERSONAL-MESSAGE-LATENCY.md).
2026-10-21 11:20:09 +07:00
wt b798b3f9b7 added AGENTS.md
Test, build and publish / All tests and Linux binary (push) Failing after 15s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
2026-10-20 17:50:43 +07:00
wt 25fa51c76d added ci
Test, build and publish / All tests and Linux binary (push) Failing after 28s
Test, build and publish / Container smoke test and Gitea publish (push) Skipped
2026-10-20 17:41:56 +07:00
wt 7f60661eef - MUC сохраняет согласованный журнал конфигурации, affiliations, nick,
ролей,
  членства и ограниченной истории без MAM. Изменение фиксируется до
  публикации
  ответа; при ошибке записи состояние комнаты откатывается.
- Crash recovery использует последнее сохранённое поколение после
  SIGKILL,
  включая временные комнаты. Heartbeat обновляет живое членство;
  ожидающие записи сохраняют исходный deadline через повторные
  перезапуски.
  `muc_recovery_grace_period`: по умолчанию 300 секунд, пределы
  1..604800.
- Подтверждённые выходы, смена nick, ролей и политик комнаты не
  заменяются
  старой записью восстановления. Native config/state остаются экспортом
  для
  Prosody; согласованное восстановление Rust использует собственный
  sidecar.
- Исправлены MUC probe, неизвестные типы presence, вход без MUC `<x/>`,
  ошибки
  доставки и статус 333. Обычные presence extensions сохраняются в живом
  обмене; поддельная MUC identity фильтруется. `subject` вместе с `body`
  либо
  `thread` не изменяет тему комнаты.
- Подключены `muc_max_rooms`, `restrict_room_creation`, политики
  регистрации
  и зарегистрированного nick, `muc_tombstone_expiry`; добавлены
  встроенные
  административные ad-hoc команды уничтожения комнат и изменения
  affiliation.
- Hub сохраняет resumable XEP-0198 сессии, счётчики и очередь. Resume
  после
  перезапуска доступен на новом TLS/SASL-транспорте через legacy SASL и
  SASL2.
  Действующие приватные SM-записи восстанавливают логические сессии и
  MUC
  до открытия listeners: offline-доставка попадает в очередь,
  синтетический
  join и история повторно не отправляются. SM resume не требует
  дополнительной
  available presence; без SM восстановление MUC требует новой
  аутентификации
  точного полного JID и available presence либо явного MUC-входа.
  Входящий handled-счётчик обновляется после обработки. Сокеты TCP/TLS
  не сохраняются; неопределённая сетевая отправка допускает повторную
  доставку.
- Известные встроенные MUC-возможности Prosody отображаются на
  Rust-реализацию.
  Произвольные Lua/community-модули не исполняются; неизвестные явно
  включённые
  MUC-модули отклоняются при запуске. Общие и host `modules_disabled`
  учитываются
  для vCard и occupant-id.

Порядок восстановления, ограничения хранения и отката на Prosody:
[docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md).

Проверка: 293 Rust tests, Clippy/fmt/release, полный сетевой прогон, 500
TLS
MUC+SM ресурсов с SIGKILL/resume и native reference — успешно.
Подробности: [docs/VALIDATION-0.8.0.md](docs/VALIDATION-0.8.0.md).
2026-10-20 17:34:43 +07:00
124 changed files with 29195 additions and 570 deletions

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
name: Test, build and publish
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_INCREMENTAL: '0'
CARGO_PROFILE_DEV_DEBUG: '0'
CARGO_PROFILE_TEST_DEBUG: '0'
RUST_BACKTRACE: '1'
jobs:
verify:
name: All tests and Linux binary
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install native test dependencies and make room for 10 GiB tests
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
sudo apt-get update
sudo apt-get install -y --no-install-recommends build-essential pkg-config libssl-dev libicu-dev lua5.4 liblua5.4-dev lua-filesystem lua-expat lua-socket lua-sec openssl
rust_version=$(python3 -c 'import tomllib; print(tomllib.load(open("rust-toolchain.toml", "rb"))["toolchain"]["channel"])')
rustup toolchain install "$rust_version" --profile minimal --component rustfmt --component clippy
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/setup-go@v5
with:
go-version: '1.24.3'
cache: false
- name: Build exact native Prosody reference
run: bash scripts/ci/prepare-reference.sh "$RUNNER_TEMP/reference"
- name: Build genuine MinIO
run: |
bash scripts/ci/prepare-minio.sh "$RUNNER_TEMP/minio"
go clean -cache -modcache
df -h . "$RUNNER_TEMP"
python3 -c 'import shutil; assert shutil.disk_usage(".").free >= 20 * 2**30, "Need 20 GiB free for actual upload tests"'
- name: Run every test, native oracle, load and real 10 GiB uploads
env:
PROSODY_REFERENCE_SOURCE: ${{ runner.temp }}/reference/prosody-cafd74b9c5ea
PROSODY_REFERENCE_LUA: lua5.4
MINIO_BINARY: ${{ runner.temp }}/minio
run: |
mkdir -p ci-results
set -o pipefail
bash scripts/ci/verify-all.sh 2>&1 | tee ci-results/verify.log
- name: Package tested binary
run: |
mkdir -p dist
cp target/release/rust-xmpp dist/rust-xmpp
chmod 0755 dist/rust-xmpp
dist/rust-xmpp --version
(cd dist && sha256sum rust-xmpp > SHA256SUMS)
- name: Save binary
uses: actions/upload-artifact@v4
with:
name: rust-xmpp-linux-amd64
path: dist/
if-no-files-found: error
retention-days: 30
- name: Save test reports even on failure
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
ci-results/
/tmp/muc-sm-load.json
/tmp/xmpp-message-latency.json
/tmp/personal-message-latency.json
/tmp/push-latency.json
/tmp/push-offline-latency.json
/tmp/bind2-archive-latency.json
/tmp/slow-storage-latency.json
/tmp/slow-storage-crash.json
/tmp/journal-recovery-sync.json
if-no-files-found: warn
retention-days: 14
image:
name: Container smoke test and Gitea publish
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 20
# Only the publication job needs registry credentials. PRs still build and test images.
env:
GITEA_REGISTRY: ${{ vars.GITEA_REGISTRY }}
GITEA_IMAGE: ${{ vars.GITEA_IMAGE }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
PUBLISH: ${{ github.event_name != 'pull_request' && (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || startsWith(github.ref, 'refs/tags/v')) }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: rust-xmpp-linux-amd64
path: dist
- name: Verify downloaded executable
run: |
(cd dist && sha256sum --check SHA256SUMS)
chmod 0755 dist/rust-xmpp
- uses: docker/setup-buildx-action@v3
- name: Build exact tested binary into runtime image
uses: docker/build-push-action@v6
with:
context: .
target: ci-runtime
platforms: linux/amd64
load: true
push: false
tags: rust-xmpp:ci
- name: Smoke-test runtime image
run: |
docker run --rm rust-xmpp:ci --version
docker run --rm --entrypoint /usr/local/bin/prosodyctl rust-xmpp:ci --version
id=$(docker create rust-xmpp:ci)
trap 'docker rm "$id" >/dev/null' EXIT
docker cp "$id:/usr/local/bin/rust-xmpp" /tmp/container-rust-xmpp
cmp dist/rust-xmpp /tmp/container-rust-xmpp
- name: Validate publication configuration
if: env.PUBLISH == 'true'
env:
REGISTRY_USER: ${{ secrets.GITEA_REGISTRY_USERNAME }}
REGISTRY_TOKEN: ${{ secrets.GITEA_REGISTRY_TOKEN }}
run: python3 scripts/ci/image-tags.py
id: image
- name: Login to private Gitea registry
if: env.PUBLISH == 'true'
uses: docker/login-action@v3
with:
registry: ${{ vars.GITEA_REGISTRY }}
username: ${{ secrets.GITEA_REGISTRY_USERNAME }}
password: ${{ secrets.GITEA_REGISTRY_TOKEN }}
- name: Push the smoke-tested image
if: env.PUBLISH == 'true'
env:
IMAGE_TAGS: ${{ steps.image.outputs.tags }}
run: |
while IFS= read -r tag; do
docker tag rust-xmpp:ci "$tag"
docker push "$tag"
printf 'Published `%s`\n' "$tag" >> "$GITHUB_STEP_SUMMARY"
done <<< "$IMAGE_TAGS"
+1
View File
@@ -2,3 +2,4 @@
*.local.*
/demo/
/bin/
**pycache**
+347
View File
@@ -0,0 +1,347 @@
# Продолжение разработки Rust XMPP
## Цель и правила работы
Пользователь хочет заменить свою установку Prosody этим сервером на Rust.
Продолжайте реализацию и исправления самостоятельно в рамках текущего запроса:
воспроизводите проблему, исправляйте причину, проверяйте результат, обновляйте
документацию. Не ограничивайтесь планом, если доступна реализация.
- Общайтесь с пользователем по-русски, коротко сообщайте существенные результаты.
- Пользователь разрешил субагентов. Передавайте короткий самостоятельный brief,
`fork_turns: "none"`, путь проекта, конкретную задачу, ограничения и проверку.
Назначайте отдельные файлы владельцам; не допускайте конкурирующих правок.
- Сначала изучите актуальные файлы и незавершённые изменения. Не откатывайте
чужую работу и не считайте предыдущую среду или набор инструментов неизменными.
- Не объявляйте полную совместимость с Prosody по результату ограниченных тестов.
Сохраняйте `--experimental`, пока не выполнены критерии COMPATIBILITY.md.
- Не подменяйте реализацию рекламой disco features. Проверяйте реальный wire flow.
- Не пишите passwords, SASL payload, tokens, private keys в логи/отчёты/репозиторий.
- Production-конфигурацию, DNS и внешний registry меняйте только в рамках
соответствующего задания и доступных полномочий. Не выдавайте локальные
fixture-проверки за проверку реального клиента или production-сервера.
## Где находится проект
Корень — каталог с этим AGENTS.md, Cargo.toml и Dockerfile. В текущем workspace
это `/workspace/scratch/362577cdf2e0/recovered/rust-xmpp`; путь непереносимый.
В нём сейчас нет `.git`. Если следующая среда содержит Git checkout, используйте
его состояние и инструкции, не создавайте второй расходящийся источник истины.
Версия Rust source — **0.8.3**. В0.8.0 добавлены durable MUC/SM, затем CI
и этот handoff;0.8.1 исправляет задержку сообщений. См. docs/MESSAGE-LATENCY.md.
Перед передачей исходники упаковываются в ZIP; актуальный архив должен содержать
этот файл и `.github/workflows/ci.yml`. В среде ChatGPT сохраняйте новый архив
через доступный Library workflow, обновляя существующий проект, если его identity
известна. Не считайте scratch единственным долговременным экземпляром проекта.
## С чего начать
1. Прочитайте README.md, docs/COMPATIBILITY.md, docs/VALIDATION.md.
2. Для MUC/SM: docs/MUC-RECOVERY.md, CHANGELOG-0.8.0.md/0.8.1.md
и docs/MESSAGE-LATENCY.md/PERSONAL-MESSAGE-LATENCY.md. Не возвращайте expiry scan архива на каждый append.
3. Для upload: docs/HTTP-UPLOAD.md, исторические VALIDATION-0.7.8.md/0.7.9.md.
4. Для CI: docs/GITHUB-ACTIONS.md, docs/CI-VALIDATION.md,
`.github/workflows/ci.yml`, scripts/ci/verify-all.sh.
5. Выберите конкретный оставшийся пробел из COMPATIBILITY.md или текущей ошибки
пользователя. Подтвердите его тестом до утверждения, что он исправлен.
## Карта исходников
- `src/server.rs`: runtime, TLS/SASL/bind, dispatch, доставка, запуск/остановка,
MUC bootstrap для сохранённых SM-сессий.
- `src/sessions.rs`, `src/session_store.rs`, `src/csi.rs`: XEP-0198, очереди,
durable slots, fencing, resume, CSI.
- `src/muc.rs`, `src/muc_recovery.rs`, `src/muc_history.rs`, `src/muc_admin.rs`:
комнаты, журнал, восстановление, история и административные команды.
- `src/storage.rs`: native Lua storage, атомарные записи.
- Остальные подсистемы ищите по именам модулей; `rg` и `rg --files` предпочтительны.
- `scripts/verify-local.sh`: основной набор регрессий.
- `tests/*_network.py`: настоящие локальные TLS/HTTP/WS сценарии.
- `docs/validation-logs-*`: зафиксированные результаты конкретных прошлых сборок.
## Инварианты, которые нельзя потерять
### MUC
`rust_muc_checkpoint` — авторитетный согласованный журнал Rust: конфигурация,
affiliations, nick/роли/членство и ограниченная история, включая временные комнаты.
Изменение фиксируется до ответа/публикации; ошибка записи откатывает состояние.
Native config/state являются экспортом после commit. Устаревший native export
не должен возвращать старые права, nick или историю после аварии.
Без действующей приватной SM-записи восстановление требует новой аутентификации
точного полного JID и available presence либо явного MUC-входа. Текущие room ACL
проверяются повторно. Уже ожидающий recovery deadline не продлевается restart,
неудачной попыткой или восстановлением другого участника. Default grace300с;
`muc_recovery_grace_period` ограничен1..604800.
С действующей durable SM-сессией логическое членство восстанавливается **до
listeners**, после проверки аккаунта и room admission. Пока транспорт offline,
room fanout должен попадать в сохранённую SM-очередь. При bootstrap не отправляйте
синтетические join presence/history. Native MUC-файл сам по себе не является
доказательством доверенной SM-сессии.
Критическая регрессия: Alice возобновила сессию и отправила сообщение, Bob ещё
не аутентифицировался. Проверяйте сообщение в durable pending Bob ДО его входа.
Поздняя история при восстановлении может ложно показать успешную доставку.
См. tests/sm_restart_network.py.
### SM и доставка
SM snapshot приватный, atomic/fsync, с owner/token/fullJID/counters/pending/expiry.
Проверяйте owner и токен; старые connection handles не могут менять новую сессию.
Account deletion должна durably отозвать токены до purge. Повторно созданный
аккаунт не должен принимать старый токен. Удаление собственного аккаунта всё ещё
должно получить terminal IQ reply, несмотря на закрытую SM-сессию.
Handled увеличивается после обработки и только после SM enable. Подтверждения
и изменения очереди сохраняются до соответствующей отправки. Offline expiry
фиксирован; heartbeat обновляет только живой транспорт. Новый TLS/SASL обязателен:
сохранения TCP/TLS fd между процессами нет. Гарантия replay — at least once,
не exactly once между всеми хранилищами и сетью.
### Upload
Не разрушайте intent/exact-key multipart recovery, ownership isolation,
completion quarantine, атомарное чтение свежего snapshot при захвате claim и
source-preserving offline migration. HEAD404 после неопределённого Complete
не доказывает отсутствие удалённого commit. Подробности — HTTP-UPLOAD.md.
## Проверки
Toolchain задан rust-toolchain.toml: Rust1.99.1, rustfmt, Clippy.
Обычная проверка из корня:
```bash
cargo fmt --check
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
cargo build --release --locked
bash scripts/verify-local.sh
```
Изменения проверяйте подходящими тестами; не повторяйте дорогие неизменившиеся
сценарии без причины. Для итогового расширенного прогона:
```bash
XMPP_LOAD_CHECK=1 bash scripts/verify-local.sh
```
Это ещё НЕ все ignored/large/native tests без дополнительных зависимостей.
Полный CI entrypoint — `scripts/ci/verify-all.sh`; ему обязательны
PROSODY_REFERENCE_SOURCE, PROSODY_REFERENCE_LUA и MINIO_BINARY. Он включает
native oracle/federation, обе нагрузки500TLS, AF_UNIX admin, archive>128MiB,
local и MinIO HTTPS PUT/GET10GiB. Нужны Lua5.4/native dependencies, OpenSSL и диск.
Точные команды установки и подготовки — workflow и scripts/ci/prepare-*.sh.
Bundled оригинальная Prosody cafd74b9c5ea:
`scripts/ci/vendor/prosody-cafd74b9c5ea.tar.gz`. Hash проверяется перед сборкой.
Не заменяйте её иной версией для получения зелёного native oracle.
В текущей среде native зависимости извлекались в `/tmp/xmpp-native/usr`:
```bash
export PATH=/root/.cargo/bin:$PATH
export CARGO_INCREMENTAL=0 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0
export LD_LIBRARY_PATH=/tmp/xmpp-native/usr/lib/x86_64-linux-gnu
export LUA_PATH='/tmp/xmpp-native/usr/share/lua/5.4/?.lua;/tmp/xmpp-native/usr/share/lua/5.4/?/init.lua;;'
export LUA_CPATH='/tmp/xmpp-native/usr/lib/x86_64-linux-gnu/lua/5.4/?.so;;'
export PROSODY_NATIVE_PREFIX=/tmp/xmpp-native/usr
export PROSODY_REFERENCE_SOURCE=/workspace/scratch/362577cdf2e0/reference/prosody-cafd74b9c5ea
export PROSODY_REFERENCE_LUA=/tmp/xmpp-native/usr/bin/lua5.4
```
Эти пути — только подсказка для сохранившейся среды. На обычном CI зависимости
ставятся системно. Не используйте отсутствующий prefix как обязательное условие.
При прежнем сбое linker `memory map must have a non-zero length` помогало
`cargo clean -p rust-xmpp --release` перед повторной release-сборкой.
Не удаляйте бинарник, используемый параллельным тестом: нагрузочный тест теперь
копирует executable в отдельный каталог перед restart.
Полезные узкие проверки:
```bash
python3 tests/muc_crash_network.py target/release/rust-xmpp
python3 tests/sm_restart_network.py target/release/rust-xmpp
python3 tests/muc_sm_load_network.py --binary target/release/rust-xmpp --clients 500
```
Readiness timeout нагрузки увеличен до60с: восстановление500слотов заняло10.559с,
поэтому старые5с давали ложный failure. Не скрывайте реальный сбой увеличением
таймаута без диагностики. На перезапуске проверяйте доставку и counters, а не
только открытие порта.
## Что действительно проверено
Rust0.8.0:293 tests passed,11 ignored в обычном cargo test; native дополнительно
8module/storage+1federation. Все10 MUC crash cases прошли. Нагрузка:100аккаунтов,
500TLS SM-ресурсов,10комнат,25000join presence,1500message deliveries, SIGKILL и
resume10представителей. Результаты и ограничения — docs/VALIDATION-0.8.0.md.
Сводный успешный лог составлен из завершённых частей и повторной нагрузки после
исправления тестового readiness timeout; первоначальный failure сохранён отдельно.
10GiB local проверялся в0.7.8; MinIO в0.7.9 с раскрытой адаптацией EPERM при
перечислении интерфейсов. Не приписывайте эти старые передачи новой сборке.
CI собирает оригинальный MinIO без адаптации. AWS/distributed MinIO и настоящий
APNs/FCM/production client cycle не подтверждены.
GitHub workflow проверен actionlint1.7.7, shell/Python syntax и случаями tags.
Реального GitHub run, Docker build/run и Gitea push пока НЕ было: Docker daemon
здесь отсутствует, доступ к пользовательскому GitHub/Gitea не предоставлен.
## Исправление задержки0.8.1
Полный MUC archive expiry на каждом сообщении был причиной seconds-level latency:
100000records, median2518мс до →4.680мс после в realTLS loopback. Убраны no-op
CSI checkpoints и перезаписи неизменённых native exports; private account traffic
не ждёт MUC mutex. Обязательные durable commits сохранены. Четыре новые
структурные регрессии прошли; измерения содержат raw samples и binary hashes.
Результаты нового итогового прогона смотрите в docs/VALIDATION-0.8.1.md.
Старые293tests и10GiB цифры выше относятся к прошлым выпускам.
## Личная доставка0.8.2
Не await push gateway в обработчике личного сообщения: очередь1024/max8worker,
RAII cancellation при stop/error. Сначала durable offline/SM data, потомpush.
Runtime CSI defaults теперь включают OMEMO encrypted/receipts/markers; explicit
список заменяет defaults. Raw native importance function не менялась. Refresh
policy при resume нужен для старых persisted slots. Логmessage-processing-slow
не включает payload. Hub.for_owners выбирает account resources до session locks;
не возвращайте сканирование всех чужих session locks при deliver/carbons.
Archive.edges_store читает только endpoints для Bind2 и metadata IQ; сохранены
bounded Lua/recovery/locks, middle не проверяется этим metadata operation.
Новые тесты: personal_message_latency_network.py, push_latency_network.py,
bind2_archive_latency_network.py, archive_edges.rs, session_routing_isolation.rs.
Результаты текущей версии: docs/VALIDATION-0.8.2.md. Полный MAM query/maintenance
и глобальный mutex SM Store остаются возможными отдельными IO bottlenecks.
## Ближайшие задачи и границы
- Проверить первый настоящий CI run после загрузки репозитория и настройки
GITEA_REGISTRY/GITEA_IMAGE variables и USERNAME/TOKEN secrets по инструкции.
Registry push разрешён основной ветке и version tags только после всех тестов.
Образ должен содержать тот же executable, что проверенный binary artifact.
- По новым сообщениям пользователя проверить реальные Conversations/Monocles,
Monal/Gajim/Converse.js и целевой proxy/TLS. Старые жалобы PEP/DNSSEC не считайте
закрытыми только потому, что локальный fixture зелёный. DNSSEC зоны требует
отдельной проверки DNS/операторской конфигурации, не фиктивного server feature.
- Оставшиеся продуктовые пробелы перечислены в COMPATIBILITY.md: один VirtualHost,
часть федерации/CSI/PEP, нет transport rate limiting, произвольных Lua plugins,
SQL и полного prosodyctl. Выбирайте и проверяйте конкретные задачи.
- Для утверждения «полная замена» нужны реальные данные, внешняя федерация,
настоящие клиенты/push, deployment/UID/TLS, rollback и длительная нагрузка.
После изменений обновляйте точные границы совместимости и результаты проверки.
Не перезаписывайте исторические отчёты как будто они относятся к новой сборке.
В передаваемый ZIP включайте AGENTS.md, workflow, vendor reference, исходники,
документацию и scripts; исключайте target, demo data, bin, .git, **pycache**, pyc
и секреты. Сохраняйте executable bits shell scripts и проверяйте содержимое ZIP.
## Durable message IO 0.8.3
Новый пользовательский log 0.8.2 содержит три local message-processing-slow
4792–5549 мс. Таймер начинается внутри server после XML, но старые поля не
доказывают причину IO. Контролируемый slow-sync fixture воспроизводит задержку
1:1 в несколько секунд. MAM 4 sync/append и SM 2 sync/checkpoint заменены
ограниченными checksummed redo journals. Обычная операция теперь требует 1 sync;
создание, recovery и rotation дороже. Точные результаты — VALIDATION-0.8.3.md
и DURABLE-MESSAGE-IO.md. Не приписывайте loopback причину production без phase logs.
SM `.wal` авторитетен, `.json` — readable export. Replay выполняется до JSON parse.
Неполный trailing frame отбрасывается; неполный baseline или полная corruption
блокируют startup. Durable tombstone предшествует delete. GC сначала делает
sync каталога с JSON deletions, затем unlink WAL и sync каталога.
Ordinary commit: append + sync_data, потом JSON export без sync. Ошибка export
после commit не откатывает память; store блокирует дальнейшие записи до recovery.
Precommit/sync error также блокирует store. Не возвращайте 2 sync на каждый
checkpoint и не удаляйте redo без checkpoint. Общий SM mutex пока сохранён.
Slot limit 8 MiB, total 256 MiB; WAL rotation 16 MiB плюс один slot, aggregate
512 MiB. Не допускайте роста каждого из тысяч slots без aggregate cap.
Archive `.rredo`: native baseline и bounded anchor, checksummed record frames.
Rotation после 128 frames/1 MiB плюс одна запись до 16 MiB. Cache ограничен 1024;
изменение metadata вызывает перепроверку. Accepted records восстанавливаются
даже при потере dirty native pages. Unjournaled native suffix не доказывает
external append: truncate до committed end. Legacy `.rappend` ещё восстанавливается.
Save/retain сначала durably фиксируют old native и удаление redo, затем
делают atomic replacement. Смена inode при копировании/backup НЕ отменяет redo: journal
остаётся авторитетным. Любой внешний writer, даже atomic replace, требует handoff. Purge включает rredo.
Recovery повторно инвалидирует `.lidx`: unlink на hot path отдельно не dirsynced.
Anchor проверяет только последние 4 KiB baseline, не всю старую историю.
Перед сменой writer/откатом остановите сервер и новым binary выполните
`rust-xmpp storage-checkpoint CONFIG`. Внешний data-directory lock обязателен.
`archive::checkpoint_all` и `session_store::checkpoint_exports` — только offline.
Сначала sync native/current JSON и каталога, потом remove journal и sync каталога.
Graceful stop сам по себе не означает handoff. Native Prosody не импортирует
Rust SM sessions. Не редактируйте JSON export для expiry fixtures: используйте
короткий настоящий timeout или проверяйте journal recovery. Не удаляйте WAL,
чтобы получить зелёный тест. Native oracle перед Prosody modification явно
вызывает checkpoint_all(root).
Новые tests: archive_redo.rs, sm_journal.rs, slow_storage_latency_network.py,
slow_storage_crash_network.py, journal_recovery_sync_network.py, fixtures/slow_sync.c. Interposer действует только
на изолированный fixture data_path. Обычный режим выполняет настоящий
fsync/fdatasync; отдельный XMPP_SYNC_FAIL_SUFFIX вводит EIO только для fault test.
Crash fixture проверяет journals и оба MAM архива до reauth после sender SM ACK
и SIGKILL. Readable JSON без redo не подтверждает durability. Phase fields —
server.rs и DURABLE-MESSAGE-IO.md.
В восстановленной среде native prefix теперь `/tmp/xmpp-native/usr/usr`, а не
старый `/tmp/xmpp-native/usr`. `/tmp/xmpp083-env.sh` содержит env текущего прогона;
это не portable config. Toolchain переустановлен; tests используют Rust 1.99.1.
Итог 0.8.3: 334 ordinary passed, 11 ignored; 9 native и 1 large-archive ignored
выполнены отдельно. Все network checks и обе нагрузки 500 TLS прошли.
Final binary SHA-256: 8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a.
Подробные samples/границы — docs/VALIDATION-0.8.3.md. Checksum и JSON syntax всех frames
проверяются при SM replay, но Record создаётся только для последнего
snapshot. Recovery WAL sync до публикации обязателен; EIO fixture защищает это.
## Monal MAM baseline 0.8.4
Баг: «archive redo baseline changed in place» на живом сервере — внешняя
перезапись native `.list` поверх зафиксированного redo-baseline (бакап/копия
data_path/Prosody/старая версия) делала каждый `archive_for_user` Err, `?` ронял
session, клиент (Monal-iOS) реконнектился и не доставлял сообщение. Причина не в
обычном append — baseline ломает только перезапись первых `base` байт в обход
`.rredo`; наш Rust-append пишет только в конец.
Фикс (commit 1000053): 1) MAM-архивация на личной доставке (отправитель,
получатель, входящая федерация) стала best-effort `archive_message_best_effort` —
storage-ошибка не роняет session и не роняет доставку, в лог `mam-archive-skipped`
(санитизированный `sanitized_label`, без payload); 2) offline `storage-checkpoint`
теперь ремонтирует непроверенный baseline: `prepare_rewrite` через
`is_untrusted_redo` считает текущий синхронизированный native авторитетным и
удаляет неприменимый журнал (`retire_redo`). Живой append остаётся fail-closed.
Новые tests: `archive.rs` `runtime_append_fails_closed_...`/
`offline_checkpoint_retires_...`, сетевой `tests/mam_broken_baseline_network.py`
(перезапись baseline не роняет доставку; checkpoint возвращает MAM).
Критическое уточнение 0.8.4-followup: **живой append больше НЕ fail-closed** — это
и было корневой причиной seconds-уровневых `message-processing-slow` у Luma и
непрерывных `mam-archive-skipped` на сервере пользователя. В `src/archive.rs`
`recover_redo` теперь сам зареТирует неприменимый журнал при первом же сбое
(ранняя проверка anchor по первым 96 байтам header до чтения кадров) и продолжит
работу на синхронизированном native как Prosody, который вообще не хранит `.rredo`.
Чтение сломанного журнала больше не стоит O(журнал) на каждый access: проверяется
только header, неподключённый журнал удаляется один раз, последующие access быстры
(см. `untrusted_redo_baseline_is_retired_once_and_recovers`). Полная коррупция
header-checksum или неполный baseline по-прежнему блокируют startup (инвариант
durability 0.8.3) — они НЕ матчатся `is_untrusted_redo` и остаются fail-closed.
Сравнение с Prosody: `util/datamanager.lua` `list_append` пишет в конец, а
`list_store` (перезапись) делает atomic rename — меняет первые bytes baseline;
`plugins/mod_storage_internal.lua` `archive:append` для MUC/`key` и trim/delete
делает `list_store` (полная перезапись). Prosody не понимает `.rredo` sidecar. Наш
native `.list` сохраняет Prosody-совместимый `item(...)` формат.
Операционный ремонт пользователя: с этим фиксом ручной `storage-checkpoint` для
починки рассинхронизированного baseline больше не требуется — журнал
зареТиров согласуется automatically при первом доступе, и append продолжает
работу на скорости Prosody. Offline `storage-checkpoint CONFIG` остаётся
доступным (и идемпотентным) для штатного durable handoff перед сменой writer.
`--experimental` не снимать. Полная
замена Prosody требует реального CI, внешней федерации, настоящих клиентов/push,
deployment/UID/TLS, rollback и длительной нагрузки — см. COMPATIBILITY.md.
+39
View File
@@ -0,0 +1,39 @@
# 0.8.0
- MUC сохраняет согласованный журнал конфигурации, affiliations, nick, ролей,
членства и ограниченной истории без MAM. Изменение фиксируется до публикации
ответа; при ошибке записи состояние комнаты откатывается.
- Crash recovery использует последнее сохранённое поколение после SIGKILL,
включая временные комнаты. Heartbeat обновляет живое членство;
ожидающие записи сохраняют исходный deadline через повторные перезапуски.
`muc_recovery_grace_period`: по умолчанию 300 секунд, пределы 1..604800.
- Подтверждённые выходы, смена nick, ролей и политик комнаты не заменяются
старой записью восстановления. Native config/state остаются экспортом для
Prosody; согласованное восстановление Rust использует собственный sidecar.
- Исправлены MUC probe, неизвестные типы presence, вход без MUC `<x/>`, ошибки
доставки и статус 333. Обычные presence extensions сохраняются в живом
обмене; поддельная MUC identity фильтруется. `subject` вместе с `body` либо
`thread` не изменяет тему комнаты.
- Подключены `muc_max_rooms`, `restrict_room_creation`, политики регистрации
и зарегистрированного nick, `muc_tombstone_expiry`; добавлены встроенные
административные ad-hoc команды уничтожения комнат и изменения affiliation.
- Hub сохраняет resumable XEP-0198 сессии, счётчики и очередь. Resume после
перезапуска доступен на новом TLS/SASL-транспорте через legacy SASL и SASL2.
Действующие приватные SM-записи восстанавливают логические сессии и MUC
до открытия listeners: offline-доставка попадает в очередь, синтетический
join и история повторно не отправляются. SM resume не требует дополнительной
available presence; без SM восстановление MUC требует новой аутентификации
точного полного JID и available presence либо явного MUC-входа.
Входящий handled-счётчик обновляется после обработки. Сокеты TCP/TLS
не сохраняются; неопределённая сетевая отправка допускает повторную доставку.
- Известные встроенные MUC-возможности Prosody отображаются на Rust-реализацию.
Произвольные Lua/community-модули не исполняются; неизвестные явно включённые
MUC-модули отклоняются при запуске. Общие и host `modules_disabled` учитываются
для vCard и occupant-id.
Порядок восстановления, ограничения хранения и отката на Prosody:
[docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md).
Проверка: 293 Rust tests, Clippy/fmt/release, полный сетевой прогон, 500 TLS
MUC+SM ресурсов с SIGKILL/resume и native reference — успешно.
Подробности: [docs/VALIDATION-0.8.0.md](docs/VALIDATION-0.8.0.md).
+16
View File
@@ -0,0 +1,16 @@
# 0.8.1
- Исправлена задержка доставки MUC с растущим архивом: после каждого сообщения
больше не выполняется полный scan/rewrite архива для expiry. Очистка остаётся
в maintenance и перед MAM-запросами; append и crash checkpoint до fanout сохранены.
- Неизменённые native config/persistent index не перезаписываются при каждом
сообщении. Удалённые файлы восстанавливаются, ошибки экспорта повторяются.
- CSI больше не сохраняет SM snapshot при отсутствии изменения durable очереди.
Изменённая очередь сохраняется до отправки; при ошибке записи очередь откатывается.
- Личные сообщения, IQ и входящая федерация к аккаунтам больше не захватывают
общий MUC mutex, ожидая несвязанную работу с комнатными архивами.
- Добавлены регрессии native exports/archive scanning/CSI rollback и реальный
TLS latency check в verify-local.sh, автоматически включённый и в CI.
Измерения и границы подтверждения: [docs/MESSAGE-LATENCY.md](docs/MESSAGE-LATENCY.md).
Результаты итогового прогона: [docs/VALIDATION-0.8.1.md](docs/VALIDATION-0.8.1.md).
+21
View File
@@ -0,0 +1,21 @@
# 0.8.2 — задержки личных сообщений
- Личные сообщения и SM ACK больше не ждут сетевую отправку push. Уведомления
отправляются через очередь1024 с максимум8параллельных маршрутов; worker
и его задачи отменяются при завершении serve. Ошибки очереди/маршрута логируются.
- Offline сообщение сохраняется до публикации push, чтобы проснувшийся клиент
мог сразу получить его. Подтверждена очередь при первом TLS stream push gateway.
- По умолчанию CSI считает важными bodyless OMEMO legacy/2, delivery receipts
и chat markers. Явный список `csi_important_payloads` заменяет расширенные defaults.
Низкоприоритетный headline по-прежнему буферизуется. Policy обновляется и при SM resume.
- Выбор адресатов и carbons фильтрует владельцев до захвата session mutex;
зависшая сессия чужого аккаунта не задерживает этот путь. Duplicate SM ACK
без изменения durable counters/queue не переписывает snapshot.
- Bind2 и MAM metadata IQ читают только первый/последний native record вместо
сканирования всей истории. Индекс не требуется; recovery/лимиты записи сохранены.
- Добавлен `message-processing-slow`: обработка message дольше250мс, поля
duration_ms/route, без payload/токенов. Это время обработки сервером, не peer receipt.
- Новые сетевые регрессии включены в verify-local.sh/GitHub CI.
Причины, изменение CSI policy, измерения и ограничения:
[docs/PERSONAL-MESSAGE-LATENCY.md](docs/PERSONAL-MESSAGE-LATENCY.md).
+25
View File
@@ -0,0 +1,25 @@
# 0.8.3 — durable IO личных сообщений
- Уменьшено число последовательных синхронизаций до доставки: ordinary MAM
append использует один sync вместо четырёх, SM checkpoint — один вместо двух.
Checksummed redo journals сохраняют commit-before-ACK и восстанавливают
native archive/SM JSON exports после аварии.
- Ограничены рост журналов, ротация и tombstone GC. Старый `.rappend` и legacy
JSON импортируются. Retain/save/purge не возвращают удалённые записи.
- Recovery синхронизирует валидный journal перед публикацией состояния; проверка
checksum не заменяет durability barrier. EIO при adoption блокирует startup
или offline handoff. Новые fault fixtures проверяют сохранность журнала и retry.
- Replay SM проверяет checksum/JSON всех frames, но создаёт и валидирует полный
Record только для последнего snapshot, сокращая работу с superseded queues.
- `message-processing-slow` содержит времена MAM отправителя/получателя,
доставки, carbons, offline/push и handled, а также число ресурсов/копий.
Payload и секреты не добавляются в логи.
- Добавлена offline команда `storage-checkpoint CONFIG`: durable экспорт и
передача archive writer/подготовка отката на legacy Rust. Не запускайте другого
writer и не удаляйте journals до успешного завершения команды.
- Новые crash, partial-write, corruption, EIO, recovery, handoff и latency
регрессии включены в GitHub Actions через verify-local.sh; отчёты сохраняются.
Измерения, ограничения и эксплуатация:
[docs/DURABLE-MESSAGE-IO.md](docs/DURABLE-MESSAGE-IO.md),
[docs/VALIDATION-0.8.3.md](docs/VALIDATION-0.8.3.md).
Generated
+1 -1
View File
@@ -1111,7 +1111,7 @@ dependencies = [
[[package]]
name = "rust-xmpp"
version = "0.7.9"
version = "0.8.3"
dependencies = [
"anyhow",
"axum",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-xmpp"
version = "0.7.9"
version = "0.8.3"
edition = "2024"
description = "Experimental Rust XMPP server with Prosody-compatible storage"
license = "MIT"
+12 -3
View File
@@ -4,14 +4,23 @@ COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release --locked
FROM debian:trixie-slim
FROM debian:trixie-slim AS runtime-base
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates gosu \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /src/target/release/rust-xmpp /usr/local/bin/rust-xmpp
RUN ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl
COPY scripts/docker-entrypoint.sh /usr/local/bin/rust-xmpp-entrypoint
RUN chmod 0755 /usr/local/bin/rust-xmpp-entrypoint
WORKDIR /work
ENTRYPOINT ["rust-xmpp-entrypoint"]
CMD ["serve", "/etc/prosody/prosody.cfg.lua", "--experimental"]
# CI downloads the exact executable produced by the verification job.
FROM runtime-base AS ci-runtime
COPY dist/rust-xmpp /usr/local/bin/rust-xmpp
RUN chmod 0755 /usr/local/bin/rust-xmpp \
&& ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl
# Default local build still compiles directly from source.
FROM runtime-base AS runtime
COPY --from=builder /src/target/release/rust-xmpp /usr/local/bin/rust-xmpp
RUN ln -s /usr/local/bin/rust-xmpp /usr/local/bin/prosodyctl
+55 -3
View File
@@ -1,4 +1,4 @@
# Rust XMPP 0.7.9
# Rust XMPP 0.8.3
Сервер на Rust, разрабатываемый для замены **Prosody 13.0 nightly 100,
cafd74b9c5ea** с предоставленными модулями. Читает существующий Lua-конфиг,
@@ -16,7 +16,9 @@ PEP/pubsub, закладки, offline, carbons, SM, push, TLS-федерация
Нестандартные HTTP-пути и TLS reverse proxy: [docs/HTTP-PROXY.md](docs/HTTP-PROXY.md).
Пример для слияния с действующим конфигом: `examples/reverse-proxy.fragment.yaml`.
MUC-расширения и условия восстановления участников: [docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md).
MUC и восстановление после сбоя, включая SM resume:
[docs/MUC-RECOVERY.md](docs/MUC-RECOVERY.md).
Изменения 0.8.0: [CHANGELOG-0.8.0.md](CHANGELOG-0.8.0.md).
Встроенный http_altconnect: [docs/HTTP-ALTCONNECT.md](docs/HTTP-ALTCONNECT.md).
@@ -150,7 +152,8 @@ inline SM/CSI/Carbons и MAM metadata. Добавьте его в modules_enable
с `sasl2` только если эта функция нужна. Исходный конфиг по-прежнему работает
без включения нового модуля. Ресурс генерируется случайно; прежние соединения
того же аккаунта с тем же валидным UUIDv4 user-agent завершаются после успешного
входа. Inline возобновление SM через SASL2 пока не объявляется.
входа. Inline возобновление SM через SASL2 поддерживается, в том числе после
перезапуска при наличии действующей сохранённой SM-сессии.
## PEP, push и pre-authenticated roster
@@ -206,3 +209,52 @@ gateway с multipart streaming описаны в [docs/HTTP-UPLOAD.md](docs/HTTP
карантин неопределённого completion. Реальные 10 GiB через MinIO проверены с
описанной адаптацией тестовой среды. Изменения и границы подтверждения —
[CHANGELOG-0.7.9.md](CHANGELOG-0.7.9.md).
## MUC и SM после сбоя в 0.8.0
MUC фиксирует конфигурацию, роли, членство и ограниченную историю без MAM
до публикации соответствующего ответа. Последний сохранённый журнал позволяет
восстановить persistent и временные комнаты после аварийной остановки.
Heartbeat обновляет живое состояние; срок уже ожидающего восстановления
не продлевается повторным запуском. `muc_recovery_grace_period` задаётся
в секундах: по умолчанию 300, пределы 1..604800.
Действующие приватные SM-записи восстанавливают логические сессии и MUC
до открытия listeners, поэтому offline-доставка может попасть в сохранённую
очередь. XEP-0198 resume через legacy SASL или SASL2 требует нового
TLS/SASL-транспорта и токена того же владельца; TCP/TLS-сокеты не сохраняются.
Без действующей SM-записи MUC ожидает новую аутентификацию точного полного JID
и available presence либо обычный явный MUC-вход.
Native-файлы экспортируются для Prosody; источником согласованного crash
recovery Rust служит собственный журнал. Перед откатом ознакомьтесь с
[порядком переключения](docs/MUC-RECOVERY.md). Произвольные community Lua-модули
не выполняются; неизвестные явно включённые MUC-модули отклоняются при запуске.
Результаты проверки — в
[docs/VALIDATION-0.8.0.md](docs/VALIDATION-0.8.0.md).
## GitHub Actions и Gitea Registry
Полные тесты, Linux binary artifact и Docker push после проверок:
[настройка workflow](docs/GITHUB-ACTIONS.md). Registry и credentials задаются
через GitHub Actions variables/secrets.
## Задержка сообщений в0.8.1
Устранены полный проход по MUC-архиву на каждом сообщении, повторные записи
неизменённых native-файлов и no-op CSI checkpoints. Проверенный сценарий
с100000 MAM-записей: median MUC delivery2.5с→4.7мс на loopback.
[Причины, измерения и границы](docs/MESSAGE-LATENCY.md).
## Личные сообщения в0.8.2
Push routing больше не блокирует отправителя/SM ACK; CSI defaults немедленно
передают bodyless OMEMO и receipts. Bind2/MAM metadata не сканируют всю историю.
[Причины, тесты и конфигурация](docs/PERSONAL-MESSAGE-LATENCY.md).
## Durable IO личных сообщений в0.8.3
Сокращены последовательные sync MAM/SM через checksummed redo journals,
добавлены замеры этапов медленной обработки и crash/replay проверки.
Перед передачей файлов Prosody/откатом выполните offline `storage-checkpoint`.
[Durability, команда handoff и диагностика](docs/DURABLE-MESSAGE-IO.md).
+17
View File
@@ -0,0 +1,17 @@
# Проверка изменения CI
2026-10-20. Изменяется только CI, упаковка Docker и документация; Rust source
и версия0.8.0 не менялись. Предыдущие293 теста относятся к этому Rust source.
Локально успешно:
- actionlint1.7.7: `.github/workflows/ci.yml` без ошибок;
- bash -n для всех трёх CI shell scripts;
- Python compile и проверка генерации default-branch/version/SHA tags;
- отклонение чужой ветки, несовпадающей версии, URL вместо hostname,
некорректного image path и отсутствующего token;
- SHA-256 bundled reference archive совпадает с ожидаемым.
Docker daemon отсутствует; GitHub Actions и реальный push в Gitea здесь
не запускались. Результат следует проверить первым workflow run после
добавления репозитория, variables и secrets по GITHUB-ACTIONS.md.
+56 -7
View File
@@ -2,21 +2,23 @@
Цель — конкретная Prosody `cafd74b9c5ea` и предоставленные community/custom
модули ревизии `dddb6c34b964`, а не все существующие модули Prosody.
Реализация содержит рабочие обработчики всех явно включённых имён модулей
из конфига. Это не означает полного совпадения их поведения.
Известные встроенные и выбранные модули предоставленной установки отображаются
на обработчики Rust. Произвольные Lua/community-модули не исполняются;
неизвестные явно включённые MUC-модули отклоняются при запуске.
Это не означает полного совпадения поведения с Prosody.
| Область | Что реализовано | Существенные пределы |
|---|---|---|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Конфигурация | Lua/JSON/YAML/YML/TOML, Include, глобальные и локальные области, наследование модулей | Lua VM ограничена; не все функции окружения конфигуратора Prosody доступны. Runtime обслуживает один VirtualHost и его компоненты |
| Хранилище | internal `.dat`, `.list`, бинарные строки, смешанные ключи Lua, атомарные записи, потоковые архивы, инвалидирование `.lidx` | SQL, external storage остальных модулей и произвольные драйверы не реализованы; S3 uploads поддержаны. Каталог блокируется между процессами Rust; Prosody в блокировке не участвует |
| Аккаунты, saslauth | internal_hashed SHA-1/SHA-256, SASLprep, PLAIN внутри TLS, SCRAM и PLUS с tls-exporter/tls-server-end-point, SSDP, SASL2 | Только internal_hashed; опциональный sasl2_bind2 (alias bind2) поддерживает inline bind/SM/CSI/Carbons. SASL2 inline SM resume и takeover живого транспорта реализованы; MAM metadata при Bind2 читает весь архив синхронно |
| Аккаунты, saslauth | internal_hashed SHA-1/SHA-256, SASLprep, PLAIN внутри TLS, SCRAM и PLUS с tls-exporter/tls-server-end-point, SSDP, SASL2 | Только internal_hashed; опциональный sasl2_bind2 (alias bind2) поддерживает inline bind/SM/CSI/Carbons. SASL2 inline SM resume и takeover живого транспорта реализованы; Bind2/MAM metadata читает endpoints без полного scan; полные MAM queries всё ещё могут сканировать историю |
| Сообщения и presence | локальная маршрутизация, приоритеты ресурсов, контакты/подписки/probes, offline, carbons, блокировки | Полная совместимость всех пограничных сценариев маршрутизации ещё не подтверждена |
| MAM | личный и комнатный MAM2, prefs, формы, RSM, фильтры времени/контакта/ID, extended queries, metadata, stanza-id, expiry | Нет доказательства совпадения каждой ошибки и порядка результатов при всех комбинациях запроса |
| MUC | комнаты, несколько ресурсов, роли/affiliations, owner/admin, конфигурация, приглашения, история без MAM и MAM, occupant-id/avatar, hats, язык, PM policies, presence broadcast, native metadata/state checkpoints и восстановление после штатной остановки | Восстановление после повторной аутентификации точного полного JID в300-секундном grace и при неизменной конфигурации. Нет сохранения TCP/SM между процессами и checkpoint на SIGKILL. Произвольные community-расширения не запускаются; MUC-RECOVERY.md |
| MUC | комнаты, несколько ресурсов, роли/affiliations, owner/admin, конфигурация, приглашения, история без MAM и MAM, occupant-id/avatar, hats, PM/presence policies, регистрация, административные ad-hoc команды; журнал конфигурации, членства и истории до ответа, crash recovery persistent/temporary комнат | Rust-журнал авторитетен, native config/state — экспорт. Без SM восстановление требует новой аутентификации точного полного JID; grace по умолчанию 300 с, `muc_recovery_grace_period` 1..604800. Действующая приватная SM-запись восстанавливает логическое членство до listeners. Произвольные Lua-модули не запускаются; [MUC-RECOVERY.md](MUC-RECOVERY.md) |
| PEP/pubsub | persistent nodes/items, ACL, subscriptions, publish/retract/purge/delete, owner/configuration, caps, OMEMO XML как payload | Не все необязательные политики узлов Prosody реализованы; OMEMO шифруют клиенты, криптография OMEMO не часть сервера |
| private/bookmarks/vCard | private XML, старые и новые закладки, vCard/temp-vCard4 и avatar-переходы | Межклиентное поведение нужно дополнительно проверить настоящими клиентами |
| SM/push | acknowledgements, resumable sessions, replay, ограниченные очереди, native регистрации push и ответы сервисов | Есть coalescing и watchdog первого push; полный цикл с настоящим APNs/FCM шлюзом не проверен. Состояние живых сессий не переживает перезапуск процесса |
| CSI | active/inactive, ограниченный буфер, native importance rules, delay, flush при клиентской активности и grace period | Событие socket drain приближено завершением записи; необязательные расширенные XPath-фильтры поддержаны не полностью |
| SM/push | acknowledgements после обработки, resumable XEP-0198 sessions, replay, ограниченные очереди, приватный durable snapshot, heartbeat и shutdown checkpoint; legacy SASL/SASL2 resume после restart; native регистрации push | Нужны новый TLS/SASL-транспорт и токен того же владельца; TCP/TLS-сокеты не сохраняются. SM-снимок не native Prosody: 256 MiB суммарно, 8 MiB/4096 stanza на сессию. При неопределённой отправке возможен replay. Есть coalescing и watchdog первого push; реальный APNs/FCM цикл не подтверждён |
| CSI | active/inactive, ограниченный буфер, native importance function + runtime defaults для OMEMO/receipts/markers, delay, flush при клиентской активности и grace period | Defaults0.8.2 важнее по доставке, чем строгая native policy; явный csi_important_payloads заменяет defaults. Событие socket drain приближено завершением записи; необязательные расширенные XPath-фильтры поддержаны не полностью |
| Федерация | входящие/исходящие STARTTLS, CA/DNS-ID, SASL EXTERNAL, DNS SRV, проверка домена каждого stanza, ограниченный пул соединений | Нет dialback/DANE/OtherName-only/SRV-ID/bidi/direct TLS |
| HTTP upload | Несколько XEP-0363 components, Host/path isolation, local/S3 gateway, multipart/SigV4, streaming PUT, GET/HEAD/range/conditional, квоты/expiry, native metadata/JWT; offline local↔S3/S3↔S3 migration с SHA-256/resume; intent/exact-key recovery и quarantine; реальный 10 GiB PUT/GET через MinIO проверен | MinIO single-node с описанным netlink adapter тестовой среды; AWS/distributed deployment не проверены. Миграция требует остановки сервера и временного места под один файл; source сохраняется. Потерянные ownership metadata и неопределённый Complete требуют отдельного разбора; HTTP-UPLOAD.md |
| WebSocket/BOSH | WS/WSS и HTTP/HTTPS, общая XMPP-сессия, origin rules, BOSH RID/retry/hold/reorder/restart; http_altconnect XRD/JRD discovery; http_paths, HTTP Host/fallback, внешний URL, доверенные IP/CIDR и forwarded headers для TLS reverse proxy | Один VirtualHost с компонентами; нет PROXYv1/v2, произвольных Lua HTTP-приложений и горячей перенастройки. См. HTTP-PROXY.md |
@@ -141,3 +143,50 @@ HTTPS учитывает https_interfaces (включая* → IPv4+IPv6), HTTP
PEP production причина по-прежнему не доказана; новая диагностика различает
parser-valid IDs и форму уведомлений. Нет подтверждения Android UI, живого
production WSS, DNSSEC signing или полной замены Prosody.
## Восстановление MUC и SM в 0.8.0
MUC-журнал хранит последнее согласованное поколение конфигурации, ролей,
членства и ограниченной истории, включая временные комнаты без MAM.
Неудачная фиксация откатывает изменение комнаты; внешний MAM-архив и прочие
побочные эффекты не составляют единую транзакцию с журналом. Heartbeat обновляет
живое состояние, но не продлевает deadline уже ожидающих записей.
При действующей сохранённой SM-сессии bootstrap восстанавливает логическую
сессию и связанное MUC-членство до начала приёма соединений. Offline-доставка
попадает в SM-очередь, а не в несуществующий сокет. Для сетевого resume нужны
новая TLS/SASL-аутентификация и действующий токен владельца. Без такой SM-записи
действует обычное повторное подтверждение полного JID и MUC admission.
Учтены `muc_max_rooms`, `restrict_room_creation`,
`allow_unaffiliated_register`, `enforce_registered_nickname`,
`muc_registration_include_form`, `muc_tombstone_expiry` и component-admin
ad-hoc destroy/set-affiliation. Общие и host `modules_disabled` применяются
к vCard и occupant-id. Эти встроенные обработчики не запускают Lua-плагины.
Ограничения журнала, native-экспорта и отката:
[MUC-RECOVERY.md](MUC-RECOVERY.md). Итоги проверки:
[VALIDATION-0.8.0.md](VALIDATION-0.8.0.md).
## Durable IO0.8.3
MAM/SM используют authoritative redo sidecars. Перед сменой writer или откатом
на старую версию требуется offline `storage-checkpoint CONFIG`; после аварии
нельзя просто удалить sidecars. Подробности — [DURABLE-MESSAGE-IO.md](DURABLE-MESSAGE-IO.md).
### Внешний перезапись MAM baseline
Внешняя перезапись native `.list` поверх зафиксированного redo-baseline (бакап,
копия data_path, Prosody, старая версия) давала «archive redo baseline changed
in place»: каждый MAM-append падал и через `?` ронял session, клиент
реконнектился, сообщение не доставлялось. В 0.8.4 MAM-архивация на личной
доставке стала best-effort (storage-ошибка не роняет session и не роняет доставку;
в лог — `mam-archive-skipped`), а `storage-checkpoint` получил ремонт непроверенного
baseline: текущий синхронизированный native считается авторитетным и неприменимый
журнал удаляется.
С 0.8.4-followup живой append больше НЕ fail-closed: `recover_redo` при первом же
неприменимом baseline читает только header и сам удаляет журнал, принимая synced
native (как Prosody), восстанавливая append на скорости Prosody без секундных
`message-processing-slow` и прекращая `mam-archive-skipped`. Полная коррупция
header-checksum или неполный baseline по-прежнему блокируют startup (fail-closed,
инвариант 0.8.3) и не матчатся `is_untrusted_redo`.
+143
View File
@@ -0,0 +1,143 @@
# Личные сообщения и 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 или отката на старую версию остановите сервер и выполните:
```bash
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 всего старого архива.
## Внешний перезапись baseline: «archive redo baseline changed in place»
Если какой-либо writer (бакап-восстановление, копия data_path, Prosody, старая
версия сервера) перезаписал native `.list` поверх зафиксированного baseline без
handoff, `.rredo` перестаёт совпадать со своим anchor. Каждый вызов
`archive_for_user` возвращал `Err`, и через `?` ошибка роняла session
(`connection-closed reason=protocol-or-transport-error`) — клиент (напр. Monal)
реконнектился по кругу, сообщение не доставлялось.
Начиная с 0.8.4 MAM-архивация на трёх местах личной доставки (отправитель,
получатель, входящая федерация) стала best-effort: при storage-ошибке исходная
stanza всё равно маршрутизируется, обе сессии остаются живы, в лог пишется
санитизированный `mam-archive-skipped`.
Критическое уточнение (0.8.4-followup): живой append больше НЕ fail-closed.
`recover_redo` при первом же untrusted-baseline сбое читает только 96-байтовый
header (ранняя anchor-проверка до чтения/валидации кадров) и сам зареТирует
неподключённый журнал (`retire_redo`), принимая синхронизированный native как
Prosody. Это устраняет O(журнал) чтение+checksum на каждый access при больших
сломанных журналах (секунды `message-processing-slow`) и возвращает append на
скорость Prosody. Полная коррупция header-checksum или неполный baseline по-прежнему
блокируют startup (инвариант 0.8.3): они не матчатся `is_untrusted_redo` и остаются
fail-closed, чтобы не потерять committed redo frames без цели.
Ручной `storage-checkpoint` для этого больше не требуется; offline handoff
остаётся идемпотентным способом штатного durable handoff перед сменой writer:
```bash
rust-xmpp storage-checkpoint /etc/prosody/prosody.cfg.lua
```
`prepare_rewrite` по-прежнему распознаёт непроверенный baseline (changed in place,
shorter, missing, journal exceeds bound), считает текущий durably-синхронизированный
native авторитетным, синхронизирует его и удаляет неприменимый журнал. После
MAM history читается, а новые append создадут свежий redo поверх нового baseline.
Сверку совместимости native-формата см. в исходниках Prosody
(`util/datamanager.lua` `list_append`/`list_store`, `plugins/mod_storage_internal.lua`
`archive:append`): Prosody пишет `item(...)` и при полной перезаписи применяет
atomic rename — что и меняет первые bytes baseline, оставляя Rust-журнал устаревшим.
## Диагностика
`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](VALIDATION.md).
Общий mutex SM store, обязательное durable IO, большие pending snapshots,
полные MAM queries/maintenance, PEP storage и federation всё ещё имеют стоимость.
Медленный диск не становится быстрым от сокращения числа sync. Проверка на
loopback не устанавливает причину каждого production warning; новые phase
fields позволяют проверить это на целевой системе.
+66
View File
@@ -0,0 +1,66 @@
# GitHub Actions → private Gitea Registry
Workflow: `.github/workflows/ci.yml`. Push, pull request и ручной запуск выполняют
проверки; публикация разрешена только для основной ветки репозитория и тегов
`v<версия Cargo.toml>`. PR, включая PR из forks, не получает registry credentials.
## Настройка GitHub
Settings → Secrets and variables → Actions:
| Тип | Имя | Значение |
|---|---|---|
| Variable | `GITEA_REGISTRY` | `gitea.example.org` или `gitea.example.org:5000`, без `https://` и пути |
| Variable | `GITEA_IMAGE` | `owner/rust-xmpp`, lowercase, без registry и тега |
| Secret | `GITEA_REGISTRY_USERNAME` | Имя пользователя Gitea с правом публикации пакетов owner |
| Secret | `GITEA_REGISTRY_TOKEN` | Personal access token с правом записи packages |
Создайте private owner/organization и проверьте права чтения пакета в Gitea:
аутентифицированный push сам по себе не меняет публичность owner/package.
Registry должен быть доступен GitHub-hosted runner по HTTPS с доверенным CA.
Для registry только в LAN потребуется runner с доступом к этой сети; настройки
runs-on для image job можно заменить на labels доверенного self-hosted runner.
Не запускайте PR-код на привилегированном self-hosted runner без изоляции.
## Проверки и результат
Job `verify` на Ubuntu24.04 ставит Rust из `rust-toolchain.toml`, Python3.12,
Lua5.4, OpenSSL/ICU и собирает оригинальную bundled Prosody cafd74b9c5ea.
MinIO собирается из unmodified upstream RELEASE.2025-04-22T22-12-26Z,
Go1.24.3, с проверкой SHA-256 исходного архива. Sandbox adapter не применяется.
`scripts/ci/verify-all.sh` запускает:
- fmt, все обычные Rust tests, Clippy и release build;
- все сетевые проверки из verify-local.sh, native module/storage и federation;
- обе нагрузки500TLS, в том числе MUC+SM SIGKILL/resume;
- оставшиеся ignored tests: AF_UNIX admin socket и archive>128MiB;
- реальные10GiB PUT/GET через MinIO и через local HTTPS backend, последовательно.
Таким образом отсутствие native reference или MinIO является ошибкой, а не skip.
Нужно не менее20GiB свободного диска перед проверками. На одноразовом hosted runner
удаляются неиспользуемые Android/.NET/GHC SDK; Go caches очищаются после сборки MinIO.
Тесты могут занимать десятки минут. Timeout verify job —90минут.
Artifact `rust-xmpp-linux-amd64`: проверенный release executable и SHA256SUMS.
Artifact `test-results`: полный лог и JSON-отчёты, также при ошибке теста.
Job `image` скачивает именно этот executable, проверяет checksum и собирает
Docker target `ci-runtime`. В образе выполняются `rust-xmpp --version` и
`prosodyctl --version`, затем извлечённый executable сравнивается побайтно с artifact.
Image smoke проходит и на PR. Только после успеха выполняются login/push.
Теги:
- `sha-<полный40символьныйcommit>` — каждый публикуемый commit;
- `latest` — только основная ветка;
- `v0.8.2` — пример version tag; он должен совпадать с Cargo.toml.
Version tag не меняет latest; prerelease также не перезаписывает latest.
Собирается Linux amd64, не multiarch. Локальный `docker build .` по-прежнему
собирает source target `runtime`; CI target использует уже проверенный binary.
Workflow добавлен и проверен локально как конфигурация. Реальный GitHub-run,
Docker daemon и push в ваш Gitea здесь не выполнялись; для этого нужны репозиторий
и перечисленные настройки. CI намеренно останавливает публикацию при любой ошибке.
Документация registry: https://docs.gitea.com/usage/packages/container/
+63
View File
@@ -0,0 +1,63 @@
# Задержка доставки: исправление 0.8.1
В0.8.0 MUC после каждого groupchat append запускал expire_before. Эта операция
под общим mutex компонента проходила по всему архиву и записывала сохранённые
записи во временный файл даже при отсутствии истёкших записей. Задержка каждого
сообщения росла с размером истории. Дополнительно переписывались неизменённые
native room config/persistent index, а CSI сохранял полную durable SM-сессию
даже при пустой/неизменённой очереди.
В0.8.1 эти лишние операции исключены из обычного пути доставки. Очистка архива
остаётся в периодическом maintenance (интервал сервера300с) и перед MAM-запросами.
Неизменённые native-файлы не переписываются; недостающие/изменённые экспорты и
повтор после ошибки сохранены. Авторитетный MUC checkpoint и SM state по-прежнему
записываются до подтверждения соответствующих изменений. CSI откатывает изменение
очереди при ошибке checkpoint. Ненаправленный в MUC account traffic обходит mutex
компонента и не ждёт его обслуживания архивов.
## Измерение
Настоящий STARTTLS, два клиента, durable SM, MUC MAM с finite retention,
active/inactive CSI. Изолированный loopback; архив native `.list` заполнен до
запуска сервера. Проверяются доставка sender echo/peer, counters, durable CSI
buffer/flush и точное число записей архива. Порог задержки в миллисекундах не
используется как flaky assertion; структурные регрессии запрещают полный scan
архива при append и запись неизменённых native-файлов.
Active CSI, MUC peer, мс:
| Записей истории | 0.8.0 median / p95 | 0.8.1 median / p95 |
|---:|---:|---:|
|0|9.425 / 11.221|5.054 / 7.643|
|1000|32.718 / 36.995|4.280 / 6.605|
|10000|259.472 / 269.067|4.895 / 6.307|
|100000|2517.940 / 2520.941|4.680 / 14.532|
Inactive CSI,100000 записей: median3866.027→4.285мс,
p95 4150.867→5.072мс. Первые три строки —5samples/state, последняя —3.
Это ограниченное сравнение на одном loopback host, а не production SLA и не
доказательство всех возможных причин задержки у реальных клиентов.
Полные сырые samples и SHA-256 обеих сборок — validation-logs-0.8.1/latency-*.json.
Команды:
```bash
python3 tests/message_latency_network.py --binary target/release/rust-xmpp \
--rounds 5 --history-records 0 1000 10000 --output /tmp/message-latency.json
python3 tests/message_latency_network.py --binary target/release/rust-xmpp \
--rounds 3 --history-records 100000 --output /tmp/message-latency-100k.json
```
Быстрая проверка0/10000 records уже включена в verify-local.sh и GitHub Actions.
Для повторной медленной проверки используйте сохранённую0.8.0 release-сборку;
не приписывайте исходному бинарнику результаты пересобранной новой версии.
## Если задержка остаётся в целевой установке
Сопоставьте время отправки, получения и отображения delivery/read receipt,
тип чата, transport, состояние CSI и наличие федерации. Доставка сообщения и
receipt — разные события. Native CSI policy для низкоприоритетных payload не
менялась. Длительный MAM query, периодическая очистка большого архива, медленный
диск или внешний XMPP-домен могут иметь отдельную задержку. Текущие изменения
не отключают fsync, не обещают exactly-once и не подменяют проверку реальных
Conversations/Monal/Gajim либо deployment.
+132 -55
View File
@@ -1,68 +1,145 @@
# MUC: расширения и восстановление0.7.6
# MUC: протокол и восстановление 0.8.0
Сопоставление выполнено с предоставленной Prosody cafd74b9c5ea: plugins/muc
и mod_muc_mam.lua. Поддержка относится к встроенным расширениям этой ревизии,
а не к произвольным community Lua plugins.
Реализация сопоставлена с предоставленной Prosody `cafd74b9c5ea`, прежде всего
`plugins/muc` и `mod_muc_mam.lua`. Известные встроенные возможности перенесены
в Rust; произвольный Lua-код и community-модули Prosody не исполняются.
Неизвестный явно включённый MUC-модуль отклоняется при запуске, а не молча
игнорируется. Список `modules_disabled` применяется с учётом общих и host-настроек,
в том числе для `muc_vcard` и `muc_occupant_id`.
Добавлены hats из affiliation data, язык комнаты, политики PM/модераторских PM,
фильтрация presence по ролям и формы конфигурации. Поддельные occupant-id/hats
и MUC user identity из клиентского сообщения удаляются. muc_vcard и
muc_occupant_id можно отключить. Настройки MAM включают muc_log_all_rooms,
muc_log_presences, muc_log_include_total; presence хранится отдельными событиями,
обычный MAM-запрос выбирает groupchat сообщения как в native.
## Что сохраняется
История работает без MAM: ограниченный буфер, maxstanzas/maxchars/seconds/since,
delay и раздельный subject. Архив MAM продолжает поддерживать историю persistent
комнат. Удаление комнаты очищает её MAM-архив.
MUC теперь записывает единый журнал компонента `rust_muc_checkpoint`:
конфигурацию комнат, affiliations, текущие nick и роли участников, безопасные
поля presence и ограниченную историю обсуждения. Это относится к persistent
и временным комнатам и работает без MAM. Журнал содержит согласованное поколение
состояния; запись через временный файл, синхронизацию и rename предшествует
возврату результата операции и публикации соответствующих MUC-ответов.
При ошибке сохранения изменение состояния комнаты откатывается в памяти.
## Как восстанавливается членство
Сохранение выполняется при изменениях членства, конфигурации, affiliations,
ролей и истории, а также при штатной остановке. Поэтому восстановление больше
не зависит только от SIGTERM: после SIGKILL используется последнее успешно
зафиксированное поколение. Heartbeat обновляет отметки живых участников даже
в тихой комнате. Неудачный heartbeat фиксируется в журнале сервера; состояние,
которое не удалось сохранить, нельзя считать защищённым от следующего сбоя.
При SIGTERM/Ctrl-C сервер сначала сохраняет метаданные комнаты и native
`state/<roomnode>.dat`, затем завершает соединения. Сохраняются nick, полный JID,
роль, show/status/priority и последнее сообщение истории. Это относится также
к временным комнатам с участниками. Транспортное отключение во время shutdown
не считается добровольным выходом из комнаты и не удаляет checkpoint.
Обычная история без MAM сохраняет порядок, время сообщений и ограничения
`maxstanzas`, `maxchars`, `seconds`, `since`; subject хранится отдельно в
конфигурации комнаты. MAM остаётся самостоятельным архивом и не превращается
в часть одной транзакции со всеми внешними эффектами. Presence-события MAM
и groupchat-запросы остаются раздельными.
После запуска сохранённые участники ожидают восстановления до300 секунд.
Они не становятся активными адресатами маршрутизации автоматически. Когда
тот же полный JID проходит SASL, bind и отправляет общую available presence,
сервер повторно проверяет комнату и восстанавливает членство, nick, роль и
presence. Одного совпадения bare JID или resource недостаточно.
Клиент может по-прежнему явно войти обычной MUC presence.
## Возвращение участников
Отпечаток конфигурации должен совпасть с сохранённым: изменение пароля или
прав комнаты отменяет автоматическое восстановление; требуется обычный вход
с актуальными параметрами. В presence state не записываются MUC-пароли и
произвольные расширения. Пароль комнаты остаётся в исходной native config,
как и до этого. Тот же checkpoint не продлевает grace при повторных перезапусках. Успешно использованная запись восстановления
погашается на диске до отправки результата клиенту, чтобы авария не вернула
старую роль после выхода или снятия модератора.
Неактивированные временные комнаты очищаются после истечения срока.
Без сохранённой SM-сессии после запуска членство становится ожидающей записью, а не активным
адресатом маршрутизации. Автоматический MUC-вход возможен после новой
аутентификации, bind того же полного JID и общей available presence. Bare JID
или чужой resource не заменяют эту проверку. Обычный явный вход с MUC `<x/>`
тоже доступен; актуальные ограничения комнаты проверяются повторно.
Native формат читается/пишется и проверяется настоящей Prosody в обе стороны.
Для автоматического восстановления используются checkpoint с отпечатком и
временем Rust; старый native state без этих полей сам по себе не является
разрешением на автоматический вход. Обычное повторное присоединение доступно.
Срок ожидания задаёт `muc_recovery_grace_period` в секундах: по умолчанию `300`,
диапазон `1..604800` (значения ограничиваются этими пределами). Живое членство
получает свежую отметку при фиксации и heartbeat. Уже ожидающая запись имеет
фиксированный срок: запросы, неудачные попытки и повторные перезапуски его
не продлевают. Неактивированные временные комнаты удаляются после истечения
срока.
Это восстановление XMPP-членства в комнате: старые TCP/TLS-сокеты после
остановки процесса не существуют. SM resume между процессами не реализован.
Аварийный SIGKILL/отказ питания не создаёт новый checkpoint: гарантируется
проверенный штатный shutdown, а не полный crash recovery.
Смена пароля, affiliations или иной конфигурации отменяет старые ожидающие
разрешения. Подтверждённые изменения роли, явный выход и удаление комнаты
попадают в журнал до ответа, поэтому следующая авария не должна возвращать
предыдущую роль или прежний nick. Успешное восстановление создаёт текущее
членство: при следующей аварии восстанавливается его последняя сохранённая
версия, а не навсегда погашенная старая запись.
История ограничена `max_history_messages`: по умолчанию100, допустимый внутренний
предел4096. Состояние комнаты ограничено4096 ресурсами. Необязательные presence
поля слишком большого размера и последнее сообщение больше16KiB не включаются
в checkpoint; это не мешает сохранить членство. Текущие сообщения доставляются
по обычным транспортным лимитам.
В checkpoint presence входят только `show`, `status`, `priority`.
MUC-пароли и произвольные расширения presence туда не копируются. При живом
обмене обычные caps, avatar и idle сохраняются; присланные клиентом MUC identity,
occupant-id и hats фильтруются. Пароль комнаты остаётся в её конфигурации.
## Проверка и границы
Если имеется действующая приватная SM-запись Rust, логическая сессия продолжает
членство без транспорта. При запуске повторно проверяются наличие и состояние
аккаунта и текущие ограничения комнаты. Маршрутизация восстанавливается до
приёма клиентов: сообщения для ещё не подключившегося участника попадают в его
сохранённую SM-очередь. Синтетические join presence и история повторно не
отправляются. Доставка по сети требует новой TLS/SASL-аутентификации и корректного
SM-токена того же владельца; native MUC-файлы сами по себе этого не разрешают.
Автоматические тесты используют отдельные временные данные. Сетевой сценарий
останавливает процесс с живыми клиентскими socket, запускает его повторно,
проверяет persistent и temporary комнаты, запрет чужих identity/resource,
восстановленные роли/presence и доставку сообщений. Native oracle проверяет
перенос state с несколькими ресурсами туда и обратно.
## Native-файлы и переход между серверами
Полная production-миграция, непрерывные федеративные reconnect и все community
расширения не проверены. Конкретные результаты — VALIDATION.md; общие пределы
сервера — COMPATIBILITY.md. Реальные клиенты должны повторно аутентифицироваться.
Native config, persistent index и `state/<roomnode>.dat` читаются и экспортируются
для совместимости с Prosody. При работе Rust источником согласованного
восстановления служит его журнал, а native-файлы являются экспортом после commit.
Устаревший экспорт после сбоя не должен возвращать предыдущее членство.
Native state без подтверждающих Rust-метаданных сам по себе не разрешает
автоматический вход.
Редактирование native-конфигурации администратором обнаруживается по отпечатку:
старые разрешения восстановления и история, привязанная к прежней политике,
отбрасываются; удаление ранее экспортированной конфигурации отменяет её
восстановление. Это не универсальное слияние параллельных записей двух серверов.
Для отката на Prosody остановите Rust и сохраните резервную копию всего
`data_path`. Начиная с0.8.3 перед сменой writer выполните новым binary
`rust-xmpp storage-checkpoint CONFIG`: он фиксирует MAM redo и SM JSON exports.
Это не перенос Rust SM-сессий в Prosody и не универсальная конверсия MUC journal.
Подробности — [DURABLE-MESSAGE-IO.md](DURABLE-MESSAGE-IO.md). Prosody использует native-экспорт и не читает Rust-журнал. Перед
возвратом с Prosody на Rust согласуйте данные и устраните устаревший
`rust_muc_checkpoint` в остановленном экземпляре: иначе неизменившиеся native
файлы могут быть приняты за экспорт предыдущего поколения Rust. Удаление
журнала означает отказ от его crash recovery, включая историю временных комнат.
Не запускайте оба сервера с общей записываемой директорией.
## Поддерживаемые политики
- Доступны hats из affiliation data, язык комнаты, роли presence broadcast,
политики PM и модераторских PM, формы конфигурации и offline affiliation presence.
- `muc_vcard` и `muc_occupant_id` управляют соответствующими возможностями.
MAM учитывает `muc_log_all_rooms`, `muc_log_presences`, `muc_log_include_total`.
- `restrict_room_creation`: `false` разрешает создание по умолчанию, `true`
ограничивает его настроенными администраторами, `"local"` — локальными
зарегистрированными пользователями и администраторами. `muc_max_rooms`
ограничивает число комнат; существующее членство не требует повторного
разрешения на создание.
- Регистрация учитывает `allow_unaffiliated_register`,
`enforce_registered_nickname`, `muc_registration_include_form`.
Срок tombstone задаёт `muc_tombstone_expiry` (по умолчанию 31 день).
- Администраторам компонента доступны ad-hoc команды
`http://prosody.im/protocol/muc#destroy` и
`http://prosody.im/protocol/muc#set-affiliation`. Полномочия проверяются
на каждом шаге; изменения проходят обычный механизм фиксации MUC.
Presence probe не создаёт участника и возвращает сведения только допущенному
участнику. Новый вход без MUC `<x/>` отклоняется; неизвестные типы presence
не превращаются во вход. Presence errors исключают соответствующую сессию;
message errors делают это для native-набора ошибок доставки. Последняя сессия
получает статус `333`; остальные ошибки приватного сообщения могут быть
переданы адресату. Сообщение с `subject` и `body` либо `thread` остаётся обычным
сообщением и не меняет тему комнаты.
## Границы и проверка
Старые TCP/TLS-сокеты после остановки процесса не существуют. Восстановление
членства MUC и XEP-0198 resume — отдельные механизмы. В 0.8.0 Hub сохраняет resumable
SM-сессии, счётчики и очередь неподтверждённых stanza. Resume проходит на новом
защищённом аутентифицированном транспорте, включая legacy SASL и SASL2;
успешный SM resume восстанавливает MUC без дополнительной available presence.
Входящая stanza учитывается в handled-счётчике после обработки. Heartbeat и
штатная остановка сохраняют снимок; offline deadline не продлевается простыми
проверками или повторными запусками.
SM-снимок приватный: предел 256 MiB суммарно, 8 MiB и 4096 stanza на сессию.
Он не является native-форматом Prosody. Нельзя обещать сохранение сокета или
ровно одну доставку при неопределённом результате сетевой отправки.
Буфер истории ограничен `max_history_messages` (по умолчанию 100, предел 4096),
состояние комнаты — 4096 ресурсами. Большие необязательные поля presence
исключаются из checkpoint; отдельные сообщения истории больше 16 KiB не
восстанавливаются. Общий checkpoint компонента ограничен 16 MiB: превышение
лимита приводит к ошибке записи, а не к успешному подтверждению несохранённого
изменения. Долговечность предполагает исправный диск и файловую систему,
соблюдающую синхронизацию записи.
Команды и результаты проверки версии 0.8.0 публикуются в
[VALIDATION-0.8.0.md](VALIDATION-0.8.0.md). Полный прогон завершён успешно. Общие границы совместимости:
[COMPATIBILITY.md](COMPATIBILITY.md).
+107
View File
@@ -0,0 +1,107 @@
# Личные сообщения: исправления0.8.2
Предыдущий0.8.1 устранил per-message MUC archive scan и часть лишних CSI writes.
Отдельная проверка личных чатов обнаружила другие причины. Испытания ниже
проведены на loopback; это не подтверждение поведения конкретной production-сессии.
## Push не должен останавливать чат
В прежнем обработчике после локальной доставки выполнялся await внешнего
push IQ route и только потом обновлялся handled и читалось следующее сообщение.
Неполадки DNS/TCP/TLS/SASL внешнего шлюза задерживали отправителя до сетевого
таймаута (30с на route). При полностью offline получателе push даже отправлялся
раньше записи offline archive.
Регрессия воспроизвела настоящий исходящий STARTTLS к шлюзу, который после TLS
не присылает features. У0.8.1 через2с первое сообщение уже в durable SM pending,
второго нет, SM ACK отсутствует. У0.8.0 с полностью offline получателем в этот
момент ещё нет даже первого offline record. В0.8.2 в обоих сценариях оба сообщения
сохранены и sender ACK h=4 соответствует входящим4 stanza при том же зависшем
шлюзе. Offline records присутствуют уже при первом post-TLS stream gateway.
Уведомления теперь маршрутизирует ограниченный worker:1024queued,8in-flight.
При переполнении/ошибке notification routing пишет warning; подтверждение
сообщения не означает успешный push. Очередь уведомлений не durable и при
остановке отменяется; сами сообщения сохраняются независимо. Проверен XMPP
push fixture, не APNs/FCM и не промышленный мобильный цикл.
## CSI и зашифрованные сообщения без body
Прежняя policy считала unimportant receipt-only и ciphertext-only OMEMO без
body/EME. При `inactive` они могли ждать активности клиента. В тесте намеренный
dwell2с показан отдельно: это время удержания, не вычислительная задержка.
Сервер уже быстро отвечал SM ACK, но пользователь ещё не видел payload/receipt.
Default `csi_important_payloads` в0.8.2:
- `{eu.siacs.conversations.axolotl}encrypted`
- `{urn:xmpp:omemo:2}encrypted`
- `{urn:xmpp:receipts}received`
- `{urn:xmpp:chat-markers:0}received`
- `{urn:xmpp:chat-markers:0}displayed`
- `{urn:xmpp:chat-markers:0}acknowledged`
Они выходят без ожидания active. Body, EME и остальные прежние important payload
сохраняют поведение; headline и прочие unimportant данные продолжают буферизоваться.
Явный список заменяет defaults: если уже настроили свой список, добавьте нужные
пути туда. Для строгой прежней policy в JSON используйте `csi_important_payloads: []`;
эквивалентный явно непустой Lua-list без новых extensions:
`csi_important_payloads = { "{urn:xmpp:eme:0}encryption" }` (EME уже important).
Defaults увеличивают число немедленных доставок и возможных пробуждений клиента.
Старая importance function/native oracle сохранена; изменён именно runtime default.
На bind и resume применяется текущая конфигурация, включая сохранённые старые slots.
В6TLS fixtures:100000MAM/user, roster0/5000/10000,500durable pending, default/explicit[]:
все проверки прошли. Default inactive OMEMO legacy/2 —2.58–4.06мс,
receipts —7.36–35.56мс; при explicit[] сохраняется hold до active.
Headline buffering, EME delivery и точный счёт100523MAM records/user проверены.
Это XML/wire проверка доставки ciphertext, не проверка OMEMO криптографии клиента.
## Большой личный архив при входе
Bind2 и обычный MAM metadata IQ раньше декодировали все записи ради start/end.
На100000records,5samples:
| Операция | Старая медиана |0.8.2медиана |
|---|---:|---:|
|SASL2 Bind2 authenticate→success|2742.353мс|1.976мс|
|TCP/STARTTLS→Bind2 success|2745.115мс|3.565мс|
|MAM metadata IQ|2437.945мс|0.355мс|
Baseline0.8.0 содержит тот же прежний scan path; hash сохранён в raw report.
Новый edges_store читает ограниченные endpoint records под существующей блокировкой,
не создаёт/не требует index и восстанавливает partial append. Проверяются exact
IDs/timestamps. Это операция metadata, не проверка integrity всех промежуточных
записей. Полные MAM queries и периодическая очистка всё ещё могут читать архив.
## Проверка и диагностика
Новые scripts:
```bash
python3 tests/push_latency_network.py --binary target/release/rust-xmpp --recipient hibernating
python3 tests/push_latency_network.py --binary target/release/rust-xmpp --recipient offline
python3 tests/personal_message_latency_network.py --binary target/release/rust-xmpp
python3 tests/bind2_archive_latency_network.py --binary target/release/rust-xmpp --records 100000 --samples 5
```
Быстрые варианты включены в verify-local.sh и CI. Push fixture использует
loopback port5269; конфликт порта является ошибкой fixture. Global DNS/resolv.conf
не меняются; fixture использует special-use localhost и другой account domain.
`message-processing-slow` warning содержит duration_ms, route и correlation connection.
Оно фиксирует завершившуюся серверную обработку>250мс, без текста сообщения.
Если задержка остаётся, сопоставьте его с клиентским временем, типом transport,
CSI state, регистрацией push и внешней федерацией. Отсутствие warning не доказывает
отсутствие сетевого/клиентского ожидания до/после обработки.
SM Store пока использует общий mutex для durable slot IO; обязательный fsync
не отключён. Полные архивные запросы/maintenance, большие roster, медленный диск
и внешняя федерация имеют собственную стоимость. Производственная квалификация
клиентов и полная замена Prosody остаются за рамками этих fixtures.
Итоговые тесты/сырые отчёты: VALIDATION-0.8.2.md и validation-logs-0.8.2/.
## Продолжение0.8.3
Новые логи показали seconds-level серверную обработку. Добавлены redo MAM/SM
и разбивка времени на этапы: [DURABLE-MESSAGE-IO.md](DURABLE-MESSAGE-IO.md).
+57 -23
View File
@@ -1,8 +1,20 @@
9984392a77fcbb6d0ee70b536ecdcfc4349101bb300a509e32353aaa1134979e .github/workflows/ci.yml
6593266e57a718762fbca26fd893fcabf1ea842dcc53dd0fd11844b2def2a543 AGENTS.md
b83b20e991af384b349825ca39aad157d8b412d0143cc34c68567ca011bccf58 Cargo.lock
3ecc46532a7fb1a231dbbd5337221d0c0c32ba36574e74655854f1a1d54d4581 Cargo.toml
6be2fbcd1da8a07dda7daf3ddd727b38f363751ebace9c0b87c282f8961cffed Dockerfile
7136ea911bb6b6d352f703f80efeebde921028ea9dc39cd0d2cd55ea465f8c37 examples/config.json
8a65739eb5c24f10b9f2c509f0bd050fb1115706e6a41fdf804b9e0e44f583fd examples/config.toml
ca86274bcf4dc5daebbf69c9f003854afca92954719aff068f810e0a3d615dea examples/config.yaml
bbc2591581d382fef5fb918a480e6c338c7e3e3f2025df5509f51708534631e7 examples/converse-origin.fragment.yaml
66ea9e2b58f6a39b70911b9981cae5e99625483af7a41168e24923464de8ae56 examples/reverse-proxy.fragment.yaml
d13306be0ecda81ce779baa290ffb417380d525928477f16200884b5169cca33 rust-toolchain.toml
c08bd5923906f4d2163a258c8b68dbef39ea6a3e07032f67e5c12d3d9ca81c5e scripts/ci/image-tags.py
8820d262f8018a822b27eb9e5f0d3b9ca0c1272001164ca8a59fcd13911a5426 scripts/ci/prepare-minio.sh
5bcd646c6c9159bbdce3a4eadce4d80d6c7422748e47c6ad04e5769efb4f2920 scripts/ci/prepare-reference.sh
cb44f2692aefd618872153aed2658a7ab5c1a420802592e3240553b1f8da5ea5 scripts/ci/vendor/README.md
1e558d3d2211ea0228596aced2b71306503190c454ec4cb44f172045b3513c34 scripts/ci/vendor/prosody-cafd74b9c5ea.tar.gz
7ae11639143308ed4acd30ef6f99f55c7575893e5f6dce819d1f9d7da00cc29c scripts/ci/verify-all.sh
16e476e50c5802efcbb7d1aa68141c183c60f5ab17037d697dce720c209141f4 scripts/docker-entrypoint.sh
6fcb8a4977fee840d826951b0c30a3f2477c55d00860c79e99f635ca6e215e2b scripts/filter-android-pep-log.py
a83cfdb6d910af1add9a14df7b0bd43fb0e2a23342f1a1eec831d47c659bba6f scripts/integration_http.py
@@ -14,13 +26,13 @@ b1018e1b279c89f6cf14b4017e4f1b14e9165afef2f668cec6ac8bbe0c710d85 scripts/integr
e08ac6136cce27cda2ddbd510da0f25750766ab0150a9f4b8f87c953276d16e9 scripts/prosody_compat/oracle.lua
2c7a2c946885205e9df38b97305073a8ac72f58ac8ff84fc6f9af320c35bbc96 scripts/prosody_compat/verify.sh
1b9ed063daf3e0320819b4fe0135c96dd27e49877044aa2f5093b8ab4fbda41d scripts/smoke.py
0b2f6209a36fe9783b83d43396da26862c89cad1da1483ae49a79800e31982c1 scripts/verify-local.sh
2cd5e17cdf0902ceaaca9b6e8d36ca766e33e66a06784ec97ad99165571eaf91 scripts/verify-local.sh
a2e47f547ab9349f84ee6c4f8e51c147b7110b41a82c17cdb938027a84d82ef3 src/account_modules.rs
5637f816586f0613b83e14d0ed478bd539f325b70ccbfc9fdf893adbe494453d src/admin.rs
89d7e29d8f8daab614c9c836864b166fdaa324ca035c2d3d5f95639c24b2a9e1 src/archive.rs
1b2011fb38482eba69dc245c367ddf496089d0cbde8a000b1ff5e9bbf8c3dea8 src/archive.rs
e15b079a420227ee59e3b7ac305783c51eb59d76a8fb5dd12760547b0f812107 src/auth.rs
83ac35a4c9d20debf006fe80fba8e423c09806fb2526ff384b188ca28d612a5e src/config.rs
6574ad70d5c60c4acf7e01e66dadaa7cd2d09d3b184a611418aeb271f27ebc3c src/csi.rs
175ab07f17ba14c51c76857c4905b0a587d8be68e2be5179456e9c71a523c487 src/csi.rs
834ebe4a1f2d46ab932c14c0a2c7be3375873a71ef56026d372c86e60ef041fc src/data_lock.rs
1c8aa5cb816a32a0f9f24589ac00beba27544a83c3d9aad771523a1d1cbcd4c8 src/extras.rs
dca8959effb123b94e1fbd42c561a79c721178c4b612a004db9f9fccb3b8950d src/federation.rs
@@ -29,32 +41,37 @@ d561a6bd4db079cfb1ef3bb7ae75f23e6080c5d5705583774f6c5f1ca6ed4420 src/http_confi
3fa73ae129fe552bd7de7afca0617a1c18bf4d3b36a39023bfec036bca17ffc5 src/http_proxy.rs
f4f87f5c20cb0602d803e86a22c209756c3b1f22c532743ec7e0dbf5bb174a0e src/http_service.rs
c604a0a764ffac6bae0c9aa15c9ef7022d35cb1f8c9159eb66257114445621c5 src/http_transport.rs
c5b7bcc0d08e48fd3805d6571428bb83b7fdc6f6e23a65bf689f079ad87bc32a src/lib.rs
5b27b6c719537af919bd6fe089574eba5e2c7bbe0d32249164914a4bb57a1427 src/lib.rs
a21e1119ea0271b63935e92eb106ab6076f0035e6d3a06c21a352f152b9b37db src/logging.rs
58ce2a9daa36ac26b39c8f263a16f4fa8cc4e57c91bbbf5b24a6aad3f1a326e0 src/lua.rs
a264e2765a31c8c096462551aca229530cad356e39e338370d1a0bdd97860680 src/main.rs
71d5d185900b9cde190e10e2f28371cd5042d98b5900b4385d823f724234fced src/mam.rs
0f780d95523b41c4ae242bd4a1fb6b65ff57826dca6d23ac73a1a1459609ec0a src/muc.rs
d8263eb28f5cdff362512a6b13217aa8f7574f4a3d54b7bcb6ba7930f8c353d8 src/muc_history.rs
4c151ffcdc36e4a3e680f859d4dc7ba16b0162d4f1fe6fb470df4c5e21197e5a src/muc_recovery.rs
f103c15e11d61b5818e9a9491b8533b904cade7b3654c481cc61d3a7d0bb8173 src/main.rs
68d97d96aa3a370474c144ccfdbf16109c46a6b14123d55d30464237ae29a958 src/mam.rs
f17b06fe5b76381b5cdbfc2b668107f6c2b0b456c05b7a7709bea6a284531ff5 src/muc.rs
c7ba46a140b9dce65cc573176265d6f77049ae657a70ffe59611a1c0f3ef89af src/muc_admin.rs
ed2f07105ffeb80396aaf91c7a1fe9a8dc88d17235d03a0796372c1797c1506a src/muc_history.rs
636f6ea7ad7cc98390ac775a93b469d2f3e0f9e138be5bd1e256cad5037c7183 src/muc_recovery.rs
3252884252158e1c4bd58922b97733f434c7d28baf059e63267632a3290f660e src/object_store.rs
7d7114a22ef3bf02f9caaf578f0a50d4f75b4dcd4b335b4cb8425d9716c97e71 src/pep_diagnostics.rs
f09a6479b76c9c601a169b5f1f9cf1364e82df5ae27e4bab8d1b1428b5cad457 src/preauth.rs
a7333b31a57780c57f2d30620cdfaeb7b1215808865960bb7e5b595d3ad98f7c src/protocol.rs
761e29d6431fac5c2a4a24bcfca1f393949a5c55585fd6b0dd7a4f6e9f94a10d src/pubsub.rs
0d7fec83ff188f9092b1945951f2879d846d5f6aef1e657671e8c4eeb4beb226 src/protocol.rs
8dbb7a6b43a5f0562a775df42a959ab8badc466135a8b32da86cd79615b5acbf src/pubsub.rs
913cd033f60a68f6c7cf9b3adbaf76db1c5c9bc88a0599196d373b16b64c0a1e src/push.rs
bec477f2520af6a9d7ca54e43eb12158b53e3cf09057653e504dfdeab956885e src/sasl.rs
10f24a2e8e776c7aa2475913e15b01b0f4e57f7a9de92660bafeddd912101d26 src/server.rs
894f11389bfc3fddc3508396d106146261376999f7adcef5a049458f6b4028cf src/sessions.rs
71d7ce1036a091d8c27016ea7ce4cffedf904dfca9d1cc4891b7dc66a1e06a4e src/storage.rs
1bcc6995e221f316bd4765eeb79c2ad67c9fe9e93d4254e6f6679244fc733e6f src/server.rs
c37f6c478d45b3c0741f0b1bed95f499e67d4d3eb50b78fe606e2b518170e45f src/session_store.rs
1d4e890315886bc4fd0b824e034a5c1833a1f2c1347e8c3ca77254e13a2acab9 src/sessions.rs
c9fb5d18da148a6366a18afb86eaee26a67d20c7b6664e425e612d44e79a2988 src/storage.rs
462b00edb2c1e02dd34524bafe4f82b5ef902028b44310ba31b085d31f4dc2aa src/tls_binding.rs
a78592aa79d8f01c22af1457d0c3f5922863ccd3e8ebb1aa05e043cc44579b8a src/upload_migration.rs
f9b023565711e0217238b53fb678c5fb14849cdcd2efcd0fce249018274fad69 src/wire.rs
b4eb7a230570dfde0fd0d301a6ace36559a5eae3e1402e42c76f014f61739c24 tests/_wire_client.py
45613ad6c53087a50c6c3a7969b30783578c334499bc6baceadb2d18b0b3ea4e tests/admin_socket.rs
0645fda01bede4362d998ef22aefdd2c02578dd2c2aae3878d0cff87b3679835 tests/android_sasl_network.py
ad225ade463f55bfd0734e67f5306e274cd9aab59e10e91469bf7263547cdf79 tests/archive_edges.rs
366b315521bd4f4448c0bac17bf74ddb77a728ff41fc48e29d46377625e0330f tests/archive_mam.rs
df8530d69beaf36c34e78f0f050930b7e0d6a296c22ca543dfc7533180ba0543 tests/archive_redo.rs
cb6b7bbcd1bc7ee01a4ccf892d4b6074765685c56c4b8bb46e0381b834577b6a tests/auth.rs
011bbdd00c8ff8b648076d133086c395f6784cdcbb0c9f4e1efcbc15c1e79d9c tests/bind2_archive_latency_network.py
b3c3470f0b9b64584abf98c26a4578b0b90089086d00f7b07187aa5f6db6a417 tests/cli.rs
cc58adbb0fc867802b09cf7bd0d25397a273e721fbd0d0cb0ce30c19e80c13a2 tests/config.rs
ba364e9ea561624b2c649cd0dda559e213c75e6d7080ae85f0c23f1f13d117e1 tests/csi.rs
@@ -63,6 +80,7 @@ d4d8d982c6f4ab96a809b598ea9ead7f64fa9d916547f1eb9d797aa71db64fdb tests/data_loc
be18a83fb5884cfdbd2ddc542cb25fd07b70f139cbdce1ae67562568df42a2e2 tests/debug_legacy.rs
63753c2e3524e8e7826bc2d48e2bd78e36e1cc56fde9da19950d5c35e1c38f77 tests/federation_tls.rs
1aaf06ba872cb5dbe0545afffdd8efddd0eae7305de06f3b3b75c19289581865 tests/fixtures/prosody.cfg.lua
33a8a5d9980bb9aa1162a1e3e562a9d889edd3d2f96a7073e14acc8e504bf9c7 tests/fixtures/slow_sync.c
19d570b92075d1355257c3798e4c52e404cc356dfe5dfd0c8fb384e4e7150682 tests/gajim_discovery_network.py
4398bf0639c31f0df328f0b0f1a5df17f2e69d2df84514845bba882146de06bc tests/gajim_features.rs
a346e70af3657fbf56e0bb1c39c427f630de8c349e6858b8933c52f42a21ab46 tests/http_altconnect.rs
@@ -71,19 +89,27 @@ f1ea3eb78d5c716f4281cf17cb41b9fb22e08dbb4cf08ed9c8477c79b7d2382b tests/http_pro
7c599c83eb999cd8776f0ce6811db8f478366b840311f2e513823f70b5d7cd30 tests/http_transport.rs
278db32ba0dec882b0af8f1969da87670ff3d2d749d32320154dabef51cff191 tests/http_upload.rs
da2fc65e931ec74fbcdcd26d04f2cb073e2dc1e354139135166c79580973a477 tests/iq_errors.rs
ec993a6e31d38f82abd13bd46ad99286cf13e8abea63c0385c848037a12b0cd4 tests/journal_recovery_sync_network.py
05160f17f8098466a67c9b069c165dc9d94a84ab03a49896169f110baf8e869a tests/logging.rs
dfc3429e3e755d4c99ec1bd8ee6fb0f1f6aaf86414d5adaca7e9e3b18c7fe3f7 tests/logging_network.py
0141961eadf9246ce287d7ee0dbbbf5243fd6e1332e09ccd2f6a591be419c2c3 tests/message_latency_network.py
329ccbd4faee6cbbfa92d2fa0248844721b7ffc033baae4ec2570e9893f5df9f tests/message_routing.rs
dafc2dd1e1d9e860482eb5bdf98c293842dea6a2790d27b94bf1885be3324690 tests/muc_access.rs
c28c7e9128cd1483df90dbccc7af0d756a5edc813bdff604b4560a349a844754 tests/muc_domain_bans.rs
b7d16e15e069649b9f0b055f0e0227117bb6d167c3a76cc713bd99fa19a6a60e tests/muc_extensions.rs
59561f914274b437497ff35be00ed535ea012d18c99b589c8f944ea365b7112d tests/muc_flow.rs
1134fefd5bb75884b096b8542c4f9745e64fd5dfcf87a6bba8d665aad2185a8a tests/muc_access.rs
8120c66b85c0884619e39eea0b8761e11b41f2ed14c5be00146e35ca4714a9a0 tests/muc_crash_network.py
168a8eb6ab892a379722dab0230efffb6585aaba639ad4691ec017626e1640d2 tests/muc_crash_state.rs
2150cc7f4dccf8f1277783f5ccc94d6d626f8cb34ae6d4a05bf249863af29939 tests/muc_domain_bans.rs
1d06b769b59012732ba04497a995f4f54df127f58c5f4f264a5df99589f31a94 tests/muc_extensions.rs
a8a01faf51bf82bce6cbb2616cdaabb6337047d1c5f17d5f39975c0528a1d5ae tests/muc_flow.rs
b24bd920f096576c2009ff1ccc0366eb8734c5f0b2feb39f04ad7087268a94b9 tests/muc_history.rs
1f3df5f3f39143adc2b2a419fac0cd74b2229337ffa86570261d1f6ac61137a1 tests/muc_native.rs
1b34ab721eec69295e1274e44bfd108c0e117dd383624cb6c98d851eb20e7a9a tests/muc_latency_storage.rs
18f18362f26710043b51168675daee91ae88c071fb44e48210b49686011f8722 tests/muc_native.rs
3df2736ccfd25a6cab290df80ceb7474ea44bb02af6cf768a9216d6b8f7f8630 tests/muc_optional_admin.rs
5dc93407a658b45e504b89e26cfc8f495cd28ad90d0a308b7d228a472435ff9f tests/muc_protocol.rs
ac1caa62a3cd91e74bd7100a5e542c404e97aec61a34e9627900b458c2ee3806 tests/muc_recovery.rs
b5c2e179ee61287a8ec8b6bc593d8c960ca4f87cc71f2230e0c033fadd701ed3 tests/muc_restart_network.py
e89a1ec8bfd4608795f6457710622b12a1520628184bc3f033cc6062e01c25d4 tests/muc_restart_state.rs
13349e35ebaf65242cfb0bdbbccf0ea83acd80a1251312975141c2b74cccc1d7 tests/muc_state_native.rs
5a742e53aa4707b4a060eeb26242006a34344ab57b8cad36668190a8dc5d0674 tests/muc_restart_state.rs
7d8cc2b55bfccc92095c08b68dd6853b4ff2b7bfa460faf6c122e7fca09cca39 tests/muc_sm_load_network.py
2e718aaa0328ac2a5f5bffbef5cd758bbfc49301d31edc77db316df8c7610acf tests/muc_state_native.rs
cd13d1481dba7725986c47b5e5eb606241c8bbc32ab05d53f536f4431d6f7a3d tests/pep_client_interop.rs
741cbcbf5552a8c3e5e7757a29de76ecd29da58f9d1184a35a075f1ed7aa5af9 tests/pep_client_network.py
dd68c50d7d7cfd7871f69331a2787e0336dcab73762a82c599cee2a39920ae95 tests/pep_diagnostics.rs
@@ -91,6 +117,7 @@ dd68c50d7d7cfd7871f69331a2787e0336dcab73762a82c599cee2a39920ae95 tests/pep_diag
42b6c19d14e60b0c93193b4df473a0dd7cfcdb19250245ab94a7e1a237608395 tests/pep_item_order.rs
3e349f9219da20602a239a6c4ce71dc045faf9afb1c36599e2b41e90b2423508 tests/pep_notifications_network.py
83cea7164e5e6aadfc126b6b8349eb242bc113ba638385fa13c51ffda32aa57a tests/pep_order_network.py
08d1a1c5585dba862b78deddab59bd45282610afa58852b29ef56f3023a79de9 tests/personal_message_latency_network.py
6a078c3fbddff07131f3247d01fb1e13bc923888d93ea30175adda471cbece2f tests/preauth.rs
b4048d34620bd0726cfb79d4d1cca9635f27ca8e58c8ad61d619af63672a2e09 tests/preauth_native.rs
06a66caa73c179b9d3b239e6f4deac6fc876d8c8dfeafe302f043d85d9f4beb9 tests/preauth_network.py
@@ -98,9 +125,10 @@ fa1eae49717ca9fb8b118f40058dff0c068c8f28823ea1b0ac42fd0c3e935bda tests/pubsub_n
359de6dd2d960e63296f1109617f139bed2eac40409e50f72ff745897ba943ac tests/pubsub_persistence.rs
6fb7475ca7359ad870a83d5f143a1994c7bd34e9b56e275f7496ba8da291ee17 tests/push.rs
b8dd71798f96931f7ab4489cdb1e258556f41411815ef38d3a03e80e4e8fbdfc tests/push_discovery.py
44c8e642f725be2292dfc5282feac8772ac34824927b63058f03f7c98285e5b6 tests/push_latency_network.py
abf5e6a32ab542ccba5861eaaf7aed9a86b8306abfe2c329646d54e43176dc0c tests/reference/COPYING
1ecd0a5c122910cbd066922c95ff0c32e945a07e2c9b0a37a30e5f035b5ecea6 tests/reference/configmanager.lua
f407c6565fa73b6653e7e73204daa65ae1d87111e82c6b96154aea76497ad65d tests/reference_prosody.rs
78fb639a334846c884f2d438407d7fa8491778b695f9e6218a161ebf8acaeced tests/reference_prosody.rs
5ab78e9319d6004d65d35cd4d04dbb99d4ea5ffd8da00473d874bb26000fce35 tests/resource_rebind.py
2e79de5ef8d3be81ac8f715bb96797b6e8e39cdcd61c13d8d873a96b53cb52e4 tests/resource_sessions.rs
cefd1001c9619456402223fa89e449f65599c4397f523860e965ca192a4fd915 tests/sasl2_bind.py
@@ -110,7 +138,13 @@ ddd9d5b46dc943138a334c64477fb11970b3d45e4cfbeaafc7348754f1e5e29e tests/sasl_acc
a7d1519f4f44cc7bfad3d20bcfdf455e92abdd084feed493ffc9e0267a387740 tests/sasl_network.py
4748fa227743686f78a0fa0364735d2dda5995f8d70a79ad03c1aa1decb42978 tests/sasl_retry_network.py
d8882b8aabd6ff603c81fbbc259eefa68772afb2b3eec2e003ac29efd65d146b tests/sasl_web_network.py
72f2d60e47e5295b82d497289b78925a64a1d986c70c277015ae228d211d32bb tests/storage_accounts.rs
7d69c14ced7884450b9a1ff63cd984cdb9dfe948d6c56631c7593b6fabdabf7f tests/session_routing_isolation.rs
7636b0767fdf24e3d4bde03779ec40309ed2b3301d4e35c6ae5f59aa065e4b2e tests/slow_storage_crash_network.py
7492523bb226b68f72b97b028a7fafeb4fb5375b1efc6fb516400279dd0743d4 tests/slow_storage_latency_network.py
d9eef5c37fcd99404619d61dbf7a2bf7673087924d93e69b08769e716afe297b tests/sm_durable_slots.rs
b4c9422cdba8fd133572c0fa3a0ae6cda14edbd14db6d984d76f4409eefd1262 tests/sm_journal.rs
8fa5de6a2291a7c35e7bbbc6334d42b4e0836a36bec252d0ef4703e8ee17aecb tests/sm_restart_network.py
f76ad9de4c7e0140b1a4c3da06e849e28bb0562cf9b576ed85a5257f7394506b tests/storage_accounts.rs
5c38afd5f77b5774552d12c461902ffaae21de11735560c6f0088e3dda8afa58 tests/storage_compat.rs
8724991fdf22471952cf1a0fac863abe837069c93d3c8d2fa751af4f6bbc3e38 tests/tls_binding.rs
5bea9b5546d3a6ae4fe78d17acbc3f6e6a1d5e1d13b32f2aa88d0e320892b7c5 tests/tls_plus_network.rs
+50
View File
@@ -0,0 +1,50 @@
# Проверка 0.8.0
Linux x86_64, Rust1.99.1, 2026-10-20. Итоговый полный прогон:
[verify.txt](validation-logs-0.8.0/verify.txt).
Структурированный отчёт: [validation-results.json](validation-results.json).
| Проверка | Результат |
|---|---|
| Rust tests | 293 passed, 0 failed, 11 ignored |
| fmt / Clippy all-targets -D warnings / release | Успешно |
| MUC SIGKILL через настоящий STARTTLS | temporary/persistent комнаты, несколько ресурсов, show/status/roles, порядок истории, подтверждённые leave/kick/demotion/password/config, фиксированный/custom grace — успешно |
| SM между процессами | SIGKILL/SIGTERM, legacy SASL/SASL2, counters, ack/replay, неверные owner/token/h, resource conflict — успешно |
| MUC offline fanout | До аутентификации Bob новое сообщение Alice уже в приватной durable SM-очереди; после resume доставка без history delay — успешно |
| Отзыв SM | Удаление аккаунта подтверждается; после повторного создания старый SM-токен отклоняется |
| MUC+SM нагрузка | 100 аккаунтов, 500 TLS-ресурсов, 10 комнат, 25000 join presence; SIGKILL и resume представителей всех комнат — успешно |
| Native reference | 8 module/storage + 1 TLS/SASL EXTERNAL federation — успешно |
| Остальные сетевые регрессии | Android SASL, WSS/BOSH/proxy/altconnect, PEP/push/preauth, resource policies, upload/S3/migration — успешно |
Нагрузка MUC+SM: connect 5.193 с; join 46.703 с;
полный сценарий 78.857 с; выборочный peak RSS 41.26 MiB.
Детали fanout, CPU, slots и replay: [muc-sm-load.json](validation-logs-0.8.0/muc-sm-load.json).
Это loopback smoke measurement, а не гарантия production capacity.
SHA-256 итоговой release-сборки: `74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a`.
Основной прогон: `XMPP_LOAD_CHECK=1 bash scripts/verify-local.sh`; native env описан
в scripts/prosody_compat/README.md. В этой среде дополнительно применены
CARGO_INCREMENTAL=0, CARGO_PROFILE_DEV_DEBUG=0, CARGO_PROFILE_TEST_DEBUG=0.
Первый основной прогон остановился на пятисекундном readiness timeout нового
нагрузочного теста после успешных обычных и сетевых проверок. В самом тесте
таймаут увеличен до60секунд; нагрузка500 повторена с той же release-сборкой,
а native-проверки выполнены отдельно. verify.txt объединяет успешные части
этих запусков; первоначальный лог сохранён в initial-load-timeout.txt.
В процессе проверки исправлены потеря сохранённых presence/history, защита
истёкших recovery claims, отсутствие успешного ответа при удалении аккаунта,
учёт stanza до SM enable и потеря MUC fanout до подключения другого участника.
Первоначальный тест последнего случая принимал позднюю историю за доставку;
итоговый тест проверяет именно private pending queue до аутентификации.
Реальные TCP/TLS-сокеты после завершения процесса отсутствуют: требуется новое
защищённое аутентифицированное соединение. SM допускает повторную доставку при
неопределённом сетевом результате; это не exactly-once транзакция между всеми
хранилищами и сетью. Известные встроенные возможности MUC перенесены, произвольные
community Lua-модули не исполняются. Пределы записей и порядок восстановления:
[MUC-RECOVERY.md](MUC-RECOVERY.md). Полная замена всей установки и проверка
реальными клиентами этим не заявляются: [COMPATIBILITY.md](COMPATIBILITY.md).
10GiB upload в0.8.0 не повторялся. Исторические испытания local/MinIO находятся
в VALIDATION-0.7.8.md и VALIDATION-0.7.9.md; их результаты не приписываются новой сборке.
+48
View File
@@ -0,0 +1,48 @@
# Проверка 0.8.1
2026-10-21, Linux x86_64, Rust1.99.1. Итоговый **единый** прогон:
[verify.txt](validation-logs-0.8.1/verify.txt). Структурированный отчёт:
[validation-results.json](validation-results.json).
| Проверка | Результат |
|---|---|
| cargo tests |297 passed,0failed,11ignored в обычном наборе|
| fmt / Clippy all-targets -D warnings / release |Успешно|
| Новые структурные регрессии |2CSI mutation/rollback +2MUC archive scan/native export/retry — успешно|
| Сеть |Весь verify-local.sh, включая10MUC crash cases, SM restart/offline fanout, PEP/push, Android SASL, WSS/BOSH/proxy, upload/S3/migration — успешно|
| Native reference |8module/storage +1TLS/SASL EXTERNAL federation — успешно|
| Общая нагрузка |100accounts,500TLS,5000ping,5000messages,10000MAM,failures=[]|
| MUC+SM нагрузка |500resources,10rooms,25000presence,1500deliveries,SIGKILL и10resume — успешно|
| Latency regression |0/10000records в полном прогоне; отдельное сравнение0/1000/10000/100000 до/после — успешно|
MUC+SM connect 3.149с; join 25.54с;
весь сценарий 39.535с; выборочный peakRSS 41.25MiB.
100000записей, active CSI MUC peer: median 2517.940→4.680мс,
p95 2520.941→14.532мс. Inactive CSI median
3866.027→4.285мс. По3samples/state для большого архива;
это loopback измерение с проверкой точного количества MAM-записей, не production SLA.
Сырые samples: validation-logs-0.8.1/latency-*.json.
Причины и изменения: [MESSAGE-LATENCY.md](MESSAGE-LATENCY.md).
Release SHA-256: `c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977`. Baseline0.8.0 SHA-256:
`74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a`. Отчёты автоматически связывают результат с бинарником.
Повторение: `XMPP_LOAD_CHECK=1 bash scripts/verify-local.sh` с native env
по scripts/prosody_compat/README.md. Использованы CARGO_INCREMENTAL=0,
CARGO_PROFILE_DEV_DEBUG=0 и CARGO_PROFILE_TEST_DEBUG=0; native Lua-зависимости
из Ubuntu packages извлечены в частный /tmp prefix из-за ограничений среды.
Оригинальные compiled Prosody modules предоставленной ревизии сохранены.
Никакой fsync не отключён. Подтверждённые изменения MUC/SM остаются durable,
CSI queue mutation откатывается при ошибке записи. Старые crash/owner/token/h/
account deletion/recreation тесты проходят. Периодический maintenance и MAM
queries всё ещё работают с архивом; исправление устраняет scan на каждый append,
а не обещает постоянную стоимость любой архивной операции.
Ignored native9тестов выполнены отдельно внутри того же verify-local.sh.
Оставшиеся AF_UNIX admin и>128MiB archive tests по-прежнему входят в полный CI
entrypoint, но в этом локальном прогоне не запускались. Передачи10GiB не
повторялись: старые0.7.8/0.7.9отчёты не относятся к новой сборке.
Настоящие production клиенты, DNSSEC, APNs/FCM и полный Prosody parity этим
не подтверждаются. Общие пределы: [COMPATIBILITY.md](COMPATIBILITY.md).
+51
View File
@@ -0,0 +1,51 @@
# Проверка0.8.2
2026-10-21, Linux x86_64, Rust1.99.1. Итоговый единый прогон:
[verify.txt](validation-logs-0.8.2/verify.txt). Machine-readable report:
[validation-results.json](validation-results.json).
|Проверка|Результат|
|---|---|
|Rust ordinary tests|305passed,0failed,11ignored|
|fmt / Clippy all-targets -D warnings / release|Успешно|
|Новые Rust regressions|6archive endpoints/recovery/limits +2account-lock/duplicateACK — успешно|
|Личная доставка|6fixtures:100000MAM/user, roster0/5000/10000,500SMpending, default/native CSI — успешно|
|Неподвижный push gateway|Настоящий STARTTLS; hibernating+offline: bothmessagesdurable, senderACKh=4 до ответа gateway; offline сохранён до первого postTLSstream|
|Bind2 и MAM metadata|100000records,5reconnects+5IQ, exactIDs/timestamps — успешно|
|Сетевые регрессии|Весь verify-local.sh, включаяprivate WSS/BOSH chat, PEP/push,AndroidSASL,MUCcrash/SMrestart,S3/migration — успешно|
|Native reference|8module/storage +1TLS/SASL EXTERNAL federation — успешно|
|Общая нагрузка|500TLS,100accounts,5000ping,5000messages,10000MAM,failures=[]|
|MUC+SM нагрузка|500resources,10rooms,25000presence,1500messages,SIGKILL+10resume — успешно|
MUC+SM total 45.54с, peak sampled RSS 40.94MiB.
Воспроизведение личного push-stall: прежний0.8.1 за2с сохраняет только первое
сообщение и не отвечаетSMACK при зависшем postTLSfeatures gateway. Новый0.8.2
сохраняет оба и отвечаетh=4 в том же2сокне; тестовое окно не является latency SLA.
Offline ordering baseline проверен на0.8.0; его точный hash указан отдельно.
Bind2 authenticate→success median2742.353→1.976мс; metadataIQ2437.945→0.355мс.
Baseline0.8.0hash74882319… содержит тот же прежний scan path. Все raw samples,
IDs, timestamps и hashes — validation-logs-0.8.2/bind2-*.json.
Default inactive OMEMO доставлен за2.58–4.06мс, receipts7.36–35.56мс в checked
fixtures; explicit[] сохраняет удержание доactive. Намеренный CSI dwell2с
отдельно обозначен в отчёте и не выдаётся за время вычисления/доставки.
Каждый personalarchive содержит ровно100523records/user. Детали и настройка:
[PERSONAL-MESSAGE-LATENCY.md](PERSONAL-MESSAGE-LATENCY.md).
Release SHA-256: `130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5`. Все новые final reports содержат этот hash.
Команда: `XMPP_LOAD_CHECK=1 bash scripts/verify-local.sh` с native env согласно
scripts/prosody_compat/README.md. Использованы CARGO_INCREMENTAL=0,
CARGO_PROFILE_DEV_DEBUG=0, CARGO_PROFILE_TEST_DEBUG=0. Перед итоговым прогоном
устранены stale package linker artifacts очисткой package build outputs;
исходники не менялись после финальной сборки. Воспроизведение прошло одним
последующим полным прогоном; preliminary compiler errors не являются test failures.
Это loopback, не production клиента/диска/сети. Fsync и durable commit до
подтверждения сохранены; boundedpushqueue не обещает durable notification delivery.
SMglobalIOmutex, полные MAMqueries и maintenance остаются отдельной стоимостью.
9nativeignoredtests выполнены; остальные AF_UNIX/largearchiveignoredtests и
10GiBtransfers в этом локальном прогоне не запускались (полный CI entrypoint
по-прежнему включает их). Старые largeupload результаты не приписываются0.8.2.
Общие границы: [COMPATIBILITY.md](COMPATIBILITY.md).
+79
View File
@@ -0,0 +1,79 @@
# Проверка 0.8.3
2026-10-21, Linux x86_64, Rust 1.99.1. Итоговый единый прогон после заморозки
исходников: [verify.txt](validation-logs-0.8.3/verify.txt).
[Machine-readable results](validation-results-0.8.3.json).
|Проверка|Результат|
|---|---|
|Rust ordinary tests|334 passed, 0 failed, 11 ignored|
|fmt / Clippy all-targets -D warnings / release|Успешно|
|Новые Rust regressions|13 archive redo + 2 rewrite crash boundaries + 14 SM journal, успешно|
|Native reference|8 module/storage + 1 TLS/SASL EXTERNAL federation, успешно|
|Archive >128 MiB|Отдельно выполненный ignored streaming test, успешно|
|Copy/restore и rewrite|Новый inode, потеря suffix, baseline0/nonzero, crash до/после rename — успешно|
|Все network regressions verify-local.sh|Успешно, включая Android, WSS/BOSH, PEP, push, MUC crash/SM resume, upload/migration|
|Slow durable IO|0/100 мс: 3 samples/scenario; 200 мс: 1 sample/scenario; exact MAM/carbons/queues и phase logs проверены|
|SIGKILL сразу после sender SM ACK|100 мс/sync, 3 сообщения, оба MAM архива, 4 resume, 9 replay, до reauth; успешно|
|Recovery sync EIO|WAL/redo: отказ handoff без удаления journal; WAL startup до listeners; retry и данные проверены|
|Offline storage-checkpoint|4 SM exports, 2 archives, durable journal removal, повтор идемпотентен|
|Общая нагрузка|500 TLS, 100 accounts, 5000 ping, 5000 messages, 10000 MAM, failures=[]|
|MUC+SM нагрузка|500 resources, 10 rooms, 25000 presence, 1500 deliveries, SIGKILL и 10 resume; успешно|
## Личная доставка
Медианы для 3 samples, при 200 мс — одна проба; числа в миллисекундах.
|Sync delay / сценарий|Body 0.8.2 → 0.8.3|Sender SM ACK 0.8.2 → 0.8.3|Проб|
|---|---:|---:|---:|
|0 мс / 1:1|25.11 → 7.87|33.41 → 15.08|3|
|0 мс / PEP → 1:1|29.71 → 20.94|32.25 → 21.86|3|
|100 мс / 1:1|1232.47 → 419.68|1641.60 → 644.08|3|
|100 мс / PEP → 1:1|2683.29 → 1378.23|3195.21 → 1581.98|3|
|200 мс / 1:1|2836.64 → 2042.56|3643.30 → 2441.99|1|
|200 мс / PEP → 1:1|5782.02 → 2676.28|6595.92 → 3094.95|1|
0 мс — отдельная последовательная quiet pair, без full verify. Финальные
100/200 мс выполнялись при фоновой работе verify; это не изолированный CPU benchmark.
Первый append включает создание/migration MAM journal. В обычном warm сообщении
6 sync вместо 16; первый — 12. Окна счётчиков могут включать heartbeat, а PEP
сценарий включает предшествующую publication. Реальные fsync/fdatasync сохранены;
interposer замедляет только isolated data_path, не меняет host mount/DNS.
Raw samples, hashes, sync counts и phase fields — validation-logs-0.8.3/*.json.
В production log 0.8.2 серверный handler занимал 4792–5549 мс; старых полей
недостаточно, чтобы доказать конкретный IO stage. Новый таймер разделяет MAM,
доставку, carbons и handled, без payload. Эти loopback результаты не доказывают
время отображения реальным Conversations/Monocles на wthome.ru.
## Нагрузка и сопоставимость
Сопоставимый повтор 0.8.2 в текущей среде: total 151.334 с,
join 104.226 с. Финальный 0.8.3: total 128.380 с,
join 94.493 с, restart 7.557 с,
peak RSS 40.87 MiB. По одной пробе; фоновые условия
различаются, вывод о CPU efficiency не делается. Исторические 45.54 с из другой
среды не являются baseline этого сравнения. Все счётчики доставки и replay
проверены, а не только открытие порта.
## Durability и native handoff
SM WAL и archive redo авторитетны после аварии. Recovery durably adopts полный
валидный journal перед публикацией; EIO regression выявляла отсутствие barrier
в предварительном кандидате a5f5b3a…, теперь проходит на final binary. Отчёты
preliminary-recovery-sync-* сохранены именно как предварительные, не результаты
0.8.2 или final. Изначальный native MUC fixture делал in-place append без передачи
владения: теперь он явно вызывает checkpoint_all перед оригинальным Prosody.
Неjournaled suffix при активном redo намеренно не объявляется external commit.
Copy/restore с новым inode сохраняет авторитет redo; внутренний rewrite
фиксирует старый native и удаление redo перед atomic replacement. Проверены
оба crash boundaries, потеря suffix после копирования и обе baseline длины.
Перед native writer/откатом требуется остановка и `storage-checkpoint CONFIG`.
[Формат, ограничения и эксплуатация](DURABLE-MESSAGE-IO.md).
Release SHA-256: `8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a`.
Один final verify с XMPP_LOAD_CHECK=1 и native env; отдельно archive ignored test
и подробные latency/crash/EIO fixtures. Исторические результаты сохранены.
Оставшийся AF_UNIX ignored test не запускался; 10 GiB upload и реальный MinIO/AWS,
Docker build/run, GitHub/Gitea publish и настоящие APNs/FCM не повторялись.
Общие границы полной совместимости: [COMPATIBILITY.md](COMPATIBILITY.md).
+67 -47
View File
@@ -1,59 +1,79 @@
# Проверка 0.7.9
# Проверка 0.8.3
Linux x86_64, Rust 1.99.1, 2026-10-20. Полный итоговый прогон:
validation-logs-0.7.9/verify.txt. Структурированный отчёт: validation-results.json.
2026-10-21, Linux x86_64, Rust 1.99.1. Итоговый единый прогон после заморозки
исходников: [verify.txt](validation-logs-0.8.3/verify.txt).
[Machine-readable results](validation-results-0.8.3.json).
|Проверка|Результат|
|---|---|
| cargo test --locked | 265 passed, 0 failed, 11 ignored |
|Rust ordinary tests|334 passed, 0 failed, 11 ignored|
|fmt / Clippy all-targets -D warnings / release|Успешно|
| Upload migration | local → S3 A → S3 B → local, SHA-256, прежние URL/JWT/HEAD, native metadata/source, dry run, lock, прерывание/resume и startup barrier — успешно |
| S3 recovery | Intent до initiation, отсутствие UploadId, pagination и exact-key cleanup без удаления чужого prefix, ambiguous completion, quarantine и late commit — успешно |
| Конкурентный PUT | Регрессия: захват слота читает свежие completion/quarantine flags атомарно — успешно |
| Реальная передача через MinIO | 10,737,418,240 байт PUT и полный GET; SHA-256 совпал; объект: 1280 multipart parts |
| Время большого файла | PUT 48.453 с; GET 13.710 с |
| Выборочный RSS каждые 50 мс | Rust 101.14 MiB; клиент 23.21 MiB; MinIO 475.84 MiB |
| MinIO дополнительные проверки | Range за 4/5 GiB и хвост, restart/read/reuse409, SIGKILL после сохранённой части и abort при recovery — успешно |
| Native reference | 8 module/storage + 1 TLS/SASL EXTERNAL federation — успешно |
| Нагрузка | 100 аккаунтов, 500 TLS клиентов, 5000 ping,5000 сообщений, 10000 MAM; failures=[] |
|Новые Rust regressions|13 archive redo + 2 rewrite crash boundaries + 14 SM journal, успешно|
|Native reference|8 module/storage + 1 TLS/SASL EXTERNAL federation, успешно|
|Archive >128 MiB|Отдельно выполненный ignored streaming test, успешно|
|Copy/restore и rewrite|Новый inode, потеря suffix, baseline0/nonzero, crash до/после rename — успешно|
|Все network regressions verify-local.sh|Успешно, включая Android, WSS/BOSH, PEP, push, MUC crash/SM resume, upload/migration|
|Slow durable IO|0/100 мс: 3 samples/scenario; 200 мс: 1 sample/scenario; exact MAM/carbons/queues и phase logs проверены|
|SIGKILL сразу после sender SM ACK|100 мс/sync, 3 сообщения, оба MAM архива, 4 resume, 9 replay, до reauth; успешно|
|Recovery sync EIO|WAL/redo: отказ handoff без удаления journal; WAL startup до listeners; retry и данные проверены|
|Offline storage-checkpoint|4 SM exports, 2 archives, durable journal removal, повтор идемпотентен|
|Общая нагрузка|500 TLS, 100 accounts, 5000 ping, 5000 messages, 10000 MAM, failures=[]|
|MUC+SM нагрузка|500 resources, 10 rooms, 25000 presence, 1500 deliveries, SIGKILL и 10 resume; успешно|
Итоговая сборка SHA-256: 24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c. MinIO report автоматически записывает
тот же hash. Большая передача повторена после последних изменений сервера.
Полный прогон включает исправление гонки устаревшего snapshot при захвате слота.
Отдельные первые прогоны нашли и помогли исправить нестабильный fingerprint
миграции и пропуск немедленного reconciliation под ещё активным claim.
Финальный результат не содержит этих ошибок. RSS — выборочный максимум, а не
гарантия точного пика для любой нагрузки. Временные 10 GiB объекты после проверки удалены.
## Личная доставка
## Точная граница MinIO проверки
Медианы для 3 samples, при 200 мс — одна проба; числа в миллисекундах.
Использован настоящий upstream MinIO RELEASE.2025-04-22T22-12-26Z, собранный
Go1.24.3 из официального GitHub archive. Немодифицированная сборка в этой среде
не запускается: net.Interfaces/netlinkrib запрещён с EPERM. Тестовая сборка
добавляет только fallback на 127.0.0.1 для этой ошибки перечисления интерфейсов.
S3, authentication, multipart и storage code не изменены. Это не qualification
немодифицированного MinIO в обычной production среде.
|Sync delay / сценарий|Body 0.8.2 → 0.8.3|Sender SM ACK 0.8.2 → 0.8.3|Проб|
|---|---:|---:|---:|
|0 мс / 1:1|25.11 → 7.87|33.41 → 15.08|3|
|0 мс / PEP → 1:1|29.71 → 20.94|32.25 → 21.86|3|
|100 мс / 1:1|1232.47 → 419.68|1641.60 → 644.08|3|
|100 мс / PEP → 1:1|2683.29 → 1378.23|3195.21 → 1581.98|3|
|200 мс / 1:1|2836.64 → 2042.56|3643.30 → 2441.99|1|
|200 мс / PEP → 1:1|5782.02 → 2676.28|6595.92 → 3094.95|1|
Точный diff, hashes исходного архива, обеих сборок и adapter:
validation-logs-0.7.9/minio-sandbox-compat.patch и minio-provenance.json.
Полный результат и 1280-part ETag: validation-logs-0.7.9/minio.json.
Проверен single-node loopback с HTTP к MinIO и CA-verified HTTPS к gateway.
AWS account, distributed MinIO и production сеть не проверены.
0 мс — отдельная последовательная quiet pair, без full verify. Финальные
100/200 мс выполнялись при фоновой работе verify; это не изолированный CPU benchmark.
Первый append включает создание/migration MAM journal. В обычном warm сообщении
6 sync вместо 16; первый — 12. Окна счётчиков могут включать heartbeat, а PEP
сценарий включает предшествующую publication. Реальные fsync/fdatasync сохранены;
interposer замедляет только isolated data_path, не меняет host mount/DNS.
Raw samples, hashes, sync counts и phase fields — validation-logs-0.8.3/*.json.
## Повторение
В production log 0.8.2 серверный handler занимал 4792–5549 мс; старых полей
недостаточно, чтобы доказать конкретный IO stage. Новый таймер разделяет MAM,
доставку, carbons и handled, без payload. Эти loopback результаты не доказывают
время отображения реальным Conversations/Monocles на wthome.ru.
Полный прогон: XMPP_LOAD_CHECK=1 bash scripts/verify-local.sh с native env
согласно scripts/prosody_compat/README.md. В этой среде дополнительно применены
CARGO_INCREMENTAL=0 CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0.
MinIO отдельно: python3 tests/upload_minio_network.py --binary target/release/rust-xmpp
--minio /path/to/minio --report /tmp/minio-report.json. При test adapter передайте
--minio-compat-patch /path/to/patch, чтобы он попал в результат. Можно задать
MINIO_BINARY перед verify-local.sh для opt-in передачи10 GiB; нужны место и MinIO executable.
## Нагрузка и сопоставимость
Инструкции migration/recovery: HTTP-UPLOAD.md. Source всегда сохраняется;
незавершённая миграция блокирует startup, неопределённый Complete quarantines
только свой slot. Потеря ownership metadata не позволяет безопасно удалить
произвольные чужие multiparts; bucket lifecycle остаётся дополнительной защитой.
Сетевой сбой с HEAD404 не выдаётся за доказанную неудачу удалённого commit.
Полная замена Prosody, настоящие production клиенты и DNSSEC этим не заявляются;
общие границы — COMPATIBILITY.md.
Сопоставимый повтор 0.8.2 в текущей среде: total 151.334 с,
join 104.226 с. Финальный 0.8.3: total 128.380 с,
join 94.493 с, restart 7.557 с,
peak RSS 40.87 MiB. По одной пробе; фоновые условия
различаются, вывод о CPU efficiency не делается. Исторические 45.54 с из другой
среды не являются baseline этого сравнения. Все счётчики доставки и replay
проверены, а не только открытие порта.
## Durability и native handoff
SM WAL и archive redo авторитетны после аварии. Recovery durably adopts полный
валидный journal перед публикацией; EIO regression выявляла отсутствие barrier
в предварительном кандидате a5f5b3a…, теперь проходит на final binary. Отчёты
preliminary-recovery-sync-* сохранены именно как предварительные, не результаты
0.8.2 или final. Изначальный native MUC fixture делал in-place append без передачи
владения: теперь он явно вызывает checkpoint_all перед оригинальным Prosody.
Неjournaled suffix при активном redo намеренно не объявляется external commit.
Copy/restore с новым inode сохраняет авторитет redo; внутренний rewrite
фиксирует старый native и удаление redo перед atomic replacement. Проверены
оба crash boundaries, потеря suffix после копирования и обе baseline длины.
Перед native writer/откатом требуется остановка и `storage-checkpoint CONFIG`.
[Формат, ограничения и эксплуатация](DURABLE-MESSAGE-IO.md).
Release SHA-256: `8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a`.
Один final verify с XMPP_LOAD_CHECK=1 и native env; отдельно archive ignored test
и подробные latency/crash/EIO fixtures. Исторические результаты сохранены.
Оставшийся AF_UNIX ignored test не запускался; 10 GiB upload и реальный MinIO/AWS,
Docker build/run, GitHub/Gitea publish и настоящие APNs/FCM не повторялись.
Общие границы полной совместимости: [COMPATIBILITY.md](COMPATIBILITY.md).
File diff suppressed because it is too large. Load diff
+11
View File
@@ -0,0 +1,11 @@
PASS: recovery-history
PASS: leave
PASS: kick
PASS: demotion
PASS: password
PASS: configuration
PASS: restart-grace
PASS: custom-grace
PASS: unknown-module
PASS: destroy-tombstone
PASS: actual STARTTLS SIGKILL MUC recovery, history, acknowledged barriers and fixed grace
@@ -0,0 +1,62 @@
{
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-g0crm4ik/rust-xmpp",
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"before": {
"rss_kib": 9696,
"cpu_seconds": 0.0,
"fd_count": 12
},
"connect_seconds": 5.193,
"connect_latency": {
"samples": 500,
"median_ms": 204.391,
"p95_ms": 270.035,
"max_ms": 332.503
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 78.857,
"join_seconds": 46.703,
"joined": {
"rss_kib": 42248,
"cpu_seconds": 17.77,
"fd_count": 513
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 10.861,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 42128,
"cpu_seconds": 23.12,
"fd_count": 514
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 10.559,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 21168,
"cpu_seconds": 5.03,
"fd_count": 23
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 78.857,
"peak_rss_kib": 42252
}
@@ -0,0 +1 @@
PASS: TLS legacy/SASL2 durable SM after SIGKILL/SIGTERM, counters, ack pruning, at-least-once pending replay, owner/token/h rejection, transport/resource conflict, MUC activation/offline fanout, grace expiry and account deletion/recreation revocation
+846
View File
@@ -0,0 +1,846 @@
Finished `test` profile [unoptimized] target(s) in 0.09s
Running unittests src/lib.rs (target/debug/deps/rust_xmpp-c18b5fae509f7258)
running 46 tests
test federation::tests::federation_domains_are_dns_names_and_case_insensitive ... ok
test federation::tests::starttls_must_be_an_immediate_stream_feature ... ok
test federation::tests::external_must_be_in_the_advertised_mechanisms ... ok
test http_config::tests::external_prefix_and_standard_port ... ok
test http_config::tests::invalid_paths_types_urls_and_conflicts_are_errors ... ok
test federation::tests::stream_restarts_compare_canonical_domains ... ok
test http_config::tests::mounts_inherit_per_app_and_expand_virtual_host ... ok
test http_config::tests::literal_pchars_and_percent_encoding_are_supported ... ok
test http_config::tests::routes_hosts_and_default_without_overriding_known_hosts ... ok
test http_proxy::tests::config_validation_and_network_boundaries ... ok
test http_proxy::tests::empty_native_lua_table_disables_proxy_trust ... ok
test http_proxy::tests::legacy_chain_uses_rightmost_untrusted_and_tls_is_preserved ... ok
test http_proxy::tests::untrusted_socket_ignores_spoofed_headers ... ok
test http_proxy::tests::malformed_forwarded_and_unknown_identifiers_are_ignored ... ok
test http_proxy::tests::forwarded_mode_stops_at_first_untrusted_hop ... ok
test http_proxy::tests::forwarded_tls_termination_cannot_upgrade_a_plaintext_chain_hop ... ok
test http_transport::tests::bosh_processing_capacity_is_bounded ... ok
test http_transport::tests::independent_framing ... ok
test http_transport::tests::websocket_preserves_inherited_namespaces_and_spaced_declarations ... ok
test logging::tests::console_aliases_share_one_sink_with_combined_levels ... ok
test http_transport::tests::configured_mount_host_origin_and_secure_dispatch ... ok
test sasl::tests::reject_malformed_and_downgrade ... ok
test sasl::tests::rfc5802_sha1_vector ... ok
test server::authorization_tests::http_interfaces_follow_secure_override_wildcards_and_global_fallback ... ok
test server::authorization_tests::pep_diagnostics_count_only_first_item_devices_without_logging_ids ... ok
test server::authorization_tests::pep_diagnostics_exclude_payloads_and_identifiers ... ok
test http_service::tests::upload_claim_refreshes_completion_state_after_an_earlier_snapshot ... ok
test sessions::push_timeout_tests::first_important_push_shortens_extended_hibernation_once ... ok
test sessions::tests::acknowledgement_wraps ... ok
test sessions::tests::graceful_disconnect_is_not_resumable ... ok
test sessions::tests::resume_replays_only_unacked_and_checks_owner ... ok
test sessions::tests::traffic_before_sm_enable_is_not_counted ... ok
test upload_migration::tests::local_spool_checks_size_and_hash_and_preserves_source ... ok
test upload_migration::tests::pending_and_corrupt_journals_block_startup ... ok
test wire::tests::arbitrary_prefix_and_inherited_namespaces ... ok
test wire::tests::cdata_and_comments_frame_without_counting_brackets ... ok
test wire::tests::declarations_rejected ... ok
test wire::tests::fragmented_and_pipelined ... ok
test wire::tests::invalid_namespace_and_declarations_rejected ... ok
test http_service::tests::upload_internal_mount_is_independent_of_advertised_proxy_prefix ... ok
test sasl::tests::sha256_independent_python_vector ... ok
test http_service::tests::abandoned_slots_release_quota_without_pruning_existing_or_active_files ... ok
test sasl::tests::plus_channel_binding_and_ssdp_are_authenticated ... ok
test server::authorization_tests::upload_requires_registered_local_account_or_explicit_remote_access ... ok
test sasl::tests::wrong_proof_nonce_binding_and_duplicates ... ok
test admin::tests::real_admin_xml_protocol_over_duplex ... ok
test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.59s
Running unittests src/main.rs (target/debug/deps/rust_xmpp-c4e45dd69a11a11d)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/admin_socket.rs (target/debug/deps/admin_socket-61170eb8eabf7ff1)
running 1 test
test prosody_wire_commands_password_prompt_and_safe_errors ... ignored, requires AF_UNIX socket permission; run on target host with --ignored
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/archive_mam.rs (target/debug/deps/archive_mam-66e5d9709846757b)
running 14 tests
test archive_larger_than_128mib_streams ... ignored, large-file scalability regression; run explicitly
test corrupt_archive_reports_server_error_and_namespace_stripping_is_precise ... ok
test mam_preferences_persist ... ok
test legacy_records_binary_cache_and_roundtrip ... ok
test append_is_incremental_and_recovers_torn_tail ... ok
test archive_roundtrip_preserves_default_namespace_resets ... ok
test equal_cursors_and_normalized_query_jids ... ok
test muc_archive_filters_stanza_kind_instead_of_sender_jid ... ok
test smart_enable_and_message_selection ... ok
test real_archive_sid_and_sender_privacy ... ok
test zero_page_dates_and_invalid_forms ... ok
test mam_paging_filters_and_forbidden ... ok
test extended_ids_ranges_flip_and_metadata ... ok
test namespaces_expiry_and_concurrent_append ... ok
test result: ok. 13 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.04s
Running tests/auth.rs (target/debug/deps/auth-3fecc5005dc5e6c7)
running 3 tests
test traversal_and_malicious_account_records_rejected ... ok
test create_and_read_hashed_accounts ... ok
test authentication_prepares_names_without_relaxing_account_management ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.11s
Running tests/cli.rs (target/debug/deps/cli-a1c004101734b0cf)
running 2 tests
test server_cannot_be_mistaken_for_running_service ... ok
test conversion_preserves_source_and_refuses_overwrite ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running tests/config.rs (target/debug/deps/config-bb0cdd36fb8d5839)
running 12 tests
test include_cycles_rejected ... ok
test component_can_explicitly_enable_global_modules ... ok
test storage_paths_encode_like_prosody ... ok
test include_has_own_global_scope_and_parent_scope_is_restored ... ok
test declarations_and_overrides ... ok
test tables_cannot_recurse_forever ... ok
test supplied_configuration_scopes_arithmetic_and_modules ... ok
test runtime_capabilities_are_unavailable ... ok
test all_formats_round_trip_actual_config ... ok
test supplied_config_matches_original_prosody_loader ... ok
test malformed_values_rejected ... ok
test loops_are_bounded ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
Running tests/csi.rs (target/debug/deps/csi-ef5a8b30cab0cbf7)
running 2 tests
test bounded_queue_flush_delay_activity_and_grace ... ok
test native_importance_rules ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/csi_native.rs (target/debug/deps/csi_native-7a0bbbead7a19df9)
running 1 test
test native_prosody_csi_importance_matches ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/data_lock.rs (target/debug/deps/data_lock-897044c51991a534)
running 2 tests
test child_process_probe ... ok
test excludes_another_process_and_releases_on_drop ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/debug_legacy.rs (target/debug/deps/debug_legacy-3c721d03a5eecf02)
running 1 test
test debug_empty_legacy_bind ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
Running tests/federation_tls.rs (target/debug/deps/federation_tls-6dc128df24b929d8)
running 3 tests
test native_prosody_tls_external_bidirectional ... ignored, requires native Prosody cafd74b9c5ea and Lua TLS dependencies
test mutual_tls_external_delivery_and_certificate_identity_rejection ... ok
test reuses_connections_serializes_writes_and_retires_idle_or_closed_peers ... ok
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.48s
Running tests/gajim_features.rs (target/debug/deps/gajim_features-f6c3a49b9451f82c)
running 6 tests
test policy_catalog_selects_default_policy_and_maps_display_and_explicit_label ... ok
test security_catalog_default_is_a_real_label_and_module_can_be_disabled ... ok
test actual_example_policy_configuration_exposes_classifications_as_catalog_labels ... ok
test unsupported_security_policy_configuration_returns_explicit_error ... ok
test security_catalogs_serve_both_versions_and_preserve_requested_target ... ok
test malformed_or_unknown_policy_catalog_schemas_are_rejected ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/http_altconnect.rs (target/debug/deps/http_altconnect-9136542484c4f432)
running 3 tests
test native_link_order_schemes_and_loaded_module_gates ... ok
test public_prefix_and_host_paths_inherit_independently ... ok
test bad_advertisement_types_and_route_collisions_are_rejected ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/http_transport.rs (target/debug/deps/http_transport-ba31449c82cc1657)
running 10 tests
test configured_origin_is_enforced ... ok
test bosh_preserves_namespace_inherited_from_body ... ok
test transport_paths_and_cors_preflight_match_prosody ... ok
test bosh_session_retries_restart_termination ... ok
test bosh_termination_waits_for_graceful_cleanup_before_reconnect ... ok
test bosh_reorders_two_requests_without_replaying_stanzas ... ok
test websocket_frames_are_independent_xml ... ok
test websocket_close_allows_graceful_engine_cleanup ... ok
test websocket_disconnect_delivers_eof_without_graceful_close ... ok
test bosh_retry_survives_aborted_http_request_without_replaying_stanzas ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Running tests/http_upload.rs (target/debug/deps/http_upload-ff5554e98fd3b948)
running 5 tests
test https_endpoint_uses_real_tls ... ok
test percent_escape_case_and_equivalent_url_spelling ... ok
test persisted_prosody_archive_and_hs256_slot_survive_without_sidecar ... ok
test expired_upload_releases_quota ... ok
test upload_authorization_streaming_ranges_and_restart ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
Running tests/iq_errors.rs (target/debug/deps/iq_errors-f6728f43bbee20cf)
running 2 tests
test pep_errors_identify_the_account_and_echo_the_request ... ok
test server_errors_without_target_remain_unaddressed_and_escape_attributes ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/logging.rs (target/debug/deps/logging-49936a1290966f86)
running 10 tests
test native_console_shorthands_are_accepted ... ok
test rejects_malformed_configuration ... ok
test json_records_and_bounded_ranges ... ok
test failed_reopen_keeps_all_original_handles ... ok
test filters_appends_escapes_and_redacts ... ok
test reopen_rotated_files_updates_clones ... ok
test explicit_sink_levels_select_exactly_and_deduplicate ... ok
test shared_sink_unions_levels_and_preserves_distinct_destinations ... ok
test overlapping_shorthand_destinations_receive_each_record_once ... ok
test cloned_loggers_serialize_records ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
Running tests/message_routing.rs (target/debug/deps/message_routing-e8cb56e9c75084cc)
running 2 tests
test unknown_resource_falls_back_to_available_bare_jid ... ok
test headline_uses_all_nonnegative_available_resources_and_errors_never_fallback ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/muc_access.rs (target/debug/deps/muc_access-3d45cfc19a946c63)
running 3 tests
test binary_occupant_id_salt_survives_saves_and_restarts ... ok
test moderators_cannot_kick_or_demote_other_moderators ... ok
test nested_affiliations_do_not_fall_back_to_obsolete_flat_entries ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
Running tests/muc_crash_state.rs (target/debug/deps/muc_crash_state-f6de95e841b62f79)
running 7 tests
test safe_presence_checkpoint_sanitization_is_idempotent ... ok
test foreign_stanzas_do_not_initialize_or_rewrite_muc_checkpoint ... ok
test durable_disconnect_removes_last_temporary_room_without_shutdown_export ... ok
test journal_failure_returns_no_routing_and_rolls_back_role_mutation ... ok
test acknowledged_kick_and_room_destruction_cannot_resurrect_membership ... ok
test dormant_deadline_stays_fixed_when_another_occupant_recovers_and_sends ... ok
test temporary_membership_and_full_bounded_history_survive_ungraceful_loss ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
Running tests/muc_domain_bans.rs (target/debug/deps/muc_domain_bans-bf16a8c751b147a4)
running 2 tests
test native_domain_bans_block_remote_users_but_allow_explicit_members ... ok
test applying_domain_ban_removes_remote_occupants_and_blocks_rejoin ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running tests/muc_extensions.rs (target/debug/deps/muc_extensions-8aa1a3ac3d2930b2)
running 10 tests
test disabled_optional_vcard_and_occupant_ids_are_not_advertised_or_emitted ... ok
test broadcast_roles_hide_visitors_and_configuration_announces_transition ... ok
test messages_cannot_spoof_room_identity_hats_or_occupant_identifiers ... ok
test mam_all_rooms_presence_logging_public_queries_and_optional_totals ... ok
test large_optional_history_does_not_prevent_checkpointing_membership ... ok
test restricted_private_messages_allow_self_and_optional_moderators ... ok
test offline_reserved_affiliates_follow_anonymity_and_roominfo_counts_nicknames ... ok
test language_config_sections_discovery_and_native_affiliation_hats ... ok
test recovery_claim_is_durable_and_unrelated_disconnect_keeps_temporary_claims ... ok
test explicit_new_nickname_consumes_old_proof_and_pending_nicks_are_reserved ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
Running tests/muc_flow.rs (target/debug/deps/muc_flow-358a28db146dbe13)
running 18 tests
test configured_lock_timeout_expires_without_owner_submission ... ok
test iq_response_requires_expected_sender ... ok
test multiple_resources_share_nick_and_private_delivery ... ok
test join_lock_chat_leave ... ok
test authorization_and_conflict ... ok
test nick_invite_destroy_and_local_creation ... ok
test occupant_id_is_stable_and_spoofed_ids_removed ... ok
test preserves_unknown_binary_mixed_room_fields ... ok
test restored_temporary_room_is_deleted_after_last_disconnect ... ok
test native_muc_archive_type_and_anonymity_are_preserved ... ok
test persistence_roundtrip ... ok
test maintenance_expires_idle_archives_and_tombstones ... ok
test persistent_destroy_creates_prosody_tombstone ... ok
test switching_to_members_only_removes_unaffiliated_users ... ok
test archive_history_and_permissions ... ok
test room_vcard_owner_only_and_avatar_presence ... ok
test visitor_voice_request_cannot_spoof_identity_or_grant_itself_voice ... ok
test reserved_nickname_blocks_other_users ... ok
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
Running tests/muc_history.rs (target/debug/deps/muc_history-f67a7a69e0bfd957)
running 10 tests
test capacity_changes_trim_and_zero_disables_history ... ok
test only_direct_history_children_are_parsed ... ok
test maxchars_counts_transmitted_utf8_bytes_including_address_and_delay ... ok
test explicit_count_and_zero_limit ... ok
test snapshot_restores_last_message_without_adding_another_delay ... ok
test replay_carries_room_delay_and_recipient ... ok
test ordinary_history_is_bounded_and_returns_oldest_first ... ok
test subjects_and_hints_follow_native_history_eligibility ... ok
test since_and_seconds_use_stricter_cutoff_inclusive ... ok
test default_count_applies_only_without_valid_explicit_constraints ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/muc_native.rs (target/debug/deps/muc_native-f792fb43c05f2836)
running 1 test
test actual_prosody_muc_mam_reads_rust_and_writes_native_archive ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/muc_optional_admin.rs (target/debug/deps/muc_optional_admin-cda5bde07d3a924e)
running 4 tests
test default_registration_denies_unaffiliated_users ... ok
test configured_tombstone_expiry_allows_immediate_recreation ... ok
test optional_registration_grants_membership_and_enforces_reserved_nickname ... ok
test administration_commands_recheck_authority_bind_sessions_and_update_live_rooms ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
Running tests/muc_protocol.rs (target/debug/deps/muc_protocol-5f42030bfe1d6522)
running 5 tests
test ordinary_presence_extensions_survive_but_client_identity_is_filtered ... ok
test subject_with_body_remains_an_ordinary_groupchat_message ... ok
test probes_and_invalid_presence_cannot_join_or_rename ... ok
test error_messages_kick_only_on_delivery_failure_and_presence_errors_kick ... ok
test room_creation_policies_apply_only_to_new_rooms ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
Running tests/muc_recovery.rs (target/debug/deps/muc_recovery-226d89017c6b58d8)
running 7 tests
test claim_is_one_time_and_does_not_change_untrusted_role ... ok
test pending_snapshot_keeps_original_checkpoint_time ... ok
test recovery_claim_requires_exact_authenticated_resource_and_expires ... ok
test native_last_message_roundtrip_preserves_namespaces_and_rejects_other_rooms ... ok
test native_checkpoint_roundtrip_filters_sensitive_extensions ... ok
test oversized_status_never_prevents_membership_checkpoint ... ok
test configuration_fingerprint_is_independent_of_lua_table_iteration_order ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running tests/muc_restart_state.rs (target/debug/deps/muc_restart_state-5cb216a0bea9ced1)
running 3 tests
test unrelated_disconnect_does_not_delete_dormant_temporary_room ... ok
test changed_configuration_invalidates_auto_recovery_without_disclosing_password ... ok
test repeated_restart_keeps_dormant_claim_and_deadline_without_ghost_routing ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
Running tests/muc_state_native.rs (target/debug/deps/muc_state_native-e120e43e0108ea7f)
running 1 test
test actual_prosody_restores_rust_checkpoint_and_rust_reads_native_freeze ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/pep_client_interop.rs (target/debug/deps/pep_client_interop-1553fc66a4e090e1)
running 7 tests
test native_component_notification_type_applies_to_events_and_subscription_replay ... ok
test missing_own_omemo_bundle_returns_recoverable_item_not_found ... ok
test publish_options_match_implicit_notification_defaults_on_old_nodes ... ok
test migrated_native_configuration_can_roundtrip_access_model_edit ... ok
test avatar_and_omemo_publish_options_support_readback_and_caps_notifications ... ok
test native_itemreply_controls_publisher_visibility_in_reads_and_replays ... ok
test native_open_and_subscriber_publish_models_preserve_outcast_and_blocking_checks ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s
Running tests/pep_diagnostics.rs (target/debug/deps/pep_diagnostics-98b92a168aa66315)
running 6 tests
test nonempty_xml_can_be_an_empty_android_device_list ... ok
test avatar_classification_does_not_inspect_or_expose_avatar_data ... ok
test arbitrary_node_payload_and_wrong_namespace_do_not_leak ... ok
test only_first_direct_item_is_parsed_and_protocols_are_distinct ... ok
test payloadless_notification_is_visible_without_identifiers ... ok
test signed_integer_limits_and_duplicates_match_java_parser ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/pep_discovery.rs (target/debug/deps/pep_discovery-3e3fb8c04ba0a062)
running 4 tests
test standalone_component_does_not_claim_pep_automatic_subscriptions ... ok
test account_disco_advertises_pep_without_requiring_a_node ... ok
test account_discovery_does_not_intercept_server_or_resource_caps ... ok
test node_discovery_keeps_node_identity_errors_and_access_checks ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s
Running tests/pep_item_order.rs (target/debug/deps/pep_item_order-77f6b485fd7811b4)
running 2 tests
test native_prosody_orders_retained_pep_items_newest_first ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
test first_pep_item_is_current_device_list_even_with_retained_old_items ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.10s
Running tests/preauth.rs (target/debug/deps/preauth-4dd44275632445e0)
running 10 tests
test duplicate_or_wrong_namespace_tokens_do_not_qualify ... ok
test standard_adhoc_command_returns_contact_uri ... ok
test unknown_tokens_do_not_create_invite_stores_and_disabled_accounts_cannot_issue ... ok
test prosody_numeric_expiry_is_consumed_and_other_entries_preserved ... ok
test concurrent_redemption_has_exactly_one_winner ... ok
test unknown_client_token_is_preserved_on_remote_and_pending_delivery ... ok
test identity_scope_does_not_burn_on_wrong_sender_or_leak_to_prosody ... ok
test invitations_expire_are_single_use_and_owner_scoped ... ok
test valid_token_autoapproves_and_sends_reciprocal_request ... ok
test local_invitation_updates_both_rosters_without_granting_reverse_subscription ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
Running tests/preauth_native.rs (target/debug/deps/preauth_native-36f66c5afb8f3b7f)
running 1 test
test native_prosody_invitation_tokens_roundtrip ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/pubsub_native.rs (target/debug/deps/pubsub_native-5bfe7997d9ab4681)
running 1 test
test native_prosody_pep_reads_rust_and_rust_reads_native ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/pubsub_persistence.rs (target/debug/deps/pubsub_persistence-96d57930ef04b71a)
running 18 tests
test modern_bookmarks_notify_legacy_capability ... ok
test native_string_max_items_satisfies_numeric_publish_precondition ... ok
test configurable_limits_component_creation_and_publisher ... ok
test configure_requires_owner ... ok
test capability_discovery_replays_and_filters_notifications ... ok
test nonpersistent_items_stay_in_memory_and_can_be_made_persistent ... ok
test open_model_and_notifications ... ok
test reads_prosody_legacy_fixture ... ok
test full_jid_block_applies_to_reads_subscription_replays_and_events ... ok
test pep_persists_namespaces_and_enforces_owner ... ok
test publish_options_conflict_is_recoverable_and_preserves_item ... ok
test remote_unavailable_is_scoped_to_addressed_owner_and_cancels_pending ... ok
test roster_presence_access_and_foreign_subscription_denied ... ok
test remote_caps_replay_and_open_node_privacy_revoke ... ok
test subscription_options_disable_events_and_require_subscriber ... ok
test payload_policy_applies_to_subscription_and_caps_replays ... ok
test native_notification_fields_and_retract_policy_are_independent ... ok
test component_default_retention_matches_prosody_twenty_items ... ok
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s
Running tests/push.rs (target/debug/deps/push-e74594c0e96b1321)
running 7 tests
test native_push_body_lookup_excludes_extensions ... ignored, requires PROSODY_REFERENCE_SOURCE and PROSODY_REFERENCE_LUA
test foreign_account_cannot_register ... ok
test queue_coalesces_and_importance_matches_native ... ok
test push_summary_never_copies_foreign_namespace_body ... ok
test reenable_cancels_old_errors_and_failure_counts_are_per_account ... ok
test stable_client_registration_and_targeted_notification ... ok
test registrations_notifications_and_authenticated_errors ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.12s
Running tests/reference_prosody.rs (target/debug/deps/reference_prosody-fe62c7cdfcb258f2)
running 1 test
test real_prosody_storage_and_auth_roundtrip ... ignored, requires native supplied Prosody source; run scripts/prosody_compat/verify.sh
test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/resource_sessions.rs (target/debug/deps/resource_sessions-c00bb0104e05dfa7)
running 7 tests
test increment_skips_occupied_suffixes_and_preserves_sessions ... ok
test invalid_resume_owner_or_ack_does_not_evict_active_session ... ok
test kick_new_leaves_existing_session_unchanged ... ok
test replacement_revokes_hibernating_resume_and_survives_old_cleanup ... ok
test random_conflicts_get_distinct_resources_for_same_owner ... ok
test concurrent_default_binds_replace_atomically ... ok
test active_resume_preserves_state_and_replaces_transport_notifier ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Running tests/sasl_accounts.rs (target/debug/deps/sasl_accounts-c2c6690c56d78954)
running 1 test
test disabled_legacy_and_unicode_accounts ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.63s
Running tests/sm_durable_slots.rs (target/debug/deps/sm_durable_slots-cf591ebfb421691b)
running 11 tests
test corrupt_committed_slot_fails_recovery_closed ... ok
test durable_tokens_and_private_pending_output_have_owner_only_permissions ... ok
test recovery_discards_owned_crash_temps_and_keeps_last_committed_slot ... ok
test session_reference_does_not_keep_store_locked_or_allow_unpersisted_writes ... ok
test disconnected_deadline_is_fixed_across_recovery ... ok
test durable_counters_wrap_without_accepting_stale_acknowledgements ... ok
test shutdown_freeze_retires_old_transport_before_cleanup ... ok
test independent_hubs_with_identical_connection_names_keep_their_own_slots ... ok
test restart_restores_owner_presence_and_only_unacknowledged_output ... ok
test graceful_close_conflict_account_purge_and_expiration_remove_tokens ... ok
test store_has_one_process_owner_and_enforces_output_byte_bound ... ok
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s
Running tests/storage_accounts.rs (target/debug/deps/storage_accounts-1d75e90b4e9212f9)
running 20 tests
test account_purge_refuses_symlink_store_without_touching_other_files ... ok
test broken_roster_is_not_overwritten ... ok
test inbound_subscription_requires_pending_to_grant_to ... ok
test blocklist_enforcement_api ... ok
test domain_roster_and_block_jid_normalization ... ok
test malformed_roster_iq_never_mutates ... ok
test malformed_legacy_vcard_and_private_bookmarks_rejected ... ok
test private_and_vcard_authorization ... ok
test privacy_migration_only_default_deny_jids ... ok
test reads_timestamp_blocklist_and_keeps_metadata ... ok
test roster_empty_name_and_foreign_group_ignored ... ok
test roster_preserves_subscription_and_metadata ... ok
test purge_account_exact_files_preserves_others_and_extras ... ok
test subscription_handshake_and_cancel ... ok
test legacy_bookmarks_respect_host_limit_and_read_over_256 ... ok
test bookmarks_private_pep_bidirectional_and_private ... ok
test legacy_migration_clears_source_after_success ... ok
test vcard_pep_conversion_and_avatar ... ok
test password_change_after_purge_never_recreates_account ... ok
test creation_and_purge_are_serialized ... ok
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.36s
Running tests/storage_compat.rs (target/debug/deps/storage_compat-147836efd1c208ca)
running 6 tests
test empty_namespace_preserved ... ok
test mixed_binary_roundtrip ... ok
test stanza_roundtrip ... ok
test path_and_atomic_update ... ok
test rejects_cycles_code_and_bad_files ... ok
test concurrent_updates_do_not_lose_entries ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
Running tests/tls_binding.rs (target/debug/deps/tls_binding-5e699c7900a4d099)
running 4 tests
test malformed_der_and_pss_parameters_fail_closed ... ok
test unknown_and_eddsa_algorithms_fail_closed ... ok
test ecdsa_uses_certificate_signature_hash ... ok
test rsa_signature_digest_and_weak_hash_upgrade ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
Running tests/tls_plus_network.rs (target/debug/deps/tls_plus_network-b82a1b9340e268e4)
running 2 tests
test disabling_plus_hides_bindings_and_accepts_scram_y_with_mechanisms_only_ssdp ... ok
test starttls_scram_plus_binds_real_tls_and_authenticates_ssdp_sasl1_and_sasl2 ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.37s
Doc-tests rust_xmpp
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Finished `dev` profile [unoptimized] target(s) in 0.10s
Finished `release` profile [optimized] target(s) in 0.11s
PASS: integrated TCP roster/subscriptions, client IQ routing, CSI buffering/keepalive flush, MAM, blocking, OMEMO PEP, MUC, offline replay and SM resumption
PASS: graceful SIGTERM shutdown
PASS: independent SCRAM-SHA-1 over TLS, SASL1/SASL2, SSDP verification, server signatures and wrong-password rejection
PASS: independent SCRAM-SHA-256 over TLS, SASL1/SASL2, SSDP verification, server signatures and wrong-password rejection
PASS: Android TLS1.2 selector, TLS1.2/1.3 SCRAM SHA1/256 PLUS, SASL1/2, SSDP on/off and invalid binding rejection
PASS: JSON file logging, TLS/auth/bind correlation, rejection reasons, secret redaction and SIGHUP rotation
PASS: 20 TLS1.2/1.3 SASL1/2 cases: prepared PLAIN identities and canonical binds, invalid authz rejection, SCRAM abort/retry and active exchange restart
PASS: WSS SCRAM-SHA-256 SASL1/SASL2 with GS2 y, absent CB/PLUS, SSDP without CB separator, server signatures, bind and ping
PASS: HTTPS-only configured interfaces (127.0.0.1,::1), verified TLS, reproduced original Origin 403 then allowed Converse Origin 101, xmpp subprotocol, both WS paths, RFC 7395 open/features/close and private HTTP diagnostics
PASS: custom WS/BOSH routes, default-route 404, exact Origin policy, verified TLS proxy rewrite, trusted HTTPS metadata, untrusted/spoofed metadata denial, IPv6 when available, authenticated WS/BOSH bind/ping in legacy and RFC7239 modes and cached BOSH RID retry
PASS: XEP-0156 XRD/JRD parity, CORS, safe methods, ignored JSONP, host checks, secure URL preference, custom mounts/public prefixes, module and advertisement gates, HTTP fallback and verified WSS framing
PASS: SASL2 Bind2 TLS flow, inline SM/CSI/Carbons, private random resource, malformed requests, authentication failure and fallback
PASS: bind2-disabled, SSDP=False, SCRAM-SHA-1/PLAIN success and wrong-password rejection
PASS: bind2-disabled, SSDP=True, SCRAM-SHA-1/PLAIN success and wrong-password rejection
PASS: bind2-enabled, SSDP=False, SCRAM-SHA-1/PLAIN success and wrong-password rejection
PASS: bind2-enabled, SSDP=True, SCRAM-SHA-1/PLAIN success and wrong-password rejection
PASS: sm-disabled, SSDP=False, SCRAM-SHA-1/PLAIN success and wrong-password rejection
PASS: sm-disabled, SSDP=True, SCRAM-SHA-1/PLAIN success and wrong-password rejection
PASS: enabled-False-registration-False
PASS: enabled-False-registration-True
PASS: enabled-True-registration-False
PASS: enabled-True-registration-True
PASS: Gajim domain/account discovery, both label catalogs, authenticated password changes, IBR registration gates and fresh logins
PASS: SASL2 inline SM active/EOF resume, authenticated owner/id/h checks, replay ordering, full JID, Bind2 fallback and malformed requests
PASS: TLS account/domain discovery with PEP and push enabled/disabled; real push enable, publish options, per-node and service-wide disable
PASS: TLS Monocles/Conversations PEP first-run, no-to publishing, owner config recovery, migrated metadata, device item selectors and restart persistence
PASS: Android first-item device-list parsing sees latest publish and replacement before and after restart
PASS: STARTTLS PEP replay/live diagnostics, legacy/OMEMO2, payloadless notifications with intact IQ payloads, and private values excluded
PASS: TLS invitation command discovery, preapproval, single-use mutual roster, replay denial, unknown preauth forwarding/offline pending, account isolation
PASS: STARTTLS MUC SIGTERM restart restores live temporary/persistent rooms, exact full-JID membership, roles, presence and groupchat routing; rejects automatic cross-resource/account claims and nickname theft
PASS: recovery-history
PASS: leave
PASS: kick
PASS: demotion
PASS: password
PASS: configuration
PASS: restart-grace
PASS: custom-grace
PASS: unknown-module
PASS: actual STARTTLS SIGKILL MUC recovery, history, acknowledged barriers and fixed grace
PASS: TLS legacy/SASL2 durable SM after SIGKILL/SIGTERM, counters, ack pruning, at-least-once pending replay, owner/token/h rejection, transport/resource conflict, MUC activation/offline fanout, grace expiry and account deletion/recreation revocation
PASS: TLS conflict_resolve=default, live and hibernating SM resources
PASS: TLS conflict_resolve=kick_old, live and hibernating SM resources
PASS: TLS conflict_resolve=kick_new, live and hibernating SM resources
PASS: TLS conflict_resolve=increment, live and hibernating SM resources
PASS: TLS conflict_resolve=random, live and hibernating SM resources
PASS: integrated WSS + HTTPS BOSH SASL/bind, CSI, inherited BOSH namespaces, roster, bidirectional chat, upload IQ/PUT/GET/reuse, graceful SM close and same-resource relogin under kick_new
PASS: integrated WSS + HTTPS BOSH SASL/bind, CSI, inherited BOSH namespaces, roster, bidirectional chat, upload IQ/PUT/GET/reuse, graceful SM close and same-resource relogin under kick_new
PASS: configured upload HTTP path with $host through integrated listener
PASS: multiple upload hosts/shared-host paths, discovery, quotas, JWT isolation, persistence, transport composition and conflict validation
PASS upload S3 protocol fixture: SigV4, multipart, abort, range, restart, expiry and backend isolation
PASS upload migration: local -> S3 -> S3 -> local, hashes, original URLs/JWT/HEAD, lock, dry-run, fail-closed resume
{
"total_load_wall_seconds": 6.57,
"requested_resources": 500,
"authenticated_bound_resources": 500,
"connection_setup_concurrency": 50,
"setup_wall_seconds": 0.617,
"setup_latency": {
"samples": 500,
"median_ms": 57.355,
"p95_ms": 75.79,
"max_ms": 93.594
},
"ping_rounds": 10,
"ping_round_trip": {
"samples": 5000,
"median_ms": 12.184,
"p95_ms": 25.508,
"max_ms": 28.647
},
"accounts": 100,
"message_rounds": 10,
"messages_sent": 5000,
"messages_received": 5000,
"message_batch_seconds": 3.668,
"metrics_before": {
"rss_kib": 8532,
"cpu_seconds": 0.0,
"fd_count": 11
},
"metrics_connected": {
"rss_kib": 27360,
"cpu_seconds": 1.06,
"fd_count": 511
},
"metrics_after_workload": {
"rss_kib": 28192,
"cpu_seconds": 8.11,
"fd_count": 511
},
"metrics_two_seconds_after_disconnect": {
"rss_kib": 28192,
"cpu_seconds": 8.13,
"fd_count": 11
},
"sampled_peak_rss_kib": 28192,
"fd_delta_after_disconnect": 0,
"failures": [],
"scope": "One loopback host, 100 accounts with distinct resources, TLS+PLAIN, ping and directed messages; not a production benchmark or proof of no memory leaks.",
"mam": {
"archive_files": 100,
"records": 10000,
"expected_records": 10000
}
}
# Repeated MUC+SM load after correcting test readiness timeout (same release binary)
MUC+SM load: connected (5.193s)
MUC+SM load: joined (51.901s)
MUC+SM load: fanout_verified (62.762s)
MUC+SM load: before_sigkill (67.577s)
{
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-g0crm4ik/rust-xmpp",
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"before": {
"rss_kib": 9696,
"cpu_seconds": 0.0,
"fd_count": 12
},
"connect_seconds": 5.193,
"connect_latency": {
"samples": 500,
"median_ms": 204.391,
"p95_ms": 270.035,
"max_ms": 332.503
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 67.577,
"join_seconds": 46.703,
"joined": {
"rss_kib": 42248,
"cpu_seconds": 17.77,
"fd_count": 513
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 10.861,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 42128,
"cpu_seconds": 23.12,
"fd_count": 514
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 10.559,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 21168,
"cpu_seconds": 5.03,
"fd_count": 23
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 78.857,
"peak_rss_kib": 42252
}
# Native reference checks
Finished `test` profile [unoptimized] target(s) in 0.09s
Running tests/csi_native.rs (target/debug/deps/csi_native-7a0bbbead7a19df9)
running 1 test
test native_prosody_csi_importance_matches ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
Running tests/muc_native.rs (target/debug/deps/muc_native-f792fb43c05f2836)
running 1 test
test actual_prosody_muc_mam_reads_rust_and_writes_native_archive ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
Running tests/muc_state_native.rs (target/debug/deps/muc_state_native-e120e43e0108ea7f)
running 1 test
test actual_prosody_restores_rust_checkpoint_and_rust_reads_native_freeze ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
Running tests/pep_item_order.rs (target/debug/deps/pep_item_order-77f6b485fd7811b4)
running 1 test
test native_prosody_orders_retained_pep_items_newest_first ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.13s
Running tests/preauth_native.rs (target/debug/deps/preauth_native-36f66c5afb8f3b7f)
running 1 test
test native_prosody_invitation_tokens_roundtrip ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s
Running tests/pubsub_native.rs (target/debug/deps/pubsub_native-5bfe7997d9ab4681)
running 1 test
test native_prosody_pep_reads_rust_and_rust_reads_native ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.13s
Running tests/push.rs (target/debug/deps/push-e74594c0e96b1321)
running 1 test
test native_push_body_lookup_excludes_extensions ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.01s
Running tests/reference_prosody.rs (target/debug/deps/reference_prosody-fe62c7cdfcb258f2)
running 1 test
PROSODY_NATIVE_ROUNDTRIP_OK SHA-1
PROSODY_NATIVE_FINAL_OK SHA-1
PROSODY_NATIVE_ROUNDTRIP_OK SHA-256
PROSODY_NATIVE_FINAL_OK SHA-256
test real_prosody_storage_and_auth_roundtrip ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.03s
Finished `test` profile [unoptimized] target(s) in 0.09s
Running tests/federation_tls.rs (target/debug/deps/federation_tls-6dc128df24b929d8)
running 1 test
test native_prosody_tls_external_bidirectional ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.88s
@@ -0,0 +1,232 @@
{
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
"cases": [
{
"mam_enabled": false,
"sm_enabled": true,
"rounds": 3,
"seeded_muc_archive_records": 0,
"active": {
"direct_peer": {
"samples": 3,
"median_ms": 2.188561000025402,
"p95_ms": 2.3107499999923675,
"max_ms": 2.3107499999923675,
"samples_ms": [
2.0956669999350197,
2.188561000025402,
2.3107499999923675
]
},
"direct_application_rtt": {
"samples": 3,
"median_ms": 4.38233800002763,
"p95_ms": 4.56658600000992,
"max_ms": 4.56658600000992,
"samples_ms": [
4.381051999985175,
4.38233800002763,
4.56658600000992
]
},
"muc_sender_echo": {
"samples": 3,
"median_ms": 3.617779000023802,
"p95_ms": 3.788068999938332,
"max_ms": 3.788068999938332,
"samples_ms": [
3.264654999952654,
3.617779000023802,
3.788068999938332
]
},
"muc_peer": {
"samples": 3,
"median_ms": 3.8124090000337674,
"p95_ms": 3.8366489999361875,
"max_ms": 3.8366489999361875,
"samples_ms": [
3.3308939999869835,
3.8124090000337674,
3.8366489999361875
]
}
},
"inactive": {
"direct_peer": {
"samples": 3,
"median_ms": 2.189512999962062,
"p95_ms": 2.2236539999767047,
"max_ms": 2.2236539999767047,
"samples_ms": [
2.1697740000945487,
2.189512999962062,
2.2236539999767047
]
},
"direct_application_rtt": {
"samples": 3,
"median_ms": 4.169533999970554,
"p95_ms": 4.451736999953937,
"max_ms": 4.451736999953937,
"samples_ms": [
4.0612270000792705,
4.169533999970554,
4.451736999953937
]
},
"muc_sender_echo": {
"samples": 3,
"median_ms": 3.311576999976751,
"p95_ms": 4.169272000012825,
"max_ms": 4.169272000012825,
"samples_ms": [
3.2493040000645124,
3.311576999976751,
4.169272000012825
]
},
"muc_peer": {
"samples": 3,
"median_ms": 3.372077999983958,
"p95_ms": 4.188992999957009,
"max_ms": 4.188992999957009,
"samples_ms": [
3.315083000074992,
3.372077999983958,
4.188992999957009
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 3.6135210000338702,
"p95_ms": 3.6135210000338702,
"max_ms": 3.6135210000338702,
"samples_ms": [
3.6135210000338702
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 0,
"muc_archive_records": 0,
"wire_body_deliveries": 27,
"status": "pass"
},
{
"mam_enabled": true,
"sm_enabled": true,
"rounds": 3,
"seeded_muc_archive_records": 100000,
"active": {
"direct_peer": {
"samples": 3,
"median_ms": 4.533615000013924,
"p95_ms": 4.635108000002219,
"max_ms": 4.635108000002219,
"samples_ms": [
3.9327949999687917,
4.533615000013924,
4.635108000002219
]
},
"direct_application_rtt": {
"samples": 3,
"median_ms": 8.584756000004745,
"p95_ms": 9.454648999962956,
"max_ms": 9.454648999962956,
"samples_ms": [
8.391761999973824,
8.584756000004745,
9.454648999962956
]
},
"muc_sender_echo": {
"samples": 3,
"median_ms": 4.660852999904819,
"p95_ms": 14.45046399999228,
"max_ms": 14.45046399999228,
"samples_ms": [
4.372101999933875,
4.660852999904819,
14.45046399999228
]
},
"muc_peer": {
"samples": 3,
"median_ms": 4.680227999983799,
"p95_ms": 14.532127000052242,
"max_ms": 14.532127000052242,
"samples_ms": [
4.40479399992455,
4.680227999983799,
14.532127000052242
]
}
},
"inactive": {
"direct_peer": {
"samples": 3,
"median_ms": 5.005547999985538,
"p95_ms": 5.457555000020875,
"max_ms": 5.457555000020875,
"samples_ms": [
4.749896000021181,
5.005547999985538,
5.457555000020875
]
},
"direct_application_rtt": {
"samples": 3,
"median_ms": 9.33329400004368,
"p95_ms": 9.77378499999304,
"max_ms": 9.77378499999304,
"samples_ms": [
8.875206999960028,
9.33329400004368,
9.77378499999304
]
},
"muc_sender_echo": {
"samples": 3,
"median_ms": 4.230191000033301,
"p95_ms": 5.015898000010566,
"max_ms": 5.015898000010566,
"samples_ms": [
4.2100000000573345,
4.230191000033301,
5.015898000010566
]
},
"muc_peer": {
"samples": 3,
"median_ms": 4.284876000042459,
"p95_ms": 5.071866999969643,
"max_ms": 5.071866999969643,
"samples_ms": [
4.257245000076182,
4.284876000042459,
5.071866999969643
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 5.098277999991296,
"p95_ms": 5.098277999991296,
"max_ms": 5.098277999991296,
"samples_ms": [
5.098277999991296
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 26,
"muc_archive_records": 100006,
"wire_body_deliveries": 27,
"status": "pass"
}
]
}
@@ -0,0 +1,232 @@
{
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
"cases": [
{
"mam_enabled": false,
"sm_enabled": true,
"rounds": 3,
"seeded_muc_archive_records": 0,
"active": {
"direct_peer": {
"samples": 3,
"median_ms": 4.910238999968897,
"p95_ms": 5.052589999991142,
"max_ms": 5.052589999991142,
"samples_ms": [
4.849958000022525,
4.910238999968897,
5.052589999991142
]
},
"direct_application_rtt": {
"samples": 3,
"median_ms": 9.99482099996385,
"p95_ms": 10.185542000044734,
"max_ms": 10.185542000044734,
"samples_ms": [
9.438553999984833,
9.99482099996385,
10.185542000044734
]
},
"muc_sender_echo": {
"samples": 3,
"median_ms": 7.185058999993998,
"p95_ms": 7.319806000054996,
"max_ms": 7.319806000054996,
"samples_ms": [
5.824679000056676,
7.185058999993998,
7.319806000054996
]
},
"muc_peer": {
"samples": 3,
"median_ms": 7.9024159999789845,
"p95_ms": 8.338388999959534,
"max_ms": 8.338388999959534,
"samples_ms": [
6.551824999974087,
7.9024159999789845,
8.338388999959534
]
}
},
"inactive": {
"direct_peer": {
"samples": 3,
"median_ms": 6.445996999900672,
"p95_ms": 7.172110000055909,
"max_ms": 7.172110000055909,
"samples_ms": [
4.87915000007888,
6.445996999900672,
7.172110000055909
]
},
"direct_application_rtt": {
"samples": 3,
"median_ms": 10.659646999897632,
"p95_ms": 12.850071000002572,
"max_ms": 12.850071000002572,
"samples_ms": [
9.232021000002533,
10.659646999897632,
12.850071000002572
]
},
"muc_sender_echo": {
"samples": 3,
"median_ms": 9.392836000074567,
"p95_ms": 14.882106999948519,
"max_ms": 14.882106999948519,
"samples_ms": [
5.741209000007075,
9.392836000074567,
14.882106999948519
]
},
"muc_peer": {
"samples": 3,
"median_ms": 10.40252400002828,
"p95_ms": 16.07552799998757,
"max_ms": 16.07552799998757,
"samples_ms": [
6.522893999999724,
10.40252400002828,
16.07552799998757
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 6.09094300000379,
"p95_ms": 6.09094300000379,
"max_ms": 6.09094300000379,
"samples_ms": [
6.09094300000379
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 0,
"muc_archive_records": 0,
"wire_body_deliveries": 27,
"status": "pass"
},
{
"mam_enabled": true,
"sm_enabled": true,
"rounds": 3,
"seeded_muc_archive_records": 100000,
"active": {
"direct_peer": {
"samples": 3,
"median_ms": 8.917705999920145,
"p95_ms": 10.107052999956068,
"max_ms": 10.107052999956068,
"samples_ms": [
8.420038000053864,
8.917705999920145,
10.107052999956068
]
},
"direct_application_rtt": {
"samples": 3,
"median_ms": 16.42384999991009,
"p95_ms": 17.16581000005135,
"max_ms": 17.16581000005135,
"samples_ms": [
16.391191000025174,
16.42384999991009,
17.16581000005135
]
},
"muc_sender_echo": {
"samples": 3,
"median_ms": 2517.3544539999284,
"p95_ms": 2520.2535930000067,
"max_ms": 2520.2535930000067,
"samples_ms": [
2501.2779300000147,
2517.3544539999284,
2520.2535930000067
]
},
"muc_peer": {
"samples": 3,
"median_ms": 2517.9402029999665,
"p95_ms": 2520.940784000004,
"max_ms": 2520.940784000004,
"samples_ms": [
2502.2604349999256,
2517.9402029999665,
2520.940784000004
]
}
},
"inactive": {
"direct_peer": {
"samples": 3,
"median_ms": 24.452612999994017,
"p95_ms": 26.775254999961362,
"max_ms": 26.775254999961362,
"samples_ms": [
8.767078000005313,
24.452612999994017,
26.775254999961362
]
},
"direct_application_rtt": {
"samples": 3,
"median_ms": 36.83773900002052,
"p95_ms": 50.86929899994175,
"max_ms": 50.86929899994175,
"samples_ms": [
17.80327500000567,
36.83773900002052,
50.86929899994175
]
},
"muc_sender_echo": {
"samples": 3,
"median_ms": 3855.8298680000007,
"p95_ms": 4150.020520999988,
"max_ms": 4150.020520999988,
"samples_ms": [
2901.40261199997,
3855.8298680000007,
4150.020520999988
]
},
"muc_peer": {
"samples": 3,
"median_ms": 3866.027479999957,
"p95_ms": 4150.866962999999,
"max_ms": 4150.866962999999,
"samples_ms": [
2901.4587140000003,
3866.027479999957,
4150.866962999999
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 22.82110700002704,
"p95_ms": 22.82110700002704,
"max_ms": 22.82110700002704,
"samples_ms": [
22.82110700002704
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 26,
"muc_archive_records": 100006,
"wire_body_deliveries": 27,
"status": "pass"
}
]
}
@@ -0,0 +1,522 @@
{
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
"cases": [
{
"mam_enabled": false,
"sm_enabled": true,
"rounds": 5,
"seeded_muc_archive_records": 0,
"active": {
"direct_peer": {
"samples": 5,
"median_ms": 2.2083669999801714,
"p95_ms": 2.7524509999921065,
"max_ms": 2.7524509999921065,
"samples_ms": [
2.0349779999833117,
2.1022979999543168,
2.2083669999801714,
2.581427999984953,
2.7524509999921065
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 4.559320999987904,
"p95_ms": 5.448408999995991,
"max_ms": 5.448408999995991,
"samples_ms": [
3.9975349999394894,
4.301345000044421,
4.559320999987904,
4.565895999917302,
5.448408999995991
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 4.29816700000174,
"p95_ms": 11.835469999937231,
"max_ms": 11.835469999937231,
"samples_ms": [
3.717826999945828,
3.95383000000038,
4.29816700000174,
11.782627000002321,
11.835469999937231
]
},
"muc_peer": {
"samples": 5,
"median_ms": 4.366868000033719,
"p95_ms": 11.859378999929504,
"max_ms": 11.859378999929504,
"samples_ms": [
3.7456439999914437,
4.002205000006143,
4.366868000033719,
11.841935000006742,
11.859378999929504
]
}
},
"inactive": {
"direct_peer": {
"samples": 5,
"median_ms": 2.1559959999422063,
"p95_ms": 2.7428159999089985,
"max_ms": 2.7428159999089985,
"samples_ms": [
1.889266000034695,
2.07170299995596,
2.1559959999422063,
2.2102450000147655,
2.7428159999089985
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 4.291513999987728,
"p95_ms": 4.628346999993482,
"max_ms": 4.628346999993482,
"samples_ms": [
4.097702999956709,
4.117672000006678,
4.291513999987728,
4.321024999967449,
4.628346999993482
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 3.607771000019966,
"p95_ms": 4.329054999971049,
"max_ms": 4.329054999971049,
"samples_ms": [
3.0511659999774565,
3.3668879999595447,
3.607771000019966,
3.629876999980297,
4.329054999971049
]
},
"muc_peer": {
"samples": 5,
"median_ms": 3.669854000008854,
"p95_ms": 4.375101999926301,
"max_ms": 4.375101999926301,
"samples_ms": [
3.109387999984392,
3.436124000018026,
3.669854000008854,
3.692369999953371,
4.375101999926301
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 3.450391999990643,
"p95_ms": 3.450391999990643,
"max_ms": 3.450391999990643,
"samples_ms": [
3.450391999990643
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 0,
"muc_archive_records": 0,
"wire_body_deliveries": 43,
"status": "pass"
},
{
"mam_enabled": true,
"sm_enabled": true,
"rounds": 5,
"seeded_muc_archive_records": 0,
"active": {
"direct_peer": {
"samples": 5,
"median_ms": 5.680922999999893,
"p95_ms": 5.92475899998135,
"max_ms": 5.92475899998135,
"samples_ms": [
4.959245000009105,
5.657132000010279,
5.680922999999893,
5.683216000079483,
5.92475899998135
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 10.62067800000932,
"p95_ms": 11.266418999980488,
"max_ms": 11.266418999980488,
"samples_ms": [
9.56820900000821,
10.354214000017237,
10.62067800000932,
11.083625999958713,
11.266418999980488
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 4.932548000056158,
"p95_ms": 7.615871999973933,
"max_ms": 7.615871999973933,
"samples_ms": [
4.048796000006405,
4.207175000033203,
4.932548000056158,
5.545106999989002,
7.615871999973933
]
},
"muc_peer": {
"samples": 5,
"median_ms": 5.05425800008652,
"p95_ms": 7.6429759999427915,
"max_ms": 7.6429759999427915,
"samples_ms": [
4.07517700000426,
4.236438000020826,
5.05425800008652,
5.570927000007941,
7.6429759999427915
]
}
},
"inactive": {
"direct_peer": {
"samples": 5,
"median_ms": 5.44478100005108,
"p95_ms": 5.556554000008873,
"max_ms": 5.556554000008873,
"samples_ms": [
4.727472000013222,
4.8114250000708125,
5.44478100005108,
5.505089000052976,
5.556554000008873
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 10.183787000073607,
"p95_ms": 10.890477999964787,
"max_ms": 10.890477999964787,
"samples_ms": [
9.888724000006732,
9.945942000058494,
10.183787000073607,
10.306656000011571,
10.890477999964787
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 5.1263200000448705,
"p95_ms": 5.255117000046994,
"max_ms": 5.255117000046994,
"samples_ms": [
4.305390000013176,
5.036372000063238,
5.1263200000448705,
5.168275000073663,
5.255117000046994
]
},
"muc_peer": {
"samples": 5,
"median_ms": 5.184012999961851,
"p95_ms": 5.310040000040317,
"max_ms": 5.310040000040317,
"samples_ms": [
4.368295000062972,
5.154129000061403,
5.184012999961851,
5.191530000047351,
5.310040000040317
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 5.2187170000479455,
"p95_ms": 5.2187170000479455,
"max_ms": 5.2187170000479455,
"samples_ms": [
5.2187170000479455
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 42,
"muc_archive_records": 10,
"wire_body_deliveries": 43,
"status": "pass"
},
{
"mam_enabled": true,
"sm_enabled": true,
"rounds": 5,
"seeded_muc_archive_records": 1000,
"active": {
"direct_peer": {
"samples": 5,
"median_ms": 5.179283999950712,
"p95_ms": 5.702402999986589,
"max_ms": 5.702402999986589,
"samples_ms": [
4.691908000040712,
5.172169999923426,
5.179283999950712,
5.635785999970722,
5.702402999986589
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 9.836759999984679,
"p95_ms": 10.818348000043443,
"max_ms": 10.818348000043443,
"samples_ms": [
9.177894999993441,
9.73506099990118,
9.836759999984679,
10.593198000037773,
10.818348000043443
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 4.2178580000609145,
"p95_ms": 6.532151999977032,
"max_ms": 6.532151999977032,
"samples_ms": [
3.8601360000711793,
4.174319999947329,
4.2178580000609145,
4.571829000042271,
6.532151999977032
]
},
"muc_peer": {
"samples": 5,
"median_ms": 4.280302000097436,
"p95_ms": 6.605132999993657,
"max_ms": 6.605132999993657,
"samples_ms": [
3.8853840000001583,
4.233521999935874,
4.280302000097436,
4.673529000001508,
6.605132999993657
]
}
},
"inactive": {
"direct_peer": {
"samples": 5,
"median_ms": 5.289302999926804,
"p95_ms": 5.9182850000070175,
"max_ms": 5.9182850000070175,
"samples_ms": [
4.637428000023647,
4.646944000000985,
5.289302999926804,
5.336470000088411,
5.9182850000070175
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 9.881779000011193,
"p95_ms": 11.126878000027318,
"max_ms": 11.126878000027318,
"samples_ms": [
9.666927000012038,
9.780869999985953,
9.881779000011193,
9.893610999938574,
11.126878000027318
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 5.337903999929949,
"p95_ms": 6.605088000014803,
"max_ms": 6.605088000014803,
"samples_ms": [
4.4430670000110695,
4.566451999949095,
5.337903999929949,
5.398619999937182,
6.605088000014803
]
},
"muc_peer": {
"samples": 5,
"median_ms": 5.365761999996721,
"p95_ms": 6.657663000055436,
"max_ms": 6.657663000055436,
"samples_ms": [
4.503487999954814,
4.589735999957156,
5.365761999996721,
5.456013999946663,
6.657663000055436
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 6.4426129999901605,
"p95_ms": 6.4426129999901605,
"max_ms": 6.4426129999901605,
"samples_ms": [
6.4426129999901605
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 42,
"muc_archive_records": 1010,
"wire_body_deliveries": 43,
"status": "pass"
},
{
"mam_enabled": true,
"sm_enabled": true,
"rounds": 5,
"seeded_muc_archive_records": 10000,
"active": {
"direct_peer": {
"samples": 5,
"median_ms": 5.5639299999938885,
"p95_ms": 6.427136000070277,
"max_ms": 6.427136000070277,
"samples_ms": [
4.3693179999308995,
5.252241999983198,
5.5639299999938885,
6.115242999953807,
6.427136000070277
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 10.580320000030952,
"p95_ms": 11.34455600004003,
"max_ms": 11.34455600004003,
"samples_ms": [
8.862616999977035,
10.534624999991138,
10.580320000030952,
10.863696000001255,
11.34455600004003
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 4.871052999988024,
"p95_ms": 6.248740000046382,
"max_ms": 6.248740000046382,
"samples_ms": [
4.298133000020243,
4.364993999956823,
4.871052999988024,
5.0552000000152475,
6.248740000046382
]
},
"muc_peer": {
"samples": 5,
"median_ms": 4.894828000033158,
"p95_ms": 6.306694000045354,
"max_ms": 6.306694000045354,
"samples_ms": [
4.328496999960407,
4.393286999970769,
4.894828000033158,
5.0838870000688985,
6.306694000045354
]
}
},
"inactive": {
"direct_peer": {
"samples": 5,
"median_ms": 4.837865000013153,
"p95_ms": 6.154906000006122,
"max_ms": 6.154906000006122,
"samples_ms": [
4.644825000013952,
4.6781300000020565,
4.837865000013153,
6.011097999930826,
6.154906000006122
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 9.839443999908326,
"p95_ms": 11.105878999956076,
"max_ms": 11.105878999956076,
"samples_ms": [
9.05296900009489,
9.732880000001387,
9.839443999908326,
10.890076000009685,
11.105878999956076
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 4.868799000064428,
"p95_ms": 5.506852999928924,
"max_ms": 5.506852999928924,
"samples_ms": [
4.652097999951366,
4.654195000057371,
4.868799000064428,
5.197563999900012,
5.506852999928924
]
},
"muc_peer": {
"samples": 5,
"median_ms": 4.921641999999338,
"p95_ms": 5.567783999936182,
"max_ms": 5.567783999936182,
"samples_ms": [
4.707422999899791,
4.71425300008832,
4.921641999999338,
5.261014999973668,
5.567783999936182
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 6.844148000027417,
"p95_ms": 6.844148000027417,
"max_ms": 6.844148000027417,
"samples_ms": [
6.844148000027417
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 42,
"muc_archive_records": 10010,
"wire_body_deliveries": 43,
"status": "pass"
}
]
}
@@ -0,0 +1,522 @@
{
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
"cases": [
{
"mam_enabled": false,
"sm_enabled": true,
"rounds": 5,
"seeded_muc_archive_records": 0,
"active": {
"direct_peer": {
"samples": 5,
"median_ms": 5.10814599999776,
"p95_ms": 6.436508999968282,
"max_ms": 6.436508999968282,
"samples_ms": [
4.687508000017715,
5.018378999977813,
5.10814599999776,
5.499873000019306,
6.436508999968282
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 10.949089000007461,
"p95_ms": 11.194100000011531,
"max_ms": 11.194100000011531,
"samples_ms": [
9.188771000026463,
9.251739999967867,
10.949089000007461,
11.097223999968264,
11.194100000011531
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 6.3955159999977695,
"p95_ms": 10.028454999996939,
"max_ms": 10.028454999996939,
"samples_ms": [
5.6768890000284955,
6.070047000036993,
6.3955159999977695,
6.783154999993712,
10.028454999996939
]
},
"muc_peer": {
"samples": 5,
"median_ms": 7.227664999959416,
"p95_ms": 10.73728499994786,
"max_ms": 10.73728499994786,
"samples_ms": [
6.249474000014743,
6.710089000023345,
7.227664999959416,
7.554007000010188,
10.73728499994786
]
}
},
"inactive": {
"direct_peer": {
"samples": 5,
"median_ms": 4.543079999962174,
"p95_ms": 5.169351999995797,
"max_ms": 5.169351999995797,
"samples_ms": [
3.9885659999754353,
4.165818999979365,
4.543079999962174,
4.987367000012455,
5.169351999995797
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 9.515092000015102,
"p95_ms": 12.47698700001365,
"max_ms": 12.47698700001365,
"samples_ms": [
8.106691999955729,
8.858637999992425,
9.515092000015102,
9.874779000028866,
12.47698700001365
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 5.794529000013426,
"p95_ms": 7.275404999973034,
"max_ms": 7.275404999973034,
"samples_ms": [
5.51126799996382,
5.724920000034217,
5.794529000013426,
6.444504000000961,
7.275404999973034
]
},
"muc_peer": {
"samples": 5,
"median_ms": 6.479804000036893,
"p95_ms": 8.207588000004762,
"max_ms": 8.207588000004762,
"samples_ms": [
6.077395999966484,
6.479181999964112,
6.479804000036893,
7.2692859999961,
8.207588000004762
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 4.576559000042835,
"p95_ms": 4.576559000042835,
"max_ms": 4.576559000042835,
"samples_ms": [
4.576559000042835
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 0,
"muc_archive_records": 0,
"wire_body_deliveries": 43,
"status": "pass"
},
{
"mam_enabled": true,
"sm_enabled": true,
"rounds": 5,
"seeded_muc_archive_records": 0,
"active": {
"direct_peer": {
"samples": 5,
"median_ms": 8.454973000027621,
"p95_ms": 9.086850000016966,
"max_ms": 9.086850000016966,
"samples_ms": [
7.875873999978467,
7.919512999990275,
8.454973000027621,
8.744820000003983,
9.086850000016966
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 15.02084100002321,
"p95_ms": 18.49304000000984,
"max_ms": 18.49304000000984,
"samples_ms": [
14.658381000003828,
15.0033279999775,
15.02084100002321,
16.241458000024522,
18.49304000000984
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 8.695557000010012,
"p95_ms": 10.132943000030536,
"max_ms": 10.132943000030536,
"samples_ms": [
6.247895000001336,
8.190369000033115,
8.695557000010012,
8.886856000003718,
10.132943000030536
]
},
"muc_peer": {
"samples": 5,
"median_ms": 9.425391000036143,
"p95_ms": 11.220557999990888,
"max_ms": 11.220557999990888,
"samples_ms": [
6.928168999991158,
9.00288799999771,
9.425391000036143,
9.836486999972749,
11.220557999990888
]
}
},
"inactive": {
"direct_peer": {
"samples": 5,
"median_ms": 8.574552000027325,
"p95_ms": 11.412799000027007,
"max_ms": 11.412799000027007,
"samples_ms": [
7.429673999979514,
8.434421999993447,
8.574552000027325,
10.991448000027049,
11.412799000027007
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 16.801955000005364,
"p95_ms": 20.960896000019602,
"max_ms": 20.960896000019602,
"samples_ms": [
15.289625999969303,
16.07350099999394,
16.801955000005364,
19.067943000038667,
20.960896000019602
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 9.673966000036671,
"p95_ms": 10.545491000016227,
"max_ms": 10.545491000016227,
"samples_ms": [
8.954777999974795,
9.244574000035755,
9.673966000036671,
10.407988999986628,
10.545491000016227
]
},
"muc_peer": {
"samples": 5,
"median_ms": 10.7329940000227,
"p95_ms": 11.54250500002263,
"max_ms": 11.54250500002263,
"samples_ms": [
9.77221699997699,
10.012581000012233,
10.7329940000227,
11.414449999961107,
11.54250500002263
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 8.784937999962494,
"p95_ms": 8.784937999962494,
"max_ms": 8.784937999962494,
"samples_ms": [
8.784937999962494
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 42,
"muc_archive_records": 10,
"wire_body_deliveries": 43,
"status": "pass"
},
{
"mam_enabled": true,
"sm_enabled": true,
"rounds": 5,
"seeded_muc_archive_records": 1000,
"active": {
"direct_peer": {
"samples": 5,
"median_ms": 7.590319999962958,
"p95_ms": 8.892007999975249,
"max_ms": 8.892007999975249,
"samples_ms": [
5.31427799995754,
6.9214119999969625,
7.590319999962958,
7.624421999992137,
8.892007999975249
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 13.376056000026892,
"p95_ms": 15.464272999963669,
"max_ms": 15.464272999963669,
"samples_ms": [
12.23329399999784,
13.25195099997245,
13.376056000026892,
14.078856999958589,
15.464272999963669
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 31.94280399998206,
"p95_ms": 35.83487499997773,
"max_ms": 35.83487499997773,
"samples_ms": [
30.683405999980096,
31.05138800003715,
31.94280399998206,
34.427220999987185,
35.83487499997773
]
},
"muc_peer": {
"samples": 5,
"median_ms": 32.718170999999074,
"p95_ms": 36.99477299994669,
"max_ms": 36.99477299994669,
"samples_ms": [
31.32484000002478,
31.708390999995117,
32.718170999999074,
35.15474099998528,
36.99477299994669
]
}
},
"inactive": {
"direct_peer": {
"samples": 5,
"median_ms": 6.955795000010312,
"p95_ms": 7.9194580000034875,
"max_ms": 7.9194580000034875,
"samples_ms": [
6.15931200002251,
6.666912000014236,
6.955795000010312,
7.675652999978411,
7.9194580000034875
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 13.159411000003729,
"p95_ms": 15.27061000001595,
"max_ms": 15.27061000001595,
"samples_ms": [
12.274962999981653,
13.057566999975734,
13.159411000003729,
14.08330199996044,
15.27061000001595
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 34.13865300001362,
"p95_ms": 36.99340499997561,
"max_ms": 36.99340499997561,
"samples_ms": [
30.982952999977442,
32.71812099995941,
34.13865300001362,
35.38880100001052,
36.99340499997561
]
},
"muc_peer": {
"samples": 5,
"median_ms": 36.06873300003599,
"p95_ms": 38.31650299997591,
"max_ms": 38.31650299997591,
"samples_ms": [
33.48175599995784,
34.2965520000007,
36.06873300003599,
36.353186000042115,
38.31650299997591
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 12.115028999971855,
"p95_ms": 12.115028999971855,
"max_ms": 12.115028999971855,
"samples_ms": [
12.115028999971855
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 42,
"muc_archive_records": 1010,
"wire_body_deliveries": 43,
"status": "pass"
},
{
"mam_enabled": true,
"sm_enabled": true,
"rounds": 5,
"seeded_muc_archive_records": 10000,
"active": {
"direct_peer": {
"samples": 5,
"median_ms": 9.04404400000658,
"p95_ms": 12.927059999981338,
"max_ms": 12.927059999981338,
"samples_ms": [
8.594286000004558,
9.04279899998528,
9.04404400000658,
10.732668000002832,
12.927059999981338
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 18.81452300000319,
"p95_ms": 21.198468999955367,
"max_ms": 21.198468999955367,
"samples_ms": [
17.06366100000878,
17.834419999985585,
18.81452300000319,
18.89581300002874,
21.198468999955367
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 258.5251909999897,
"p95_ms": 269.03445800002146,
"max_ms": 269.03445800002146,
"samples_ms": [
243.74590799999396,
244.56996899999695,
258.5251909999897,
260.5109279999738,
269.03445800002146
]
},
"muc_peer": {
"samples": 5,
"median_ms": 259.47216499997694,
"p95_ms": 269.0669140000068,
"max_ms": 269.0669140000068,
"samples_ms": [
244.58446500000264,
245.61970199999905,
259.47216499997694,
261.3186459999497,
269.0669140000068
]
}
},
"inactive": {
"direct_peer": {
"samples": 5,
"median_ms": 9.321043999989342,
"p95_ms": 11.691385000005994,
"max_ms": 11.691385000005994,
"samples_ms": [
9.130955999978596,
9.257138999998915,
9.321043999989342,
9.393109000029654,
11.691385000005994
]
},
"direct_application_rtt": {
"samples": 5,
"median_ms": 22.537706999969487,
"p95_ms": 22.7979030000256,
"max_ms": 22.7979030000256,
"samples_ms": [
18.378415999961817,
18.92694800000072,
22.537706999969487,
22.614556000007724,
22.7979030000256
]
},
"muc_sender_echo": {
"samples": 5,
"median_ms": 257.6139750000266,
"p95_ms": 278.14462699996056,
"max_ms": 278.14462699996056,
"samples_ms": [
248.2455429999959,
251.84251700000004,
257.6139750000266,
262.41991000000553,
278.14462699996056
]
},
"muc_peer": {
"samples": 5,
"median_ms": 258.46415800003797,
"p95_ms": 278.8930929999651,
"max_ms": 278.8930929999651,
"samples_ms": [
250.01428399997394,
253.1065140000237,
258.46415800003797,
263.1856349999566,
278.8930929999651
]
}
},
"urgent_flush": {
"samples": 1,
"median_ms": 11.461830000030204,
"p95_ms": 11.461830000030204,
"max_ms": 11.461830000030204,
"samples_ms": [
11.461830000030204
]
},
"headline_durably_buffered": true,
"activation_drained_queue": true,
"personal_archive_records": 42,
"muc_archive_records": 10010,
"wire_body_deliveries": 43,
"status": "pass"
}
]
}
@@ -0,0 +1,62 @@
{
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-p95vsvii/rust-xmpp",
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
"before": {
"rss_kib": 10076,
"cpu_seconds": 0.0,
"fd_count": 12
},
"connect_seconds": 3.149,
"connect_latency": {
"samples": 500,
"median_ms": 121.817,
"p95_ms": 169.081,
"max_ms": 257.299
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 39.535,
"join_seconds": 25.54,
"joined": {
"rss_kib": 42224,
"cpu_seconds": 14.41,
"fd_count": 512
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 5.504,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 42224,
"cpu_seconds": 17.88,
"fd_count": 512
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 2.962,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 20796,
"cpu_seconds": 2.81,
"fd_count": 22
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 39.535,
"peak_rss_kib": 42240
}
File diff suppressed because it is too large. Load diff
+164
View File
@@ -0,0 +1,164 @@
{
"binary_source": "/tmp/rust-xmpp-0.8.2-final",
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
"scope": "real loopback TCP/STARTTLS/SASL2 Bind2; isolated personal MAM fixture; no production latency claim",
"timing": "raw ordered samples; auth timer starts before authenticate and ends at parsed success; full timer includes TCP/STARTTLS/features; metadata IQ timer runs from send to parsed result",
"result": {
"status": "pass",
"seeded_personal_archive_records": 100000,
"archive_bytes": 21488890,
"archive_expires_after": "never",
"index_present_before_start": false,
"reconnect_samples": 5,
"expected_metadata": {
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
"observed_metadata": [
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
}
],
"mam_metadata_iq": {
"samples_ms": [
0.4802089999884629,
0.35487300010572653,
0.3393980000510055,
0.49224100007450033,
0.3266720000283385
],
"median_ms": 0.35487300010572653,
"p95_ms": 0.49224100007450033,
"max_ms": 0.49224100007450033
},
"observed_iq_metadata": [
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
}
],
"sasl2_bind2_success": {
"samples_ms": [
1.9306740000502032,
2.103263999970295,
2.0025989999794547,
1.9764060000397876,
1.8955069999719854
],
"median_ms": 1.9764060000397876,
"p95_ms": 2.103263999970295,
"max_ms": 2.103263999970295
},
"tcp_starttls_to_bind2_success": {
"samples_ms": [
6.099597000002177,
3.735450000021956,
3.564695000022766,
3.3520640001825086,
3.3587010000246664
],
"median_ms": 3.564695000022766,
"p95_ms": 6.099597000002177,
"max_ms": 6.099597000002177
}
}
}
@@ -0,0 +1,164 @@
{
"binary_source": "/tmp/xmpp-message-latency-original-binary",
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"scope": "real loopback TCP/STARTTLS/SASL2 Bind2; isolated personal MAM fixture; no production latency claim",
"timing": "raw ordered samples; auth timer starts before authenticate and ends at parsed success; full timer includes TCP/STARTTLS/features; metadata IQ timer runs from send to parsed result",
"result": {
"status": "pass",
"seeded_personal_archive_records": 100000,
"archive_bytes": 21488890,
"archive_expires_after": "never",
"index_present_before_start": false,
"reconnect_samples": 5,
"expected_metadata": {
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
"observed_metadata": [
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
}
],
"mam_metadata_iq": {
"samples_ms": [
2809.4374219999736,
2219.536220000009,
1944.7504689999278,
2437.94546800018,
3715.6457869998576
],
"median_ms": 2437.94546800018,
"p95_ms": 3715.6457869998576,
"max_ms": 3715.6457869998576
},
"observed_iq_metadata": [
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-99999",
"timestamp": "2026-01-02T03:46:39Z"
}
}
],
"sasl2_bind2_success": {
"samples_ms": [
2973.741726999833,
1934.8616100000982,
2034.18535600008,
3162.465005000058,
2742.352718999882
],
"median_ms": 2742.352718999882,
"p95_ms": 3162.465005000058,
"max_ms": 3162.465005000058
},
"tcp_starttls_to_bind2_success": {
"samples_ms": [
2977.800272999957,
1937.2807600000215,
2036.7481539999517,
3165.2697970000645,
2745.1149930000156
],
"median_ms": 2745.1149930000156,
"p95_ms": 3165.2697970000645,
"max_ms": 3165.2697970000645
}
}
}
@@ -0,0 +1,62 @@
{
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-qywt10tu/rust-xmpp",
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
"before": {
"rss_kib": 9548,
"cpu_seconds": 0.0,
"fd_count": 12
},
"connect_seconds": 2.918,
"connect_latency": {
"samples": 500,
"median_ms": 114.756,
"p95_ms": 154.077,
"max_ms": 206.971
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 45.54,
"join_seconds": 30.37,
"joined": {
"rss_kib": 41856,
"cpu_seconds": 16.64,
"fd_count": 514
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 6.696,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 41884,
"cpu_seconds": 20.77,
"fd_count": 512
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 2.763,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 20984,
"cpu_seconds": 2.66,
"fd_count": 22
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 45.54,
"peak_rss_kib": 41920
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,498 @@
{
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
"scope": "loopback STARTTLS; fixture only; forced CSI dwell explicitly separated",
"cases": [
{
"active": {
"delivery": {
"samples": 3,
"median_ms": 4.817936999870653,
"p95_ms": 16.517306000196186,
"max_ms": 16.517306000196186,
"samples_ms": [
4.748034999920492,
4.817936999870653,
16.517306000196186
]
},
"sender_sm_handled": {
"samples": 3,
"median_ms": 4.7456789998250315,
"p95_ms": 16.426100000217048,
"max_ms": 16.426100000217048,
"samples_ms": [
4.626912000048833,
4.7456789998250315,
16.426100000217048
]
},
"client_receipt_end_to_end": {
"samples": 3,
"median_ms": 10.344463999899745,
"p95_ms": 21.304692000057912,
"max_ms": 21.304692000057912,
"samples_ms": [
9.215212999833966,
10.344463999899745,
21.304692000057912
]
},
"receipt_return_leg": {
"samples": 3,
"median_ms": 4.755211999963649,
"p95_ms": 5.568677999917782,
"max_ms": 5.568677999917782,
"samples_ms": [
4.3716870000025665,
4.755211999963649,
5.568677999917782
]
}
},
"inactive": {
"delivery": {
"samples": 3,
"median_ms": 4.89018300004318,
"p95_ms": 5.018079000137732,
"max_ms": 5.018079000137732,
"samples_ms": [
4.7031279998464015,
4.89018300004318,
5.018079000137732
]
},
"sender_sm_handled": {
"samples": 3,
"median_ms": 4.817049000166662,
"p95_ms": 5.079797999997027,
"max_ms": 5.079797999997027,
"samples_ms": [
4.60740799985615,
4.817049000166662,
5.079797999997027
]
},
"client_receipt_end_to_end": {
"samples": 3,
"median_ms": 9.543050000047515,
"p95_ms": 10.455423000166775,
"max_ms": 10.455423000166775,
"samples_ms": [
9.271581000120932,
9.543050000047515,
10.455423000166775
]
},
"receipt_return_leg": {
"samples": 3,
"median_ms": 4.79821100020672,
"p95_ms": 5.540141000210497,
"max_ms": 5.540141000210497,
"samples_ms": [
4.134176999968986,
4.79821100020672,
5.540141000210497
]
}
},
"receipt_only-active": {
"delivery_ms": 5.081753000013123,
"sender_sm_handled_ms": 4.81151700000737,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"receipt_only-inactive": {
"delivery_ms": 2010.5149120001897,
"sender_sm_handled_ms": 4.923649000147634,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 1.6535839999960444
},
"omemo_legacy_only-active": {
"delivery_ms": 2.6542460000200663,
"sender_sm_handled_ms": 2.3668179999276617,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_legacy_only-inactive": {
"delivery_ms": 2012.9395249998652,
"sender_sm_handled_ms": 2.5684999998247804,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 3.8570599999729893
},
"omemo_2_only-active": {
"delivery_ms": 2.803322000090702,
"sender_sm_handled_ms": 2.5052290000076027,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_2_only-inactive": {
"delivery_ms": 2006.170849,
"sender_sm_handled_ms": 2.1322459999737475,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 1.5037350001421146
},
"omemo_with_eme-active": {
"delivery_ms": 5.247026000006372,
"sender_sm_handled_ms": 4.908949999844481,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_with_eme-inactive": {
"delivery_ms": 6.537689000197133,
"sender_sm_handled_ms": 6.264521000048262,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"sm_backlog": {
"seeded_messages": 500,
"durable_pending_before_sample": 500,
"delivery_ms": 4.346552000015436,
"sender_sm_handled_ms": 4.268297999942661
},
"status": "pass",
"final_personal_mam_records_per_user": [
100517,
100517
],
"roster_contacts_per_user": 0,
"seeded_personal_mam_records_per_user": 100000,
"archive_policy": "always"
},
{
"active": {
"delivery": {
"samples": 3,
"median_ms": 19.816834000039307,
"p95_ms": 34.282772000096884,
"max_ms": 34.282772000096884,
"samples_ms": [
19.449956000016755,
19.816834000039307,
34.282772000096884
]
},
"sender_sm_handled": {
"samples": 3,
"median_ms": 19.71937500002241,
"p95_ms": 34.19807699992816,
"max_ms": 34.19807699992816,
"samples_ms": [
19.322317000160183,
19.71937500002241,
34.19807699992816
]
},
"client_receipt_end_to_end": {
"samples": 3,
"median_ms": 39.9533730001167,
"p95_ms": 54.972926000118605,
"max_ms": 54.972926000118605,
"samples_ms": [
39.557586999990235,
39.9533730001167,
54.972926000118605
]
},
"receipt_return_leg": {
"samples": 3,
"median_ms": 20.108162000042284,
"p95_ms": 20.66232500010301,
"max_ms": 20.66232500010301,
"samples_ms": [
20.07690499999626,
20.108162000042284,
20.66232500010301
]
}
},
"inactive": {
"delivery": {
"samples": 3,
"median_ms": 21.33975200013083,
"p95_ms": 22.656845000028625,
"max_ms": 22.656845000028625,
"samples_ms": [
19.565955999951257,
21.33975200013083,
22.656845000028625
]
},
"sender_sm_handled": {
"samples": 3,
"median_ms": 21.24135799999749,
"p95_ms": 22.574971000040023,
"max_ms": 22.574971000040023,
"samples_ms": [
19.466118999844184,
21.24135799999749,
22.574971000040023
]
},
"client_receipt_end_to_end": {
"samples": 3,
"median_ms": 42.33967000004668,
"p95_ms": 42.759643999943364,
"max_ms": 42.759643999943364,
"samples_ms": [
39.931753000018944,
42.33967000004668,
42.759643999943364
]
},
"receipt_return_leg": {
"samples": 3,
"median_ms": 20.32766500019534,
"p95_ms": 21.3887639999939,
"max_ms": 21.3887639999939,
"samples_ms": [
19.65616699999373,
20.32766500019534,
21.3887639999939
]
}
},
"receipt_only-active": {
"delivery_ms": 20.706908999954976,
"sender_sm_handled_ms": 20.764115000019956,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"receipt_only-inactive": {
"delivery_ms": 2024.603683000123,
"sender_sm_handled_ms": 19.517362000215144,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 2.5455170000441285
},
"omemo_legacy_only-active": {
"delivery_ms": 2.6721889998952975,
"sender_sm_handled_ms": 2.4056919999111415,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_legacy_only-inactive": {
"delivery_ms": 2008.3468199998151,
"sender_sm_handled_ms": 2.188200999853507,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 2.0773089997874195
},
"omemo_2_only-active": {
"delivery_ms": 3.2299929998771404,
"sender_sm_handled_ms": 2.707185999952344,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_2_only-inactive": {
"delivery_ms": 2008.4954139999809,
"sender_sm_handled_ms": 2.423592000013741,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 2.214231000152722
},
"omemo_with_eme-active": {
"delivery_ms": 22.333799999842086,
"sender_sm_handled_ms": 22.02385900000081,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_with_eme-inactive": {
"delivery_ms": 36.01745500009201,
"sender_sm_handled_ms": 35.61825799988583,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"sm_backlog": {
"seeded_messages": 500,
"durable_pending_before_sample": 500,
"delivery_ms": 18.815608000068096,
"sender_sm_handled_ms": 18.779131000201232
},
"status": "pass",
"final_personal_mam_records_per_user": [
100517,
100517
],
"roster_contacts_per_user": 5000,
"seeded_personal_mam_records_per_user": 100000,
"archive_policy": "roster"
},
{
"active": {
"delivery": {
"samples": 3,
"median_ms": 36.994213000070886,
"p95_ms": 46.157045999962065,
"max_ms": 46.157045999962065,
"samples_ms": [
32.354088000147385,
36.994213000070886,
46.157045999962065
]
},
"sender_sm_handled": {
"samples": 3,
"median_ms": 37.03395700017609,
"p95_ms": 46.22371799996472,
"max_ms": 46.22371799996472,
"samples_ms": [
32.41653200007022,
37.03395700017609,
46.22371799996472
]
},
"client_receipt_end_to_end": {
"samples": 3,
"median_ms": 70.08860200016898,
"p95_ms": 79.21533199987607,
"max_ms": 79.21533199987607,
"samples_ms": [
64.34645700005603,
70.08860200016898,
79.21533199987607
]
},
"receipt_return_leg": {
"samples": 3,
"median_ms": 32.9383449998204,
"p95_ms": 33.00209500002893,
"max_ms": 33.00209500002893,
"samples_ms": [
31.880290000117384,
32.9383449998204,
33.00209500002893
]
}
},
"inactive": {
"delivery": {
"samples": 3,
"median_ms": 36.834342000020115,
"p95_ms": 39.296434999869234,
"max_ms": 39.296434999869234,
"samples_ms": [
32.84419600004185,
36.834342000020115,
39.296434999869234
]
},
"sender_sm_handled": {
"samples": 3,
"median_ms": 36.63234700002249,
"p95_ms": 39.167316999964896,
"max_ms": 39.167316999964896,
"samples_ms": [
32.901452000032805,
36.63234700002249,
39.167316999964896
]
},
"client_receipt_end_to_end": {
"samples": 3,
"median_ms": 74.8365649999414,
"p95_ms": 82.56400300001587,
"max_ms": 82.56400300001587,
"samples_ms": [
65.61269799999536,
74.8365649999414,
82.56400300001587
]
},
"receipt_return_leg": {
"samples": 3,
"median_ms": 35.5047470000045,
"p95_ms": 45.70364199980759,
"max_ms": 45.70364199980759,
"samples_ms": [
32.67074199993658,
35.5047470000045,
45.70364199980759
]
}
},
"receipt_only-active": {
"delivery_ms": 35.88422400002855,
"sender_sm_handled_ms": 35.596954999846275,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"receipt_only-inactive": {
"delivery_ms": 2042.8565239999443,
"sender_sm_handled_ms": 37.33574900002168,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 1.5680029998748068
},
"omemo_legacy_only-active": {
"delivery_ms": 2.439558000105535,
"sender_sm_handled_ms": 2.5105040001562884,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_legacy_only-inactive": {
"delivery_ms": 2008.4427409999535,
"sender_sm_handled_ms": 2.87832799995158,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 1.7736980000790936
},
"omemo_2_only-active": {
"delivery_ms": 2.8043899999374844,
"sender_sm_handled_ms": 2.46161499990194,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_2_only-inactive": {
"delivery_ms": 2006.2423169999875,
"sender_sm_handled_ms": 2.324273000112953,
"durably_csi_buffered": true,
"forced_dwell_seconds": 2.0,
"after_activation_ms": 1.4116319998720428
},
"omemo_with_eme-active": {
"delivery_ms": 37.31560400001399,
"sender_sm_handled_ms": 37.00228699995023,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_with_eme-inactive": {
"delivery_ms": 34.92131200005133,
"sender_sm_handled_ms": 34.618570000020554,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"sm_backlog": {
"seeded_messages": 500,
"durable_pending_before_sample": 500,
"delivery_ms": 37.54468600004657,
"sender_sm_handled_ms": 37.61967200011895
},
"status": "pass",
"final_personal_mam_records_per_user": [
100517,
100517
],
"roster_contacts_per_user": 10000,
"seeded_personal_mam_records_per_user": 100000,
"archive_policy": "roster"
}
]
}
@@ -0,0 +1,25 @@
{
"status": "pass",
"recipient_state": "hibernating",
"durable_pending_at_gateway_stream": {
"push-first": true,
"push-second": true
},
"observation_seconds": 2,
"elapsed_seconds": 2.0038609230000475,
"gateway_post_tls_stream_observed": true,
"gateway_post_tls_stream_at_bound": true,
"blocked_stage": "post-tls-features",
"durable_pending": {
"push-first": true,
"push-second": true
},
"sender_sm_ack_h": [
4
],
"sender_inputs": 4,
"sender_handled_before": 2,
"binary_version": "rust-xmpp 0.8.2 \u2014 experimental XMPP runtime",
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
"scope": "loopback TLS fixture; stalled external XMPP push service; no APNs/FCM"
}
@@ -0,0 +1,17 @@
{
"status": "reproduced-blocking",
"observation_seconds": 2,
"elapsed_seconds": 2.005253799000002,
"gateway_post_tls_stream_observed": true,
"gateway_post_tls_stream_at_bound": true,
"blocked_stage": "post-tls-features",
"durable_pending": {
"push-first": true,
"push-second": false
},
"sender_sm_ack_h": [],
"sender_inputs": 4,
"sender_handled_before": 2,
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
"scope": "loopback TLS fixture; stalled external XMPP push service; no APNs/FCM"
}
@@ -0,0 +1,25 @@
{
"status": "pass",
"recipient_state": "offline",
"durable_pending_at_gateway_stream": {
"push-first": true,
"push-second": true
},
"observation_seconds": 2,
"elapsed_seconds": 2.00361997899995,
"gateway_post_tls_stream_observed": true,
"gateway_post_tls_stream_at_bound": true,
"blocked_stage": "post-tls-features",
"durable_pending": {
"push-first": true,
"push-second": true
},
"sender_sm_ack_h": [
4
],
"sender_inputs": 4,
"sender_handled_before": 2,
"binary_version": "rust-xmpp 0.8.2 \u2014 experimental XMPP runtime",
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
"scope": "loopback TLS fixture; stalled external XMPP push service; no APNs/FCM"
}
@@ -0,0 +1,23 @@
{
"status": "reproduced-blocking",
"recipient_state": "offline",
"durable_pending_at_gateway_stream": {
"push-first": false,
"push-second": false
},
"observation_seconds": 2,
"elapsed_seconds": 2.007238984000196,
"gateway_post_tls_stream_observed": true,
"gateway_post_tls_stream_at_bound": true,
"blocked_stage": "post-tls-features",
"durable_pending": {
"push-first": false,
"push-second": false
},
"sender_sm_ack_h": [],
"sender_inputs": 4,
"sender_handled_before": 2,
"binary_version": "rust-xmpp 0.8.0 \u2014 experimental XMPP runtime",
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"scope": "loopback TLS fixture; stalled external XMPP push service; no APNs/FCM"
}
File diff suppressed because it is too large. Load diff
+2
View File
@@ -0,0 +1,2 @@
Checking rust-xmpp v0.8.3 (/workspace/scratch/362577cdf2e0/recovered/rust-xmpp)
Finished `dev` profile [unoptimized] target(s) in 14.04s
@@ -0,0 +1,35 @@
{
"status": "pass",
"delay_ms_per_sync": 100,
"acknowledged_messages": 3,
"mam_records_per_account": [
3,
3
],
"resumed_resources": 4,
"replayed_personal_deliveries": 9,
"offline_checkpoint": {
"exported_sessions": 4,
"native_archives": 2,
"journals_removed": true,
"second_invocation_idempotent": true
},
"pending_before_reauthentication": [
2,
3,
4,
3
],
"sync_between_publication_and_sigkill": {
"total": 32,
"by_store_kind_op": {
"mam:directory:fsync": 2,
"mam:file:fsync": 10,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 16
}
},
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback STARTTLS; real delayed fsync/fdatasync; sender SM acknowledgement then SIGKILL; authoritative queues and MAM checked before reauthentication; at-least-once replay"
}
@@ -0,0 +1,9 @@
Compiling rust-xmpp v0.8.3 (/workspace/scratch/362577cdf2e0/recovered/rust-xmpp)
Finished `test` profile [unoptimized] target(s) in 3.26s
Running tests/archive_mam.rs (target/debug/deps/archive_mam-1243a5e0f6034fe2)
running 1 test
test archive_larger_than_128mib_streams ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 13 filtered out; finished in 2.76s
@@ -0,0 +1,62 @@
{
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-ssixqqk9/rust-xmpp",
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"before": {
"rss_kib": 9516,
"cpu_seconds": 0.01,
"fd_count": 12
},
"connect_seconds": 12.048,
"connect_latency": {
"samples": 500,
"median_ms": 398.044,
"p95_ms": 1086.236,
"max_ms": 1403.7
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 128.38,
"join_seconds": 94.493,
"joined": {
"rss_kib": 41828,
"cpu_seconds": 55.61,
"fd_count": 513
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 9.585,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 41740,
"cpu_seconds": 62.6,
"fd_count": 512
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 7.557,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 20896,
"cpu_seconds": 7.09,
"fd_count": 22
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 128.38,
"peak_rss_kib": 41852
}
@@ -0,0 +1,62 @@
{
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-wgcesopv/rust-xmpp",
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
"before": {
"rss_kib": 9468,
"cpu_seconds": 0.0,
"fd_count": 12
},
"connect_seconds": 8.677,
"connect_latency": {
"samples": 500,
"median_ms": 337.216,
"p95_ms": 475.311,
"max_ms": 632.696
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 151.334,
"join_seconds": 104.226,
"joined": {
"rss_kib": 41468,
"cpu_seconds": 39.74,
"fd_count": 513
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 17.857,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 41464,
"cpu_seconds": 48.58,
"fd_count": 512
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 10.083,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 20660,
"cpu_seconds": 9.73,
"fd_count": 22
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 151.334,
"peak_rss_kib": 41476
}
@@ -0,0 +1,6 @@
{
"status": "fail",
"cases": [],
"binary_sha256": "a5f5b3a552be44fb8ab8b6525dc228bc2dc97f81aecfbc559e12cc29b3e8c00b",
"failure": ".rredo recovery skipped journal sync and falsely reported successful handoff"
}
@@ -0,0 +1,6 @@
{
"status": "fail",
"cases": [],
"binary_sha256": "a5f5b3a552be44fb8ab8b6525dc228bc2dc97f81aecfbc559e12cc29b3e8c00b",
"failure": ".wal recovery skipped journal sync and falsely reported successful handoff"
}
@@ -0,0 +1,95 @@
{
"binary_source": "/workspace/scratch/362577cdf2e0/recovered/rust-xmpp/target/release/rust-xmpp",
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "real loopback TCP/STARTTLS/SASL2 Bind2; isolated personal MAM fixture; no production latency claim",
"timing": "raw ordered samples; auth timer starts before authenticate and ends at parsed success; full timer includes TCP/STARTTLS/features; metadata IQ timer runs from send to parsed result",
"result": {
"status": "pass",
"seeded_personal_archive_records": 10000,
"archive_bytes": 2138890,
"archive_expires_after": "never",
"index_present_before_start": false,
"reconnect_samples": 2,
"expected_metadata": {
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-9999",
"timestamp": "2026-01-01T02:46:39Z"
}
},
"observed_metadata": [
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-9999",
"timestamp": "2026-01-01T02:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-9999",
"timestamp": "2026-01-01T02:46:39Z"
}
}
],
"mam_metadata_iq": {
"samples_ms": [
1.066574000105902,
0.6786549997741531
],
"median_ms": 0.8726144999400276,
"p95_ms": 1.066574000105902,
"max_ms": 1.066574000105902
},
"observed_iq_metadata": [
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-9999",
"timestamp": "2026-01-01T02:46:39Z"
}
},
{
"start": {
"id": "seed-0",
"timestamp": "2026-01-01T00:00:00Z"
},
"end": {
"id": "seed-9999",
"timestamp": "2026-01-01T02:46:39Z"
}
}
],
"sasl2_bind2_success": {
"samples_ms": [
5.694160000075499,
4.350195999904827
],
"median_ms": 5.022177999990163,
"p95_ms": 5.694160000075499,
"max_ms": 5.694160000075499
},
"tcp_starttls_to_bind2_success": {
"samples_ms": [
51.93691700014824,
9.144003000074008
],
"median_ms": 30.540460000111125,
"p95_ms": 51.93691700014824,
"max_ms": 51.93691700014824
}
}
}
@@ -0,0 +1,418 @@
{
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback STARTTLS; fixture only; forced CSI dwell explicitly separated",
"cases": [
{
"active": {
"delivery": {
"samples": 1,
"median_ms": 42.84763399982694,
"p95_ms": 42.84763399982694,
"max_ms": 42.84763399982694,
"samples_ms": [
42.84763399982694
]
},
"sender_sm_handled": {
"samples": 1,
"median_ms": 41.45799199977773,
"p95_ms": 41.45799199977773,
"max_ms": 41.45799199977773,
"samples_ms": [
41.45799199977773
]
},
"client_receipt_end_to_end": {
"samples": 1,
"median_ms": 51.320845000191184,
"p95_ms": 51.320845000191184,
"max_ms": 51.320845000191184,
"samples_ms": [
51.320845000191184
]
},
"receipt_return_leg": {
"samples": 1,
"median_ms": 8.362257000044337,
"p95_ms": 8.362257000044337,
"max_ms": 8.362257000044337,
"samples_ms": [
8.362257000044337
]
}
},
"inactive": {
"delivery": {
"samples": 1,
"median_ms": 7.461921999947663,
"p95_ms": 7.461921999947663,
"max_ms": 7.461921999947663,
"samples_ms": [
7.461921999947663
]
},
"sender_sm_handled": {
"samples": 1,
"median_ms": 6.771670000034646,
"p95_ms": 6.771670000034646,
"max_ms": 6.771670000034646,
"samples_ms": [
6.771670000034646
]
},
"client_receipt_end_to_end": {
"samples": 1,
"median_ms": 12.700967000000674,
"p95_ms": 12.700967000000674,
"max_ms": 12.700967000000674,
"samples_ms": [
12.700967000000674
]
},
"receipt_return_leg": {
"samples": 1,
"median_ms": 5.201973000112048,
"p95_ms": 5.201973000112048,
"max_ms": 5.201973000112048,
"samples_ms": [
5.201973000112048
]
}
},
"receipt_only-active": {
"delivery_ms": 23.464398000214715,
"sender_sm_handled_ms": 23.388774000068224,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"receipt_only-inactive": {
"delivery_ms": 11.619346000316,
"sender_sm_handled_ms": 11.170642000251974,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_legacy_only-active": {
"delivery_ms": 3.3123490002253675,
"sender_sm_handled_ms": 3.2682549999663024,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_legacy_only-inactive": {
"delivery_ms": 5.355494000014005,
"sender_sm_handled_ms": 4.585807000239583,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_2_only-active": {
"delivery_ms": 6.6929370000252675,
"sender_sm_handled_ms": 5.942287999914697,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_2_only-inactive": {
"delivery_ms": 7.484531000045536,
"sender_sm_handled_ms": 7.103434999862657,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_with_eme-active": {
"delivery_ms": 8.187372000065807,
"sender_sm_handled_ms": 7.642809000117268,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_with_eme-inactive": {
"delivery_ms": 8.972958999947878,
"sender_sm_handled_ms": 8.508876000178134,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_received-active": {
"delivery_ms": 7.674009000311344,
"sender_sm_handled_ms": 7.3800410000330885,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_received-inactive": {
"delivery_ms": 5.613406000065879,
"sender_sm_handled_ms": 5.646823999995831,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_displayed-active": {
"delivery_ms": 5.180439000014303,
"sender_sm_handled_ms": 4.853748000186897,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_displayed-inactive": {
"delivery_ms": 8.705914999609377,
"sender_sm_handled_ms": 8.28981999984535,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_acknowledged-active": {
"delivery_ms": 6.910242000230937,
"sender_sm_handled_ms": 6.799871000112034,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_acknowledged-inactive": {
"delivery_ms": 8.707511000011436,
"sender_sm_handled_ms": 8.398837999720854,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"low_priority_headline-active": {
"delivery_ms": 4.267847999926744,
"sender_sm_handled_ms": 4.236566000145103,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"low_priority_headline-inactive": {
"delivery_ms": 108.03854200003116,
"sender_sm_handled_ms": 2.853955999853497,
"durably_csi_buffered": true,
"forced_dwell_seconds": 0.1,
"after_activation_ms": 3.106009000021004
},
"sm_backlog": {
"seeded_messages": 30,
"durable_pending_before_sample": 30,
"delivery_ms": 8.070619000136503,
"sender_sm_handled_ms": 7.959626000229036
},
"status": "pass",
"final_personal_mam_records_per_user": [
1045,
1045
],
"csi_policy": "default",
"baseline_expect_held": false,
"roster_contacts_per_user": 0,
"seeded_personal_mam_records_per_user": 1000,
"archive_policy": "always"
},
{
"active": {
"delivery": {
"samples": 1,
"median_ms": 9.351153999887174,
"p95_ms": 9.351153999887174,
"max_ms": 9.351153999887174,
"samples_ms": [
9.351153999887174
]
},
"sender_sm_handled": {
"samples": 1,
"median_ms": 9.169040999950084,
"p95_ms": 9.169040999950084,
"max_ms": 9.169040999950084,
"samples_ms": [
9.169040999950084
]
},
"client_receipt_end_to_end": {
"samples": 1,
"median_ms": 13.487032000284671,
"p95_ms": 13.487032000284671,
"max_ms": 13.487032000284671,
"samples_ms": [
13.487032000284671
]
},
"receipt_return_leg": {
"samples": 1,
"median_ms": 4.101098000319325,
"p95_ms": 4.101098000319325,
"max_ms": 4.101098000319325,
"samples_ms": [
4.101098000319325
]
}
},
"inactive": {
"delivery": {
"samples": 1,
"median_ms": 5.198949999794422,
"p95_ms": 5.198949999794422,
"max_ms": 5.198949999794422,
"samples_ms": [
5.198949999794422
]
},
"sender_sm_handled": {
"samples": 1,
"median_ms": 5.164645999684581,
"p95_ms": 5.164645999684581,
"max_ms": 5.164645999684581,
"samples_ms": [
5.164645999684581
]
},
"client_receipt_end_to_end": {
"samples": 1,
"median_ms": 13.508577999800764,
"p95_ms": 13.508577999800764,
"max_ms": 13.508577999800764,
"samples_ms": [
13.508577999800764
]
},
"receipt_return_leg": {
"samples": 1,
"median_ms": 8.226426999954128,
"p95_ms": 8.226426999954128,
"max_ms": 8.226426999954128,
"samples_ms": [
8.226426999954128
]
}
},
"receipt_only-active": {
"delivery_ms": 8.37234799973885,
"sender_sm_handled_ms": 7.964516999891202,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"receipt_only-inactive": {
"delivery_ms": 109.20343799989496,
"sender_sm_handled_ms": 5.452117000004364,
"durably_csi_buffered": true,
"forced_dwell_seconds": 0.1,
"after_activation_ms": 2.879265000046871
},
"omemo_legacy_only-active": {
"delivery_ms": 4.961447999903612,
"sender_sm_handled_ms": 4.207620999750361,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_legacy_only-inactive": {
"delivery_ms": 107.77542400001039,
"sender_sm_handled_ms": 4.299592999814195,
"durably_csi_buffered": true,
"forced_dwell_seconds": 0.1,
"after_activation_ms": 1.5704200000072888
},
"omemo_2_only-active": {
"delivery_ms": 3.2654749998073385,
"sender_sm_handled_ms": 3.3216599999832397,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_2_only-inactive": {
"delivery_ms": 109.47085399993739,
"sender_sm_handled_ms": 5.282463000185089,
"durably_csi_buffered": true,
"forced_dwell_seconds": 0.1,
"after_activation_ms": 2.073672999813425
},
"omemo_with_eme-active": {
"delivery_ms": 6.262568000238389,
"sender_sm_handled_ms": 6.371549000050436,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"omemo_with_eme-inactive": {
"delivery_ms": 5.960880999737128,
"sender_sm_handled_ms": 5.667837000146392,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_received-active": {
"delivery_ms": 7.435425000039686,
"sender_sm_handled_ms": 7.0634520002386125,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_received-inactive": {
"delivery_ms": 110.35115000004225,
"sender_sm_handled_ms": 5.86408199978905,
"durably_csi_buffered": true,
"forced_dwell_seconds": 0.1,
"after_activation_ms": 2.434945000004518
},
"marker_displayed-active": {
"delivery_ms": 3.6287610000727,
"sender_sm_handled_ms": 3.696032999869203,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_displayed-inactive": {
"delivery_ms": 112.26732199975231,
"sender_sm_handled_ms": 8.923948999836284,
"durably_csi_buffered": true,
"forced_dwell_seconds": 0.1,
"after_activation_ms": 1.43316000003324
},
"marker_acknowledged-active": {
"delivery_ms": 4.8418340002172044,
"sender_sm_handled_ms": 4.916578000120353,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"marker_acknowledged-inactive": {
"delivery_ms": 110.98447399990619,
"sender_sm_handled_ms": 5.764318000274216,
"durably_csi_buffered": true,
"forced_dwell_seconds": 0.1,
"after_activation_ms": 3.3109219998550543
},
"low_priority_headline-active": {
"delivery_ms": 4.907228000320174,
"sender_sm_handled_ms": 4.593587000272237,
"durably_csi_buffered": false,
"forced_dwell_seconds": 0,
"after_activation_ms": null
},
"low_priority_headline-inactive": {
"delivery_ms": 106.62631800005329,
"sender_sm_handled_ms": 2.990170999964903,
"durably_csi_buffered": true,
"forced_dwell_seconds": 0.1,
"after_activation_ms": 1.6857880000316072
},
"sm_backlog": {
"seeded_messages": 30,
"durable_pending_before_sample": 30,
"delivery_ms": 8.221272999890061,
"sender_sm_handled_ms": 8.186153000224294
},
"status": "pass",
"final_personal_mam_records_per_user": [
1045,
1045
],
"csi_policy": "native",
"baseline_expect_held": false,
"roster_contacts_per_user": 0,
"seeded_personal_mam_records_per_user": 1000,
"archive_policy": "always"
}
]
}
@@ -0,0 +1,25 @@
{
"status": "pass",
"recipient_state": "hibernating",
"durable_pending_at_gateway_stream": {
"push-first": true,
"push-second": true
},
"observation_seconds": 2,
"elapsed_seconds": 2.013597706999917,
"gateway_post_tls_stream_observed": true,
"gateway_post_tls_stream_at_bound": true,
"blocked_stage": "post-tls-features",
"durable_pending": {
"push-first": true,
"push-second": true
},
"sender_sm_ack_h": [
4
],
"sender_inputs": 4,
"sender_handled_before": 2,
"binary_version": "rust-xmpp 0.8.3 \u2014 experimental XMPP runtime",
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback TLS fixture; stalled external XMPP push service; no APNs/FCM"
}
@@ -0,0 +1,25 @@
{
"status": "pass",
"recipient_state": "offline",
"durable_pending_at_gateway_stream": {
"push-first": true,
"push-second": true
},
"observation_seconds": 2,
"elapsed_seconds": 2.1132061280000016,
"gateway_post_tls_stream_observed": true,
"gateway_post_tls_stream_at_bound": true,
"blocked_stage": "post-tls-features",
"durable_pending": {
"push-first": true,
"push-second": true
},
"sender_sm_ack_h": [
4
],
"sender_inputs": 4,
"sender_handled_before": 2,
"binary_version": "rust-xmpp 0.8.3 \u2014 experimental XMPP runtime",
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback TLS fixture; stalled external XMPP push service; no APNs/FCM"
}
@@ -0,0 +1,24 @@
{
"status": "pass",
"cases": [
{
"suffix": ".wal",
"status": "pass",
"checkpoint_rejected_sync_failure": true,
"journals_preserved_on_failure": 4,
"serve_rejected_before_listeners": true,
"clean_retry_preserved_sessions": 4,
"clean_retry_preserved_archives": 2
},
{
"suffix": ".rredo",
"status": "pass",
"checkpoint_rejected_sync_failure": true,
"journals_preserved_on_failure": 2,
"serve_rejected_before_listeners": false,
"clean_retry_preserved_sessions": 4,
"clean_retry_preserved_archives": 2
}
],
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a"
}
@@ -0,0 +1,2 @@
Compiling rust-xmpp v0.8.3 (/workspace/scratch/362577cdf2e0/recovered/rust-xmpp)
Finished `release` profile [optimized] target(s) in 1m 43s
@@ -0,0 +1,99 @@
{
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar metadata PEP; fixture-only fsync/fdatasync including data directories; real sync retained; operation windows include background SM heartbeat",
"cases": [
{
"samples": [
{
"phase": "settled",
"round": 0,
"delivery_ms": {
"direct": 2042.56345,
"sent": 2239.283932,
"received": 2442.564925
},
"sender_sm_handled_ms": 2441.985416,
"publication_plus_delivery_ms": 2442.641984,
"sync": {
"total": 12,
"by_store_kind_op": {
"mam:directory:fsync": 2,
"mam:file:fsync": 6,
"sm:file:fdatasync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "avatar_pending",
"round": 0,
"delivery_ms": {
"direct": 2676.283903,
"sent": 2476.200907,
"received": 3095.924978
},
"sender_sm_handled_ms": 3094.949323,
"publication_plus_delivery_ms": 3096.115223,
"sync": {
"total": 15,
"by_store_kind_op": {
"mam:file:fsync": 2,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 9
}
},
"pending_per_resource": [
3,
2,
3,
2
]
}
],
"mam_records_per_account": [
102,
102
],
"status": "pass",
"delay_ms_per_sync": 200,
"server_message_processing_slow_events": [
{
"duration_ms": 2440,
"route": "local",
"pre_routing_ms": 0,
"sender_mam_ms": 813,
"recipient_mam_ms": 816,
"delivery_ms": 202,
"carbons_ms": 404,
"offline_push_ms": 0,
"handled_ms": 202,
"target_count": 1,
"carbon_count": 2
},
{
"duration_ms": 1259,
"route": "local",
"pre_routing_ms": 0,
"sender_mam_ms": 204,
"recipient_mam_ms": 208,
"delivery_ms": 223,
"carbons_ms": 410,
"offline_push_ms": 0,
"handled_ms": 211,
"target_count": 1,
"carbon_count": 2
}
],
"server_message_processing_slow_ms": [
2440,
1259
]
}
]
}
@@ -0,0 +1,74 @@
{
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar PEP; fixture-only fsync/fdatasync including data directories; real sync retained",
"cases": [
{
"samples": [
{
"phase": "settled",
"round": 0,
"delivery_ms": {
"direct": 2836.63993,
"sent": 3238.840305,
"received": 3643.707041
},
"sender_sm_handled_ms": 3643.297905,
"publication_plus_delivery_ms": 3643.772507,
"sync": {
"total": 24,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "avatar_pending",
"round": 0,
"delivery_ms": {
"direct": 5782.018326,
"sent": 5384.85421,
"received": 6596.874338
},
"sender_sm_handled_ms": 6595.924675,
"publication_plus_delivery_ms": 6597.492339,
"sync": {
"total": 30,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 9,
"sm:file:fsync": 9
}
},
"pending_per_resource": [
3,
2,
3,
2
]
}
],
"mam_records_per_account": [
102,
102
],
"status": "pass",
"delay_ms_per_sync": 200,
"server_message_processing_slow_ms": [
3245,
3235
]
}
]
}
@@ -0,0 +1,255 @@
{
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar metadata PEP; fixture-only fsync/fdatasync including data directories; real sync retained; operation windows include background SM heartbeat",
"cases": [
{
"samples": [
{
"phase": "settled",
"round": 0,
"delivery_ms": {
"direct": 1083.729337,
"sent": 1183.535171,
"received": 1301.736052
},
"sender_sm_handled_ms": 1300.213796,
"publication_plus_delivery_ms": 1309.067665,
"sync": {
"total": 12,
"by_store_kind_op": {
"mam:directory:fsync": 2,
"mam:file:fsync": 6,
"sm:file:fdatasync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 1,
"delivery_ms": {
"direct": 416.413847,
"sent": 521.445561,
"received": 622.692061
},
"sender_sm_handled_ms": 622.557259,
"publication_plus_delivery_ms": 627.341892,
"sync": {
"total": 6,
"by_store_kind_op": {
"mam:file:fsync": 2,
"sm:file:fdatasync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 2,
"delivery_ms": {
"direct": 419.683064,
"sent": 551.764241,
"received": 644.751777
},
"sender_sm_handled_ms": 644.082891,
"publication_plus_delivery_ms": 644.882621,
"sync": {
"total": 6,
"by_store_kind_op": {
"mam:file:fsync": 2,
"sm:file:fdatasync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "avatar_pending",
"round": 0,
"delivery_ms": {
"direct": 1376.989787,
"sent": 1273.220821,
"received": 1478.990457
},
"sender_sm_handled_ms": 1477.870242,
"publication_plus_delivery_ms": 1479.132408,
"sync": {
"total": 14,
"by_store_kind_op": {
"mam:file:fsync": 2,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 1,
"delivery_ms": {
"direct": 1523.59144,
"sent": 1288.285577,
"received": 1626.311997
},
"sender_sm_handled_ms": 1626.005979,
"publication_plus_delivery_ms": 1626.574287,
"sync": {
"total": 14,
"by_store_kind_op": {
"mam:file:fsync": 2,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 2,
"delivery_ms": {
"direct": 1378.231158,
"sent": 1276.210346,
"received": 1582.305858
},
"sender_sm_handled_ms": 1581.975429,
"publication_plus_delivery_ms": 1582.463448,
"sync": {
"total": 15,
"by_store_kind_op": {
"mam:file:fsync": 2,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 9
}
},
"pending_per_resource": [
3,
2,
3,
2
]
}
],
"mam_records_per_account": [
106,
106
],
"status": "pass",
"delay_ms_per_sync": 100,
"server_message_processing_slow_events": [
{
"duration_ms": 1298,
"route": "local",
"pre_routing_ms": 0,
"sender_mam_ms": 421,
"recipient_mam_ms": 416,
"delivery_ms": 109,
"carbons_ms": 232,
"offline_push_ms": 0,
"handled_ms": 117,
"target_count": 1,
"carbon_count": 2
},
{
"duration_ms": 621,
"route": "local",
"pre_routing_ms": 0,
"sender_mam_ms": 101,
"recipient_mam_ms": 104,
"delivery_ms": 104,
"carbons_ms": 206,
"offline_push_ms": 0,
"handled_ms": 103,
"target_count": 1,
"carbon_count": 2
},
{
"duration_ms": 638,
"route": "local",
"pre_routing_ms": 0,
"sender_mam_ms": 101,
"recipient_mam_ms": 102,
"delivery_ms": 104,
"carbons_ms": 219,
"offline_push_ms": 1,
"handled_ms": 108,
"target_count": 1,
"carbon_count": 2
},
{
"duration_ms": 625,
"route": "local",
"pre_routing_ms": 1,
"sender_mam_ms": 104,
"recipient_mam_ms": 102,
"delivery_ms": 106,
"carbons_ms": 208,
"offline_push_ms": 0,
"handled_ms": 101,
"target_count": 1,
"carbon_count": 2
},
{
"duration_ms": 756,
"route": "local",
"pre_routing_ms": 1,
"sender_mam_ms": 105,
"recipient_mam_ms": 103,
"delivery_ms": 102,
"carbons_ms": 340,
"offline_push_ms": 0,
"handled_ms": 102,
"target_count": 1,
"carbon_count": 2
},
{
"duration_ms": 617,
"route": "local",
"pre_routing_ms": 1,
"sender_mam_ms": 102,
"recipient_mam_ms": 104,
"delivery_ms": 101,
"carbons_ms": 204,
"offline_push_ms": 0,
"handled_ms": 101,
"target_count": 1,
"carbon_count": 2
}
],
"server_message_processing_slow_ms": [
1298,
621,
638,
625,
756,
617
]
}
]
}
@@ -0,0 +1,350 @@
{
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar PEP; fixture-only fsync/fdatasync including data directories; real sync retained",
"cases": [
{
"samples": [
{
"phase": "settled",
"round": 0,
"delivery_ms": {
"direct": 13.745165,
"sent": 16.132819,
"received": 18.746494
},
"sender_sm_handled_ms": 18.699594,
"publication_plus_delivery_ms": 18.823329,
"sync": {
"total": 16,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 4,
"sm:file:fsync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 1,
"delivery_ms": {
"direct": 15.636949,
"sent": 18.138746,
"received": 20.807559
},
"sender_sm_handled_ms": 20.705943,
"publication_plus_delivery_ms": 20.843968,
"sync": {
"total": 16,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 4,
"sm:file:fsync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 2,
"delivery_ms": {
"direct": 21.358818,
"sent": 24.788203,
"received": 26.125405
},
"sender_sm_handled_ms": 26.090729,
"publication_plus_delivery_ms": 26.187115,
"sync": {
"total": 16,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 4,
"sm:file:fsync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "avatar_pending",
"round": 0,
"delivery_ms": {
"direct": 32.67965,
"sent": 29.361139,
"received": 35.870217
},
"sender_sm_handled_ms": 35.819252,
"publication_plus_delivery_ms": 35.991777,
"sync": {
"total": 28,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 1,
"delivery_ms": {
"direct": 30.47626,
"sent": 28.128456,
"received": 32.654735
},
"sender_sm_handled_ms": 32.540092,
"publication_plus_delivery_ms": 32.738534,
"sync": {
"total": 28,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 2,
"delivery_ms": {
"direct": 30.492529,
"sent": 28.027238,
"received": 33.697165
},
"sender_sm_handled_ms": 33.576139,
"publication_plus_delivery_ms": 33.786933,
"sync": {
"total": 28,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
}
],
"mam_records_per_account": [
106,
106
],
"status": "pass",
"delay_ms_per_sync": 0
},
{
"samples": [
{
"phase": "settled",
"round": 0,
"delivery_ms": {
"direct": 1226.407308,
"sent": 1431.404759,
"received": 1837.301696
},
"sender_sm_handled_ms": 1633.73631,
"publication_plus_delivery_ms": 1837.391548,
"sync": {
"total": 18,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 5,
"sm:file:fsync": 5
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 1,
"delivery_ms": {
"direct": 1234.414585,
"sent": 1436.607311,
"received": 1642.84668
},
"sender_sm_handled_ms": 1641.598264,
"publication_plus_delivery_ms": 1642.913856,
"sync": {
"total": 16,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 4,
"sm:file:fsync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 2,
"delivery_ms": {
"direct": 1232.468024,
"sent": 1441.824206,
"received": 1872.009731
},
"sender_sm_handled_ms": 1652.92941,
"publication_plus_delivery_ms": 1872.108448,
"sync": {
"total": 18,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 5,
"sm:file:fsync": 5
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "avatar_pending",
"round": 0,
"delivery_ms": {
"direct": 3209.381557,
"sent": 3419.876568,
"received": 3614.69826
},
"sender_sm_handled_ms": 3613.565949,
"publication_plus_delivery_ms": 3615.484237,
"sync": {
"total": 28,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 1,
"delivery_ms": {
"direct": 2665.684412,
"sent": 2461.378505,
"received": 2870.784696
},
"sender_sm_handled_ms": 2870.455915,
"publication_plus_delivery_ms": 2871.000834,
"sync": {
"total": 28,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 2,
"delivery_ms": {
"direct": 2683.292251,
"sent": 2475.225974,
"received": 3636.635597
},
"sender_sm_handled_ms": 3195.209226,
"publication_plus_delivery_ms": 3640.372412,
"sync": {
"total": 30,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 9,
"sm:file:fsync": 9
}
},
"pending_per_resource": [
3,
2,
3,
2
]
}
],
"mam_records_per_account": [
106,
106
],
"status": "pass",
"delay_ms_per_sync": 100
}
]
}
+111
View File
@@ -0,0 +1,111 @@
Compiling rust-xmpp v0.8.3 (/workspace/scratch/362577cdf2e0/recovered/rust-xmpp)
Finished `test` profile [unoptimized] target(s) in 29.67s
Running unittests src/lib.rs (target/debug/deps/rust_xmpp-792bf902c86621ec)
running 50 tests
test csi::checkpoint_tests::failed_queue_checkpoints_restore_append_and_drain_without_copying_queue ... ok
test federation::tests::external_must_be_in_the_advertised_mechanisms ... ok
test federation::tests::starttls_must_be_an_immediate_stream_feature ... ok
test csi::checkpoint_tests::only_queue_changes_require_a_checkpoint ... ok
test federation::tests::federation_domains_are_dns_names_and_case_insensitive ... ok
test federation::tests::stream_restarts_compare_canonical_domains ... ok
test http_config::tests::literal_pchars_and_percent_encoding_are_supported ... ok
test http_config::tests::invalid_paths_types_urls_and_conflicts_are_errors ... ok
test http_config::tests::external_prefix_and_standard_port ... ok
test http_config::tests::routes_hosts_and_default_without_overriding_known_hosts ... ok
test http_proxy::tests::config_validation_and_network_boundaries ... ok
test http_proxy::tests::empty_native_lua_table_disables_proxy_trust ... ok
test http_config::tests::mounts_inherit_per_app_and_expand_virtual_host ... ok
test http_proxy::tests::forwarded_tls_termination_cannot_upgrade_a_plaintext_chain_hop ... ok
test http_proxy::tests::malformed_forwarded_and_unknown_identifiers_are_ignored ... ok
test http_proxy::tests::untrusted_socket_ignores_spoofed_headers ... ok
test http_proxy::tests::legacy_chain_uses_rightmost_untrusted_and_tls_is_preserved ... ok
test http_proxy::tests::forwarded_mode_stops_at_first_untrusted_hop ... ok
test http_transport::tests::bosh_processing_capacity_is_bounded ... ok
test http_transport::tests::configured_mount_host_origin_and_secure_dispatch ... ok
test http_transport::tests::independent_framing ... ok
test http_transport::tests::websocket_preserves_inherited_namespaces_and_spaced_declarations ... ok
test logging::tests::console_aliases_share_one_sink_with_combined_levels ... ok
test sasl::tests::reject_malformed_and_downgrade ... ok
test archive::redo_rewrite_tests::crash_gap_after_redo_retirement_before_native_rename_keeps_old_snapshot ... ok
test archive::redo_rewrite_tests::crash_gap_after_native_rename_has_no_old_redo_to_resurrect_deletions ... ok
test sasl::tests::rfc5802_sha1_vector ... ok
test server::authorization_tests::http_interfaces_follow_secure_override_wildcards_and_global_fallback ... ok
test server::authorization_tests::pep_diagnostics_count_only_first_item_devices_without_logging_ids ... ok
test server::authorization_tests::pep_diagnostics_exclude_payloads_and_identifiers ... ok
test http_service::tests::upload_claim_refreshes_completion_state_after_an_earlier_snapshot ... ok
test sessions::push_timeout_tests::first_important_push_shortens_extended_hibernation_once ... ok
test sessions::tests::acknowledgement_wraps ... ok
test sessions::tests::graceful_disconnect_is_not_resumable ... ok
test sessions::tests::resume_replays_only_unacked_and_checks_owner ... ok
test sessions::tests::traffic_before_sm_enable_is_not_counted ... ok
test upload_migration::tests::local_spool_checks_size_and_hash_and_preserves_source ... ok
test sasl::tests::sha256_independent_python_vector ... ok
test wire::tests::arbitrary_prefix_and_inherited_namespaces ... ok
test wire::tests::cdata_and_comments_frame_without_counting_brackets ... ok
test wire::tests::declarations_rejected ... ok
test wire::tests::fragmented_and_pipelined ... ok
test wire::tests::invalid_namespace_and_declarations_rejected ... ok
test upload_migration::tests::pending_and_corrupt_journals_block_startup ... ok
test http_service::tests::upload_internal_mount_is_independent_of_advertised_proxy_prefix ... ok
test http_service::tests::abandoned_slots_release_quota_without_pruning_existing_or_active_files ... ok
test sasl::tests::plus_channel_binding_and_ssdp_are_authenticated ... ok
test server::authorization_tests::upload_requires_registered_local_account_or_explicit_remote_access ... ok
test sasl::tests::wrong_proof_nonce_binding_and_duplicates ... ok
test admin::tests::real_admin_xml_protocol_over_duplex ... ok
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.18s
Running tests/archive_edges.rs (target/debug/deps/archive_edges-da556b3567110d0b)
running 6 tests
test missing_empty_whitespace_and_single_record ... ok
test endpoint_read_recovers_partial_append_before_seeking ... ok
test multiline_endpoints_match_native_scanner_with_blank_lines ... ok
test oversized_prefix_and_malformed_endpoints_fail ... ok
test metadata_skips_large_malformed_middle_without_creating_cache ... ok
test tail_padding_respects_record_bound ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.26s
Running tests/archive_redo.rs (target/debug/deps/archive_redo-d041d27370ba29bd)
running 13 tests
test atomic_native_replace_without_handoff_cannot_discard_committed_frames ... ok
test explicit_native_atomic_handoff_and_missing_baseline_fail_closed ... ok
running 1 test
test durable_handoff_allows_native_in_place_append_and_next_rust_append ... ok
test legacy_rollback_marker_is_migrated_before_redo_append ... ok
test committed_redo_recovers_missing_and_partial_native_without_duplicates ... ok
test redo_process_child ... ok
test killed_writer_retains_accepted_records_after_native_data_loss ... ok
test native_baseline_in_place_edits_are_rejected ... ok
test complete_checksum_corruption_fails_without_modifying_native_or_journal ... ok
test copied_archive_replays_committed_redo_despite_new_native_inode ... ok
test incomplete_final_frame_and_uncommitted_native_suffix_are_discarded ... ok
test byte_limit_rotates_before_frame_count_limit ... ok
test bounded_rotation_recovers_suffix_and_rewrites_never_resurrect_deleted_records ... ok
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.91s
Running tests/sm_journal.rs (target/debug/deps/sm_journal-79fad6209c2b84bb)
running 14 tests
test offline_checkpoint_refuses_live_store_and_preserves_redo ... ok
test complete_corrupt_frame_fails_closed_even_with_valid_export ... ok
test journal_symlink_is_rejected_without_following_target ... ok
test export_failure_after_commit_advances_memory_and_fences_later_writes ... ok
test durable_tombstone_overrides_resurrected_stale_json ... ok
test offline_checkpoint_durably_applies_expiration_and_tombstones ... ok
test legacy_json_migrates_and_baseline_survives_partial_first_append ... ok
test offline_checkpoint_repairs_exports_removes_redo_and_allows_legacy_restart ... ok
test partial_tail_is_discarded_and_subsequent_commits_survive_restart ... ok
test redo_repairs_missing_or_torn_export_before_json_validation ... ok
test superseded_frames_still_require_valid_checksums_json_and_baseline_flags ... ok
test incomplete_baseline_or_magic_never_falls_back_to_json ... ok
test retired_journals_are_collected_in_bounded_batches ... ok
test rotation_preserves_latest_snapshot_and_bounds_redo_size ... ok
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.97s
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,169 @@
{
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar metadata PEP; fixture-only fsync/fdatasync including data directories; real sync retained; operation windows include background SM heartbeat",
"cases": [
{
"samples": [
{
"phase": "settled",
"round": 0,
"delivery_ms": {
"direct": 18.615095,
"sent": 19.940848,
"received": 26.953392
},
"sender_sm_handled_ms": 26.88493,
"publication_plus_delivery_ms": 27.065981,
"sync": {
"total": 12,
"by_store_kind_op": {
"mam:directory:fsync": 2,
"mam:file:fsync": 6,
"sm:file:fdatasync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 1,
"delivery_ms": {
"direct": 6.003256,
"sent": 7.461811,
"received": 10.135311
},
"sender_sm_handled_ms": 9.993133,
"publication_plus_delivery_ms": 10.173245,
"sync": {
"total": 6,
"by_store_kind_op": {
"mam:file:fsync": 2,
"sm:file:fdatasync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 2,
"delivery_ms": {
"direct": 7.871244,
"sent": 13.331419,
"received": 15.325955
},
"sender_sm_handled_ms": 15.079213,
"publication_plus_delivery_ms": 15.363148,
"sync": {
"total": 6,
"by_store_kind_op": {
"mam:file:fsync": 2,
"sm:file:fdatasync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "avatar_pending",
"round": 0,
"delivery_ms": {
"direct": 38.754553,
"sent": 25.865646,
"received": 40.65703
},
"sender_sm_handled_ms": 40.40549,
"publication_plus_delivery_ms": 40.769686,
"sync": {
"total": 14,
"by_store_kind_op": {
"mam:file:fsync": 2,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 1,
"delivery_ms": {
"direct": 20.942607,
"sent": 20.204899,
"received": 22.05411
},
"sender_sm_handled_ms": 21.856674,
"publication_plus_delivery_ms": 23.679041,
"sync": {
"total": 14,
"by_store_kind_op": {
"mam:file:fsync": 2,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 2,
"delivery_ms": {
"direct": 17.980156,
"sent": 18.759152,
"received": 21.369599
},
"sender_sm_handled_ms": 21.332607,
"publication_plus_delivery_ms": 21.518131,
"sync": {
"total": 14,
"by_store_kind_op": {
"mam:file:fsync": 2,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
}
],
"mam_records_per_account": [
106,
106
],
"status": "pass",
"delay_ms_per_sync": 0,
"server_message_processing_slow_events": [],
"server_message_processing_slow_ms": []
}
]
}
@@ -0,0 +1,180 @@
{
"binary_sha256": "130431025c4c7c2f72e28b77a31c83867f857ef8bb56d1f71b10e4b19d7caaf5",
"scope": "loopback STARTTLS; four SM resources/two accounts; MAM + pending avatar metadata PEP; fixture-only fsync/fdatasync including data directories; real sync retained; operation windows include background SM heartbeat",
"cases": [
{
"samples": [
{
"phase": "settled",
"round": 0,
"delivery_ms": {
"direct": 26.330895,
"sent": 29.575091,
"received": 33.552895
},
"sender_sm_handled_ms": 33.407559,
"publication_plus_delivery_ms": 33.592558,
"sync": {
"total": 16,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 4,
"sm:file:fsync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 1,
"delivery_ms": {
"direct": 25.112248,
"sent": 31.688103,
"received": 39.725842
},
"sender_sm_handled_ms": 39.683719,
"publication_plus_delivery_ms": 39.867826,
"sync": {
"total": 16,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 4,
"sm:file:fsync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "settled",
"round": 2,
"delivery_ms": {
"direct": 11.894093,
"sent": 11.977544,
"received": 13.638711
},
"sender_sm_handled_ms": 13.371964,
"publication_plus_delivery_ms": 13.720665,
"sync": {
"total": 16,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"sm:directory:fsync": 4,
"sm:file:fsync": 4
}
},
"pending_per_resource": [
1,
2,
2,
2
]
},
{
"phase": "avatar_pending",
"round": 0,
"delivery_ms": {
"direct": 42.768972,
"sent": 32.889933,
"received": 48.207819
},
"sender_sm_handled_ms": 47.576405,
"publication_plus_delivery_ms": 51.552818,
"sync": {
"total": 28,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 1,
"delivery_ms": {
"direct": 28.176101,
"sent": 26.386914,
"received": 29.822606
},
"sender_sm_handled_ms": 29.714999,
"publication_plus_delivery_ms": 30.011235,
"sync": {
"total": 28,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
},
{
"phase": "avatar_pending",
"round": 2,
"delivery_ms": {
"direct": 29.714366,
"sent": 26.01394,
"received": 32.367508
},
"sender_sm_handled_ms": 32.252144,
"publication_plus_delivery_ms": 33.076522,
"sync": {
"total": 28,
"by_store_kind_op": {
"mam:directory:fsync": 4,
"mam:file:fsync": 4,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:directory:fsync": 8,
"sm:file:fsync": 8
}
},
"pending_per_resource": [
3,
2,
3,
2
]
}
],
"mam_records_per_account": [
106,
106
],
"status": "pass",
"delay_ms_per_sync": 0,
"server_message_processing_slow_events": [],
"server_message_processing_slow_ms": []
}
]
}
+133
View File
@@ -0,0 +1,133 @@
{
"version": "0.7.9",
"date_utc": "2026-10-20",
"ordinary_tests": {
"passed": 265,
"failed": 0,
"ignored": 11
},
"fmt": "passed",
"clippy": "passed",
"release": "passed",
"native_module_tests": 8,
"native_federation_tests": 1,
"release_binary_sha256": "24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c",
"upload": {
"minio_10gib": {
"ok": true,
"minio_version": "minio-sandbox-compat version DEVELOPMENT.GOGET (commit-id=DEVELOPMENT.GOGET)\nRuntime: go1.24.3 linux/amd64\nLicense: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html\nCopyright: 2015-0000 MinIO, Inc.",
"minio_binary_sha256": "8bbb9bccfbac69869ab499c39cfd8731b2b770b86ccf37cca1c8267556ad9bf0",
"binary": "/workspace/scratch/362577cdf2e0/recovered/rust-xmpp/target/release/rust-xmpp",
"binary_version": "rust-xmpp 0.7.9 — experimental XMPP runtime",
"binary_sha256": "24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c",
"size": 10737418240,
"qualifies_over_5gib": true,
"minio_test_only_compatibility_patch": {
"sha256": "e60592a65273f28a1375eb6ecbc0966f7fbd2ba7567d2ffedc13ffd95ed224f5",
"diff": "--- a/cmd/net.go\n+++ b/cmd/net.go\n@@ -24,6 +24,7 @@\n \t\"runtime\"\n \t\"sort\"\n \t\"strings\"\n+\t\"syscall\"\n \n \t\"github.com/minio/minio-go/v7/pkg/set\"\n \t\"github.com/minio/minio/internal/config\"\n@@ -54,6 +55,10 @@\n // mustGetLocalIPs returns IPs of local interface\n func mustGetLocalIPs() (ipList []net.IP) {\n \tifs, err := net.Interfaces()\n+\t// TEST ONLY: the sandbox denies netlink enumeration; all listeners bind loopback.\n+\tif errors.Is(err, syscall.EPERM) {\n+\t\treturn []net.IP{net.ParseIP(\"127.0.0.1\")}\n+\t}\n \tlogger.FatalIf(err, \"Unable to get IP addresses of this host\")\n \n \tfor _, interf := range ifs {\n",
"limitation": "Modified upstream MinIO: EPERM interface enumeration uses loopback; all S3/auth/storage code remains upstream."
},
"max_rss_bytes": {
"server": 106057728,
"client": 24338432,
"minio": 498958336
},
"put_bytes": 10737418240,
"put_seconds": 48.45253416699961,
"sha256": "e4c296c41d3a9605f0348961e41ed835b5f42c220751ed2269a726951546c935",
"minio_etag": "958f42d583ac40ff209eb865c5eb4bd6-1280",
"multipart_parts": 1280,
"minio_allocated_bytes": 10747953152,
"free_disk_after_put_bytes": 15180328960,
"get_bytes": 10737418240,
"get_sha256": "e4c296c41d3a9605f0348961e41ed835b5f42c220751ed2269a726951546c935",
"get_seconds": 13.709708632000911,
"range_offsets": [
0,
4294967289,
5368709113,
10737418208
],
"restart_range_ok": true,
"restart_reuse_status": 409,
"crash_multipart_cleanup_ok": true,
"minio_upstream_provenance": {
"source_url": "https://github.com/minio/minio/archive/refs/tags/RELEASE.2025-04-22T22-12-26Z.tar.gz",
"upstream_release": "RELEASE.2025-04-22T22-12-26Z",
"build": "Go1.24.3 go build -p4; no dependency changes",
"sha256": {
"/tmp/minio-source.tar.gz": "1a63209927b9d1fb239b2a776b7b01d1902eff19379a108f4bc7005edcc0968c",
"/tmp/minio-genuine": "3dd5d6756ad17c599fbc1c6f6a2f5b590242c049522e848c700949dbf8e60ad9",
"/tmp/minio-sandbox-compat": "8bbb9bccfbac69869ab499c39cfd8731b2b770b86ccf37cca1c8267556ad9bf0",
"/tmp/minio-sandbox-compat.patch": "e60592a65273f28a1375eb6ecbc0966f7fbd2ba7567d2ffedc13ffd95ed224f5"
},
"compatibility_limitation": "Only net.Interfaces EPERM becomes loopback127.0.0.1; all S3/auth/multipart/storage source unchanged."
}
},
"migration_network": "passed: local -> S3 A -> S3 B -> local, dry-run, lock, native metadata/source preservation, interrupted copy/resume, native final file with incomplete cache",
"s3_fault_network": "passed: SigV4, paginated exact-key orphan cleanup, foreign prefix safety, missing UploadId crash recovery, conditional Complete, ambiguous completion quarantine and late recovery",
"claim_race_regression": "passed: atomic fresh snapshot and active claim retains latest completion/quarantine flags"
},
"load": {
"total_load_wall_seconds": 7.094,
"requested_resources": 500,
"authenticated_bound_resources": 500,
"connection_setup_concurrency": 50,
"setup_wall_seconds": 0.671,
"setup_latency": {
"samples": 500,
"median_ms": 62.192,
"p95_ms": 83.883,
"max_ms": 114.563
},
"ping_rounds": 10,
"ping_round_trip": {
"samples": 5000,
"median_ms": 13.502,
"p95_ms": 19.31,
"max_ms": 24.054
},
"accounts": 100,
"message_rounds": 10,
"messages_sent": 5000,
"messages_received": 5000,
"message_batch_seconds": 4.134,
"metrics_before": {
"rss_kib": 8048,
"cpu_seconds": 0.0,
"fd_count": 11
},
"metrics_connected": {
"rss_kib": 26804,
"cpu_seconds": 1.02,
"fd_count": 511
},
"metrics_after_workload": {
"rss_kib": 27400,
"cpu_seconds": 8.85,
"fd_count": 511
},
"metrics_two_seconds_after_disconnect": {
"rss_kib": 27464,
"cpu_seconds": 8.9,
"fd_count": 11
},
"sampled_peak_rss_kib": 27464,
"fd_delta_after_disconnect": 0,
"failures": [],
"scope": "One loopback host, 100 accounts with distinct resources, TLS+PLAIN, ping and directed messages; not a production benchmark or proof of no memory leaks.",
"mam": {
"archive_files": 100,
"records": 10000,
"expected_records": 10000
}
},
"limitations": [
"MinIO upstream RELEASE.2025-04-22T22-12-26Z with disclosed five-line EPERM loopback interface enumeration adapter; S3/auth/storage/multipart code unchanged",
"Single-node loopback MinIO, HTTP gateway-to-store and verified HTTPS client-to-gateway; no AWS account or distributed/production storage qualification",
"Missing/corrupt ownership metadata cannot identify arbitrary bucket orphans; lifecycle remains defense in depth",
"Ambiguous completion with HEAD404 quarantines slot until a trustworthy remote outcome; no automatic destructive reset",
"Migration is offline, source preserving, requires temporary disk up to one largest file; no live migration",
"Previous full Prosody parity / production client / DNSSEC limits unchanged"
]
}
+88
View File
@@ -0,0 +1,88 @@
{
"version": "0.8.0",
"date_utc": "2026-10-20",
"ordinary_tests": {
"passed": 293,
"failed": 0,
"ignored": 11
},
"fmt": "passed",
"clippy": "passed",
"release": "passed",
"release_binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"native_module_tests": 8,
"native_federation_tests": 1,
"muc_crash_network": "passed: real TLS, SIGKILL, temporary/persistent rooms, multiple resources, saved presence/roles, ordered history, acknowledged leave/kick/demotion/password/config, fixed grace, unsupported module rejection; destroy/tombstone separate log",
"sm_restart_network": "passed: SIGKILL/SIGTERM, legacy/SASL2, counter/replay/ack pruning, owner/token/h isolation, resource conflict, account deletion/recreation revocation, MUC offline fanout durable before recipient authentication",
"muc_sm_load": {
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-g0crm4ik/rust-xmpp",
"binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"before": {
"rss_kib": 9696,
"cpu_seconds": 0.0,
"fd_count": 12
},
"connect_seconds": 5.193,
"connect_latency": {
"samples": 500,
"median_ms": 204.391,
"p95_ms": 270.035,
"max_ms": 332.503
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 78.857,
"join_seconds": 46.703,
"joined": {
"rss_kib": 42248,
"cpu_seconds": 17.77,
"fd_count": 513
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 10.861,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 42128,
"cpu_seconds": 23.12,
"fd_count": 514
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 10.559,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 21168,
"cpu_seconds": 5.03,
"fd_count": 23
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 78.857,
"peak_rss_kib": 42252
},
"upload": "S3 fixture/migration/network regression passed; large 10GiB transfer retained as historical0.7.9 validation, not repeated0.8.0",
"limitations": [
"New TLS/SASL connection required; TCP/TLS sockets cannot survive process exit",
"At-least-once SM replay, not cross-store/network exactly-once transaction",
"Known builtin MUC features ported; arbitrary community Lua modules not executed",
"Private durable SM format is Rust-specific, bounded queues and component checkpoint; see MUC-RECOVERY.md",
"Loopback smoke tests, not production capacity or real client qualification",
"Full Prosody parity remains subject to COMPATIBILITY.md"
]
}
+190
View File
@@ -0,0 +1,190 @@
{
"version": "0.8.1",
"date_utc": "2026-10-21",
"ordinary_tests": {
"passed": 297,
"failed": 0,
"ignored": 11
},
"fmt": "passed",
"clippy": "passed",
"release": "passed",
"release_binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
"native_module_tests": 8,
"native_federation_tests": 1,
"full_verify": "passed in single final run with native and both500-client loads",
"message_latency": {
"scope": "loopback STARTTLS; isolated fixture; no production latency claim",
"before_binary_sha256": "74882319fcb467dd1e1813ab61e85441ffd3dae2f9f7961b28fd98ec5da7c89a",
"after_binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
"muc_history_records": 100000,
"rounds_per_state": 3,
"active_before": {
"samples": 3,
"median_ms": 2517.9402029999665,
"p95_ms": 2520.940784000004,
"max_ms": 2520.940784000004,
"samples_ms": [
2502.2604349999256,
2517.9402029999665,
2520.940784000004
]
},
"active_after": {
"samples": 3,
"median_ms": 4.680227999983799,
"p95_ms": 14.532127000052242,
"max_ms": 14.532127000052242,
"samples_ms": [
4.40479399992455,
4.680227999983799,
14.532127000052242
]
},
"inactive_before": {
"samples": 3,
"median_ms": 3866.027479999957,
"p95_ms": 4150.866962999999,
"max_ms": 4150.866962999999,
"samples_ms": [
2901.4587140000003,
3866.027479999957,
4150.866962999999
]
},
"inactive_after": {
"samples": 3,
"median_ms": 4.284876000042459,
"p95_ms": 5.071866999969643,
"max_ms": 5.071866999969643,
"samples_ms": [
4.257245000076182,
4.284876000042459,
5.071866999969643
]
}
},
"general_load": {
"total_load_wall_seconds": 8.892,
"requested_resources": 500,
"authenticated_bound_resources": 500,
"connection_setup_concurrency": 50,
"setup_wall_seconds": 0.646,
"setup_latency": {
"samples": 500,
"median_ms": 60.166,
"p95_ms": 81.981,
"max_ms": 105.233
},
"ping_rounds": 10,
"ping_round_trip": {
"samples": 5000,
"median_ms": 10.879,
"p95_ms": 21.103,
"max_ms": 23.338
},
"accounts": 100,
"message_rounds": 10,
"messages_sent": 5000,
"messages_received": 5000,
"message_batch_seconds": 6.009,
"metrics_before": {
"rss_kib": 8412,
"cpu_seconds": 0.0,
"fd_count": 11
},
"metrics_connected": {
"rss_kib": 27472,
"cpu_seconds": 1.09,
"fd_count": 511
},
"metrics_after_workload": {
"rss_kib": 28176,
"cpu_seconds": 11.23,
"fd_count": 511
},
"metrics_two_seconds_after_disconnect": {
"rss_kib": 28240,
"cpu_seconds": 11.27,
"fd_count": 11
},
"sampled_peak_rss_kib": 28240,
"fd_delta_after_disconnect": 0,
"failures": [],
"scope": "One loopback host, 100 accounts with distinct resources, TLS+PLAIN, ping and directed messages; not a production benchmark or proof of no memory leaks.",
"mam": {
"archive_files": 100,
"records": 10000,
"expected_records": 10000
}
},
"muc_sm_load": {
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-p95vsvii/rust-xmpp",
"binary_sha256": "c57757476f2019c60ac877e4a66364b17283218e3cc244ae3f29cde1d3be2977",
"before": {
"rss_kib": 10076,
"cpu_seconds": 0.0,
"fd_count": 12
},
"connect_seconds": 3.149,
"connect_latency": {
"samples": 500,
"median_ms": 121.817,
"p95_ms": 169.081,
"max_ms": 257.299
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 39.535,
"join_seconds": 25.54,
"joined": {
"rss_kib": 42224,
"cpu_seconds": 14.41,
"fd_count": 512
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 5.504,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 42224,
"cpu_seconds": 17.88,
"fd_count": 512
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 2.962,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 20796,
"cpu_seconds": 2.81,
"fd_count": 22
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 39.535,
"peak_rss_kib": 42240
},
"large_upload": "10GiB not repeated in0.8.1; S3 fixture/migration/network regression passed; historical0.7.8/0.7.9 reports retained",
"limitations": [
"Loopback timing, not production SLA or real client qualification",
"Required disk synchronization remains, at-least-once SM replay",
"Periodic archive maintenance and large MAM queries still perform archive IO",
"Full Prosody compatibility and production deployment not asserted; see COMPATIBILITY.md"
]
}
File diff suppressed because it is too large. Load diff
+209
View File
@@ -0,0 +1,209 @@
{
"version": "0.8.3",
"date_utc": "2026-10-21",
"release_binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"ordinary_tests": {
"passed": 334,
"failed": 0,
"ignored": 11
},
"fmt": "passed",
"clippy": "passed",
"release": "passed",
"full_verify": "passed in one final frozen-source run",
"native_module_tests": 8,
"native_federation_tests": 1,
"extra_large_archive_tests": 1,
"ignored_not_run": [
"AF_UNIX admin_socket"
],
"load": {
"general_tls_clients": 500,
"general_accounts": 100,
"muc_sm": {
"clients": 500,
"accounts": 100,
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-ssixqqk9/rust-xmpp",
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"before": {
"rss_kib": 9516,
"cpu_seconds": 0.01,
"fd_count": 12
},
"connect_seconds": 12.048,
"connect_latency": {
"samples": 500,
"median_ms": 398.044,
"p95_ms": 1086.236,
"max_ms": 1403.7
},
"phase": "completed",
"status": "pass",
"elapsed_seconds": 128.38,
"join_seconds": 94.493,
"joined": {
"rss_kib": 41828,
"cpu_seconds": 55.61,
"fd_count": 513
},
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"message_seconds": 9.585,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 41740,
"cpu_seconds": 62.6,
"fd_count": 512
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 7.557,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 20896,
"cpu_seconds": 7.09,
"fd_count": 22
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 128.38,
"peak_rss_kib": 41852
}
},
"personal_delivery_comparisons": [
{
"delay_ms_per_sync": 0,
"phase": "settled",
"samples_per_binary": 3,
"body_before_ms": 25.112248,
"body_after_ms": 7.871244,
"sender_ack_before_ms": 33.407559,
"sender_ack_after_ms": 15.079213
},
{
"delay_ms_per_sync": 0,
"phase": "avatar_pending",
"samples_per_binary": 3,
"body_before_ms": 29.714366,
"body_after_ms": 20.942607,
"sender_ack_before_ms": 32.252144,
"sender_ack_after_ms": 21.856674
},
{
"delay_ms_per_sync": 100,
"phase": "settled",
"samples_per_binary": 3,
"body_before_ms": 1232.468024,
"body_after_ms": 419.683064,
"sender_ack_before_ms": 1641.598264,
"sender_ack_after_ms": 644.082891
},
{
"delay_ms_per_sync": 100,
"phase": "avatar_pending",
"samples_per_binary": 3,
"body_before_ms": 2683.292251,
"body_after_ms": 1378.231158,
"sender_ack_before_ms": 3195.209226,
"sender_ack_after_ms": 1581.975429
},
{
"delay_ms_per_sync": 200,
"phase": "settled",
"samples_per_binary": 1,
"body_before_ms": 2836.63993,
"body_after_ms": 2042.56345,
"sender_ack_before_ms": 3643.297905,
"sender_ack_after_ms": 2441.985416
},
{
"delay_ms_per_sync": 200,
"phase": "avatar_pending",
"samples_per_binary": 1,
"body_before_ms": 5782.018326,
"body_after_ms": 2676.283903,
"sender_ack_before_ms": 6595.924675,
"sender_ack_after_ms": 3094.949323
}
],
"durability_crash": {
"status": "pass",
"delay_ms_per_sync": 100,
"acknowledged_messages": 3,
"mam_records_per_account": [
3,
3
],
"resumed_resources": 4,
"replayed_personal_deliveries": 9,
"offline_checkpoint": {
"exported_sessions": 4,
"native_archives": 2,
"journals_removed": true,
"second_invocation_idempotent": true
},
"pending_before_reauthentication": [
2,
3,
4,
3
],
"sync_between_publication_and_sigkill": {
"total": 32,
"by_store_kind_op": {
"mam:directory:fsync": 2,
"mam:file:fsync": 10,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 16
}
},
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback STARTTLS; real delayed fsync/fdatasync; sender SM acknowledgement then SIGKILL; authoritative queues and MAM checked before reauthentication; at-least-once replay"
},
"recovery_sync_faults": {
"status": "pass",
"cases": [
{
"suffix": ".wal",
"status": "pass",
"checkpoint_rejected_sync_failure": true,
"journals_preserved_on_failure": 4,
"serve_rejected_before_listeners": true,
"clean_retry_preserved_sessions": 4,
"clean_retry_preserved_archives": 2
},
{
"suffix": ".rredo",
"status": "pass",
"checkpoint_rejected_sync_failure": true,
"journals_preserved_on_failure": 2,
"serve_rejected_before_listeners": false,
"clean_retry_preserved_sessions": 4,
"clean_retry_preserved_archives": 2
}
],
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a"
},
"limitations": [
"Loopback fixture, not production client or disk qualification",
"0ms pair ran sequentially without full verify; 100/200ms final runs had background final verify",
"One200ms sample; startup/migration/rotation and background heartbeat affect sync counts",
"SM global IO mutex and full snapshots remain; no atomic transaction spanning MAM/SM/network",
"Offline storage-checkpoint required before legacy/native writer handoff",
"10GiB transfers, real MinIO/AWS, Docker/Gitea/GitHub run were not repeated in this release"
]
}
+187 -111
View File
@@ -1,133 +1,209 @@
{
"version": "0.7.9",
"date_utc": "2026-10-20",
"version": "0.8.3",
"date_utc": "2026-10-21",
"release_binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"ordinary_tests": {
"passed": 265,
"passed": 334,
"failed": 0,
"ignored": 11
},
"fmt": "passed",
"clippy": "passed",
"release": "passed",
"full_verify": "passed in one final frozen-source run",
"native_module_tests": 8,
"native_federation_tests": 1,
"release_binary_sha256": "24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c",
"upload": {
"minio_10gib": {
"ok": true,
"minio_version": "minio-sandbox-compat version DEVELOPMENT.GOGET (commit-id=DEVELOPMENT.GOGET)\nRuntime: go1.24.3 linux/amd64\nLicense: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html\nCopyright: 2015-0000 MinIO, Inc.",
"minio_binary_sha256": "8bbb9bccfbac69869ab499c39cfd8731b2b770b86ccf37cca1c8267556ad9bf0",
"binary": "/workspace/scratch/362577cdf2e0/recovered/rust-xmpp/target/release/rust-xmpp",
"binary_version": "rust-xmpp 0.7.9 — experimental XMPP runtime",
"binary_sha256": "24c0dca637e7ce4e238ef6becad16bb5a18a33546554b0eecf310fd8927f021c",
"size": 10737418240,
"qualifies_over_5gib": true,
"minio_test_only_compatibility_patch": {
"sha256": "e60592a65273f28a1375eb6ecbc0966f7fbd2ba7567d2ffedc13ffd95ed224f5",
"diff": "--- a/cmd/net.go\n+++ b/cmd/net.go\n@@ -24,6 +24,7 @@\n \t\"runtime\"\n \t\"sort\"\n \t\"strings\"\n+\t\"syscall\"\n \n \t\"github.com/minio/minio-go/v7/pkg/set\"\n \t\"github.com/minio/minio/internal/config\"\n@@ -54,6 +55,10 @@\n // mustGetLocalIPs returns IPs of local interface\n func mustGetLocalIPs() (ipList []net.IP) {\n \tifs, err := net.Interfaces()\n+\t// TEST ONLY: the sandbox denies netlink enumeration; all listeners bind loopback.\n+\tif errors.Is(err, syscall.EPERM) {\n+\t\treturn []net.IP{net.ParseIP(\"127.0.0.1\")}\n+\t}\n \tlogger.FatalIf(err, \"Unable to get IP addresses of this host\")\n \n \tfor _, interf := range ifs {\n",
"limitation": "Modified upstream MinIO: EPERM interface enumeration uses loopback; all S3/auth/storage code remains upstream."
},
"max_rss_bytes": {
"server": 106057728,
"client": 24338432,
"minio": 498958336
},
"put_bytes": 10737418240,
"put_seconds": 48.45253416699961,
"sha256": "e4c296c41d3a9605f0348961e41ed835b5f42c220751ed2269a726951546c935",
"minio_etag": "958f42d583ac40ff209eb865c5eb4bd6-1280",
"multipart_parts": 1280,
"minio_allocated_bytes": 10747953152,
"free_disk_after_put_bytes": 15180328960,
"get_bytes": 10737418240,
"get_sha256": "e4c296c41d3a9605f0348961e41ed835b5f42c220751ed2269a726951546c935",
"get_seconds": 13.709708632000911,
"range_offsets": [
0,
4294967289,
5368709113,
10737418208
"extra_large_archive_tests": 1,
"ignored_not_run": [
"AF_UNIX admin_socket"
],
"restart_range_ok": true,
"restart_reuse_status": 409,
"crash_multipart_cleanup_ok": true,
"minio_upstream_provenance": {
"source_url": "https://github.com/minio/minio/archive/refs/tags/RELEASE.2025-04-22T22-12-26Z.tar.gz",
"upstream_release": "RELEASE.2025-04-22T22-12-26Z",
"build": "Go1.24.3 go build -p4; no dependency changes",
"sha256": {
"/tmp/minio-source.tar.gz": "1a63209927b9d1fb239b2a776b7b01d1902eff19379a108f4bc7005edcc0968c",
"/tmp/minio-genuine": "3dd5d6756ad17c599fbc1c6f6a2f5b590242c049522e848c700949dbf8e60ad9",
"/tmp/minio-sandbox-compat": "8bbb9bccfbac69869ab499c39cfd8731b2b770b86ccf37cca1c8267556ad9bf0",
"/tmp/minio-sandbox-compat.patch": "e60592a65273f28a1375eb6ecbc0966f7fbd2ba7567d2ffedc13ffd95ed224f5"
},
"compatibility_limitation": "Only net.Interfaces EPERM becomes loopback127.0.0.1; all S3/auth/multipart/storage source unchanged."
}
},
"migration_network": "passed: local -> S3 A -> S3 B -> local, dry-run, lock, native metadata/source preservation, interrupted copy/resume, native final file with incomplete cache",
"s3_fault_network": "passed: SigV4, paginated exact-key orphan cleanup, foreign prefix safety, missing UploadId crash recovery, conditional Complete, ambiguous completion quarantine and late recovery",
"claim_race_regression": "passed: atomic fresh snapshot and active claim retains latest completion/quarantine flags"
},
"load": {
"total_load_wall_seconds": 7.094,
"requested_resources": 500,
"authenticated_bound_resources": 500,
"connection_setup_concurrency": 50,
"setup_wall_seconds": 0.671,
"setup_latency": {
"samples": 500,
"median_ms": 62.192,
"p95_ms": 83.883,
"max_ms": 114.563
},
"ping_rounds": 10,
"ping_round_trip": {
"samples": 5000,
"median_ms": 13.502,
"p95_ms": 19.31,
"max_ms": 24.054
},
"general_tls_clients": 500,
"general_accounts": 100,
"muc_sm": {
"clients": 500,
"accounts": 100,
"message_rounds": 10,
"messages_sent": 5000,
"messages_received": 5000,
"message_batch_seconds": 4.134,
"metrics_before": {
"rss_kib": 8048,
"cpu_seconds": 0.0,
"fd_count": 11
"rooms": 10,
"message_rounds": 3,
"concurrency": 20,
"binary": "/tmp/xmpp-durable-muc-load-ssixqqk9/rust-xmpp",
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"before": {
"rss_kib": 9516,
"cpu_seconds": 0.01,
"fd_count": 12
},
"metrics_connected": {
"rss_kib": 26804,
"cpu_seconds": 1.02,
"fd_count": 511
"connect_seconds": 12.048,
"connect_latency": {
"samples": 500,
"median_ms": 398.044,
"p95_ms": 1086.236,
"max_ms": 1403.7
},
"metrics_after_workload": {
"rss_kib": 27400,
"cpu_seconds": 8.85,
"fd_count": 511
"phase": "completed",
"status": "pass",
"elapsed_seconds": 128.38,
"join_seconds": 94.493,
"joined": {
"rss_kib": 41828,
"cpu_seconds": 55.61,
"fd_count": 513
},
"metrics_two_seconds_after_disconnect": {
"rss_kib": 27464,
"cpu_seconds": 8.9,
"fd_count": 11
"join_presence_deliveries": 25000,
"slots_joined": {
"count": 500,
"bytes": 309790
},
"sampled_peak_rss_kib": 27464,
"fd_delta_after_disconnect": 0,
"failures": [],
"scope": "One loopback host, 100 accounts with distinct resources, TLS+PLAIN, ping and directed messages; not a production benchmark or proof of no memory leaks.",
"mam": {
"archive_files": 100,
"records": 10000,
"expected_records": 10000
"message_seconds": 9.585,
"room_messages_sent": 30,
"room_messages_delivered": 1500,
"before_kill": {
"rss_kib": 41740,
"cpu_seconds": 62.6,
"fd_count": 512
},
"slots_before_kill": {
"count": 500,
"bytes": 514030
},
"restart_seconds": 7.557,
"resumed_resources": 10,
"replayed_pending_room_messages": 10,
"membership_groupchat_deliveries_after_resume": 10,
"after_resume": {
"rss_kib": 20896,
"cpu_seconds": 7.09,
"fd_count": 22
},
"slots_after_resume": {
"count": 500,
"bytes": 671070
},
"total_seconds": 128.38,
"peak_rss_kib": 41852
}
},
"personal_delivery_comparisons": [
{
"delay_ms_per_sync": 0,
"phase": "settled",
"samples_per_binary": 3,
"body_before_ms": 25.112248,
"body_after_ms": 7.871244,
"sender_ack_before_ms": 33.407559,
"sender_ack_after_ms": 15.079213
},
{
"delay_ms_per_sync": 0,
"phase": "avatar_pending",
"samples_per_binary": 3,
"body_before_ms": 29.714366,
"body_after_ms": 20.942607,
"sender_ack_before_ms": 32.252144,
"sender_ack_after_ms": 21.856674
},
{
"delay_ms_per_sync": 100,
"phase": "settled",
"samples_per_binary": 3,
"body_before_ms": 1232.468024,
"body_after_ms": 419.683064,
"sender_ack_before_ms": 1641.598264,
"sender_ack_after_ms": 644.082891
},
{
"delay_ms_per_sync": 100,
"phase": "avatar_pending",
"samples_per_binary": 3,
"body_before_ms": 2683.292251,
"body_after_ms": 1378.231158,
"sender_ack_before_ms": 3195.209226,
"sender_ack_after_ms": 1581.975429
},
{
"delay_ms_per_sync": 200,
"phase": "settled",
"samples_per_binary": 1,
"body_before_ms": 2836.63993,
"body_after_ms": 2042.56345,
"sender_ack_before_ms": 3643.297905,
"sender_ack_after_ms": 2441.985416
},
{
"delay_ms_per_sync": 200,
"phase": "avatar_pending",
"samples_per_binary": 1,
"body_before_ms": 5782.018326,
"body_after_ms": 2676.283903,
"sender_ack_before_ms": 6595.924675,
"sender_ack_after_ms": 3094.949323
}
],
"durability_crash": {
"status": "pass",
"delay_ms_per_sync": 100,
"acknowledged_messages": 3,
"mam_records_per_account": [
3,
3
],
"resumed_resources": 4,
"replayed_personal_deliveries": 9,
"offline_checkpoint": {
"exported_sessions": 4,
"native_archives": 2,
"journals_removed": true,
"second_invocation_idempotent": true
},
"pending_before_reauthentication": [
2,
3,
4,
3
],
"sync_between_publication_and_sigkill": {
"total": 32,
"by_store_kind_op": {
"mam:directory:fsync": 2,
"mam:file:fsync": 10,
"pep:directory:fsync": 2,
"pep:file:fsync": 2,
"sm:file:fdatasync": 16
}
},
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a",
"scope": "loopback STARTTLS; real delayed fsync/fdatasync; sender SM acknowledgement then SIGKILL; authoritative queues and MAM checked before reauthentication; at-least-once replay"
},
"recovery_sync_faults": {
"status": "pass",
"cases": [
{
"suffix": ".wal",
"status": "pass",
"checkpoint_rejected_sync_failure": true,
"journals_preserved_on_failure": 4,
"serve_rejected_before_listeners": true,
"clean_retry_preserved_sessions": 4,
"clean_retry_preserved_archives": 2
},
{
"suffix": ".rredo",
"status": "pass",
"checkpoint_rejected_sync_failure": true,
"journals_preserved_on_failure": 2,
"serve_rejected_before_listeners": false,
"clean_retry_preserved_sessions": 4,
"clean_retry_preserved_archives": 2
}
],
"binary_sha256": "8ceb8de3a2d02bc4399b8ae817dff7e3c3fe9bff83dc0c6669b7f19210a3847a"
},
"limitations": [
"MinIO upstream RELEASE.2025-04-22T22-12-26Z with disclosed five-line EPERM loopback interface enumeration adapter; S3/auth/storage/multipart code unchanged",
"Single-node loopback MinIO, HTTP gateway-to-store and verified HTTPS client-to-gateway; no AWS account or distributed/production storage qualification",
"Missing/corrupt ownership metadata cannot identify arbitrary bucket orphans; lifecycle remains defense in depth",
"Ambiguous completion with HEAD404 quarantines slot until a trustworthy remote outcome; no automatic destructive reset",
"Migration is offline, source preserving, requires temporary disk up to one largest file; no live migration",
"Previous full Prosody parity / production client / DNSSEC limits unchanged"
"Loopback fixture, not production client or disk qualification",
"0ms pair ran sequentially without full verify; 100/200ms final runs had background final verify",
"One200ms sample; startup/migration/rotation and background heartbeat affect sync counts",
"SM global IO mutex and full snapshots remain; no atomic transaction spanning MAM/SM/network",
"Offline storage-checkpoint required before legacy/native writer handoff",
"10GiB transfers, real MinIO/AWS, Docker/Gitea/GitHub run were not repeated in this release"
]
}
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Validate Gitea publication inputs and emit literal, shell-safe image tags."""
import os
import re
import tomllib
from pathlib import Path
def image_tags(env, version):
registry = env.get('GITEA_REGISTRY', '')
image = env.get('GITEA_IMAGE', '')
if not re.fullmatch(r'[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]{1,5})?', registry):
raise ValueError('GITEA_REGISTRY must be a lowercase hostname[:port], without scheme or path')
if not re.fullmatch(r'[a-z0-9]+(?:[._-][a-z0-9]+)*(?:/[a-z0-9]+(?:[._-][a-z0-9]+)*)+', image):
raise ValueError('GITEA_IMAGE must be lowercase owner/image, without registry or tag')
if not env.get('REGISTRY_USER') or not env.get('REGISTRY_TOKEN'):
raise ValueError('Set GITEA_REGISTRY_USERNAME and GITEA_REGISTRY_TOKEN secrets')
sha = env['GITHUB_SHA']
if not re.fullmatch(r'[0-9a-f]{40}', sha):
raise ValueError('Invalid commit SHA')
base = f'{registry}/{image}'
tags = [f'{base}:sha-{sha}']
ref = env['GITHUB_REF']
if ref.startswith('refs/tags/'):
release = ref.removeprefix('refs/tags/')
if release != f'v{version}' or not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?', release):
raise ValueError('Release tag must be v<Cargo.toml package.version> (no build metadata)')
tags.append(f'{base}:{release.lower()}')
elif ref == 'refs/heads/' + env['DEFAULT_BRANCH']:
tags.append(f'{base}:latest')
else:
raise ValueError('Only default branch or version tags may publish')
return tags
def main():
version = tomllib.loads(Path('Cargo.toml').read_text())['package']['version']
tags = image_tags(os.environ, version)
with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as out:
out.write('tags<<IMAGE_TAGS\n' + '\n'.join(tags) + '\nIMAGE_TAGS\n')
if __name__ == '__main__':
main()
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Unmodified upstream source; never apply the sandbox adapter in hosted CI.
set -euo pipefail
output=${1:?Usage: prepare-minio.sh ABSOLUTE_OUTPUT_BINARY}
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
curl --fail --location --retry 3 --connect-timeout 20 --max-time 300 \
https://github.com/minio/minio/archive/refs/tags/RELEASE.2025-04-22T22-12-26Z.tar.gz \
--output "$work/minio.tar.gz"
printf '%s %s\n' 1a63209927b9d1fb239b2a776b7b01d1902eff19379a108f4bc7005edcc0968c "$work/minio.tar.gz" | sha256sum --check --strict
tar --no-same-owner -xzf "$work/minio.tar.gz" -C "$work"
cd "$work/minio-RELEASE.2025-04-22T22-12-26Z"
mkdir -p "$(dirname "$output")"
GOTOOLCHAIN=local go build -mod=readonly -p 2 -o "$output" .
"$output" --version
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
reference_parent=${1:?Usage: prepare-reference.sh OUTPUT_DIRECTORY}
mkdir -p "$reference_parent"
reference_parent=$(cd -- "$reference_parent" && pwd)
cd "$project_dir/scripts/ci/vendor"
printf '%s\n' '1e558d3d2211ea0228596aced2b71306503190c454ec4cb44f172045b3513c34 prosody-cafd74b9c5ea.tar.gz' | sha256sum --check --strict
tar --no-same-owner -xzf prosody-cafd74b9c5ea.tar.gz -C "$reference_parent"
cd "$reference_parent/prosody-cafd74b9c5ea"
./configure --ostype=debian --lua-version=5.4 --lua-suffix=5.4
make -j2
+11
View File
@@ -0,0 +1,11 @@
# Native reference test source
`prosody-cafd74b9c5ea.tar.gz` is the original, unmodified source archive supplied
with this project. Revision: `cafd74b9c5ead46eb58910359eb266e1732b0aa1`.
SHA-256: `1e558d3d2211ea0228596aced2b71306503190c454ec4cb44f172045b3513c34`.
The source is bundled so CI cannot silently skip native oracle tests when a
private upload URL expires. Prosody's MIT/X11 license and copyright notices
are retained inside the archive (`COPYING`). No compiled shared objects are
bundled. `prepare-reference.sh` checks the archive hash and builds native
modules against the runner's Lua/OpenSSL/ICU installation.
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
: "${PROSODY_REFERENCE_SOURCE:?Build the bundled reference first}"
: "${MINIO_BINARY:?Build genuine MinIO first}"
export XMPP_LOAD_CHECK=1
mkdir -p ci-results
bash scripts/verify-local.sh
# These two tests are deliberately ignored by the ordinary test command.
cargo test --locked --test admin_socket --test archive_mam -- --ignored --nocapture
python3 tests/upload_large_network.py --binary target/release/rust-xmpp --report ci-results/upload-local-10gib.json
+12
View File
@@ -18,6 +18,7 @@ python3 tests/wss_interop.py --binary target/release/rust-xmpp
python3 tests/http_proxy_paths_network.py --binary target/release/rust-xmpp
python3 tests/http_altconnect_network.py --binary target/release/rust-xmpp
python3 tests/sasl2_bind.py target/release/rust-xmpp
python3 tests/bind2_archive_latency_network.py --binary target/release/rust-xmpp --records 10000 --samples 2 --output /tmp/bind2-archive-latency.json
python3 tests/sasl2_cached_inline.py target/release/rust-xmpp
python3 tests/gajim_discovery_network.py target/release/rust-xmpp
python3 tests/sasl2_resume.py target/release/rust-xmpp
@@ -27,6 +28,16 @@ python3 tests/pep_order_network.py target/release/rust-xmpp
python3 tests/pep_notifications_network.py target/release/rust-xmpp
python3 tests/preauth_network.py target/release/rust-xmpp
python3 tests/muc_restart_network.py target/release/rust-xmpp
python3 tests/muc_crash_network.py target/release/rust-xmpp
python3 tests/sm_restart_network.py target/release/rust-xmpp
python3 tests/slow_storage_latency_network.py --binary target/release/rust-xmpp --delay-ms 0 25 --rounds 1 --output /tmp/slow-storage-latency.json
python3 tests/slow_storage_crash_network.py --binary target/release/rust-xmpp --delay-ms 25 --messages 3 --output /tmp/slow-storage-crash.json
python3 tests/journal_recovery_sync_network.py --binary target/release/rust-xmpp --output /tmp/journal-recovery-sync.json
python3 tests/mam_broken_baseline_network.py --binary target/release/rust-xmpp
python3 tests/push_latency_network.py --binary target/release/rust-xmpp --recipient hibernating --output /tmp/push-latency.json
python3 tests/push_latency_network.py --binary target/release/rust-xmpp --recipient offline --output /tmp/push-offline-latency.json
python3 tests/personal_message_latency_network.py --binary target/release/rust-xmpp --rounds 1 --history-records 1000 --roster-sizes 0 --backlog 30 --csi-dwell 0.1 --output /tmp/personal-message-latency.json
python3 tests/message_latency_network.py --binary target/release/rust-xmpp --rounds 3 --history-records 0 10000 --output /tmp/xmpp-message-latency.json
python3 tests/resource_rebind.py target/release/rust-xmpp
python3 scripts/integration_http.py
python3 scripts/integration_http.py --custom-upload-path
@@ -39,6 +50,7 @@ if [[ -n ${MINIO_BINARY:-} ]]; then
fi
if [[ ${XMPP_LOAD_CHECK:-0} == 1 ]]; then
python3 scripts/load_smoke.py --clients 500 --accounts 100 --rounds 10 --message-rounds 10 --archive
python3 tests/muc_sm_load_network.py --binary target/release/rust-xmpp --clients 500
fi
if [[ -n ${PROSODY_REFERENCE_SOURCE:-} ]]; then
RUSTUP_TOOLCHAIN=${RUSTUP_TOOLCHAIN:-1.99.1} scripts/prosody_compat/verify.sh \
+732 -41
View File
@@ -5,7 +5,7 @@ use crate::{
};
use anyhow::{Result, anyhow, bail};
use std::{
io::{BufRead, BufReader, Read, Write},
io::{BufRead, BufReader, Read, Seek, SeekFrom, Write},
path::Path,
sync::{Arc, Mutex, OnceLock, Weak},
};
@@ -174,7 +174,103 @@ pub fn scan_store<F: FnMut(Entry) -> Result<()>>(
let _guard = lock.lock().map_err(|_| anyhow!("archive lock poisoned"))?;
scan_unlocked(root, host, user, store, visit)
}
fn recover_append(p: &Path) -> Result<()> {
// The native file remains directly readable by Prosody. While this sidecar exists,
// its complete frames define the committed suffix: do not edit a live native file
// in place or replace it until checkpoint_all has durably retired the journal.
const REDO_MAGIC: &[u8; 8] = b"XARRED01";
const REDO_HEADER: usize = 96;
const REDO_LIMIT: u64 = 1024 * 1024;
const REDO_FRAMES: usize = 128;
#[derive(Clone)]
struct RedoState {
length: u64,
bytes: u64,
frames: usize,
}
#[derive(Clone, PartialEq, Eq)]
struct FileStamp {
len: u64,
modified: Option<std::time::SystemTime>,
identity: (u64, u64),
#[cfg(unix)]
changed: (i64, i64),
}
fn identity(meta: &std::fs::Metadata) -> (u64, u64) {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
(meta.dev(), meta.ino())
}
#[cfg(not(unix))]
{
let _ = meta;
(0, 0)
}
}
fn stamp(p: &Path) -> Result<FileStamp> {
let m = std::fs::metadata(p)?;
Ok(FileStamp {
len: m.len(),
modified: m.modified().ok(),
identity: identity(&m),
#[cfg(unix)]
changed: {
use std::os::unix::fs::MetadataExt;
(m.ctime(), m.ctime_nsec())
},
})
}
type RedoCache = std::collections::HashMap<std::path::PathBuf, (FileStamp, FileStamp, RedoState)>;
static REDO_CACHE: OnceLock<Mutex<RedoCache>> = OnceLock::new();
fn remember_redo(p: &Path, state: &RedoState) -> Result<()> {
let native = stamp(p)?;
let journal = stamp(&p.with_extension("rredo"))?;
let mut cache = REDO_CACHE
.get_or_init(|| Mutex::new(RedoCache::new()))
.lock()
.map_err(|_| anyhow!("archive redo cache poisoned"))?;
if cache.len() >= 1024 {
cache.clear();
}
cache.insert(p.to_path_buf(), (native, journal, state.clone()));
Ok(())
}
fn digest(data: &[u8]) -> [u8; 32] {
use sha2::{Digest, Sha256};
Sha256::digest(data).into()
}
fn baseline_anchor(file: &mut std::fs::File, length: u64) -> Result<[u8; 32]> {
let count = length.min(4096);
file.seek(SeekFrom::Start(length - count))?;
let mut bytes = vec![0; count as usize];
file.read_exact(&mut bytes)?;
Ok(digest(&bytes))
}
/// Native fsync precedes journal replacement. Either old or new header can
/// survive a crash, and both refer to an already durable native baseline.
fn checkpoint_redo(p: &Path) -> Result<RedoState> {
let mut file = std::fs::OpenOptions::new().read(true).write(true).open(p)?;
file.sync_all()?;
let meta = file.metadata()?;
let length = meta.len();
let (dev, ino) = identity(&meta);
let mut header = Vec::with_capacity(REDO_HEADER);
header.extend_from_slice(REDO_MAGIC);
for n in [length, dev, ino] {
header.extend_from_slice(&n.to_le_bytes());
}
header.extend_from_slice(&baseline_anchor(&mut file, length)?);
header.extend_from_slice(&digest(&header));
storage::atomic_write(&p.with_extension("rredo"), &header)?;
let state = RedoState {
length,
bytes: REDO_HEADER as u64,
frames: 0,
};
remember_redo(p, &state)?;
Ok(state)
}
fn recover_legacy_append(p: &Path) -> Result<()> {
let journal = p.with_extension("rappend");
match std::fs::read(&journal) {
Ok(data) => {
@@ -195,6 +291,263 @@ fn recover_append(p: &Path) -> Result<()> {
}
Ok(())
}
fn recover_redo(p: &Path) -> Result<Option<RedoState>> {
recover_legacy_append(p)?;
let journal = p.with_extension("rredo");
let jm = match stamp(&journal) {
Ok(m) => m,
Err(e)
if e.downcast_ref::<std::io::Error>()
.is_some_and(|e| e.kind() == std::io::ErrorKind::NotFound) =>
{
return Ok(None);
}
Err(e) => return Err(e),
};
let nm = stamp(p).ok();
if let Some(native) = &nm {
let cache = REDO_CACHE
.get_or_init(|| Mutex::new(RedoCache::new()))
.lock()
.map_err(|_| anyhow!("archive redo cache poisoned"))?;
if let Some((n, j, state)) = cache.get(p)
&& n == native
&& j == &jm
{
return Ok(Some(state.clone()));
}
}
// Defer the recovery body so an untrusted-baseline failure is handled once:
// the journal is retired and the durable synced native becomes authoritative.
let recovered = (|| -> Result<Option<RedoState>> {
// One oversized record is allowed, but the journal never grows without bound.
anyhow::ensure!(
jm.len <= REDO_LIMIT + MAX_RECORD_BYTES as u64 + REDO_HEADER as u64 + 72,
"archive redo journal exceeds bound"
);
// Read only the fixed header first. Validating the baseline anchor against
// the native file requires only the header; the expensive part (reading and
// checksumming every frame) is deferred until we know the journal is a
// trusted match. This keeps a large `.rredo` with a broken baseline from
// costing seconds on every access, which is exactly the work Prosody never
// performs.
let mut header = [0u8; REDO_HEADER];
let mut jf = std::fs::File::open(&journal)?;
let header_len = jf.read(&mut header)?;
anyhow::ensure!(
header_len == REDO_HEADER && &header[..8] == REDO_MAGIC,
"invalid archive redo header"
);
anyhow::ensure!(
digest(&header[..64]) == header[64..96],
"archive redo header checksum mismatch"
);
let base = u64::from_le_bytes(header[8..16].try_into().unwrap());
// Inodes change when an archive is copied/restored. The journal remains
// authoritative across that change; identity is only a cache stamp.
anyhow::ensure!(nm.is_some() || base == 0, "missing archive redo baseline");
{
let mut opts = std::fs::OpenOptions::new();
opts.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut native = opts.open(p)?;
anyhow::ensure!(
native.metadata()?.len() >= base,
"archive shorter than redo baseline"
);
anyhow::ensure!(
baseline_anchor(&mut native, base)? == header[32..64],
"archive redo baseline changed in place"
);
}
// Trusted baseline. Now read and validate the remainder of the journal and
// replay its frames into the native file (durable redo recovery).
let data = std::fs::read(&journal)?;
let mut pos = REDO_HEADER;
let mut frames = Vec::new();
let mut length = base;
while pos + 72 <= data.len() {
anyhow::ensure!(
digest(&data[pos..pos + 8]) == data[pos + 8..pos + 40],
"archive redo frame length checksum mismatch"
);
let size = u64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
anyhow::ensure!(
size > 0 && size <= MAX_RECORD_BYTES as u64,
"invalid archive redo frame length"
);
let end = pos + 72 + size as usize;
if end > data.len() {
break;
}
let payload = &data[pos + 72..end];
anyhow::ensure!(
digest(payload) == data[pos + 40..pos + 72],
"archive redo frame checksum mismatch"
);
frames.push((length, payload));
anyhow::ensure!(
frames.len() <= REDO_FRAMES,
"archive redo frame count exceeds bound"
);
length = length
.checked_add(size)
.ok_or_else(|| anyhow!("archive redo offset overflow"))?;
pos = end;
}
drop(jf);
// Native indexes are rebuildable. A crash may have resurrected an unlink
// from the append path; invalidate it again before exposing recovered data.
match std::fs::remove_file(p.with_extension("lidx")) {
Ok(()) => std::fs::File::open(p.parent().unwrap())?.sync_all()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
Err(e) => return Err(e.into()),
}
// A complete frame can survive a process crash in page cache even if its
// writer never completed sync. Durably adopt all validated frames before
// exposing them or allowing later recovery to truncate the native file.
let redo = std::fs::OpenOptions::new().write(true).open(&journal)?;
if pos < data.len() {
redo.set_len(pos as u64)?;
}
redo.sync_all()?;
// Validate the complete journal before changing native data. Replaying is
// idempotent, including after a second crash midway through recovery.
let mut file = std::fs::OpenOptions::new().read(true).write(true).open(p)?;
let mut changed = file.metadata()?.len() != length;
for (offset, payload) in &frames {
file.seek(SeekFrom::Start(*offset))?;
let mut native = vec![0; payload.len()];
let equal = file.read_exact(&mut native).is_ok() && native.as_slice() == *payload;
if !equal {
file.seek(SeekFrom::Start(*offset))?;
file.write_all(payload)?;
changed = true;
}
}
if changed {
file.set_len(length)?;
file.sync_all()?;
}
let state = RedoState {
length,
bytes: pos as u64,
frames: frames.len(),
};
if nm.is_none() {
return checkpoint_redo(p).map(Some);
}
remember_redo(p, &state)?;
Ok(Some(state))
})();
match recovered {
Err(error) if is_untrusted_redo(&error) => {
// The journal no longer matches its native baseline. Prosody itself
// would keep appending to the native file without any redo view:
// adopt that currently synced native as authoritative, retire the
// untrusted journal, and report "no redo". This keeps archive access
// at Prosody-level speed and makes the archive writable again without
// an explicit offline handoff, matching how the same data behaves
// under Prosody.
retire_redo(p)?;
Ok(None)
}
Ok(state) => Ok(state),
Err(error) => Err(error),
}
}
/// Recovery for read-only archive access. A journal whose baseline an external
/// writer invalidated cannot be replayed safely; treat the durably synced native
/// as authoritative and read it as-is instead of failing every edge/scan/query.
/// Returns `Ok(true)` only when the baseline is untrusted (caller should proceed
/// on native), `Ok(false)` after a normal recovery, or `Err` for other failures.
fn recover_append_readonly(p: &Path) -> Result<bool> {
match recover_redo(p) {
Ok(_) => Ok(false),
Err(error) if is_untrusted_redo(&error) => Ok(true),
Err(error) => Err(error),
}
}
/// Durable handoff to a native writer. Call after stopping archive users and
/// before letting Prosody modify `.list` files in place. Abrupt shutdown leaves
/// redo recovery active; unjournaled native suffixes are then uncommitted.
pub fn checkpoint_all(root: &Path) -> Result<usize> {
with_exclusive_access(|| {
let mut pending = vec![root.to_path_buf()];
let mut count = 0;
while let Some(dir) = pending.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e.into()),
};
for entry in entries {
let entry = entry?;
let kind = entry.file_type()?;
if kind.is_dir() {
pending.push(entry.path());
} else if kind.is_file() && entry.path().extension().is_some_and(|e| e == "rredo") {
let path = entry.path().with_extension("list");
prepare_rewrite(&path)?;
count += 1;
}
}
}
Ok(count)
})
}
/// Retire redo before an atomic snapshot replacement. The old native snapshot
/// is synced first, so a crash after unlink and before rename retains every
/// committed record. After rename there is no old redo to resurrect deletions.
fn retire_redo(p: &Path) -> Result<()> {
// The native file may not exist when the journal arrived without one (e.g.
// a `missing baseline` journal); it is still fine to retire the sidecar.
match std::fs::File::open(p) {
Ok(f) => f.sync_all()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
Err(e) => return Err(e.into()),
}
std::fs::remove_file(p.with_extension("rredo"))?;
std::fs::File::open(p.parent().unwrap())?.sync_all()?;
REDO_CACHE
.get_or_init(|| Mutex::new(RedoCache::new()))
.lock()
.map_err(|_| anyhow!("archive redo cache poisoned"))?
.remove(p);
Ok(())
}
/// True when the redo journal no longer matches its native baseline after an
/// external in-place write or replacement. In that state the journal is no
/// longer authoritative and must not be replayed over the native file.
fn is_untrusted_redo(error: &anyhow::Error) -> bool {
let text = format!("{error:#}");
text.contains("archive redo baseline changed in place")
|| text.contains("archive shorter than redo baseline")
|| text.contains("missing archive redo baseline")
|| text.contains("archive redo journal exceeds bound")
}
/// Retire redo before an atomic snapshot replacement or with an explicit offline
/// handoff (storage-checkpoint). A journal whose baseline was externally
/// rewritten in place can no longer be trusted: because the caller is about to
/// replace the native file (rewrite) or to hand control of it to a native
/// writer (checkpoint), the current durably synced native becomes authoritative
/// and the untrusted journal is retired rather than failing the operation.
fn prepare_rewrite(p: &Path) -> Result<()> {
match recover_redo(p) {
Ok(Some(_)) => retire_redo(p),
Ok(None) => Ok(()),
Err(error) if is_untrusted_redo(&error) => {
// The journal no longer matches its baseline. Treat the native file
// as authoritative, sync it, and retire the untrusted journal.
retire_redo(p)
}
Err(error) => Err(error),
}
}
fn scan_unlocked<F: FnMut(Entry) -> Result<()>>(
root: &Path,
host: &str,
@@ -203,7 +556,7 @@ fn scan_unlocked<F: FnMut(Entry) -> Result<()>>(
mut visit: F,
) -> Result<()> {
let path = storage::path(root, host, user, store).with_extension("list");
recover_append(&path)?;
recover_append_readonly(&path)?;
let file = match std::fs::File::open(&path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
@@ -212,14 +565,61 @@ fn scan_unlocked<F: FnMut(Entry) -> Result<()>>(
let mut reader = BufReader::new(file);
let mut line = Vec::new();
let mut record = Vec::new();
let decoder = RecordDecoder::new()?;
let mut decode = |data: &[u8]| -> Result<()> {
if let Some(entry) = decoder.decode(data)? {
visit(entry)?;
}
Ok(())
};
loop {
line.clear();
let n = (&mut reader)
.take(MAX_RECORD_BYTES as u64 + 1)
.read_until(b'\n', &mut line)?;
if n == 0 {
break;
}
anyhow::ensure!(
line.len() <= MAX_RECORD_BYTES,
"archive record line exceeds 16 MiB"
);
if line.starts_with(b"item") && !record.is_empty() {
decode(&record)?;
record.clear();
}
record.extend_from_slice(&line);
anyhow::ensure!(
record.len() <= MAX_RECORD_BYTES,
"archive record exceeds 16 MiB"
);
}
decode(&record)
}
const MAX_RECORD_BYTES: usize = 16 * 1024 * 1024;
/// One bounded Lua sandbox shared by the scan and endpoint readers.
struct RecordDecoder {
lua: mlua::Lua,
env: mlua::Table,
}
impl RecordDecoder {
fn new() -> Result<Self> {
let lua = crate::lua::bounded_vm().map_err(|e| anyhow!(e.to_string()))?;
let env = lua.create_table().map_err(|e| anyhow!(e.to_string()))?;
let mut decode = |data: &[u8]| -> Result<()> {
Ok(Self { lua, env })
}
fn decode(&self, data: &[u8]) -> Result<Option<Entry>> {
anyhow::ensure!(
data.len() <= MAX_RECORD_BYTES,
"archive record exceeds 16 MiB"
);
if data.iter().all(u8::is_ascii_whitespace) {
return Ok(());
return Ok(None);
}
let count = std::rc::Rc::new(std::cell::Cell::new(0));
lua.set_hook(
self.lua
.set_hook(
mlua::HookTriggers::new().every_nth_instruction(1000),
move |_, _| {
count.set(count.get() + 1);
@@ -232,39 +632,105 @@ fn scan_unlocked<F: FnMut(Entry) -> Result<()>>(
.map_err(|e| anyhow!(e.to_string()))?;
let mut chunk = b"local function item(v) return v end; return ".to_vec();
chunk.extend(data);
let value = lua
let value = self
.lua
.load(&chunk)
.set_mode(mlua::chunk::ChunkMode::Text)
.set_environment(env.clone())
.set_environment(self.env.clone())
.eval::<mlua::Value>()
.map_err(|e| anyhow!(e.to_string()))?;
visit(decode_entry(&storage::from_lua(value)?)?)?;
lua.gc_collect().map_err(|e| anyhow!(e.to_string()))?;
Ok(())
let entry = decode_entry(&storage::from_lua(value)?)?;
self.lua.gc_collect().map_err(|e| anyhow!(e.to_string()))?;
Ok(Some(entry))
}
}
/// Read just the first and last native records of a consistent archive snapshot.
/// This metadata operation deliberately does not validate intervening records.
/// Neither a missing nor a stale native binary index causes a history scan.
pub fn edges_store(
root: &Path,
host: &str,
user: Option<&str>,
store: &str,
) -> Result<Option<(Entry, Entry)>> {
let _life = LIFECYCLE
.read()
.map_err(|_| anyhow!("archive lifecycle lock poisoned"))?;
let lock = archive_lock(root, host, user, store)?;
let _guard = lock.lock().map_err(|_| anyhow!("archive lock poisoned"))?;
let path = storage::path(root, host, user, store).with_extension("list");
recover_append_readonly(&path)?;
let mut file = match std::fs::File::open(&path) {
Ok(file) => file,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
};
let decoder = RecordDecoder::new()?;
let first = match first_record(&mut BufReader::new(&mut file), &decoder)? {
Some(entry) => entry,
None => return Ok(None),
};
let length = file.metadata()?.len();
let mut window = 8192u64.min(length);
loop {
line.clear();
let n = (&mut reader)
.take(16 * 1024 * 1024 + 1)
.read_until(b'\n', &mut line)?;
if n == 0 {
break;
let start = length - window;
// Include one preceding byte to distinguish a real line boundary from
// an `item` prefix coincidentally at the beginning of the seek window.
let read_start = start.saturating_sub(1);
file.seek(SeekFrom::Start(read_start))?;
let mut tail = vec![0; (length - read_start) as usize];
file.read_exact(&mut tail)?;
let boundary = (0..tail.len()).rev().find(|&i| {
(i > 0 && tail[i - 1] == b'\n' || i == 0 && read_start == 0)
&& tail[i..].starts_with(b"item")
});
if let Some(boundary) = boundary {
let last = decoder
.decode(&tail[boundary..])?
.ok_or_else(|| anyhow!("empty archive endpoint"))?;
return Ok(Some((first, last)));
}
anyhow::ensure!(
line.len() <= 16 * 1024 * 1024,
"archive record line exceeds 16 MiB"
start > 0 && window < MAX_RECORD_BYTES as u64,
"archive last record exceeds 16 MiB or has no native item boundary"
);
if line.starts_with(b"item") && !record.is_empty() {
decode(&record)?;
window = (window * 2).min(MAX_RECORD_BYTES as u64).min(length);
}
}
fn first_record<R: BufRead>(reader: &mut R, decoder: &RecordDecoder) -> Result<Option<Entry>> {
let mut record = Vec::new();
loop {
// Inspect only the next line's marker before reading its contents: the
// second record may be enormous or malformed and is irrelevant here.
let mut prefix = Vec::new();
let n = (&mut *reader).take(4).read_until(b'\n', &mut prefix)?;
if n == 0 {
return decoder.decode(&record);
}
if prefix.starts_with(b"item") && !record.is_empty() {
if let Some(entry) = decoder.decode(&record)? {
return Ok(Some(entry));
}
record.clear();
}
record.extend_from_slice(&line);
record.extend_from_slice(&prefix);
anyhow::ensure!(
record.len() <= 16 * 1024 * 1024,
record.len() <= MAX_RECORD_BYTES,
"archive record exceeds 16 MiB"
);
if !prefix.ends_with(b"\n") {
let remaining = (MAX_RECORD_BYTES - record.len() + 1) as u64;
(&mut *reader)
.take(remaining)
.read_until(b'\n', &mut record)?;
anyhow::ensure!(
record.len() <= MAX_RECORD_BYTES,
"archive record exceeds 16 MiB"
);
}
decode(&record)
}
}
fn load_unlocked(root: &Path, host: &str, user: Option<&str>, store: &str) -> Result<Vec<Entry>> {
let mut out = vec![];
@@ -345,7 +811,7 @@ fn save_unlocked(
data.push_str(&encode_entry(e)?);
}
let p = storage::path(root, host, user, store).with_extension("list");
recover_append(&p)?;
prepare_rewrite(&p)?;
match std::fs::remove_file(p.with_extension("lidx")) {
Ok(()) => (),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
@@ -406,37 +872,57 @@ fn append_unlocked(
entry: &Entry,
) -> Result<()> {
let data = encode_entry(entry)?;
anyhow::ensure!(
data.len() <= MAX_RECORD_BYTES,
"archive record exceeds 16 MiB"
);
let path = storage::path(root, host, user, store).with_extension("list");
let parent = path.parent().unwrap();
std::fs::create_dir_all(parent)?;
recover_append(&path)?;
std::fs::create_dir_all(path.parent().unwrap())?;
let state = recover_redo(&path)?;
let mut opts = std::fs::OpenOptions::new();
opts.create(true).append(true);
opts.create(true).read(true).write(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts.open(&path)?;
let length = file.metadata()?.len();
let journal = path.with_extension("rappend");
storage::atomic_write(&journal, &length.to_le_bytes())?;
let mut state = match state {
Some(state) => state,
None => checkpoint_redo(&path)?,
};
if state.bytes >= REDO_LIMIT || state.frames >= REDO_FRAMES {
state = checkpoint_redo(&path)?;
}
match std::fs::remove_file(path.with_extension("lidx")) {
Ok(()) => (),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
Err(e) => return Err(e.into()),
}
let result = (|| -> Result<()> {
let mut frame = Vec::with_capacity(72 + data.len());
frame.extend_from_slice(&(data.len() as u64).to_le_bytes());
frame.extend_from_slice(&digest(&(data.len() as u64).to_le_bytes()));
frame.extend_from_slice(&digest(data.as_bytes()));
frame.extend_from_slice(data.as_bytes());
// Invalidate the cache before any write, including error paths. A failed
// sync is indeterminate and must be recovered, never blindly rolled back.
REDO_CACHE
.get_or_init(|| Mutex::new(RedoCache::new()))
.lock()
.map_err(|_| anyhow!("archive redo cache poisoned"))?
.remove(&path);
file.seek(SeekFrom::Start(state.length))?;
file.write_all(data.as_bytes())?;
file.sync_all()?;
std::fs::remove_file(&journal)?;
std::fs::File::open(parent)?.sync_all()?;
let mut journal = std::fs::OpenOptions::new()
.append(true)
.open(path.with_extension("rredo"))?;
journal.write_all(&frame)?;
journal.sync_all()?;
state.length += data.len() as u64;
state.bytes += frame.len() as u64;
state.frames += 1;
remember_redo(&path, &state)?;
Ok(())
})();
if result.is_err() {
recover_append(&path)?;
}
result
}
/// Owner is localpart, peer is bare JID. Caller enforces archive preferences.
pub fn archive_message(
@@ -501,6 +987,7 @@ fn retain_unlocked<F: FnMut(&Entry) -> bool>(
mut retain: F,
) -> Result<usize> {
let path = storage::path(root, host, user, store).with_extension("list");
recover_append_readonly(&path)?;
if !path.exists() {
return Ok(0);
}
@@ -528,6 +1015,7 @@ fn retain_unlocked<F: FnMut(&Entry) -> bool>(
return Ok(0);
}
file.sync_all()?;
prepare_rewrite(&path)?;
match std::fs::remove_file(path.with_extension("lidx")) {
Ok(()) => (),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
@@ -540,3 +1028,206 @@ fn retain_unlocked<F: FnMut(&Entry) -> bool>(
let _ = std::fs::remove_file(&temp);
result
}
#[cfg(test)]
mod redo_rewrite_tests {
use super::*;
fn fixture() -> (tempfile::TempDir, std::path::PathBuf) {
let root = tempfile::tempdir().unwrap();
append_new(
root.path(),
"example.org",
Some("alice"),
"archive",
Entry {
id: "committed-before-rewrite".into(),
when: 42.,
peer: String::new(),
xml: "<message><body>durable</body></message>".into(),
},
)
.unwrap();
let path = storage::path(root.path(), "example.org", Some("alice"), "archive")
.with_extension("list");
(root, path)
}
#[test]
fn crash_gap_after_redo_retirement_before_native_rename_keeps_old_snapshot() {
let (root, path) = fixture();
let expected = std::fs::read(&path).unwrap();
prepare_rewrite(&path).unwrap();
// Crash boundary: redo unlink and directory sync completed, replacement
// has not happened. Restart must retain the complete durable old list.
assert!(!path.with_extension("rredo").exists());
assert_eq!(std::fs::read(&path).unwrap(), expected);
let rows = load(root.path(), "example.org", "alice").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].id, "committed-before-rewrite");
}
#[test]
fn crash_gap_after_native_rename_has_no_old_redo_to_resurrect_deletions() {
let (root, path) = fixture();
prepare_rewrite(&path).unwrap();
storage::atomic_write(&path, b"").unwrap();
// Crash boundary: the atomic replacement committed. Recovery sees the
// new snapshot directly and cannot replay the removed old record.
assert!(!path.with_extension("rredo").exists());
assert!(
load(root.path(), "example.org", "alice")
.unwrap()
.is_empty()
);
}
/// Build an archive large enough to have rolled its redo journal, so the
/// baseline is non-empty (`base > 0`) and the anchor guard is meaningful.
fn rolled_fixture() -> (tempfile::TempDir, std::path::PathBuf) {
let root = tempfile::tempdir().unwrap();
// One more than REDO_FRAMES forces a redo rotation at frame 128, which
// snapshots the current native length as the new non-empty baseline.
for i in 0..=REDO_FRAMES as u64 {
append_new(
root.path(),
"example.org",
Some("alice"),
"archive",
Entry {
id: format!("roll-{i}"),
when: i as f64,
peer: String::new(),
xml: "<message><body>x</body></message>".into(),
},
)
.unwrap();
}
let path = storage::path(root.path(), "example.org", Some("alice"), "archive")
.with_extension("list");
// Recovery must see a non-empty baseline (rotation happened).
let data = std::fs::read(path.with_extension("rredo")).unwrap();
let base = u64::from_le_bytes(data[8..16].try_into().unwrap());
assert!(base > 0, "fixture should have rolled a non-empty baseline");
(root, path)
}
/// An external writer that rewrote the native archive in place over the
/// committed baseline without a handoff leaves the redo journal untrusted.
fn corrupt_baseline(path: &std::path::Path) -> u64 {
let data = std::fs::read(path.with_extension("rredo")).unwrap();
let base = u64::from_le_bytes(data[8..16].try_into().unwrap());
let mut native = std::fs::read(path).unwrap();
// Flip the trailing bytes of the committed baseline so its anchor changes.
let start = (base.saturating_sub(64)) as usize;
for i in start..native.len().min(base as usize) {
native[i] ^= 0x5a;
}
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(false)
.open(path)
.unwrap();
std::io::Write::write_all(&mut file, &native).unwrap();
file.sync_all().unwrap();
base
}
#[test]
fn runtime_append_recovers_by_retiring_untrusted_redo_baseline() {
let (root, path) = rolled_fixture();
corrupt_baseline(&path);
// Prosody never keeps a redo journal: it just appends to the native
// file. When an external writer invalidated our baseline, behave the
// same way instead of blocking the archive. A live append retires the
// untrusted journal, adopts the synced native as authoritative, writes
// the record, and recreates a fresh journal on the new baseline.
let id = append_new(
root.path(),
"example.org",
Some("alice"),
"archive",
Entry {
id: "post-corruption".into(),
when: 100000.,
peer: String::new(),
xml: "<message><body>new</body></message>".into(),
},
)
.expect("live append must recover by retiring the untrusted baseline");
assert_eq!(id, "post-corruption");
// A fresh journal on the current native length is recreated.
assert!(path.with_extension("rredo").exists());
}
#[test]
fn untrusted_redo_baseline_is_retired_once_and_recovers() {
let (root, path) = rolled_fixture();
corrupt_baseline(&path);
let make_entry = || Entry {
id: "post-corruption".into(),
when: 100000.,
peer: String::new(),
xml: "<message><body>new</body></message>".into(),
};
// The first access performs the full journal read+validation, then
// retires the untrusted journal exactly once.
append_new(
root.path(),
"example.org",
Some("alice"),
"archive",
make_entry(),
)
.expect("first append retires the untrusted journal");
// Subsequent appends are now fast and succeed on a healthy journal.
for _ in 0..20 {
append_new(
root.path(),
"example.org",
Some("alice"),
"archive",
make_entry(),
)
.expect("archive stays writable after recovery");
}
}
#[test]
fn offline_checkpoint_retires_untrusted_redo_baseline() {
let (root, path) = rolled_fixture();
let base = corrupt_baseline(&path);
assert!(base > 0);
assert!(path.with_extension("rredo").exists());
// An external rewrite removed the trusted baseline; an explicit offline
// handoff accepts the current synced native as authoritative and retires
// the untrusted journal so the archive becomes writable again.
let retired = checkpoint_all(root.path()).unwrap();
assert_eq!(retired, 1);
assert!(
!path.with_extension("rredo").exists(),
"offline checkpoint must retire the untrusted journal"
);
// The archive accepts new appends: with the journal retired the current
// (now authoritative) native length becomes the fresh baseline.
let id = append_new(
root.path(),
"example.org",
Some("alice"),
"archive",
Entry {
id: "after-handoff".into(),
when: 200000.,
peer: String::new(),
xml: "<message><body>new</body></message>".into(),
},
)
.unwrap();
assert_eq!(id, "after-handoff");
// The original baseline tail was externally rewritten, so the native
// holds whatever the external writer put there; checkpoint only retires
// the journal (handoff), it does not try to re-encode damaged records.
// A fresh valid journal is (re)created on the next append.
assert!(path.with_extension("rredo").exists());
}
}
+193 -6
View File
@@ -1,6 +1,17 @@
//! Write buffering matching the supplied Prosody mod_csi_simple importance policy.
use crate::{protocol, wire::escape};
use std::time::{Duration, Instant};
/// Runtime defaults: visible encrypted chat and delivery/read confirmation must
/// not remain asleep merely because a client omitted a fallback body or EME.
/// An explicit csi_important_payloads list replaces these defaults.
pub const DEFAULT_IMPORTANT_PAYLOADS: &[&str] = &[
"{eu.siacs.conversations.axolotl}encrypted",
"{urn:xmpp:omemo:2}encrypted",
"{urn:xmpp:receipts}received",
"{urn:xmpp:chat-markers:0}received",
"{urn:xmpp:chat-markers:0}displayed",
"{urn:xmpp:chat-markers:0}acknowledged",
];
#[derive(Clone, Debug, Default)]
pub struct Buffer {
inactive: bool,
@@ -11,7 +22,67 @@ pub struct Buffer {
pub resume_delay: Duration,
pub important_payloads: Vec<String>,
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub(crate) struct DurableBuffer {
inactive: bool,
queue: Vec<String>,
limit: usize,
resume_delay_ms: u64,
important_payloads: Vec<String>,
}
/// Output awaiting persistence. Rollback reuses the drained queue without cloning it.
pub(crate) struct PendingOutput {
output: Vec<String>,
previous: Option<QueueChange>,
}
struct QueueChange {
queued: usize,
bytes: usize,
flushing_until: Option<Instant>,
appended: bool,
}
impl PendingOutput {
pub(crate) fn durable_changed(&self) -> bool {
self.previous.is_some()
}
pub(crate) fn commit(self) -> Vec<String> {
self.output
}
pub(crate) fn rollback(mut self, buffer: &mut Buffer) {
if let Some(previous) = self.previous {
if previous.appended {
buffer.queue.pop();
} else {
self.output.truncate(previous.queued);
buffer.queue = self.output;
}
buffer.bytes = previous.bytes;
buffer.flushing_until = previous.flushing_until;
}
}
}
impl Buffer {
pub(crate) fn durable(&self) -> DurableBuffer {
DurableBuffer {
inactive: self.inactive,
queue: self.queue.clone(),
limit: self.limit,
resume_delay_ms: self.resume_delay.as_millis().min(u64::MAX as u128) as u64,
important_payloads: self.important_payloads.clone(),
}
}
pub(crate) fn from_durable(value: DurableBuffer) -> Self {
Self {
inactive: value.inactive,
flushing_until: None,
bytes: value.queue.iter().map(String::len).sum(),
queue: value.queue,
limit: value.limit,
resume_delay: Duration::from_millis(value.resume_delay_ms),
important_payloads: value.important_payloads,
}
}
pub fn configure(&mut self, limit: usize, delay: Duration, payloads: Vec<String>) {
self.limit = limit.max(1);
self.resume_delay = delay;
@@ -34,17 +105,47 @@ impl Buffer {
}
/// Client input flushes held output without declaring the client active.
pub fn activity(&mut self, now: Instant) -> Vec<String> {
self.pending_activity(now).commit()
}
pub(crate) fn pending_activity(&mut self, now: Instant) -> PendingOutput {
if self.inactive && !self.queue.is_empty() {
let previous = self.queue_change(false);
self.flushing_until = Some(now + self.resume_delay);
self.drain()
PendingOutput {
output: self.drain(),
previous: Some(previous),
}
} else {
Vec::new()
PendingOutput {
output: Vec::new(),
previous: None,
}
}
}
pub fn output(&mut self, xml: String, owner: &str, now: Instant) -> Vec<String> {
if !self.inactive || self.flushing_until.is_some_and(|until| now < until) {
return vec![xml];
self.pending_output(xml, owner, now).commit()
}
fn queue_change(&self, appended: bool) -> QueueChange {
QueueChange {
queued: self.queue.len(),
bytes: self.bytes,
flushing_until: self.flushing_until,
appended,
}
}
pub(crate) fn pending_output(
&mut self,
xml: String,
owner: &str,
now: Instant,
) -> PendingOutput {
if !self.inactive || self.flushing_until.is_some_and(|until| now < until) {
return PendingOutput {
output: vec![xml],
previous: None,
};
}
let mut previous = self.queue_change(false);
self.flushing_until = None;
if self.queue.len() >= self.limit.max(1)
|| self.bytes.saturating_add(xml.len()) > 4 * 1024 * 1024
@@ -53,12 +154,19 @@ impl Buffer {
self.flushing_until = Some(now + self.resume_delay);
let mut output = self.drain();
output.push(xml);
output
PendingOutput {
output,
previous: (previous.queued > 0).then_some(previous),
}
} else {
previous.appended = true;
let xml = delayed(&xml, owner);
self.bytes += xml.len();
self.queue.push(xml);
Vec::new()
PendingOutput {
output: Vec::new(),
previous: Some(previous),
}
}
}
}
@@ -170,3 +278,82 @@ fn delayed(xml: &str, owner: &str) -> String {
}
output
}
#[cfg(test)]
mod checkpoint_tests {
use super::*;
#[test]
fn only_queue_changes_require_a_checkpoint() {
let now = Instant::now();
let mut buffer = Buffer::default();
buffer.configure(2, Duration::from_secs(5), vec![]);
assert!(!buffer.pending_activity(now).durable_changed());
assert!(
!buffer
.pending_output("<presence/>".into(), "a@example", now)
.durable_changed()
);
buffer.set_inactive(true);
// An important stanza starts the transient grace period but changes no durable queue.
let output = buffer.pending_output(
"<message><body>hi</body></message>".into(),
"a@example",
now,
);
assert!(!output.durable_changed());
assert_eq!(output.commit().len(), 1);
assert!(
!buffer
.pending_output("<presence/>".into(), "a@example", now)
.durable_changed()
);
let later = now + Duration::from_secs(6);
let output = buffer.pending_output("<presence/>".into(), "a@example", later);
assert!(output.durable_changed());
assert!(output.commit().is_empty());
let output = buffer.pending_activity(later);
assert!(output.durable_changed());
assert_eq!(output.commit().len(), 1);
assert!(!buffer.pending_activity(later).durable_changed());
}
#[test]
fn failed_queue_checkpoints_restore_append_and_drain_without_copying_queue() {
let now = Instant::now();
let mut buffer = Buffer::default();
buffer.configure(2, Duration::from_secs(5), vec![]);
buffer.set_inactive(true);
buffer.output("<presence id='held'/>".into(), "a@example", now);
let before = serde_json::to_value(buffer.durable()).unwrap();
let bytes = buffer.bytes;
let until = buffer.flushing_until;
for operation in 0..3 {
let output = match operation {
0 => buffer.pending_output("<presence id='new'/>".into(), "a@example", now),
1 => buffer.pending_output(
"<message><body>hi</body></message>".into(),
"a@example",
now,
),
_ => buffer.pending_activity(now),
};
assert!(output.durable_changed());
output.rollback(&mut buffer);
assert_eq!(serde_json::to_value(buffer.durable()).unwrap(), before);
assert_eq!(buffer.bytes, bytes);
assert_eq!(buffer.flushing_until, until);
}
// A successful retry still sends the held stanza before the new message.
let output = buffer
.pending_output(
"<message><body>hi</body></message>".into(),
"a@example",
now,
)
.commit();
assert_eq!(output.len(), 2);
assert!(output[0].contains("id='held'"));
assert!(output[1].contains("<body>hi</body>"));
}
}
+2
View File
@@ -51,3 +51,5 @@ pub mod http_altconnect;
pub mod object_store;
pub mod upload_migration;
pub mod session_store;
+20 -1
View File
@@ -12,6 +12,25 @@ async fn main() {
async fn run() -> Result<()> {
let args: Vec<_> = env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("storage-checkpoint") if args.len() == 2 => {
let (c, domain) = rust_xmpp::server::settings_config(Path::new(&args[1]))?;
let data = Path::new(
c.option(&domain, "data_path")
.and_then(|v| v.as_str())
.context("data_path missing")?,
);
let _data_guard = rust_xmpp::data_lock::DataDirectoryGuard::acquire(data)?;
let archives = rust_xmpp::archive::checkpoint_all(data)?;
let sm_path = data
.join(rust_xmpp::storage::encode_name(&domain))
.join("rust_smacks");
let sessions = if sm_path.try_exists()? {
rust_xmpp::session_store::checkpoint_exports(&sm_path)?
} else {
0
};
println!("Durable native archive handoff: {archives}; SM JSON exports: {sessions}");
}
Some("upload-migrate") if args.len() == 5 || args.len() == 6 => {
anyhow::ensure!(
args[3] == "--component" && (args.len() == 5 || args[5] == "--execute"),
@@ -121,7 +140,7 @@ async fn run() -> Result<()> {
println!("Account created: {}@{}", args[2], domain);
}
Some("--help") | None => println!(
"rust-xmpp upload-migrate OLD_CONFIG NEW_CONFIG --component HOST [--execute]\nrust-xmpp check-config PATH\nrust-xmpp convert PATH json|yaml|toml OUTPUT\nrust-xmpp --version\n\nExperimental runtime: serve CONFIG --experimental\ncreate-user CONFIG USER --password-stdin\ncheck [CONFIG] (running server healthcheck)\nshell CONFIG COMMAND\n\nSTARTTLS, SCRAM/PLAIN, local messaging, roster, MAM, MUC, PEP/pubsub, stream management, push, federation and HTTP transports are implemented with limitations; see docs/COMPATIBILITY.md."
"rust-xmpp upload-migrate OLD_CONFIG NEW_CONFIG --component HOST [--execute]\nrust-xmpp check-config PATH\nrust-xmpp convert PATH json|yaml|toml OUTPUT\nrust-xmpp --version\nrust-xmpp storage-checkpoint CONFIG (offline native handoff/downgrade)\n\nExperimental runtime: serve CONFIG --experimental\ncreate-user CONFIG USER --password-stdin\ncheck [CONFIG] (running server healthcheck)\nshell CONFIG COMMAND\n\nSTARTTLS, SCRAM/PLAIN, local messaging, roster, MAM, MUC, PEP/pubsub, stream management, push, federation and HTTP transports are implemented with limitations; see docs/COMPATIBILITY.md."
),
_ => bail!("invalid command; use --help"),
}
+6 -10
View File
@@ -274,16 +274,12 @@ pub fn handle_archive_query_with_options(
let (iq, q) = request;
let typ = iq.attribute("type").unwrap_or("");
if q.tag_name().name() == "metadata" && typ == "get" {
let mut first = None;
let mut last = None;
archive::scan_store(root, host, user, store, |e| {
if first.is_none() {
first = Some((e.id.clone(), e.when));
}
last = Some((e.id, e.when));
Ok(())
})
.context("MAM archive read failed")?;
let edges =
archive::edges_store(root, host, user, store).context("MAM archive read failed")?;
let (first, last) = match edges {
Some((first, last)) => (Some((first.id, first.when)), Some((last.id, last.when))),
None => (None, None),
};
let mut payload = format!("<metadata xmlns='{NS}'>");
if let Some((id, when)) = first {
payload.push_str(&format!(
+769 -55
View File
File diff suppressed because it is too large. Load diff
+319
View File
@@ -0,0 +1,319 @@
//! The two native MUC component administration commands. Authority belongs to
//! configured component administrators and is checked again on every step.
use super::*;
const COMMANDS: &str = "http://jabber.org/protocol/commands";
const DESTROY: &str = "http://prosody.im/protocol/muc#destroy";
const AFFILIATION: &str = "http://prosody.im/protocol/muc#set-affiliation";
const INFO: &str = "http://jabber.org/protocol/disco#info";
const ITEMS: &str = "http://jabber.org/protocol/disco#items";
pub(super) fn period(value: Option<&Value>) -> Option<f64> {
let seconds = if let Some(number) = value.and_then(Value::as_f64) {
number
} else {
let text = value?.as_str()?.trim();
let split = text
.find(|c: char| !(c.is_ascii_digit() || c == '.' || c == '+' || c == '-'))
.unwrap_or(text.len());
let number: f64 = text[..split].trim().parse().ok()?;
let unit = text[split..].trim();
number
* match unit {
"" | "s" | "second" | "seconds" => 1.0,
"m" | "minute" | "minutes" => 60.0,
"h" | "hour" | "hours" => 3600.0,
"d" | "day" | "days" => 86400.0,
"w" | "week" | "weeks" => 604800.0,
_ => return None,
}
};
(seconds.is_finite() && seconds >= 0.0).then_some(seconds)
}
pub(super) fn registration_form(nick: Option<&str>) -> String {
format!(
"<x xmlns='jabber:x:data' type='{}'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/muc#register</value></field><field var='muc#register_roomnick' type='text-single'>{}<required/></field></x>",
if nick.is_some() { "result" } else { "form" },
nick.map(|v| format!("<value>{}</value>", escape(v)))
.unwrap_or_default()
)
}
fn command_reply(
stanza: roxmltree::Node,
node: &str,
session: &str,
status: &str,
body: &str,
) -> ModuleResult {
ModuleResult::reply(iq_result(
stanza,
&format!(
"<command xmlns='{COMMANDS}' node='{}' sessionid='{}' status='{status}'>{body}</command>",
escape(node),
escape(session)
),
))
}
fn form(muc: &Muc, node: &str) -> String {
let fields = if node == DESTROY {
let rooms = muc
.rooms
.keys()
.map(|room| {
format!(
"<option label='{}'><value>{}</value></option>",
escape(room),
escape(room)
)
})
.collect::<String>();
format!(
"<field var='rooms' type='list-multi' label='Rooms to destroy'><required/>{rooms}</field>"
)
} else {
"<field var='room' type='jid-single' label='Room'><required/></field><field var='jid' type='jid-single' label='JID'><required/></field><field var='affiliation' type='list-single' label='Affiliation'><required/><option><value>owner</value></option><option><value>admin</value></option><option><value>member</value></option><option><value>none</value></option><option><value>outcast</value></option></field><field var='reason' type='text-single' label='Reason'/>".to_owned()
};
format!(
"<actions execute='complete'><complete/></actions><x xmlns='jabber:x:data' type='form'><title>{}</title><field var='FORM_TYPE' type='hidden'><value>{node}</value></field>{fields}</x>",
if node == DESTROY {
"Destroy rooms"
} else {
"Set affiliation"
}
)
}
pub(super) fn handle(
muc: &mut Muc,
jid: &str,
stanza: roxmltree::Node,
) -> Result<Option<ModuleResult>> {
if stanza.tag_name().name() != "iq" {
return Ok(None);
}
let Some(query) = stanza.children().find(|n| n.is_element()) else {
return Ok(None);
};
let ns = query.tag_name().namespace().unwrap_or("");
let node = query.attribute("node").unwrap_or("");
let is_admin = muc
.component_admins
.iter()
.any(|admin| bare(admin) == bare(jid));
if matches!(ns, INFO | ITEMS)
&& (node == COMMANDS || matches!(node, DESTROY | AFFILIATION))
&& stanza.attribute("type") == Some("get")
{
let body = if ns == ITEMS && node == COMMANDS {
if is_admin {
format!(
"<item jid='{}' node='{DESTROY}' name='Destroy Rooms'/><item jid='{}' node='{AFFILIATION}' name='Set affiliation in room'/>",
escape(&muc.domain),
escape(&muc.domain)
)
} else {
String::new()
}
} else if !is_admin && node != COMMANDS {
return Ok(Some(ModuleResult::reply(iq_error(stanza, "forbidden"))));
} else if ns == INFO {
format!(
"<identity category='automation' type='{}'/><feature var='{COMMANDS}'/><feature var='jabber:x:data'/>",
if node == COMMANDS {
"command-list"
} else {
"command-node"
}
)
} else {
String::new()
};
return Ok(Some(ModuleResult::reply(iq_result(
stanza,
&format!("<query xmlns='{ns}' node='{}'>{body}</query>", escape(node)),
))));
}
if ns != COMMANDS || query.tag_name().name() != "command" {
return Ok(None);
}
if !is_admin {
return Ok(Some(ModuleResult::reply(iq_error(stanza, "forbidden"))));
}
if stanza.attribute("type") != Some("set") {
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
}
if !matches!(node, DESTROY | AFFILIATION) {
return Ok(Some(ModuleResult::reply(iq_error(
stanza,
"item-not-found",
))));
}
muc.command_sessions
.retain(|_, (_, _, started)| started.elapsed() < std::time::Duration::from_secs(600));
let session = query.attribute("sessionid");
let action = query.attribute("action").unwrap_or("execute");
if session.is_none() {
if action != "execute"
|| query
.children()
.any(|n| n.has_tag_name(("jabber:x:data", "x")))
{
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
}
if muc.command_sessions.len() >= 1000 {
return Ok(Some(ModuleResult::reply(iq_error(
stanza,
"resource-constraint",
))));
}
let id = uuid::Uuid::new_v4().to_string();
muc.command_sessions.insert(
id.clone(),
(
bare(jid).to_owned(),
node.to_owned(),
std::time::Instant::now(),
),
);
return Ok(Some(command_reply(
stanza,
node,
&id,
"executing",
&form(muc, node),
)));
}
let session = session.unwrap();
if !muc
.command_sessions
.get(session)
.is_some_and(|(owner, command, _)| owner == bare(jid) && command == node)
{
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
}
if action == "cancel" {
muc.command_sessions.remove(session);
return Ok(Some(command_reply(stanza, node, session, "canceled", "")));
}
if !matches!(action, "execute" | "complete") {
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
}
let Some(data) = query
.children()
.find(|n| n.has_tag_name(("jabber:x:data", "x")) && n.attribute("type") == Some("submit"))
else {
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
};
let values = |name: &str| {
data.children()
.filter(|f| {
f.has_tag_name(("jabber:x:data", "field")) && f.attribute("var") == Some(name)
})
.flat_map(|f| {
f.children()
.filter(|v| v.has_tag_name(("jabber:x:data", "value")))
.filter_map(|v| v.text())
})
.collect::<Vec<_>>()
};
if values("FORM_TYPE") != vec![node] {
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
}
let mut out = ModuleResult::default();
if node == DESTROY {
let rooms = values("rooms");
if rooms.is_empty()
|| rooms
.iter()
.any(|room| bare(room) != *room || !room.ends_with(&format!("@{}", muc.domain)))
{
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
}
for name in rooms {
muc.load(name)?;
if !muc.rooms.contains_key(name) {
continue;
}
// Only this internal, ACL-checked command bypasses room ownership.
let packet = format!(
"<iq type='set' to='{}'><query xmlns='{OWNER}'><destroy/></query></iq>",
escape(name)
);
let doc = roxmltree::Document::parse(&packet)?;
if let Some(mut destroyed) =
muc.handle_inner_authorized(jid, doc.root_element(), true)?
{
out.deliveries.append(&mut destroyed.deliveries);
}
}
} else {
let rooms = values("room");
let targets = values("jid");
let affiliations = values("affiliation");
if rooms.len() != 1 || targets.len() != 1 || affiliations.len() != 1 {
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
}
let name = rooms[0];
let target = targets[0];
let affiliation = affiliations[0];
if bare(name) != name
|| !name.ends_with(&format!("@{}", muc.domain))
|| crate::account_modules::normalize_bare_jid(bare(target)).is_err()
|| !matches!(
affiliation,
"none" | "member" | "admin" | "owner" | "outcast"
)
{
return Ok(Some(ModuleResult::reply(iq_error(stanza, "bad-request"))));
}
muc.load(name)?;
let Some(room) = muc.rooms.get_mut(name) else {
return Ok(Some(command_reply(
stanza,
node,
session,
"canceled",
"<note type='error'>No such room</note>",
)));
};
let reason = values("reason").first().copied().unwrap_or("");
let packet = format!(
"<iq type='set'><query xmlns='{ADMIN}'><item jid='{}' affiliation='{affiliation}'><reason>{}</reason></item></query></iq>",
escape(&crate::account_modules::normalize_bare_jid(bare(target))?),
escape(reason)
);
let doc = roxmltree::Document::parse(&packet)?;
let iq = doc.root_element();
let mut applied = ModuleResult::default();
admin_with_authority(
room,
name,
jid,
iq,
iq.children().find(|n| n.is_element()).unwrap(),
&mut applied,
true,
);
if applied.replies.iter().any(|p| p.contains("type='error'")) {
return Ok(Some(command_reply(
stanza,
node,
session,
"canceled",
"<note type='error'>Affiliation change failed</note>",
)));
}
out.deliveries.append(&mut applied.deliveries);
muc.save(name)?;
}
muc.command_sessions.remove(session);
out.replies.append(
&mut command_reply(
stanza,
node,
session,
"completed",
"<note type='info'>Command completed</note>",
)
.replies,
);
Ok(Some(out))
}
+59 -5
View File
@@ -66,14 +66,14 @@ pub struct RoomHistory {
impl RoomHistory {
pub fn new(capacity: usize) -> Self {
Self {
capacity,
capacity: capacity.min(4096),
default_messages: 20,
entries: VecDeque::new(),
}
}
pub fn set_capacity(&mut self, capacity: usize) {
self.capacity = capacity;
while self.entries.len() > capacity {
self.capacity = capacity.min(4096);
while self.entries.len() > self.capacity {
self.entries.pop_front();
}
}
@@ -90,7 +90,7 @@ impl RoomHistory {
/// Add one room broadcast, before per-recipient routing. Subject-only changes
/// are sent separately on join and do not consume discussion history slots.
pub fn push(&mut self, stanza: &str, timestamp: i64) {
if self.capacity == 0 {
if self.capacity == 0 || stanza.len() > 16 * 1024 {
return;
}
let Ok(doc) = roxmltree::Document::parse(stanza) else {
@@ -132,7 +132,7 @@ impl RoomHistory {
}
/// Restore a snapshot stanza that already carries its original delay.
pub fn restore_last_message(&mut self, stanza: &str, timestamp: i64) {
if self.capacity == 0 {
if self.capacity == 0 || stanza.len() > 16 * 1024 {
return;
}
let Ok(doc) = roxmltree::Document::parse(stanza) else {
@@ -148,6 +148,60 @@ impl RoomHistory {
});
}
/// Lossless bounded crash snapshot, including original delay stamps.
pub fn checkpoint(&self) -> crate::storage::LuaValue {
use crate::storage::LuaValue as V;
let mut value = V::table();
for (i, entry) in self.entries.iter().enumerate() {
let mut item = V::table();
item.set("stanza", entry.stanza.clone().into());
item.set("timestamp", V::Integer(entry.timestamp));
value.set_key(V::Integer(i as i64 + 1), item);
}
value
}
pub fn restore_checkpoint(&mut self, value: &crate::storage::LuaValue) {
self.entries.clear();
// Lua pairs iteration is not sequence order, including numeric keys.
let mut ordered = value
.entries()
.iter()
.filter_map(|(key, item)| {
key.as_i64()
.filter(|index| *index > 0)
.map(|index| (index, item))
})
.collect::<Vec<_>>();
ordered.sort_by_key(|(index, _)| *index);
for (_, item) in ordered.into_iter().take(4096) {
let Some(stanza) = item
.get("stanza")
.and_then(crate::storage::LuaValue::as_str)
else {
continue;
};
let Some(timestamp) = item
.get("timestamp")
.and_then(crate::storage::LuaValue::as_i64)
else {
continue;
};
if stanza.len() > 16 * 1024 {
continue;
}
let Ok(doc) = roxmltree::Document::parse(stanza) else {
continue;
};
if is_historic(doc.root_element()) {
self.entries.push_back(Entry {
stanza: stanza.into(),
timestamp,
});
}
}
self.set_capacity(self.capacity);
}
pub fn replay(&self, _room: &str, to: &str, presence: &str, now: i64) -> Vec<String> {
let request = HistoryRequest::parse(presence, now);
let max = request.maxstanzas.unwrap_or_else(|| {
+138 -21
View File
@@ -57,7 +57,7 @@ fn safe_payload(v: &V) -> String {
if child
.get("attr")
.and_then(|a| field(a, "xmlns"))
.is_some_and(|ns| ns != "jabber:client")
.is_some_and(|ns| !matches!(ns, "" | "jabber:client"))
{
continue;
}
@@ -71,6 +71,17 @@ fn safe_payload(v: &V) -> String {
}
out
}
/// Keep the crash image transport-independent and exclude passwords/extensions.
pub fn sanitize_presence_payload(payload: &str) -> String {
if payload.len() > MAX_PRESENCE {
return String::new();
}
let packet = format!("<presence>{payload}</presence>");
let Ok(doc) = roxmltree::Document::parse(&packet) else {
return String::new();
};
safe_payload(&crate::storage::stanza_value(doc.root_element()))
}
/// Decode Prosody's state store (and old frozen room maps) without trusting roles,
/// primary-session pointers, or unvalidated stanza extensions.
pub fn decode_room(room: &str, value: &V) -> Result<Vec<OccupantSnapshot>> {
@@ -235,11 +246,29 @@ pub fn save_room_checkpoint(
}
/// Dormant claims have a fixed restart deadline; probes never extend it.
#[derive(Default)]
#[derive(Clone)]
pub struct PendingOccupants {
rooms: BTreeMap<String, (u64, Vec<OccupantSnapshot>)>,
deadlines: BTreeMap<(String, String), u64>,
grace_seconds: u64,
}
impl Default for PendingOccupants {
fn default() -> Self {
Self {
rooms: BTreeMap::new(),
deadlines: BTreeMap::new(),
grace_seconds: RECOVERY_SECONDS,
}
}
}
impl PendingOccupants {
pub fn set_grace_seconds(&mut self, seconds: u64) {
self.grace_seconds = seconds.clamp(1, 7 * 86400);
}
pub fn grace_seconds(&self) -> u64 {
self.grace_seconds
}
pub fn insert(
&mut self,
room: String,
@@ -252,8 +281,15 @@ impl PendingOccupants {
<= MAX_OCCUPANTS,
"pending recovery limit"
);
self.deadlines.retain(|(name, _), _| name != &room);
for occupant in &occupants {
self.deadlines.insert(
(room.clone(), occupant.jid.clone()),
now.saturating_add(self.grace_seconds),
);
}
self.rooms
.insert(room, (now.saturating_add(RECOVERY_SECONDS), occupants));
.insert(room, (now.saturating_add(self.grace_seconds), occupants));
Ok(())
}
/// Preserve a checkpoint's original deadline over repeated process restarts.
@@ -264,7 +300,7 @@ impl PendingOccupants {
saved_at: u64,
now: u64,
) -> Result<()> {
if saved_at > now || saved_at.saturating_add(RECOVERY_SECONDS) <= now {
if saved_at > now || saved_at.saturating_add(self.grace_seconds) <= now {
return Ok(());
}
self.insert(room, occupants, saved_at)
@@ -285,6 +321,8 @@ impl PendingOccupants {
.iter()
.position(|o| o.jid == authenticated_jid && o.nick == nick)?;
let mut claim = occupants.remove(i);
self.deadlines
.remove(&(room.into(), authenticated_jid.into()));
claim.jid = authenticated_jid.into();
Some(claim)
}
@@ -306,10 +344,61 @@ impl PendingOccupants {
})
.collect()
}
pub fn checkpoint_members(&mut self, room: &str, now: u64) -> Vec<(u64, OccupantSnapshot)> {
self.expire(now);
self.rooms
.get(room)
.map(|(_, occupants)| {
occupants
.iter()
.map(|o| {
(
self.deadlines
.get(&(room.into(), o.jid.clone()))
.copied()
.unwrap_or(now)
.saturating_sub(self.grace_seconds),
o.clone(),
)
})
.collect()
})
.unwrap_or_default()
}
pub fn insert_checkpoint_members(
&mut self,
room: String,
members: Vec<(u64, OccupantSnapshot)>,
now: u64,
) -> Result<()> {
let members = members
.into_iter()
.filter(|(at, _)| {
at.saturating_add(self.grace_seconds) > now
&& at.saturating_add(self.grace_seconds) <= now.saturating_add(7 * 86400)
})
.collect::<Vec<_>>();
let Some(saved_at) = members.iter().map(|(at, _)| *at).min() else {
return Ok(());
};
self.insert(
room.clone(),
members.iter().map(|(_, o)| o.clone()).collect(),
saved_at,
)?;
for (at, o) in members {
self.deadlines
.insert((room.clone(), o.jid), at.saturating_add(self.grace_seconds));
}
Ok(())
}
pub fn room_snapshot(&mut self, room: &str, now: u64) -> Option<(u64, Vec<OccupantSnapshot>)> {
self.expire(now);
self.rooms.get(room).map(|(deadline, occupants)| {
(deadline.saturating_sub(RECOVERY_SECONDS), occupants.clone())
(
deadline.saturating_sub(self.grace_seconds),
occupants.clone(),
)
})
}
/// Read-only membership probe; deadline-sensitive callers use contains_room.
@@ -322,22 +411,31 @@ impl PendingOccupants {
pub fn identities(&self, now: u64) -> Vec<(String, String)> {
self.rooms
.iter()
.filter(|(_, (deadline, _))| *deadline > now)
.flat_map(|(room, (_, occupants))| {
occupants.iter().map(|o| (room.clone(), o.jid.clone()))
occupants
.iter()
.filter(|o| {
self.deadlines
.get(&(room.clone(), o.jid.clone()))
.is_some_and(|deadline| *deadline > now)
})
.map(|o| (room.clone(), o.jid.clone()))
})
.collect()
}
/// A dormant nickname remains owned by its authenticated bare account until
/// claim expiry; a reused resource belonging to another account cannot take it.
pub fn reserved_by(&self, room: &str, nick: &str, now: u64) -> Option<String> {
let (deadline, occupants) = self.rooms.get(room)?;
if *deadline <= now {
return None;
}
let (_, occupants) = self.rooms.get(room)?;
occupants
.iter()
.find(|o| o.nick == nick)
.find(|o| {
o.nick == nick
&& self
.deadlines
.get(&(room.into(), o.jid.clone()))
.is_some_and(|deadline| *deadline > now)
})
.map(|o| bare(&o.jid).to_owned())
}
pub fn contains_room(&mut self, room: &str, now: u64) -> bool {
@@ -346,17 +444,36 @@ impl PendingOccupants {
.get(room)
.is_some_and(|(_, occupants)| !occupants.is_empty())
}
pub fn expire(&mut self, now: u64) -> usize {
let mut removed = 0;
self.rooms.retain(|_, (deadline, occupants)| {
if *deadline <= now {
removed += occupants.len();
false
} else {
true
pub fn remove_room(&mut self, room: &str) {
self.rooms.remove(room);
self.deadlines.retain(|(name, _), _| name != room);
}
pub fn remove_jid(&mut self, jid: &str) {
self.deadlines.retain(|(_, identity), _| identity != jid);
for (_, occupants) in self.rooms.values_mut() {
occupants.retain(|o| o.jid != jid);
}
}
pub fn expire(&mut self, now: u64) -> usize {
let before = self.len();
let deadlines = &self.deadlines;
self.rooms.retain(|room, (deadline, occupants)| {
occupants.retain(|o| {
deadlines
.get(&(room.clone(), o.jid.clone()))
.is_some_and(|until| *until > now)
});
removed
if let Some(first) = occupants
.iter()
.filter_map(|o| deadlines.get(&(room.clone(), o.jid.clone())))
.min()
{
*deadline = *first;
}
!occupants.is_empty()
});
self.deadlines.retain(|_, until| *until > now);
before - self.len()
}
pub fn len(&self) -> usize {
self.rooms.values().map(|(_, v)| v.len()).sum()
+3
View File
@@ -2,6 +2,8 @@ use crate::wire::escape;
#[derive(Default, Debug)]
pub struct ModuleResult {
pub replies: Vec<String>,
/// A final response after durable session revocation; never retained for resume.
pub terminal_reply: bool,
/// (destination full or bare JID, serialized stanza)
pub deliveries: Vec<(String, String)>,
}
@@ -9,6 +11,7 @@ impl ModuleResult {
pub fn reply(xml: String) -> Self {
Self {
replies: vec![xml],
terminal_reply: false,
deliveries: vec![],
}
}
+2
View File
@@ -1253,6 +1253,7 @@ pub fn on_presence(
escape(&id)
);
Ok(ModuleResult {
terminal_reply: false,
replies: vec![],
deliveries: vec![(jid.into(), query)],
})
@@ -1594,6 +1595,7 @@ pub fn on_remote_presence(
vec![owner.into()],
);
Ok(ModuleResult {
terminal_reply: false,
replies: vec![],
deliveries: vec![(
verified_from.into(),
+494 -108
View File
File diff suppressed because it is too large. Load diff
+686
View File
@@ -0,0 +1,686 @@
//! Bounded, per-session durable stream-management slots. Socket handles are never stored.
//!
//! Recovery is deliberately strict: malformed, oversized, or inconsistent committed
//! slots fail startup instead of silently losing acknowledged session state. Uncommitted
//! UUID temporary files are discarded; checksummed redo journals are authoritative.
//! The readable JSON files are exports and are repaired from redo before loading.
use crate::sessions::Session;
use anyhow::{Result, ensure};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
collections::BTreeMap,
fs::{self, File, OpenOptions},
io::{Read, Write},
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
const SLOT_LIMIT: usize = 8 * 1024 * 1024;
const TOTAL_LIMIT: usize = 256 * 1024 * 1024;
const SESSION_LIMIT: usize = 4096;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Record {
version: u8,
pub jid: String,
pub owner: String,
pub connection: String,
pub client_id: Option<String>,
pub push_identifier: Option<String>,
pub first_push_sent: bool,
pub available: bool,
pub priority: i8,
pub presence: Option<String>,
pub carbons: bool,
pub csi: crate::csi::DurableBuffer,
pub roster_interested: bool,
pub resume_id: String,
pub handled: u32,
pub sent: u32,
pub acked: u32,
pub ack_limit: usize,
pub pending: std::collections::VecDeque<String>,
pub deadline_ms: u64,
pub timeout_ms: u64,
}
pub(crate) fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.min(u64::MAX as u128) as u64
}
impl Record {
fn from_session(s: &Session) -> Result<Self> {
ensure!(!s.closed && s.sm, "session is not resumable");
let remaining = s
.expires
.map(|v| v.saturating_duration_since(Instant::now()))
.unwrap_or(s.timeout);
Ok(Self {
version: 1,
jid: s.jid.clone(),
owner: s.owner.clone(),
connection: s.connection.clone(),
client_id: s.client_id.clone(),
push_identifier: s.push_identifier.clone(),
first_push_sent: s.first_push_sent,
available: s.available,
priority: s.priority,
presence: s.presence.clone(),
carbons: s.carbons,
csi: s.csi.durable(),
roster_interested: s.roster_interested,
resume_id: s
.resume_id
.clone()
.ok_or_else(|| anyhow::anyhow!("missing resume token"))?,
handled: s.handled,
sent: s.sent,
acked: s.acked,
ack_limit: s.ack_limit,
pending: s.pending.clone(),
deadline_ms: now_ms()
.saturating_add(remaining.as_millis().min(u64::MAX as u128) as u64),
timeout_ms: s.timeout.as_millis().min(u64::MAX as u128) as u64,
})
}
pub fn session(self) -> Session {
Session {
jid: self.jid,
owner: self.owner,
connection: self.connection,
client_id: self.client_id,
push_identifier: self.push_identifier,
first_push_sent: self.first_push_sent,
sender: None,
available: self.available,
priority: self.priority,
presence: self.presence,
carbons: self.carbons,
csi: crate::csi::Buffer::from_durable(self.csi),
roster_interested: self.roster_interested,
sm: true,
resume_id: Some(self.resume_id),
handled: self.handled,
sent: self.sent,
acked: self.acked,
ack_limit: self.ack_limit,
ack_requested: false,
pending: self.pending,
expires: Some(
Instant::now() + Duration::from_millis(self.deadline_ms.saturating_sub(now_ms())),
),
timeout: Duration::from_millis(self.timeout_ms),
closed: false,
close_condition: None,
cancel: Default::default(),
durable: None,
}
}
}
#[derive(Debug)]
pub(crate) struct Store {
root: PathBuf,
_lock: File,
slots: Mutex<BTreeMap<String, (String, usize)>>,
failed: std::sync::atomic::AtomicBool,
retired: Mutex<Vec<String>>,
journals: Mutex<BTreeMap<String, u64>>,
}
fn store(session: &Session) -> Result<Option<Arc<Store>>> {
session
.durable
.as_ref()
.map(|weak| {
weak.upgrade()
.ok_or_else(|| anyhow::anyhow!("durable session hub was dropped"))
})
.transpose()
}
pub(crate) fn enable(s: &Session) -> Result<()> {
if let Some(store) = store(s)?
&& s.resume_id.is_some()
{
store.write(s, None)?;
}
Ok(())
}
pub(crate) fn checkpoint(s: &Session) -> Result<()> {
if let Some(store) = store(s)?
&& s.resume_id.is_some()
{
store.write(s, Some(&s.connection))?;
}
Ok(())
}
pub(crate) fn claim(old: &Session, new: &Session) -> Result<()> {
if let Some(store) = store(old)?
&& old.resume_id.is_some()
{
store.write(new, Some(&old.connection))?;
}
Ok(())
}
pub(crate) fn remove(s: &Session) -> Result<()> {
if let Some(store) = store(s)?
&& let Some(id) = &s.resume_id
{
store.remove(id, &s.connection)?;
}
Ok(())
}
impl Store {
pub fn open(path: &Path) -> Result<(Arc<Self>, Vec<Record>)> {
fs::create_dir_all(path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
}
let mut options = OpenOptions::new();
options.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
regular_or_missing(&path.join("store.lock"))?;
let lock = options.open(path.join("store.lock"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
lock.set_permissions(fs::Permissions::from_mode(0o600))?;
}
lock.try_lock()
.map_err(|error| anyhow::anyhow!("stream management store already in use: {error}"))?;
let store = Arc::new(Self {
root: path.into(),
_lock: lock,
slots: Default::default(),
failed: std::sync::atomic::AtomicBool::new(false),
retired: Default::default(),
journals: Default::default(),
});
let mut recovered = vec![];
let mut total = 0usize;
let entries = fs::read_dir(path)?.collect::<std::io::Result<Vec<_>>>()?;
// A crash before rename can leave an incomplete slot. Only canonical UUID
// regular files belong to our writer; preserve unknown names and symlinks.
for entry in &entries {
let name = entry.file_name();
let name = name.to_string_lossy();
if let Some(id) = name.strip_suffix(".tmp") {
let id = id.strip_suffix(".wal").unwrap_or(id);
let owned = safe_id(id).is_ok()
&& uuid::Uuid::parse_str(id).is_ok_and(|uuid| uuid.to_string() == id);
if owned && entry.file_type()?.is_file() {
fs::remove_file(entry.path())?;
}
}
}
File::open(path)?.sync_all()?;
// Redo precedes JSON validation: the export may be stale or torn.
for entry in &entries {
let name = entry.file_name();
let name = name.to_string_lossy();
if let Some(id) = name.strip_suffix(".wal") {
safe_id(id)?;
ensure!(
entry.file_type()?.is_file(),
"invalid stream management journal"
);
let snapshot = replay(&entry.path())?;
// A process crash can leave a complete, checksummed append
// only in page cache. Make its adoption durable before any
// recovered slot is exposed to bootstrap or resume consumers.
OpenOptions::new()
.write(true)
.open(entry.path())?
.sync_data()?;
{
let mut journals = store.journals.lock().unwrap();
journals.insert(id.to_string(), entry.metadata()?.len());
ensure!(
journals.len() <= SESSION_LIMIT + 64
&& journals.values().sum::<u64>() <= JOURNAL_TOTAL_LIMIT,
"stream management aggregate journal limit"
);
}
if let Some(record) = &snapshot {
validate_record(record, id)?;
}
store.export(
id,
snapshot
.as_ref()
.map(serde_json::to_vec)
.transpose()?
.as_deref(),
)?;
if snapshot.is_none() {
store.retired.lock().unwrap().push(id.to_string());
}
}
}
let entries = fs::read_dir(path)?.collect::<std::io::Result<Vec<_>>>()?;
for entry in entries {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.ends_with(".json") {
continue;
}
ensure!(
entry.file_type()?.is_file(),
"invalid stream management slot"
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(entry.path(), fs::Permissions::from_mode(0o600))?;
}
let len = entry.metadata()?.len() as usize;
ensure!(len <= SLOT_LIMIT, "stream management slot too large");
total = total.saturating_add(len);
ensure!(
total <= TOTAL_LIMIT && recovered.len() < SESSION_LIMIT,
"stream management store limit"
);
let mut bytes = vec![];
File::open(entry.path())?
.take(SLOT_LIMIT as u64 + 1)
.read_to_end(&mut bytes)?;
let record: Record = serde_json::from_slice(&bytes)?;
let id = name.strip_suffix(".json").unwrap();
validate_record(&record, id)?;
if record.deadline_ms <= now_ms() {
store.commit(id, None)?;
continue;
}
store
.slots
.lock()
.unwrap()
.insert(record.resume_id.clone(), (record.connection.clone(), len));
recovered.push(record);
}
File::open(path)?.sync_all()?;
store.collect_retired()?;
Ok((store, recovered))
}
fn write(&self, s: &Session, expected: Option<&str>) -> Result<()> {
let record = Record::from_session(s)?;
ensure!(
record.pending.len() <= 4096,
"stream management queue limit"
);
let bytes = serde_json::to_vec(&record)?;
ensure!(bytes.len() <= SLOT_LIMIT, "stream management slot limit");
let name = safe_id(&record.resume_id)?;
let mut slots = self.slots.lock().unwrap();
if let Some(expected) = expected {
ensure!(
slots
.get(&record.resume_id)
.is_some_and(|v| v.0 == expected),
"retired stream transport"
);
} else {
ensure!(
!slots.contains_key(&record.resume_id)
&& !self.retired.lock().unwrap().contains(&record.resume_id),
"duplicate stream token"
);
}
ensure!(
slots.len() < SESSION_LIMIT || slots.contains_key(&record.resume_id),
"stream management session limit"
);
let total: usize = slots.values().map(|v| v.1).sum();
let previous = slots.get(&record.resume_id).map_or(0, |v| v.1);
ensure!(
total - previous + bytes.len() <= TOTAL_LIMIT,
"stream management storage limit"
);
self.commit(name, Some(&bytes))?;
slots.insert(record.resume_id, (record.connection, bytes.len()));
Ok(())
}
fn remove(&self, id: &str, connection: &str) -> Result<()> {
let mut slots = self.slots.lock().unwrap();
if slots.get(id).is_none_or(|v| v.0 != connection) {
return Ok(());
}
self.commit(safe_id(id)?, None)?;
slots.remove(id);
Ok(())
}
// A journal failure may have reached stable storage. Fence this store rather
// than allowing an in-memory rollback to overwrite an uncertain commit.
fn commit(&self, id: &str, bytes: Option<&[u8]>) -> Result<()> {
use std::sync::atomic::Ordering;
ensure!(
!self.failed.load(Ordering::SeqCst),
"stream management store failed"
);
let result = self.commit_inner(id, bytes);
if result.is_err() {
self.failed.store(true, Ordering::SeqCst);
}
result
}
fn commit_inner(&self, id: &str, bytes: Option<&[u8]>) -> Result<()> {
let path = self.root.join(format!("{id}.wal"));
regular_or_missing(&path)?;
if !path.try_exists()? {
// The baseline embeds the previously committed JSON, so recovery
// never depends on an export modified without a subsequent fsync.
let old = self.root.join(format!("{id}.json"));
let baseline = if old.try_exists()? {
regular_or_missing(&old)?;
let mut baseline = Vec::new();
File::open(old)?
.take(SLOT_LIMIT as u64 + 1)
.read_to_end(&mut baseline)?;
ensure!(baseline.len() <= SLOT_LIMIT, "stream management slot limit");
let record: Record = serde_json::from_slice(&baseline)?;
validate_record(&record, id)?;
Some(baseline)
} else {
None
};
let baseline_frame = frame(true, baseline.as_deref())?;
if self.journals.lock().unwrap().values().sum::<u64>()
+ JOURNAL_MAGIC.len() as u64
+ baseline_frame.len() as u64
> JOURNAL_TOTAL_LIMIT
{
self.compact_journals()?;
}
self.reset_journal(id, baseline.as_deref())?;
}
// Rotation is amortized over at least several small commits. Sync the
// exported snapshot and its rename BEFORE replacing the redo history.
let len = fs::metadata(&path)?.len();
if len >= JOURNAL_ROTATE {
let old = replay(&path)?;
let old = old.as_ref().map(serde_json::to_vec).transpose()?;
self.checkpoint_journal(id, old.as_deref())?;
}
let frame = frame(false, bytes)?;
if self.journals.lock().unwrap().values().sum::<u64>() + frame.len() as u64
> JOURNAL_TOTAL_LIMIT
{
// An aggregate cap prevents many small slots each growing a full
// per-slot rotation budget. Compaction is rare and amortized over
// hundreds of MiB of durable appends.
self.compact_journals()?;
}
let mut journal = private_options().append(true).open(&path)?;
let new_len = journal.metadata()?.len() + frame.len() as u64;
journal.write_all(&frame)?;
journal.sync_data()?;
self.journals
.lock()
.unwrap()
.insert(id.to_string(), new_len);
// This remains readable for diagnostics and wire assertions, but only
// the journal is authoritative after a crash.
if self.export(id, bytes).is_err() {
// The durable commit succeeded. Its caller must advance memory to
// the same state rather than rolling it back. Fence future writes;
// startup will reconstruct the export from redo.
self.failed.store(true, std::sync::atomic::Ordering::SeqCst);
return Ok(());
}
if bytes.is_none() {
let mut retired = self.retired.lock().unwrap();
retired.push(id.to_string());
let collect = retired.len() >= 64;
drop(retired);
if collect && self.collect_retired().is_err() {
self.failed.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
Ok(())
}
fn compact_journals(&self) -> Result<()> {
let ids: Vec<String> = self.journals.lock().unwrap().keys().cloned().collect();
for id in ids {
let snapshot = replay(&self.root.join(format!("{id}.wal")))?;
let snapshot = snapshot.as_ref().map(serde_json::to_vec).transpose()?;
self.checkpoint_journal(&id, snapshot.as_deref())?;
}
Ok(())
}
fn collect_retired(&self) -> Result<()> {
let mut retired = self.retired.lock().unwrap();
if retired.is_empty() {
return Ok(());
}
// Deletions become durable before their redo tombstones disappear.
File::open(&self.root)?.sync_all()?;
for id in retired.iter() {
let path = self.root.join(format!("{id}.wal"));
if path.try_exists()? {
fs::remove_file(path)?;
}
self.journals.lock().unwrap().remove(id);
}
File::open(&self.root)?.sync_all()?;
retired.clear();
Ok(())
}
fn checkpoint_journal(&self, id: &str, bytes: Option<&[u8]>) -> Result<()> {
self.export(id, bytes)?;
if bytes.is_some() {
File::open(self.root.join(format!("{id}.json")))?.sync_all()?;
}
File::open(&self.root)?.sync_all()?;
self.reset_journal(id, bytes)
}
fn reset_journal(&self, id: &str, baseline: Option<&[u8]>) -> Result<()> {
let tmp = self.root.join(format!("{id}.wal.tmp"));
regular_or_missing(&tmp)?;
let mut file = private_options().create(true).truncate(true).open(&tmp)?;
private_permissions(&file)?;
file.write_all(JOURNAL_MAGIC)?;
file.write_all(&frame(true, baseline)?)?;
file.sync_all()?;
let len = file.metadata()?.len();
fs::rename(tmp, self.root.join(format!("{id}.wal")))?;
File::open(&self.root)?.sync_all()?;
self.journals.lock().unwrap().insert(id.to_string(), len);
Ok(())
}
fn export(&self, id: &str, bytes: Option<&[u8]>) -> Result<()> {
let dest = self.root.join(format!("{id}.json"));
if let Some(bytes) = bytes {
let tmp = self.root.join(format!("{id}.tmp"));
regular_or_missing(&tmp)?;
let mut file = private_options().create(true).truncate(true).open(&tmp)?;
private_permissions(&file)?;
file.write_all(bytes)?;
fs::rename(tmp, dest)?;
} else if dest.try_exists()? {
fs::remove_file(dest)?;
}
Ok(())
}
}
/// Materialize the authoritative redo state as durable legacy JSON snapshots.
///
/// This is an offline handoff operation: it acquires the same exclusive store
/// lock as a persistent Hub and fails while a live owner exists. Recovery and
/// expiry run first. All current JSON data and directory entries become durable
/// before any redo journal is removed. A failed checkpoint may leave redundant
/// journals, but never removes the only durable copy of an acknowledged state.
/// The caller should additionally hold its server-wide data-directory lock.
pub fn checkpoint_exports(path: &Path) -> Result<usize> {
let (store, records) = Store::open(path)?;
for record in &records {
let json = path.join(format!("{}.json", safe_id(&record.resume_id)?));
regular_or_missing(&json)?;
File::open(json)?.sync_all()?;
}
File::open(path)?.sync_all()?;
let journals: Vec<String> = store.journals.lock().unwrap().keys().cloned().collect();
for id in journals {
let journal = path.join(format!("{id}.wal"));
regular_or_missing(&journal)?;
fs::remove_file(journal)?;
}
File::open(path)?.sync_all()?;
Ok(records.len())
}
fn safe_id(id: &str) -> Result<&str> {
ensure!(
!id.is_empty()
&& id.len() <= 64
&& id.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-'),
"invalid resume token"
);
Ok(id)
}
const JOURNAL_MAGIC: &[u8; 8] = b"XMPPSM01";
const JOURNAL_ROTATE: u64 = 16 * 1024 * 1024;
const JOURNAL_TOTAL_LIMIT: u64 = (TOTAL_LIMIT * 2) as u64;
const FRAME_LIMIT: usize = SLOT_LIMIT + 1024;
#[derive(Serialize, Deserialize)]
struct JournalEntry<T> {
baseline: bool,
snapshot: Option<T>,
}
fn private_options() -> OpenOptions {
let mut options = OpenOptions::new();
options.write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
options
}
fn private_permissions(file: &File) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
file.set_permissions(fs::Permissions::from_mode(0o600))?;
}
Ok(())
}
fn frame(baseline: bool, bytes: Option<&[u8]>) -> Result<Vec<u8>> {
// The snapshot was already validated and serialized by write() or replay().
// Copy its JSON directly into the frame rather than parsing and serializing
// the entire pending queue again on every checkpoint.
if let Some(bytes) = bytes {
ensure!(bytes.len() <= SLOT_LIMIT, "stream management slot limit");
}
let mut payload = Vec::with_capacity(bytes.map_or(4, |snapshot| snapshot.len()) + 32);
payload.extend_from_slice(if baseline {
b"{\"baseline\":true,\"snapshot\":"
} else {
b"{\"baseline\":false,\"snapshot\":"
});
payload.extend_from_slice(bytes.unwrap_or(b"null"));
payload.push(b'}');
ensure!(
payload.len() <= FRAME_LIMIT,
"stream management journal frame limit"
);
let len = payload.len() as u32;
let mut result = Vec::with_capacity(payload.len() + 40);
result.extend_from_slice(&len.to_le_bytes());
result.extend_from_slice(&(!len).to_le_bytes());
result.extend_from_slice(&Sha256::digest(&payload));
result.extend_from_slice(&payload);
Ok(result)
}
fn replay(path: &Path) -> Result<Option<Record>> {
let mut file = OpenOptions::new().read(true).write(true).open(path)?;
private_permissions(&file)?;
ensure!(
file.metadata()?.len() <= JOURNAL_ROTATE + (FRAME_LIMIT + 40) as u64 + 8,
"stream management journal limit"
);
let mut bytes = Vec::new();
(&mut file)
.take(JOURNAL_ROTATE + FRAME_LIMIT as u64 + 49)
.read_to_end(&mut bytes)?;
ensure!(
bytes.starts_with(JOURNAL_MAGIC),
"invalid stream management journal header"
);
let mut offset = 8;
let mut first = true;
let mut latest_payload = None;
while offset < bytes.len() {
if bytes.len() - offset < 8 {
break;
}
let len = u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap());
let complement = u32::from_le_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
ensure!(
len == !complement && len as usize <= FRAME_LIMIT,
"corrupt stream management journal frame"
);
let end = offset + 40 + len as usize;
if end > bytes.len() {
break;
}
let payload = &bytes[offset + 40..end];
ensure!(
Sha256::digest(payload).as_slice() == &bytes[offset + 8..offset + 40],
"stream management journal checksum mismatch"
);
// Historical snapshots were validated before their original commit.
// Check every byte and the full JSON syntax again, without allocating
// old pending queues or serializing superseded snapshots during replay.
let entry: JournalEntry<serde::de::IgnoredAny> = serde_json::from_slice(payload)?;
ensure!(
entry.baseline == first,
"invalid stream management journal baseline"
);
latest_payload = Some(payload);
first = false;
offset = end;
}
ensure!(!first, "incomplete stream management journal baseline");
let latest: JournalEntry<Record> = serde_json::from_slice(latest_payload.unwrap())?;
if let Some(record) = &latest.snapshot {
let id = path.file_stem().unwrap().to_string_lossy();
validate_record(record, &id)?;
ensure!(
serde_json::to_vec(record)?.len() <= SLOT_LIMIT,
"stream management slot limit"
);
}
if offset < bytes.len() {
file.set_len(offset as u64)?;
file.sync_all()?;
}
Ok(latest.snapshot)
}
fn validate_record(record: &Record, id: &str) -> Result<()> {
ensure!(
record.version == 1
&& record.pending.len() <= 4096
&& record.sent.wrapping_sub(record.acked) as usize == record.pending.len()
&& crate::protocol::bare(&record.jid) == record.owner
&& record.jid.contains('/'),
"invalid durable session"
);
ensure!(safe_id(&record.resume_id)? == id, "invalid slot token");
Ok(())
}
fn regular_or_missing(path: &Path) -> Result<()> {
match fs::symlink_metadata(path) {
Ok(metadata) => ensure!(metadata.is_file(), "invalid stream management file"),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
Ok(())
}
+205 -32
View File
@@ -35,8 +35,51 @@ pub struct Session {
pub closed: bool,
pub close_condition: Option<String>,
pub cancel: Arc<tokio::sync::Notify>,
pub(crate) durable: Option<std::sync::Weak<crate::session_store::Store>>,
}
impl Session {
/// Persist state before acknowledging client input or exposing output to a transport.
pub fn checkpoint(&self) -> Result<()> {
crate::session_store::checkpoint(self)
}
pub fn mark_handled(&mut self) -> Result<()> {
ensure!(!self.closed, "session closed");
if !self.sm {
return Ok(());
}
let previous = self.handled;
self.handled = self.handled.wrapping_add(1);
if let Err(error) = self.checkpoint() {
self.handled = previous;
return Err(error);
}
Ok(())
}
pub fn track_outbound(&mut self, xml: &str) -> Result<()> {
ensure!(!self.closed, "session closed");
if !self.sm {
return Ok(());
}
ensure!(self.pending.len() < 4096, "stream management queue limit");
// The byte bound applies to memory-only sessions too.
ensure!(
self.pending
.iter()
.map(String::len)
.sum::<usize>()
.saturating_add(xml.len())
<= 8 * 1024 * 1024,
"stream management byte limit"
);
self.sent = self.sent.wrapping_add(1);
self.pending.push_back(xml.into());
if let Err(error) = self.checkpoint() {
self.sent = self.sent.wrapping_sub(1);
self.pending.pop_back();
return Err(error);
}
Ok(())
}
pub fn first_important_push(&mut self, normal_timeout: Duration) {
if self.sender.is_none() && self.expires.is_some() && !self.first_push_sent {
self.first_push_sent = true;
@@ -45,25 +88,18 @@ impl Session {
}
pub fn transmit(&mut self, xml: String) -> Result<()> {
ensure!(!self.closed, "session closed");
if self.sm {
ensure!(self.pending.len() < 4096, "stream management queue limit");
}
if let Some(tx) = &self.sender {
if let Some(tx) = self.sender.clone() {
let permit = tx
.try_reserve()
.map_err(|_| anyhow::anyhow!("recipient queue unavailable"))?;
if self.sm {
self.sent = self.sent.wrapping_add(1);
self.pending.push_back(xml.clone());
}
self.track_outbound(&xml)?;
permit.send(xml);
} else {
ensure!(
self.sm && self.resume_id.is_some(),
"recipient disconnected"
);
self.sent = self.sent.wrapping_add(1);
self.pending.push_back(xml);
self.track_outbound(&xml)?;
}
Ok(())
}
@@ -79,9 +115,18 @@ impl Session {
ensure!(!self.closed, "session closed");
let n = h.wrapping_sub(self.acked) as usize;
ensure!(n <= self.pending.len(), "invalid stream acknowledgement");
if n == 0 {
self.ack_requested = false;
return Ok(());
}
let previous = self.clone();
self.pending.drain(..n);
self.acked = h;
self.ack_requested = false;
if let Err(error) = self.checkpoint() {
*self = previous;
return Err(error);
}
Ok(())
}
pub fn enable(&mut self, resume: bool) -> Result<String> {
@@ -91,6 +136,11 @@ impl Session {
if resume {
self.resume_id = Some(uuid::Uuid::new_v4().to_string());
}
if let Err(error) = crate::session_store::enable(self) {
self.sm = false;
self.resume_id = None;
return Err(error);
}
Ok(match &self.resume_id {
Some(id) => format!(
"<enabled xmlns='urn:xmpp:sm:3' id='{id}' resume='true' max='{}'/>",
@@ -122,8 +172,62 @@ impl ConflictPolicy {
#[derive(Clone, Default)]
pub struct Hub {
sessions: Arc<Mutex<BTreeMap<String, SharedSession>>>,
store: Option<Arc<crate::session_store::Store>>,
}
impl Hub {
pub fn persistent(path: impl AsRef<std::path::Path>) -> Result<Self> {
let (store, records) = crate::session_store::Store::open(path.as_ref())?;
let mut sessions = BTreeMap::new();
for record in records {
let mut session = record.session();
session.durable = Some(Arc::downgrade(&store));
ensure!(
!sessions.contains_key(&session.jid),
"duplicate durable resource"
);
sessions.insert(session.jid.clone(), Arc::new(Mutex::new(session)));
}
Ok(Self {
sessions: Arc::new(Mutex::new(sessions)),
store: Some(store),
})
}
/// Refresh only connected deadlines; disconnected deadlines never slide.
pub fn checkpoint_live(&self) -> Result<()> {
for session in self.all() {
let state = session.lock().unwrap();
if state.sender.is_some() && !state.closed {
state.checkpoint()?;
}
}
Ok(())
}
/// Retire in-flight transport handles and commit offline state before shutdown.
pub fn freeze(&self) -> Result<()> {
let mut map = self.sessions.lock().unwrap();
for shared in map.values_mut() {
let previous = shared.clone();
let mut old = previous.lock().unwrap();
if old.closed || !old.sm || old.resume_id.is_none() {
continue;
}
let mut state = old.clone();
state.sender = None;
if state.expires.is_none() {
state.expires = Some(Instant::now() + state.timeout);
}
state.connection = format!("shutdown-{}", uuid::Uuid::new_v4());
state.cancel = Default::default();
crate::session_store::claim(&old, &state)?;
old.closed = true;
old.sender = None;
old.resume_id = None;
old.expires = None;
*shared = Arc::new(Mutex::new(state));
}
Ok(())
}
pub fn bind(
&self,
jid: &str,
@@ -150,9 +254,10 @@ impl Hub {
match policy {
ConflictPolicy::KickNew => anyhow::bail!("resource conflict"),
ConflictPolicy::KickOld => {
let old = map.remove(&jid).unwrap();
let old = map.get(&jid).unwrap().clone();
{
let mut state = old.lock().unwrap();
crate::session_store::remove(&state)?;
state.closed = true;
state.close_condition = Some("conflict".into());
state.sender = None;
@@ -160,6 +265,7 @@ impl Hub {
state.expires = None;
state.cancel.notify_one();
}
map.remove(&jid);
replaced = Some(old);
}
ConflictPolicy::Increment => {
@@ -208,22 +314,34 @@ impl Hub {
closed: false,
close_condition: None,
cancel: Default::default(),
durable: self.store.as_ref().map(Arc::downgrade),
}));
map.insert(jid, s.clone());
Ok((s, replaced))
}
pub fn kick_owner(&self, owner: &str) {
self.sessions.lock().unwrap().retain(|_, s| {
let mut s = s.lock().unwrap();
if s.owner == owner {
s.sender = None;
s.closed = true;
s.cancel.notify_one();
false
} else {
true
pub fn kick_owner_checked(&self, owner: &str) -> Result<()> {
let mut map = self.sessions.lock().unwrap();
let victims: Vec<_> = map
.iter()
.filter_map(|(jid, session)| {
(session.lock().unwrap().owner == owner).then_some(jid.clone())
})
.collect();
for jid in victims {
let shared = map.get(&jid).unwrap().clone();
let mut state = shared.lock().unwrap();
// Do not let account deletion/recreation proceed if durable revocation failed.
crate::session_store::remove(&state)?;
state.resume_id = None;
state.sender = None;
state.closed = true;
state.cancel.notify_one();
map.remove(&jid);
}
});
Ok(())
}
pub fn kick_owner(&self, owner: &str) {
let _ = self.kick_owner_checked(owner);
}
pub fn get(&self, jid: &str) -> Option<SharedSession> {
self.sessions.lock().unwrap().get(jid).cloned()
@@ -231,13 +349,29 @@ impl Hub {
pub fn all(&self) -> Vec<SharedSession> {
self.sessions.lock().unwrap().values().cloned().collect()
}
/// Filter immutable resource keys before taking any per-session lock. A slow
/// client from another account must not block this account's routing/carbons.
pub fn for_owners(&self, owners: &[&str]) -> Vec<SharedSession> {
self.sessions
.lock()
.unwrap()
.iter()
.filter(|(jid, _)| owners.contains(&bare(jid)))
.map(|(_, session)| session.clone())
.collect()
}
pub fn deliver(&self, to: &str, xml: &str) -> usize {
let roster_push = roxmltree::Document::parse(xml).ok().is_some_and(|d| {
d.root_element()
.attribute("id")
.is_some_and(|id| id.starts_with("roster-"))
});
self.all()
let targets = if to.contains('/') {
self.get(to).into_iter().collect()
} else {
self.for_owners(&[to])
};
targets
.into_iter()
.filter(|s| {
let mut s = s.lock().unwrap();
@@ -269,7 +403,7 @@ impl Hub {
let bare = to.split_once('/').map(|(bare, _)| bare).unwrap_or(to);
let mut max = None;
let mut out = vec![];
for s in self.all() {
for s in self.for_owners(&[bare]) {
let p = {
let state = s.lock().unwrap();
if state.owner != bare || !state.available || state.priority < 0 {
@@ -301,9 +435,21 @@ impl Hub {
if !graceful && s.sm && s.resume_id.is_some() {
s.csi.reset();
s.sender = None;
// Repeated cleanup must not renew an already offline token.
if s.expires.is_none() {
s.expires = Some(Instant::now() + s.timeout);
}
if s.checkpoint().is_err() {
return true;
}
true
} else {
if crate::session_store::remove(&s).is_err() {
return true;
}
s.closed = true;
s.resume_id = None;
s.sender = None;
removed.push(jid.clone());
false
}
@@ -312,12 +458,16 @@ impl Hub {
}
pub fn expired(&self) -> Vec<String> {
let mut out = vec![];
self.sessions.lock().unwrap().retain(|jid, s| {
if s.lock()
.unwrap()
.expires
.is_some_and(|t| Instant::now() >= t)
{
self.sessions.lock().unwrap().retain(|jid, session| {
let mut state = session.lock().unwrap();
if state.expires.is_some_and(|t| Instant::now() >= t) {
if crate::session_store::remove(&state).is_err() {
return true;
}
state.closed = true;
state.resume_id = None;
state.sender = None;
state.cancel.notify_one();
out.push(jid.clone());
false
} else {
@@ -340,7 +490,12 @@ impl Hub {
.values()
.find(|session| {
let state = session.lock().unwrap();
!state.closed && state.owner == owner && state.resume_id.as_deref() == Some(id)
!state.closed
&& state
.expires
.is_none_or(|deadline| deadline > Instant::now())
&& state.owner == owner
&& state.resume_id.as_deref() == Some(id)
})
.cloned();
if let Some(previous) = candidate {
@@ -351,13 +506,16 @@ impl Hub {
// Keep a retired transport's in-flight operations isolated from the
// resumed queue, acknowledgements and CSI state.
let mut state = old.clone();
state.acknowledge(h)?;
state.pending.drain(..count);
state.acked = h;
state.ack_requested = false;
state.cancel = Default::default();
state.csi.reset();
state.first_push_sent = false;
state.sender = Some(sender);
state.connection = connection.into();
state.expires = None;
crate::session_store::claim(&old, &state)?;
let mut replay = vec![format!(
"<resumed xmlns='urn:xmpp:sm:3' previd='{id}' h='{}'/>",
state.handled
@@ -383,6 +541,21 @@ impl Hub {
mod tests {
use super::*;
#[test]
fn traffic_before_sm_enable_is_not_counted() {
let hub = Hub::default();
let (tx, _rx) = mpsc::channel(8);
let session = hub
.bind("a@x/r", "plain", tx, Duration::from_secs(30))
.unwrap();
let mut session = session.lock().unwrap();
session.mark_handled().unwrap();
session.track_outbound("plain reply").unwrap();
session.enable(true).unwrap();
assert_eq!((session.handled, session.sent), (0, 0));
session.mark_handled().unwrap();
assert_eq!(session.handled, 1);
}
#[test]
fn resume_replays_only_unacked_and_checks_owner() {
let h = Hub::default();
let (tx, _rx) = mpsc::channel(8);
+1 -1
View File
@@ -427,7 +427,7 @@ pub fn purge_user(root: &Path, host: &str, user: &str) -> Result<()> {
if !ty.is_dir() {
continue;
}
for extension in ["dat", "list", "lidx", "rappend"] {
for extension in ["dat", "list", "lidx", "rappend", "rredo"] {
let p = entry.path().join(format!("{user}.{extension}"));
match fs::symlink_metadata(&p) {
Ok(m) => {
+133
View File
@@ -0,0 +1,133 @@
use rust_xmpp::{archive, storage};
use std::{
fs::{self, File},
io::{Seek, SeekFrom, Write},
path::{Path, PathBuf},
};
const MAX_RECORD: usize = 16 * 1024 * 1024;
fn path(root: &Path) -> PathBuf {
let p = storage::path(root, "example.org", Some("alice"), "archive").with_extension("list");
fs::create_dir_all(p.parent().unwrap()).unwrap();
p
}
fn record(id: &str) -> String {
format!(
"item({{name='message';attr={{xmlns='jabber:client'}};key='{id}';when=42;with='bob@example.org';{{name='body';attr={{}};'hello'}}}});\n"
)
}
fn edges(root: &Path) -> anyhow::Result<Option<(archive::Entry, archive::Entry)>> {
archive::edges_store(root, "example.org", Some("alice"), "archive")
}
#[test]
fn metadata_skips_large_malformed_middle_without_creating_cache() {
let t = tempfile::tempdir().unwrap();
let p = path(t.path());
let mut file = File::create(&p).unwrap();
file.write_all(record("first").as_bytes()).unwrap();
file.write_all(b"item(not valid Lua in the middle\n")
.unwrap();
// A sparse >128 MiB history makes accidental full scans fail the record
// bound; only the endpoints are part of this metadata operation.
file.seek(SeekFrom::Start(129 * 1024 * 1024)).unwrap();
file.write_all(b"\n").unwrap();
file.write_all(record("last").as_bytes()).unwrap();
drop(file);
let (first, last) = edges(t.path()).unwrap().unwrap();
assert_eq!((first.id.as_str(), last.id.as_str()), ("first", "last"));
assert_eq!(first.when, 42.0);
assert_eq!(last.peer, "bob@example.org");
assert!(!p.with_extension("lidx").exists());
assert!(archive::load(t.path(), "example.org", "alice").is_err());
// An opaque native index is irrelevant and must remain untouched.
fs::write(p.with_extension("lidx"), b"stale binary index").unwrap();
assert_eq!(edges(t.path()).unwrap().unwrap().1.id, "last");
assert_eq!(
fs::read(p.with_extension("lidx")).unwrap(),
b"stale binary index"
);
}
#[test]
fn multiline_endpoints_match_native_scanner_with_blank_lines() {
let t = tempfile::tempdir().unwrap();
let p = path(t.path());
let first = record("first").replace(";", ";\n");
let last = format!(
"item({{\nname='message';attr={{}};key='last';when=43;\n{{name='body';attr={{}};[=[{}]=]}}\n}});\n \t\n",
"long multiline text\n".repeat(1800)
);
fs::write(&p, format!(" \t\n\n{first}\n{}{last}", record("middle"))).unwrap();
let rows = archive::load(t.path(), "example.org", "alice").unwrap();
let (a, b) = edges(t.path()).unwrap().unwrap();
assert_eq!(a.id, rows.first().unwrap().id);
assert_eq!(b.id, rows.last().unwrap().id);
assert_eq!(b.xml, rows.last().unwrap().xml);
}
#[test]
fn missing_empty_whitespace_and_single_record() {
let t = tempfile::tempdir().unwrap();
assert!(edges(t.path()).unwrap().is_none());
let p = path(t.path());
for contents in ["", " \t\r\n\n"] {
fs::write(&p, contents).unwrap();
assert!(edges(t.path()).unwrap().is_none());
}
// EOF need not have a newline.
fs::write(&p, record("only").trim_end()).unwrap();
let (first, last) = edges(t.path()).unwrap().unwrap();
assert_eq!(first.id, "only");
assert_eq!(last.id, first.id);
assert_eq!(last.xml, first.xml);
}
#[test]
fn tail_padding_respects_record_bound() {
let t = tempfile::tempdir().unwrap();
let p = path(t.path());
let mut data = record("first").into_bytes();
let last = record("last");
data.extend_from_slice(last.as_bytes());
data.resize(data.len() + MAX_RECORD - last.len(), b' ');
fs::write(&p, &data).unwrap();
assert_eq!(edges(t.path()).unwrap().unwrap().1.id, "last");
data.push(b' ');
fs::write(&p, data).unwrap();
assert!(edges(t.path()).unwrap_err().to_string().contains("16 MiB"));
}
#[test]
fn oversized_prefix_and_malformed_endpoints_fail() {
let t = tempfile::tempdir().unwrap();
let p = path(t.path());
let mut data = vec![b' '; MAX_RECORD + 1];
data.extend_from_slice(b"\n");
data.extend_from_slice(record("last").as_bytes());
fs::write(&p, data).unwrap();
assert!(edges(t.path()).unwrap_err().to_string().contains("16 MiB"));
fs::write(&p, format!("item(bad first);\n{}", record("last"))).unwrap();
assert!(edges(t.path()).is_err());
fs::write(&p, format!("{}item(bad last);\n", record("first"))).unwrap();
assert!(edges(t.path()).is_err());
}
#[test]
fn endpoint_read_recovers_partial_append_before_seeking() {
let t = tempfile::tempdir().unwrap();
let p = path(t.path());
let committed = format!("{}{}", record("first"), record("last"));
fs::write(&p, format!("{committed}item({{broken partial")).unwrap();
fs::write(
p.with_extension("rappend"),
(committed.len() as u64).to_le_bytes(),
)
.unwrap();
let (first, last) = edges(t.path()).unwrap().unwrap();
assert_eq!((first.id.as_str(), last.id.as_str()), ("first", "last"));
assert_eq!(fs::read(&p).unwrap(), committed.as_bytes());
assert!(!p.with_extension("rappend").exists());
}
+358
View File
@@ -0,0 +1,358 @@
//! Crash fixtures model independent loss of unsynced native writes. The redo
//! sidecar is the durable commit source until a native checkpoint or handoff.
use rust_xmpp::{archive, storage};
use std::{
fs,
io::Write,
path::{Path, PathBuf},
process::Command,
time::{Duration, Instant},
};
fn path(root: &Path) -> PathBuf {
storage::path(root, "example.org", Some("alice"), "archive").with_extension("list")
}
fn entry(id: &str) -> archive::Entry {
archive::Entry {
id: id.into(),
when: 42.,
peer: "bob@example.org".into(),
xml: "<message xmlns='jabber:client'><body>hello</body></message>".into(),
}
}
fn append(root: &Path, id: &str) {
archive::append_new(root, "example.org", Some("alice"), "archive", entry(id)).unwrap();
}
fn ids(root: &Path) -> Vec<String> {
archive::load(root, "example.org", "alice")
.unwrap()
.into_iter()
.map(|e| e.id)
.collect()
}
#[test]
fn committed_redo_recovers_missing_and_partial_native_without_duplicates() {
let t = tempfile::tempdir().unwrap();
append(t.path(), "one");
append(t.path(), "two");
let p = path(t.path());
let expected = fs::read(&p).unwrap();
for length in [0, 1, expected.len() / 2, expected.len() - 1] {
fs::OpenOptions::new()
.write(true)
.open(&p)
.unwrap()
.set_len(length as u64)
.unwrap();
assert_eq!(ids(t.path()), ["one", "two"]);
assert_eq!(fs::read(&p).unwrap(), expected);
assert_eq!(ids(t.path()), ["one", "two"]);
}
fs::remove_file(&p).unwrap();
assert_eq!(ids(t.path()), ["one", "two"]);
assert_eq!(fs::read(&p).unwrap(), expected);
}
#[test]
fn incomplete_final_frame_and_uncommitted_native_suffix_are_discarded() {
for extra in [0, 1, 7, 20, 39, 40, 71, 72, 90] {
let t = tempfile::tempdir().unwrap();
append(t.path(), "one");
let p = path(t.path());
let committed = fs::read(&p).unwrap();
let journal = p.with_extension("rredo");
let committed_journal = fs::metadata(&journal).unwrap().len();
append(t.path(), "two");
// Construct a crash before the second frame was fully persisted.
fs::OpenOptions::new()
.write(true)
.open(&journal)
.unwrap()
.set_len(committed_journal + extra)
.unwrap();
assert_eq!(ids(t.path()), ["one"]);
assert_eq!(fs::read(&p).unwrap(), committed);
assert_eq!(fs::metadata(&journal).unwrap().len(), committed_journal);
append(t.path(), "three");
assert_eq!(ids(t.path()), ["one", "three"]);
}
}
#[test]
fn complete_checksum_corruption_fails_without_modifying_native_or_journal() {
for offset in [8, 64, 96, 104, 136, 168] {
let t = tempfile::tempdir().unwrap();
append(t.path(), "one");
let p = path(t.path());
let native = fs::read(&p).unwrap();
let journal = p.with_extension("rredo");
let mut corrupt = fs::read(&journal).unwrap();
corrupt[offset] ^= 1;
fs::write(&journal, &corrupt).unwrap();
assert!(archive::load(t.path(), "example.org", "alice").is_err());
assert_eq!(fs::read(&p).unwrap(), native);
assert_eq!(fs::read(&journal).unwrap(), corrupt);
}
}
#[test]
fn bounded_rotation_recovers_suffix_and_rewrites_never_resurrect_deleted_records() {
let t = tempfile::tempdir().unwrap();
for i in 0..300 {
append(t.path(), &format!("id{i}"));
}
let p = path(t.path());
let journal = p.with_extension("rredo");
let data = fs::read(&journal).unwrap();
assert!(data.len() < 1024 * 1024);
let baseline = u64::from_le_bytes(data[8..16].try_into().unwrap());
assert!(baseline > 0);
fs::OpenOptions::new()
.write(true)
.open(&p)
.unwrap()
.set_len(baseline)
.unwrap();
assert_eq!(ids(t.path()).len(), 300);
archive::delete_ids(
t.path(),
"example.org",
Some("alice"),
"archive",
&["id299".into()],
)
.unwrap();
assert_eq!(ids(t.path()).len(), 299);
append(t.path(), "after-delete");
assert!(!ids(t.path()).contains(&"id299".into()));
archive::save_store(
t.path(),
"example.org",
Some("alice"),
"archive",
&[entry("replacement")],
)
.unwrap();
assert_eq!(ids(t.path()), ["replacement"]);
append(t.path(), "after-save");
assert_eq!(ids(t.path()), ["replacement", "after-save"]);
archive::expire_before(t.path(), "example.org", Some("alice"), "archive", 43.).unwrap();
assert!(ids(t.path()).is_empty());
append(t.path(), "after-expire");
assert_eq!(ids(t.path()), ["after-expire"]);
}
#[test]
fn byte_limit_rotates_before_frame_count_limit() {
let t = tempfile::tempdir().unwrap();
for i in 0..20 {
let mut e = entry(&format!("big{i}"));
e.xml = format!("<message><body>{}</body></message>", "x".repeat(100_000));
archive::append_new(t.path(), "example.org", Some("alice"), "archive", e).unwrap();
}
let data = fs::read(path(t.path()).with_extension("rredo")).unwrap();
assert!(u64::from_le_bytes(data[8..16].try_into().unwrap()) > 0);
assert!(data.len() < 1024 * 1024 + 110_000);
assert_eq!(ids(t.path()).len(), 20);
}
#[test]
fn explicit_native_atomic_handoff_and_missing_baseline_never_drops_live_access() {
let t = tempfile::tempdir().unwrap();
append(t.path(), "old");
let p = path(t.path());
archive::checkpoint_all(t.path()).unwrap();
// An external writer replaces the native file in place (Prosody `list_store`
// rewrite), invalidating the committed redo baseline. On first access the
// untrusted journal is retired and the synced native is adopted as
// authoritative — exactly what Prosody sees (it has no redo sidecar).
storage::atomic_write(
&p,
b"item({name='message';attr={};key='native';when=42});\n",
)
.unwrap();
assert_eq!(ids(t.path()), ["native"]);
append(t.path(), "rust");
assert_eq!(ids(t.path()), ["native", "rust"]);
// A dirty, uncommitted native suffix is never surfaced.
fs::OpenOptions::new()
.append(true)
.open(&p)
.unwrap()
.write_all(b"item(bad uncommitted);")
.unwrap();
assert_eq!(ids(t.path()), ["native", "rust"]);
// Once the untrusted journal has been retired (native adopted as
// authoritative), externally removing the archive yields the empty history
// Prosody would return — never a panic or a hang.
fs::remove_file(&p).unwrap();
assert!(
archive::load(t.path(), "example.org", "alice")
.unwrap()
.is_empty()
);
}
#[test]
fn native_baseline_in_place_edits_are_rejected() {
let t = tempfile::tempdir().unwrap();
let p = path(t.path());
fs::create_dir_all(p.parent().unwrap()).unwrap();
let original = b"item({name='message';attr={};key='native';when=42});\n";
fs::write(&p, original).unwrap();
append(t.path(), "rust");
let mut data = fs::read(&p).unwrap();
data[5] ^= 1;
fs::write(&p, &data).unwrap();
assert!(archive::load(t.path(), "example.org", "alice").is_err());
assert_eq!(fs::read(&p).unwrap(), data);
}
#[test]
fn redo_process_child() {
let Some(root) = std::env::var_os("XMPP_ARCHIVE_REDO_CHILD") else {
return;
};
let root = Path::new(&root);
append(root, "accepted-before-kill");
fs::write(root.join("ready"), b"ready").unwrap();
loop {
std::thread::park();
}
}
#[test]
fn killed_writer_retains_accepted_records_after_native_data_loss() {
let t = tempfile::tempdir().unwrap();
let mut child = Command::new(std::env::current_exe().unwrap())
.args(["--exact", "redo_process_child", "--nocapture"])
.env("XMPP_ARCHIVE_REDO_CHILD", t.path())
.spawn()
.unwrap();
let deadline = Instant::now() + Duration::from_secs(10);
while !t.path().join("ready").exists() {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!("redo child not ready");
}
assert!(
child.try_wait().unwrap().is_none(),
"redo child exited early"
);
std::thread::sleep(Duration::from_millis(10));
}
child.kill().unwrap();
child.wait().unwrap();
// Simulate loss of dirty native pages independently of the synced journal.
fs::OpenOptions::new()
.write(true)
.open(path(t.path()))
.unwrap()
.set_len(0)
.unwrap();
assert_eq!(ids(t.path()), ["accepted-before-kill"]);
assert_eq!(ids(t.path()), ["accepted-before-kill"]);
}
#[test]
fn durable_handoff_allows_native_in_place_append_and_next_rust_append() {
let t = tempfile::tempdir().unwrap();
append(t.path(), "rust-before");
let p = path(t.path());
assert_eq!(archive::checkpoint_all(t.path()).unwrap(), 1);
assert!(!p.with_extension("rredo").exists());
fs::OpenOptions::new()
.append(true)
.open(&p)
.unwrap()
.write_all(b"item({name='message';attr={};key='native';when=42});\n")
.unwrap();
assert_eq!(ids(t.path()), ["rust-before", "native"]);
append(t.path(), "rust-after");
assert_eq!(ids(t.path()), ["rust-before", "native", "rust-after"]);
assert_eq!(archive::checkpoint_all(t.path()).unwrap(), 1);
assert_eq!(archive::checkpoint_all(t.path()).unwrap(), 0);
}
#[test]
fn legacy_rollback_marker_is_migrated_before_redo_append() {
let t = tempfile::tempdir().unwrap();
let p = path(t.path());
fs::create_dir_all(p.parent().unwrap()).unwrap();
let original = b"item({name='message';attr={};key='legacy';when=42});\n";
fs::write(&p, [original.as_slice(), b"item(broken"].concat()).unwrap();
fs::write(
p.with_extension("rappend"),
(original.len() as u64).to_le_bytes(),
)
.unwrap();
append(t.path(), "redo");
assert_eq!(ids(t.path()), ["legacy", "redo"]);
assert!(!p.with_extension("rappend").exists());
assert!(p.with_extension("rredo").exists());
}
#[test]
fn copied_archive_replays_committed_redo_despite_new_native_inode() {
for native_baseline in [false, true] {
for lose_entire_suffix in [false, true] {
let source = tempfile::tempdir().unwrap();
let source_path = path(source.path());
if native_baseline {
fs::create_dir_all(source_path.parent().unwrap()).unwrap();
fs::write(
&source_path,
b"item({name='message';attr={};key='baseline';when=42});\n",
)
.unwrap();
}
append(source.path(), "accepted-one");
append(source.path(), "accepted-two");
let expected = fs::read(&source_path).unwrap();
let redo = fs::read(source_path.with_extension("rredo")).unwrap();
let baseline = u64::from_le_bytes(redo[8..16].try_into().unwrap());
let restored = tempfile::tempdir().unwrap();
let restored_path = path(restored.path());
fs::create_dir_all(restored_path.parent().unwrap()).unwrap();
fs::copy(&source_path, &restored_path).unwrap();
fs::copy(
source_path.with_extension("rredo"),
restored_path.with_extension("rredo"),
)
.unwrap();
let length = if lose_entire_suffix {
baseline
} else {
expected.len() as u64 - 1
};
fs::OpenOptions::new()
.write(true)
.open(&restored_path)
.unwrap()
.set_len(length)
.unwrap();
let recovered = ids(restored.path());
assert_eq!(recovered.last().unwrap(), "accepted-two");
assert_eq!(recovered.len(), if native_baseline { 3 } else { 2 });
assert_eq!(fs::read(&restored_path).unwrap(), expected);
append(restored.path(), "after-restore");
assert_eq!(ids(restored.path()).last().unwrap(), "after-restore");
}
}
}
#[test]
fn atomic_native_replace_without_handoff_cannot_discard_committed_frames() {
let t = tempfile::tempdir().unwrap();
append(t.path(), "accepted");
let p = path(t.path());
storage::atomic_write(
&p,
b"item({name='message';attr={};key='replacement';when=42});\n",
)
.unwrap();
// A replacement without handoff is not authority to discard committed WAL.
assert_eq!(ids(t.path()), ["accepted"]);
}
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Measure real TLS/SASL2 Bind2 reconnects with a large personal MAM history.
Usage: python3 tests/bind2_archive_latency_network.py --binary BINARY \
--records 100000 --output REPORT.json
Seeds valid native records before startup, then checks exact first/last metadata
on every fresh connection. Latency measurements have no performance threshold.
The executable is copied before use so concurrent rebuilds cannot replace it.
"""
import argparse
import datetime
import hashlib
import json
from pathlib import Path
import shutil
import socket
import statistics
import subprocess
import tempfile
import time
import xml.etree.ElementTree as ET
from sasl2_bind import BIND2, SASL2, Client
MAM = 'urn:xmpp:mam:2'
BASE_TIME = 1767225600
def stamp(seconds):
return datetime.datetime.fromtimestamp(seconds, datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
def distribution(values):
ordered = sorted(values)
return {'samples_ms': values, 'median_ms': statistics.median(values),
'p95_ms': ordered[min(len(ordered) - 1, int(len(ordered) * .95))],
'max_ms': ordered[-1]}
def exercise(binary, root, records, samples):
with socket.socket() as probe:
probe.bind(('127.0.0.1', 0))
port = probe.getsockname()[1]
subprocess.run(['openssl', 'req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256',
'-nodes', '-days', '1', '-subj', '/CN=localhost', '-addext', 'subjectAltName=DNS:localhost',
'-addext', 'basicConstraints=critical,CA:FALSE', '-keyout', str(root/'key.pem'),
'-out', str(root/'cert.pem')], check=True, capture_output=True)
config = root/'config.json'
config.write_text(json.dumps({'global': {'data_path': str(root/'data'), 'interfaces': ['127.0.0.1'],
'c2s_ports': [port], 's2s_ports': [], 'http_ports': [], 'https_ports': [],
'modules_enabled': ['tls', 'saslauth', 'sasl2', 'sasl2_bind2', 'mam', 'carbons', 'csi_simple', 'smacks'],
'default_archive_policy': True, 'archive_expires_after': 'never'},
'hosts': {'localhost': {'options': {'authentication': 'internal_hashed', 'password_hash': 'SHA-256',
'ssl': {'key': str(root/'key.pem'), 'certificate': str(root/'cert.pem')}}}}}))
subprocess.run([str(binary), 'create-user', str(config), 'alice', '--password-stdin'],
input='secret\n', text=True, check=True, capture_output=True)
archive = root/'data'/'localhost'/'archive'/'alice.list'
archive.parent.mkdir(parents=True, exist_ok=True)
with archive.open('w') as output:
for number in range(records):
output.write("item({name='message';attr={xmlns='jabber:client';type='chat';"
f"from='bob@localhost';stamp='{stamp(BASE_TIME + number)}'" + "};"
f"key='seed-{number}';when={BASE_TIME + number};with='bob@localhost';"
"{name='body';attr={};'seed personal history'}});\n")
expected = {'start': {'id': 'seed-0', 'timestamp': stamp(BASE_TIME)},
'end': {'id': f'seed-{records - 1}', 'timestamp': stamp(BASE_TIME + records - 1)}}
archive_bytes = archive.stat().st_size
index_before_start = archive.with_suffix('.lidx').exists()
assert not index_before_start, 'seed unexpectedly has a native index'
auth_ms, connect_ms, metadata_ms = [], [], []
metadata_samples, iq_metadata_samples = [], []
with (root/'server.log').open('w+') as log:
server = subprocess.Popen([str(binary), 'serve', str(config), '--experimental'], stdout=log, stderr=log)
try:
deadline = time.monotonic() + 30
while True:
assert server.poll() is None, 'server stopped before readiness'
try:
with socket.create_connection(('127.0.0.1', port), timeout=.1):
break
except OSError:
assert time.monotonic() < deadline, 'server did not start'
time.sleep(.05)
for sample in range(samples):
# Each sample reconnects and authenticates the same account.
# Client verifies the advertised SASL2/Bind2 inline features.
connected = time.perf_counter()
client = Client(port, root/'cert.pem')
client.sock.settimeout(30)
try:
begun = time.perf_counter()
response = client.auth(f"<bind xmlns='{BIND2}'><tag>HistoryReconnect</tag></bind>")
arrived = time.perf_counter()
auth_ms.append((arrived - begun) * 1000)
connect_ms.append((arrived - connected) * 1000)
jid = response.find('{' + SASL2 + '}authorization-identifier')
assert jid is not None and jid.text.startswith('alice@localhost/HistoryReconnect/'), 'missing bound JID'
bound = response.find('{' + BIND2 + '}bound')
assert bound is not None, 'missing Bind2 bound result'
metadata = bound.find('{' + MAM + '}metadata')
assert metadata is not None, 'missing MAM metadata'
actual = {}
for edge in ('start', 'end'):
node = metadata.find('{' + MAM + '}' + edge)
assert node is not None, f'missing MAM {edge}'
actual[edge] = dict(node.attrib)
assert actual == expected, (actual, expected)
assert len(list(metadata)) == 2, 'unexpected MAM metadata children'
metadata_samples.append(actual)
# Session is immediately bound; the post-success features
# must not ask this client to perform legacy resource bind.
assert b'xmpp-bind' not in client.until(b'</stream:features>')
query_id = f'metadata-{sample}'
begun = time.perf_counter()
client.send(f"<iq type='get' id='{query_id}'><metadata xmlns='{MAM}'/></iq>")
iq = ET.fromstring(client.until(b'</iq>'))
metadata_ms.append((time.perf_counter() - begun) * 1000)
assert iq.get('type') == 'result' and iq.get('id') == query_id, 'metadata IQ failed'
iq_metadata = iq.find('{' + MAM + '}metadata')
assert iq_metadata is not None, 'metadata IQ payload missing'
iq_actual = {}
for edge in ('start', 'end'):
node = iq_metadata.find('{' + MAM + '}' + edge)
assert node is not None, f'missing metadata IQ {edge}'
iq_actual[edge] = dict(node.attrib)
assert iq_actual == expected, (iq_actual, expected)
assert len(list(iq_metadata)) == 2, 'unexpected metadata IQ children'
iq_metadata_samples.append(iq_actual)
finally:
client.close()
assert archive.stat().st_size == archive_bytes, 'reconnect modified seeded history'
finally:
server.terminate()
try:
server.wait(timeout=5)
except subprocess.TimeoutExpired:
server.kill()
server.wait()
return {'status': 'pass', 'seeded_personal_archive_records': records, 'archive_bytes': archive_bytes,
'archive_expires_after': 'never', 'index_present_before_start': index_before_start,
'reconnect_samples': samples, 'expected_metadata': expected, 'observed_metadata': metadata_samples,
'mam_metadata_iq': distribution(metadata_ms), 'observed_iq_metadata': iq_metadata_samples,
'sasl2_bind2_success': distribution(auth_ms), 'tcp_starttls_to_bind2_success': distribution(connect_ms)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', required=True)
parser.add_argument('--records', type=int, default=100000)
parser.add_argument('--samples', type=int, default=10)
parser.add_argument('--output')
args = parser.parse_args()
assert args.records > 0 and args.samples > 0
source = Path(args.binary).resolve()
with tempfile.TemporaryDirectory(prefix='xmpp-bind2-archive-latency-') as directory:
root = Path(directory)
binary = root/'rust-xmpp'
shutil.copy2(source, binary)
report = {'binary_source': str(source), 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(),
'scope': 'real loopback TCP/STARTTLS/SASL2 Bind2; isolated personal MAM fixture; no production latency claim',
'timing': 'raw ordered samples; auth timer starts before authenticate and ends at parsed success; full timer includes TCP/STARTTLS/features; metadata IQ timer runs from send to parsed result',
'result': exercise(binary, root, args.records, args.samples)}
text = json.dumps(report, indent=2) + '\n'
if args.output:
Path(args.output).write_text(text)
print(text, end='')
if __name__ == '__main__':
main()
+74
View File
@@ -0,0 +1,74 @@
/* Test-only LD_PRELOAD shim: delay, then perform the real durable operation.
* Scope is strictly the canonical fixture data root selected by the harness.
* No host mount or durability settings are changed. Log contains no file data.
* Exceptional XMPP_SYNC_FAIL_SUFFIX=.wal|.rredo injects EIO for matching
* fixture regular files. Without that explicit fault mode all syncs are real.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
static long long ns(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (long long)t.tv_sec * 1000000000LL + t.tv_nsec;
}
static int sync_call(int fd, const char *operation) {
int (*real_sync)(int) = dlsym(RTLD_NEXT, operation);
if (!real_sync) { errno = ENOSYS; return -1; }
const char *prefix = getenv("XMPP_SLOW_SYNC_ROOT");
const char *log = getenv("XMPP_SLOW_SYNC_LOG");
char link[64], path[PATH_MAX];
snprintf(link, sizeof link, "/proc/self/fd/%d", fd);
ssize_t length = readlink(link, path, sizeof path - 1);
if (!prefix || !log || length < 0) return real_sync(fd);
path[length] = '\0';
size_t size = strlen(prefix);
if (strncmp(path, prefix, size) || (path[size] && path[size] != '/'))
return real_sync(fd);
struct stat st;
if (fstat(fd, &st) || (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)))
return real_sync(fd);
const char *delay = getenv("XMPP_SLOW_SYNC_MS");
long ms = delay ? strtol(delay, NULL, 10) : 100;
if (ms < 0 || ms > 10000) ms = 100;
long long begin = ns();
struct timespec wait = {.tv_sec = ms / 1000, .tv_nsec = (ms % 1000) * 1000000};
while (nanosleep(&wait, &wait) && errno == EINTR) {}
const char *suffix = getenv("XMPP_SYNC_FAIL_SUFFIX");
int injected = 0;
if (suffix && (!strcmp(suffix, ".wal") || !strcmp(suffix, ".rredo")) && S_ISREG(st.st_mode)) {
size_t suffix_length = strlen(suffix);
injected = (size_t)length >= suffix_length &&
!strcmp(path + length - suffix_length, suffix);
}
int result;
if (injected) { errno = EIO; result = -1; }
else result = real_sync(fd);
int saved_errno = errno;
long long end = ns();
/* One append write per operation avoids partial concurrent log records.
* Paths are not logged: only a path-derived store category. */
const char *store = strstr(path, "/rust_smacks") ? "sm" :
strstr(path, "/archive") ? "mam" :
strstr(path, "/pep") ? "pep" : "other";
char row[512];
int count = snprintf(row, sizeof row,
"{\"op\":\"%s\",\"kind\":\"%s\",\"store\":\"%s\",\"begin_ns\":%lld,\"end_ns\":%lld,\"result\":%d,\"injected\":%s}\n",
operation, S_ISDIR(st.st_mode) ? "directory" : "file", store, begin, end, result,
injected ? "true" : "false");
int output = open(log, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0600);
if (output >= 0) { ssize_t ignored = write(output, row, count); (void)ignored; close(output); }
errno = saved_errno;
return result;
}
int fsync(int fd) { return sync_call(fd, "fsync"); }
int fdatasync(int fd) { return sync_call(fd, "fdatasync"); }
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""Fail recovery journal syncs explicitly; never publish a false durable handoff.
Uses independent real TLS fixtures for .wal and .rredo so archive native inode
identity remains genuine. SIGKILL leaves complete valid journals. The scoped
test interposer returns EIO only for the selected suffix beneath fixture data.
"""
import argparse
import asyncio
import hashlib
import json
import os
from pathlib import Path
import shutil
import signal
import socket
import ssl
import subprocess
import tempfile
import time
from muc_restart_network import stop
from sm_restart_network import durable_slot
from slow_storage_latency_network import CARBONS, MeasuredClient, check_shim_scope, iq, sync_rows
from slow_storage_crash_network import committed_archive, start
async def seed(root, binary, config, port, log, env):
context = ssl.create_default_context(cafile=str(root/'cert.pem'))
clients, server = [], None
identifier = 'recovery-sync-body'
try:
server = await asyncio.to_thread(start, binary, config, port, log, env)
for number in range(4):
client = await MeasuredClient.connect(number, port, context, 2)
clients.append(client)
client.auto_ack = False
await client.enable()
await iq(client, f'carbons-{number}', f"<enable xmlns='{CARBONS}'/>")
await client.stanza('<presence/>')
await client.fence(f'ready-{number}')
alice, bob, _, _ = clients
await alice.stanza(f"<message to='{bob.jid}' type='chat' id='{identifier}'><body>{identifier}</body></message>")
await alice.handled()
for client, kind in zip(clients[1:], ('direct', 'sent', 'received')):
await client.wait(lambda c=client,k=kind: c.arrivals[(identifier,k)], 'message/carbon before crash')
server.send_signal(signal.SIGKILL)
assert await asyncio.to_thread(server.wait, 10) == -signal.SIGKILL
records = {client.token: durable_slot(root, client.token) for client in clients}
assert len(records) == 4
for client in clients[1:]:
assert sum(identifier in xml for xml in records[client.token]['pending']) == 1
archives = {p: committed_archive(p) for p in (root/'data'/'localhost'/'archive').glob('*.list')}
assert len(archives) == 2
assert all(sum(identifier in row for row in content.splitlines()) == 1 for content in archives.values())
return records, archives
finally:
if server is not None and server.poll() is None:
await asyncio.to_thread(stop, server)
await asyncio.gather(*(c.close_raw() for c in clients), return_exceptions=True)
def journal_bytes(root, suffix):
return {p: p.read_bytes() for p in (root/'data').rglob('*'+suffix)}
def assert_serve_rejects(binary, config, port, log, env):
server = subprocess.Popen([str(binary), 'serve', str(config), '--experimental'],
env=env, stdout=log, stderr=log)
listened = False
try:
deadline = time.monotonic()+15
while server.poll() is None and time.monotonic() < deadline:
try:
with socket.create_connection(('127.0.0.1', port), timeout=.05):
listened = True
break
except OSError:
time.sleep(.01)
assert not listened, 'SM recovery sync failure exposed a listener'
assert server.wait(timeout=5) != 0, 'SM recovery sync failure reported successful serve'
finally:
if server.poll() is None:
server.kill()
server.wait()
def scope_fault(work, shim):
root = work/'fault-scope'
root.mkdir()
code = """import errno,json,os,pathlib,sys
root,outside,suffix=pathlib.Path(sys.argv[1]),pathlib.Path(sys.argv[2]),sys.argv[3]
statuses=[]
for path in (root/('inside'+suffix),root/'unmatched.txt',outside):
with path.open('wb') as stream:
stream.write(b'fixture');stream.flush()
for sync in (os.fsync,os.fdatasync):
try:sync(stream.fileno());statuses.append(0)
except OSError as error:statuses.append(error.errno)
assert statuses==[errno.EIO,errno.EIO,0,0,0,0],statuses
"""
for suffix in ('.wal', '.rredo'):
log = work/('scope-'+suffix[1:]+'.jsonl')
env = dict(os.environ, LD_PRELOAD=str(shim), XMPP_SLOW_SYNC_ROOT=str(root),
XMPP_SLOW_SYNC_LOG=str(log), XMPP_SLOW_SYNC_MS='0', XMPP_SYNC_FAIL_SUFFIX=suffix)
subprocess.run(['python3', '-c', code, str(root), str(work/('outside'+suffix)), suffix],
env=env, check=True, capture_output=True)
rows = sync_rows(log)
assert len(rows) == 4 and sum(row['injected'] for row in rows) == 2
def run_case(work, binary, shim, suffix):
root = work/suffix[1:]
root.mkdir()
subprocess.run(['openssl', 'req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256',
'-nodes', '-days', '1', '-subj', '/CN=localhost', '-addext', 'subjectAltName=DNS:localhost',
'-keyout', str(root/'key.pem'), '-out', str(root/'cert.pem')], check=True, capture_output=True)
with socket.socket() as probe:
probe.bind(('127.0.0.1', 0))
port = probe.getsockname()[1]
config = root/'config.json'
config.write_text(json.dumps({'global': {'data_path': str(root/'data'), 'interfaces': ['127.0.0.1'],
'c2s_ports': [port], 's2s_ports': [], 'http_ports': [], 'https_ports': [],
'modules_enabled': ['tls', 'saslauth', 'smacks', 'ping', 'mam', 'carbons'],
'smacks_hibernation_time': 600, 'archive_expires_after': 'never', 'default_archive_policy': True,
'ssl': {'key': str(root/'key.pem'), 'certificate': str(root/'cert.pem')}},
'hosts': {'localhost': {'options': {'authentication': 'internal_hashed', 'password_hash': 'SHA-256'}}}}))
for number in range(2):
subprocess.run([str(binary), 'create-user', str(config), f'load-{number}', '--password-stdin'],
input='load-test-password\n', text=True, check=True, capture_output=True)
sync_log = root/'sync.jsonl'
env = dict(os.environ, LD_PRELOAD=str(shim), XMPP_SLOW_SYNC_ROOT=str(root/'data'),
XMPP_SLOW_SYNC_LOG=str(sync_log), XMPP_SLOW_SYNC_MS='0')
env.pop('XMPP_SYNC_FAIL_SUFFIX', None)
with (root/'server.log').open('w+') as log:
records, archives = asyncio.run(seed(root, binary, config, port, log, env))
journals = journal_bytes(root, suffix)
assert journals, 'network fixture did not leave requested journals'
fault_env = dict(env, XMPP_SYNC_FAIL_SUFFIX=suffix)
before = len(sync_rows(sync_log))
command = [str(binary), 'storage-checkpoint', str(config)]
result = subprocess.run(command, env=fault_env, capture_output=True, text=True)
assert result.returncode != 0, f'{suffix} recovery skipped journal sync and falsely reported successful handoff'
assert 'Durable native archive handoff:' not in result.stdout, 'failed checkpoint advertised handoff success'
attempts = [row for row in sync_rows(sync_log)[before:] if row.get('injected')]
assert attempts and all(row['result'] == -1 for row in attempts), 'recovery did not attempt faulted journal sync'
assert journal_bytes(root, suffix) == journals, 'failed checkpoint removed or changed authoritative journal'
if suffix == '.wal':
before_serve = len(sync_rows(sync_log))
assert_serve_rejects(binary, config, port, log, fault_env)
assert any(row.get('injected') and row['store'] == 'sm'
for row in sync_rows(sync_log)[before_serve:]), 'serve failed without attempting SM recovery sync'
assert journal_bytes(root, suffix) == journals, 'failed serve changed authoritative SM journals'
subprocess.run(command, env=env, check=True, capture_output=True, text=True)
assert not journal_bytes(root, '.wal') and not journal_bytes(root, '.rredo'), 'successful handoff left journals'
for token, record in records.items():
paths = list((root/'data').rglob(token+'.json'))
assert len(paths) == 1
exported = json.loads(paths[0].read_text())
assert {k:v for k,v in exported.items() if k != 'deadline_ms'} == {
k:v for k,v in record.items() if k != 'deadline_ms'}, 'handoff changed acknowledged SM state'
assert all(path.read_text() == content for path, content in archives.items()), 'handoff lost acknowledged MAM body'
return {'suffix': suffix, 'status': 'pass', 'checkpoint_rejected_sync_failure': True,
'journals_preserved_on_failure': len(journals),
'serve_rejected_before_listeners': suffix == '.wal',
'clean_retry_preserved_sessions': len(records), 'clean_retry_preserved_archives': len(archives)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', default='target/release/rust-xmpp')
parser.add_argument('--output', default='/tmp/journal-recovery-sync.json')
parser.add_argument('--suffix', nargs='+', choices=['.wal', '.rredo'], default=['.wal', '.rredo'])
args = parser.parse_args()
report = {'status': 'running', 'cases': []}
try:
with tempfile.TemporaryDirectory(prefix='xmpp-journal-recovery-sync-') as directory:
work = Path(directory)
binary = work/'rust-xmpp'
shutil.copy2(Path(args.binary).resolve(), binary)
report['binary_sha256'] = hashlib.sha256(binary.read_bytes()).hexdigest()
shim = work/'slow_sync.so'
subprocess.run(['cc', '-shared', '-fPIC', '-O2', '-Wall', '-Wextra', '-Werror',
str(Path(__file__).parent/'fixtures'/'slow_sync.c'), '-o', str(shim), '-ldl'], check=True)
check_shim_scope(work, shim)
scope_fault(work, shim)
for suffix in args.suffix:
report['cases'].append(run_case(work, binary, shim, suffix))
report['status'] = 'pass'
except BaseException as error:
report.update(status='fail', failure=str(error))
raise
finally:
Path(args.output).write_text(json.dumps(report, indent=2)+'\n')
print(json.dumps(report, indent=2))
if __name__ == '__main__':
main()
+347
View File
@@ -0,0 +1,347 @@
#!/usr/bin/env python3
"""A damaged MAM redo baseline must not take down the live message session.
Usage: python3 tests/mam_broken_baseline_network.py target/release/rust-xmpp
Reproduces the production symptom reported with Monal/Luma: the native archive at
`data/<host>/archive/<user>.list` no longer matches the anchor recorded in its
`.rredo` journal ("archive redo baseline changed in place"). On every message the
previous build drained the session or stalled delivery for seconds, so the client
timed out MAM and its message was delayed.
Prosody has no `.rredo` sidecar: it simply appends to the native `.list`. This
test seeds an archive for `load-0`, corrupts its redo baseline offline to force
that recovery error, then verifies that:
* a fresh message is still routed load-0 -> load-1 quickly,
* BOTH sessions survive (ping round-trip), no `connection-closed`,
* the runtime self-repairs: on the first access it retires the untrusted
journal, adopts the synced native as authoritative (exactly what Prosody
would treat as the archive), and continues appending on a fresh journal
without requiring an offline `storage-checkpoint` handoff,
* MAM history reads succeed on the recovered archive.
"""
import argparse
import hashlib
import json
import socket
import subprocess
import sys
import tempfile
import time
import xml.etree.ElementTree as ET
from pathlib import Path
from _wire_client import Client, local
REDO_MAGIC = b"XARRED01"
REDO_HEADER = 96
BODY = "must-survive-broken-baseline"
def start(binary, config, port, log):
server = subprocess.Popen(
[binary, "serve", str(config), "--experimental"], stdout=log, stderr=log
)
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
assert server.poll() is None, "server stopped during startup"
try:
with socket.create_connection(("127.0.0.1", port), timeout=0.1):
return server
except OSError:
time.sleep(0.05)
server.kill()
server.wait()
raise AssertionError("server did not start")
def stop(server):
server.terminate()
try:
code = server.wait(timeout=10)
except subprocess.TimeoutExpired:
server.kill()
server.wait()
raise AssertionError("SIGTERM did not complete orderly shutdown")
assert code == 0, f"SIGTERM shutdown exited {code}"
def corrupt_baseline(path):
"""Force the `archive redo baseline changed in place` recovery error.
Rebuilds a rotated-style journal header leaving the native file untouched:
the header records the native length as a non-empty baseline but stores a
wrong anchor. `recover_redo` then rejects it exactly as when an external
writer rewrote the native file in place after the journal rotated.
"""
native = path.read_bytes()
length = len(native)
assert length > 0, "seed produced an empty native MAM archive"
header = bytearray(REDO_HEADER)
header[:8] = REDO_MAGIC
header[8:16] = length.to_bytes(8, "little")
header[16:32] = b"\0" * 16 # dev/ino are only a cache identity, not validated
tail = native[-min(length, 4096) :]
real_anchor = hashlib.sha256(tail).digest()
wrong = bytearray(real_anchor)
wrong[0] ^= 0xFF
header[32:64] = wrong
header[64:96] = hashlib.sha256(header[:64]).digest()
path.with_suffix(".rredo").write_bytes(header)
assert hashlib.sha256(header[:64]).digest() == header[64:96], (
"rebuilt header checksum"
)
assert header[32:64] != real_anchor, "anchor did not diverge from native"
def run(binary):
with tempfile.TemporaryDirectory(prefix="xmpp-mam-broken-") as tmp:
root = Path(tmp)
subprocess.run(
[
"openssl",
"req",
"-x509",
"-newkey",
"rsa:2048",
"-nodes",
"-days",
"1",
"-keyout",
str(root / "key.pem"),
"-out",
str(root / "cert.pem"),
"-subj",
"/CN=localhost",
"-addext",
"subjectAltName=DNS:localhost",
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
with socket.socket() as probe:
probe.bind(("127.0.0.1", 0))
port = probe.getsockname()[1]
config = root / "config.json"
config.write_text(
json.dumps(
{
"global": {
"data_path": str(root / "data"),
"interfaces": ["127.0.0.1"],
"c2s_ports": [port],
"s2s_ports": [],
"http_ports": [],
"https_ports": [],
"modules_enabled": [
"tls",
"saslauth",
"smacks",
"ping",
"mam",
"carbons",
],
"smacks_hibernation_time": 600,
"archive_expires_after": "never",
"default_archive_policy": True,
"ssl": {
"key": str(root / "key.pem"),
"certificate": str(root / "cert.pem"),
},
},
"hosts": {
"localhost": {
"options": {
"authentication": "internal_hashed",
"password_hash": "SHA-256",
}
}
},
}
)
)
for number in range(2):
subprocess.run(
[
binary,
"create-user",
str(config),
f"load-{number}",
"--password-stdin",
],
input="load-test-password\n",
text=True,
check=True,
capture_output=True,
)
with (root / "server.log").open("w+") as log:
server = None
try:
# Seed: leave a real, valid MAM entry for load-0 so its archive
# carries a genuine .list + .rredo pair with a non-zero baseline.
server = start(binary, config, port, log)
alice = Client(
port, root / "cert.pem", "load-0", "load-test-password", "desktop"
)
bob = Client(
port, root / "cert.pem", "load-1", "load-test-password", "desktop"
)
alice.available()
bob.available()
alice.send(
f"<message to='{bob.jid}' type='chat' id='seed'><body>seed</body></message>"
)
bob.body("seed")
alice.sync()
stop(server)
server = None
for client in (alice, bob):
client.close()
finally:
if server is not None and server.poll() is None:
server.kill()
server.wait()
# The sender localpart is percent-encoded on disk (`load-0` -> `load%2d0`).
sender_archive = root / "data" / "localhost" / "archive" / "load%2d0.list"
assert sender_archive.exists(), "seed did not create load-0 MAM archive"
assert sender_archive.with_suffix(".rredo").exists(), (
"seed did not attach load-0 redo journal"
)
corrupt_baseline(sender_archive)
damaged_journal = sender_archive.with_suffix(".rredo").read_bytes()
with (root / "server.log").open("a") as log:
server = None
try:
server = start(binary, config, port, log)
alice = Client(
port, root / "cert.pem", "load-0", "load-test-password", "desktop"
)
bob = Client(
port, root / "cert.pem", "load-1", "load-test-password", "desktop"
)
alice.available()
bob.available()
# A damaged MAM baseline must not drop the dispatch: deliver and
# keep both sessions alive.
alice.send(
f"<message to='{bob.jid}' type='chat' id='live'><body>{BODY}</body></message>"
)
bob.body(BODY)
# Ping round-trips prove neither session was closed by the archive fault.
alice.sync()
bob.sync()
alice.send(
f"<message to='{bob.jid}' type='chat' id='second'><body>second</body></message>"
)
bob.body("second")
alice.sync()
bob.sync()
finally:
if server is not None and server.poll() is None:
server.kill()
server.wait()
log_text = (root / "server.log").read_text()
assert not any(
"connection-closed" in l and "protocol-or-transport-error" in l
for l in log_text.splitlines()
), "a session was closed by the archive fault"
assert not any("session-error-label" in l for l in log_text.splitlines()), (
"unhandled archive fault labelled a session error"
)
# Prosody-compatible self-repair: the first access after the corruption
# retired the untrusted journal and the archive was appended to on a fresh
# journal. No `mam-archive-skipped` should have been logged because the
# event delivery succeeded on the recovered native.
assert not any("mam-archive-skipped" in l for l in log_text.splitlines())
# A fresh journal must have replaced the damaged one (the archive kept
# accepting records). Its baseline now matches the current native.
journal = sender_archive.with_suffix(".rredo")
assert journal.exists(), "runtime did not recreate a fresh redo journal"
assert journal.read_bytes() != damaged_journal, (
"runtime kept the damaged journal instead of retiring it"
)
fresh = journal.read_bytes()
assert len(fresh) >= REDO_HEADER
assert fresh[:8] == REDO_MAGIC
# The fresh journal is valid: its header digest checks out, so recovery
# will accept it as the authoritative pairing with the current native.
assert hashlib.sha256(fresh[:64]).digest() == fresh[64:96], (
"recreated journal header is not self-consistent"
)
# MAM history on the recovered, now-authoritative native must serve reads.
phase3_offset = len((root / "server.log").read_text().splitlines())
with (root / "server.log").open("a") as log:
server = None
try:
server = start(binary, config, port, log)
alice = Client(
port, root / "cert.pem", "load-0", "load-test-password", "desktop"
)
bob = Client(
port, root / "cert.pem", "load-1", "load-test-password", "desktop"
)
alice.available()
bob.available()
# A MAM query to load-0's own archive must serve history.
alice.send(
f"<iq type='set' id='mam' to='load-0@localhost'>"
f"<query xmlns='urn:xmpp:mam:2'/>"
f"<x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE'>"
f"<value>urn:xmpp:mam:2</value></field></x></iq>"
)
result_iq = alice.iq("mam")
assert (
result_iq.find(
".//{urn:ietf:params:xml:ns:xmpp-stanzas}internal-server-error"
)
is None
), ET.tostring(result_iq)
# A new message still archives cleanly on the recovered journal.
alice.send(
f"<message to='{bob.jid}' type='chat' id='post-repair'><body>post-repair</body></message>"
)
bob.body("post-repair")
alice.sync()
finally:
if server is not None and server.poll() is None:
server.kill()
server.wait()
post_log = (root / "server.log").read_text().splitlines()[phase3_offset:]
fresh_errors = [
l for l in post_log if "archive redo baseline changed in place" in l
]
assert not fresh_errors, (
"archive fault returned after self-repair:\n" + "\n".join(fresh_errors)
)
# Offline handoff stays safe: checkpoint of an already-repaired archive is
# idempotent (retires an absent journal) and still reports a handoff.
result = subprocess.run(
[binary, "storage-checkpoint", str(config)],
capture_output=True,
text=True,
check=True,
)
assert "Durable native archive handoff:" in result.stdout
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--binary", default="target/release/rust-xmpp")
args = parser.parse_args()
run(args.binary)
print("PASS: damaged MAM redo baseline does not drop live message delivery")
if __name__ == "__main__":
main()
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more