- Исправлена задержка доставки 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).
This commit is contained in:
wt committed 2026-10-21 11:20:09 +07:00
1 parent b798b3f9b7
commit e9329ccbb2
48 files changed
+11073 -235

No files matched your search

+5
View File
@@ -80,6 +80,11 @@ jobs:
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
if-no-files-found: warn
retention-days: 14
+30 -3
View File
@@ -28,8 +28,8 @@
В нём сейчас нет `.git`. Если следующая среда содержит Git checkout, используйте
его состояние и инструкции, не создавайте второй расходящийся источник истины.
Версия Rust source — **0.8.0**. Последующие изменения добавили GitHub Actions,
Docker target для проверенного бинарника и этот handoff без изменения Rust source.
Версия Rust source — **0.8.2**. В0.8.0 добавлены durable MUC/SM, затем CI
и этот handoff;0.8.1 исправляет задержку сообщений. См. docs/MESSAGE-LATENCY.md.
Перед передачей исходники упаковываются в ZIP; актуальный архив должен содержать
этот файл и `.github/workflows/ci.yml`. В среде ChatGPT сохраняйте новый архив
через доступный Library workflow, обновляя существующий проект, если его identity
@@ -38,7 +38,8 @@ Docker target для проверенного бинарника и этот han
## С чего начать
1. Прочитайте README.md, docs/COMPATIBILITY.md, docs/VALIDATION.md.
2. Для MUC/SM: docs/MUC-RECOVERY.md и CHANGELOG-0.8.0.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.
@@ -189,6 +190,32 @@ GitHub workflow проверен actionlint1.7.7, shell/Python syntax и слу
Реального 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 после загрузки репозитория и настройки
+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).
Generated
+1 -1
View File
@@ -1111,7 +1111,7 @@ dependencies = [
[[package]]
name = "rust-xmpp"
version = "0.8.0"
version = "0.8.2"
dependencies = [
"anyhow",
"axum",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-xmpp"
version = "0.8.0"
version = "0.8.2"
edition = "2024"
description = "Experimental Rust XMPP server with Prosody-compatible storage"
license = "MIT"
+14 -1
View File
@@ -1,4 +1,4 @@
# Rust XMPP 0.8.0
# Rust XMPP 0.8.2
Сервер на Rust, разрабатываемый для замены **Prosody 13.0 nightly 100,
cafd74b9c5ea** с предоставленными модулями. Читает существующий Lua-конфиг,
@@ -238,3 +238,16 @@ recovery Rust служит собственный журнал. Перед от
Полные тесты, 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).
+2 -2
View File
@@ -11,14 +11,14 @@
|---|---|---|
| Конфигурация | 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/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 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 rules, delay, flush при клиентской активности и grace period | Событие socket drain приближено завершением записи; необязательные расширенные XPath-фильтры поддержаны не полностью |
| 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 |
+1 -1
View File
@@ -53,7 +53,7 @@ Image smoke проходит и на PR. Только после успеха в
- `sha-<полный40символьныйcommit>` — каждый публикуемый commit;
- `latest` — только основная ветка;
- `v0.8.0` — пример version tag; он должен совпадать с Cargo.toml.
- `v0.8.2` — пример version tag; он должен совпадать с Cargo.toml.
Version tag не меняет latest; prerelease также не перезаписывает latest.
Собирается Linux amd64, не multiarch. Локальный `docker build .` по-прежнему
+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.
+102
View File
@@ -0,0 +1,102 @@
# Личные сообщения: исправления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/.
+19 -9
View File
@@ -1,5 +1,8 @@
9c4e46e42c549436c503a4cd70f90efd8f7754ce8c2103a6d1352cd34174aac0 .github/workflows/ci.yml
06e1035f41aa02cd4d0fbc14db3160290f9e5384791c1b93f3268bc67aeee3e2 AGENTS.md
79f3f5cd46255826869c9151151d3471dd7139c2ea8fb2da4b2b6fbea113f0cf .github/workflows/ci.yml
d9732ba55b93d2907cd1a2a0fa20bc1f46e0f2970de0e9f2bf730048212cfc31 AGENTS.md
f8e6a12039b067ed5af42320bca42fb0555d9a5f438d1652860a5be4534469d1 Cargo.lock
4e8d7ee328f044be284e0cb436f13a468adcac3058bbb1be381f8bff96603e5a Cargo.toml
6be2fbcd1da8a07dda7daf3ddd727b38f363751ebace9c0b87c282f8961cffed Dockerfile
7136ea911bb6b6d352f703f80efeebde921028ea9dc39cd0d2cd55ea465f8c37 examples/config.json
8a65739eb5c24f10b9f2c509f0bd050fb1115706e6a41fdf804b9e0e44f583fd examples/config.toml
ca86274bcf4dc5daebbf69c9f003854afca92954719aff068f810e0a3d615dea examples/config.yaml
@@ -22,13 +25,13 @@ b1018e1b279c89f6cf14b4017e4f1b14e9165afef2f668cec6ac8bbe0c710d85 scripts/integr
e08ac6136cce27cda2ddbd510da0f25750766ab0150a9f4b8f87c953276d16e9 scripts/prosody_compat/oracle.lua
2c7a2c946885205e9df38b97305073a8ac72f58ac8ff84fc6f9af320c35bbc96 scripts/prosody_compat/verify.sh
1b9ed063daf3e0320819b4fe0135c96dd27e49877044aa2f5093b8ab4fbda41d scripts/smoke.py
caa74bdd83f0b952b875f613cdbd238d7f903408c2176d54178ce18994b53e95 scripts/verify-local.sh
c32bc7ed2d5f954a2b3de06db31fb1580378259edfc8ce574aa5cd5719cc2a59 scripts/verify-local.sh
a2e47f547ab9349f84ee6c4f8e51c147b7110b41a82c17cdb938027a84d82ef3 src/account_modules.rs
5637f816586f0613b83e14d0ed478bd539f325b70ccbfc9fdf893adbe494453d src/admin.rs
89d7e29d8f8daab614c9c836864b166fdaa324ca035c2d3d5f95639c24b2a9e1 src/archive.rs
7b844cbe377b057555436378ab8bd784f99b85e7c9992cb3c5a044f40326951f src/archive.rs
e15b079a420227ee59e3b7ac305783c51eb59d76a8fb5dd12760547b0f812107 src/auth.rs
83ac35a4c9d20debf006fe80fba8e423c09806fb2526ff384b188ca28d612a5e src/config.rs
55f8e68d0d41a81d2254a0c5aef4146488127f771c5b71ccd2e23d17dd1e8e79 src/csi.rs
175ab07f17ba14c51c76857c4905b0a587d8be68e2be5179456e9c71a523c487 src/csi.rs
834ebe4a1f2d46ab932c14c0a2c7be3375873a71ef56026d372c86e60ef041fc src/data_lock.rs
1c8aa5cb816a32a0f9f24589ac00beba27544a83c3d9aad771523a1d1cbcd4c8 src/extras.rs
dca8959effb123b94e1fbd42c561a79c721178c4b612a004db9f9fccb3b8950d src/federation.rs
@@ -41,8 +44,8 @@ c604a0a764ffac6bae0c9aa15c9ef7022d35cb1f8c9159eb66257114445621c5 src/http_trans
a21e1119ea0271b63935e92eb106ab6076f0035e6d3a06c21a352f152b9b37db src/logging.rs
58ce2a9daa36ac26b39c8f263a16f4fa8cc4e57c91bbbf5b24a6aad3f1a326e0 src/lua.rs
a264e2765a31c8c096462551aca229530cad356e39e338370d1a0bdd97860680 src/main.rs
71d5d185900b9cde190e10e2f28371cd5042d98b5900b4385d823f724234fced src/mam.rs
d8b69775bcdb01597bcaffa6123c636a226922f349beaa274fe77ff1e36bc5ef src/muc.rs
68d97d96aa3a370474c144ccfdbf16109c46a6b14123d55d30464237ae29a958 src/mam.rs
f17b06fe5b76381b5cdbfc2b668107f6c2b0b456c05b7a7709bea6a284531ff5 src/muc.rs
c7ba46a140b9dce65cc573176265d6f77049ae657a70ffe59611a1c0f3ef89af src/muc_admin.rs
ed2f07105ffeb80396aaf91c7a1fe9a8dc88d17235d03a0796372c1797c1506a src/muc_history.rs
636f6ea7ad7cc98390ac775a93b469d2f3e0f9e138be5bd1e256cad5037c7183 src/muc_recovery.rs
@@ -53,9 +56,9 @@ f09a6479b76c9c601a169b5f1f9cf1364e82df5ae27e4bab8d1b1428b5cad457 src/preauth.rs
8dbb7a6b43a5f0562a775df42a959ab8badc466135a8b32da86cd79615b5acbf src/pubsub.rs
913cd033f60a68f6c7cf9b3adbaf76db1c5c9bc88a0599196d373b16b64c0a1e src/push.rs
bec477f2520af6a9d7ca54e43eb12158b53e3cf09057653e504dfdeab956885e src/sasl.rs
9cfef150f88a08ee0cfb33b63b621e5ecd058daa08f19119379c099f9c7f7e7a src/server.rs
a445dcab6d0275861b08412a4bd622cb72c3f462ebba5eaedad360e740a6513c src/server.rs
c7450f9058528e3030df559840f87dae591a2b88be8503872956e9b78435d916 src/session_store.rs
a4e6faac3b5ab9b7e6e2c586387cb32d18a01fbd1ef65302cbc92ccbce934044 src/sessions.rs
1d4e890315886bc4fd0b824e034a5c1833a1f2c1347e8c3ca77254e13a2acab9 src/sessions.rs
71d7ce1036a091d8c27016ea7ce4cffedf904dfca9d1cc4891b7dc66a1e06a4e src/storage.rs
462b00edb2c1e02dd34524bafe4f82b5ef902028b44310ba31b085d31f4dc2aa src/tls_binding.rs
a78592aa79d8f01c22af1457d0c3f5922863ccd3e8ebb1aa05e043cc44579b8a src/upload_migration.rs
@@ -63,8 +66,10 @@ 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
cb6b7bbcd1bc7ee01a4ccf892d4b6074765685c56c4b8bb46e0381b834577b6a tests/auth.rs
011bbdd00c8ff8b648076d133086c395f6784cdcbb0c9f4e1efcbc15c1e79d9c tests/bind2_archive_latency_network.py
b3c3470f0b9b64584abf98c26a4578b0b90089086d00f7b07187aa5f6db6a417 tests/cli.rs
cc58adbb0fc867802b09cf7bd0d25397a273e721fbd0d0cb0ce30c19e80c13a2 tests/config.rs
ba364e9ea561624b2c649cd0dda559e213c75e6d7080ae85f0c23f1f13d117e1 tests/csi.rs
@@ -83,6 +88,7 @@ f1ea3eb78d5c716f4281cf17cb41b9fb22e08dbb4cf08ed9c8477c79b7d2382b tests/http_pro
da2fc65e931ec74fbcdcd26d04f2cb073e2dc1e354139135166c79580973a477 tests/iq_errors.rs
05160f17f8098466a67c9b069c165dc9d94a84ab03a49896169f110baf8e869a tests/logging.rs
dfc3429e3e755d4c99ec1bd8ee6fb0f1f6aaf86414d5adaca7e9e3b18c7fe3f7 tests/logging_network.py
0141961eadf9246ce287d7ee0dbbbf5243fd6e1332e09ccd2f6a591be419c2c3 tests/message_latency_network.py
329ccbd4faee6cbbfa92d2fa0248844721b7ffc033baae4ec2570e9893f5df9f tests/message_routing.rs
1134fefd5bb75884b096b8542c4f9745e64fd5dfcf87a6bba8d665aad2185a8a tests/muc_access.rs
8120c66b85c0884619e39eea0b8761e11b41f2ed14c5be00146e35ca4714a9a0 tests/muc_crash_network.py
@@ -91,6 +97,7 @@ dfc3429e3e755d4c99ec1bd8ee6fb0f1f6aaf86414d5adaca7e9e3b18c7fe3f7 tests/logging_
1d06b769b59012732ba04497a995f4f54df127f58c5f4f264a5df99589f31a94 tests/muc_extensions.rs
a8a01faf51bf82bce6cbb2616cdaabb6337047d1c5f17d5f39975c0528a1d5ae tests/muc_flow.rs
b24bd920f096576c2009ff1ccc0366eb8734c5f0b2feb39f04ad7087268a94b9 tests/muc_history.rs
1b34ab721eec69295e1274e44bfd108c0e117dd383624cb6c98d851eb20e7a9a tests/muc_latency_storage.rs
1f3df5f3f39143adc2b2a419fac0cd74b2229337ffa86570261d1f6ac61137a1 tests/muc_native.rs
3df2736ccfd25a6cab290df80ceb7474ea44bb02af6cf768a9216d6b8f7f8630 tests/muc_optional_admin.rs
5dc93407a658b45e504b89e26cfc8f495cd28ad90d0a308b7d228a472435ff9f tests/muc_protocol.rs
@@ -106,6 +113,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
@@ -113,6 +121,7 @@ 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
@@ -125,6 +134,7 @@ ddd9d5b46dc943138a334c64477fb11970b3d45e4cfbeaafc7348754f1e5e29e tests/sasl_acc
a7d1519f4f44cc7bfad3d20bcfdf455e92abdd084feed493ffc9e0267a387740 tests/sasl_network.py
4748fa227743686f78a0fa0364735d2dda5995f8d70a79ad03c1aa1decb42978 tests/sasl_retry_network.py
d8882b8aabd6ff603c81fbbc259eefa68772afb2b3eec2e003ac29efd65d146b tests/sasl_web_network.py
7d69c14ced7884450b9a1ff63cd984cdb9dfe948d6c56631c7593b6fabdabf7f tests/session_routing_isolation.rs
d9eef5c37fcd99404619d61dbf7a2bf7673087924d93e69b08769e716afe297b tests/sm_durable_slots.rs
0b74b2ff47b8ae681b5613bc87e914fdf8e5033156180df06ff2174df2781a5a tests/sm_restart_network.py
72f2d60e47e5295b82d497289b78925a64a1d986c70c277015ae228d211d32bb tests/storage_accounts.rs
+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).
+42 -41
View File
@@ -1,50 +1,51 @@
# Проверка 0.8.0
# Проверка0.8.2
Linux x86_64, Rust1.99.1, 2026-10-20. Итоговый полный прогон:
[verify.txt](validation-logs-0.8.0/verify.txt).
Структурированный отчёт: [validation-results.json](validation-results.json).
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 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 — успешно |
|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: 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.
MUC+SM total 45.54с, peak sampled RSS 40.94MiB.
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.
Воспроизведение личного push-stall: прежний0.8.1 за2с сохраняет только первое
сообщение и не отвечаетSMACK при зависшем postTLSfeatures gateway. Новый0.8.2
сохраняет оба и отвечаетh=4 в том же2сокне; тестовое окно не является latency SLA.
Offline ordering baseline проверен на0.8.0; его точный hash указан отдельно.
Первый основной прогон остановился на пятисекундном readiness timeout нового
нагрузочного теста после успешных обычных и сетевых проверок. В самом тесте
таймаут увеличен до60секунд; нагрузка500 повторена с той же release-сборкой,
а native-проверки выполнены отдельно. verify.txt объединяет успешные части
этих запусков; первоначальный лог сохранён в initial-load-timeout.txt.
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.
В процессе проверки исправлены потеря сохранённых presence/history, защита
истёкших recovery claims, отсутствие успешного ответа при удалении аккаунта,
учёт stanza до SM enable и потеря MUC fanout до подключения другого участника.
Первоначальный тест последнего случая принимал позднюю историю за доставку;
итоговый тест проверяет именно private pending queue до аутентификации.
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).
Реальные TCP/TLS-сокеты после завершения процесса отсутствуют: требуется новое
защищённое аутентифицированное соединение. SM допускает повторную доставку при
неопределённом сетевом результате; это не exactly-once транзакция между всеми
хранилищами и сетью. Известные встроенные возможности MUC перенесены, произвольные
community Lua-модули не исполняются. Пределы записей и порядок восстановления:
[MUC-RECOVERY.md](MUC-RECOVERY.md). Полная замена всей установки и проверка
реальными клиентами этим не заявляются: [COMPATIBILITY.md](COMPATIBILITY.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.
10GiB upload в0.8.0 не повторялся. Исторические испытания local/MinIO находятся
в VALIDATION-0.7.8.md и VALIDATION-0.7.9.md; их результаты не приписываются новой сборке.
Это 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).
@@ -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
+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
+5
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
@@ -29,6 +30,10 @@ 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/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
+143 -30
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},
};
@@ -212,46 +212,23 @@ fn scan_unlocked<F: FnMut(Entry) -> Result<()>>(
let mut reader = BufReader::new(file);
let mut line = Vec::new();
let mut record = Vec::new();
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 decoder = RecordDecoder::new()?;
let mut decode = |data: &[u8]| -> Result<()> {
if data.iter().all(u8::is_ascii_whitespace) {
return Ok(());
if let Some(entry) = decoder.decode(data)? {
visit(entry)?;
}
let count = std::rc::Rc::new(std::cell::Cell::new(0));
lua.set_hook(
mlua::HookTriggers::new().every_nth_instruction(1000),
move |_, _| {
count.set(count.get() + 1);
if count.get() > 2000 {
return Err(mlua::Error::runtime("archive record instruction limit"));
}
Ok(mlua::VmState::Continue)
},
)
.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
.load(&chunk)
.set_mode(mlua::chunk::ChunkMode::Text)
.set_environment(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(())
};
loop {
line.clear();
let n = (&mut reader)
.take(16 * 1024 * 1024 + 1)
.take(MAX_RECORD_BYTES as u64 + 1)
.read_until(b'\n', &mut line)?;
if n == 0 {
break;
}
anyhow::ensure!(
line.len() <= 16 * 1024 * 1024,
line.len() <= MAX_RECORD_BYTES,
"archive record line exceeds 16 MiB"
);
if line.starts_with(b"item") && !record.is_empty() {
@@ -260,12 +237,148 @@ fn scan_unlocked<F: FnMut(Entry) -> Result<()>>(
}
record.extend_from_slice(&line);
anyhow::ensure!(
record.len() <= 16 * 1024 * 1024,
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()))?;
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(None);
}
let count = std::rc::Rc::new(std::cell::Cell::new(0));
self.lua
.set_hook(
mlua::HookTriggers::new().every_nth_instruction(1000),
move |_, _| {
count.set(count.get() + 1);
if count.get() > 2000 {
return Err(mlua::Error::runtime("archive record instruction limit"));
}
Ok(mlua::VmState::Continue)
},
)
.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 = self
.lua
.load(&chunk)
.set_mode(mlua::chunk::ChunkMode::Text)
.set_environment(self.env.clone())
.eval::<mlua::Value>()
.map_err(|e| anyhow!(e.to_string()))?;
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(&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 {
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!(
start > 0 && window < MAX_RECORD_BYTES as u64,
"archive last record exceeds 16 MiB or has no native item boundary"
);
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(&prefix);
anyhow::ensure!(
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"
);
}
}
}
fn load_unlocked(root: &Path, host: &str, user: Option<&str>, store: &str) -> Result<Vec<Entry>> {
let mut out = vec![];
scan_unlocked(root, host, user, store, |e| {
+164 -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,
@@ -19,6 +30,37 @@ pub(crate) struct DurableBuffer {
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 {
@@ -63,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
@@ -82,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),
}
}
}
}
@@ -199,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>"));
}
}
+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!(
+18 -18
View File
@@ -990,10 +990,18 @@ impl Muc {
if r.data("persistent").as_bool() == Some(true) || self.pending_occupants.has_room(room) {
let mut merged = r.original.clone();
merge_value(&mut merged, &r.frozen);
crate::storage::atomic_write(
&self.path(room),
crate::storage::serialize(&merged)?.as_bytes(),
)?;
let path = self.path(room);
let encoded = crate::storage::serialize(&merged)?;
// Native files are compatibility exports. Keep retrying missing or
// changed exports, but avoid an atomic write/fsync for unchanged rooms.
let unchanged = match std::fs::read(&path) {
Ok(bytes) => bytes == encoded.as_bytes(),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
Err(error) => return Err(error.into()),
};
if !unchanged {
crate::storage::atomic_write(&path, encoded.as_bytes())?;
}
let path = self
.root
.join(crate::storage::encode_name(&self.domain))
@@ -1003,8 +1011,10 @@ impl Muc {
} else {
json!({})
};
index[room] = json!(true);
write_value(&path, &index)?;
if index.get(room) != Some(&json!(true)) {
index[room] = json!(true);
write_value(&path, &index)?;
}
} else if self.path(room).exists() {
std::fs::remove_file(self.path(room))?;
crate::storage::update(&self.root, &self.domain, None, "persistent", |v| {
@@ -2059,18 +2069,8 @@ impl Muc {
xml: archived,
},
)?;
if let Some(seconds) = self.retention_seconds {
crate::archive::expire_before(
&self.root,
&self.domain,
Some(room.split('@').next().unwrap()),
"muc_log",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs_f64()
- seconds,
)?;
}
// Retention runs during maintenance and before MAM queries.
// A normal append must not scan/rewrite the entire archive.
payload
} else {
payload
+178 -75
View File
@@ -46,6 +46,7 @@ pub struct Runtime {
uploads: Arc<BTreeMap<String, crate::http_service::UploadService>>,
started: Instant,
push: Arc<Mutex<crate::push::Push>>,
push_routes: mpsc::Sender<(String, String)>,
federation: Option<crate::federation::Federation>,
stopping: Arc<std::sync::atomic::AtomicBool>,
}
@@ -318,33 +319,24 @@ impl Drop for Cleanup {
));
}
}
if !queued.is_empty()
&& let Ok(handle) = tokio::runtime::Handle::try_current()
{
let rt = self.runtime.clone();
handle.spawn(async move {
for (user, identifier, messages) in queued {
let result = rt.push.lock().unwrap().notify_queue(
&rt.data,
&rt.domain,
&user,
&messages,
&rt.config,
&identifier,
);
let notifications = if let Ok(result) = result {
if result.reset_watchdog {
reset_push_watchdog(&rt, &user, &identifier);
}
result.deliveries
} else {
Vec::new()
};
for (to, payload) in notifications {
let _ = route_delivery(&rt, &to, &payload).await;
}
let rt = &self.runtime;
for (user, identifier, messages) in queued {
let result = rt.push.lock().unwrap().notify_queue(
&rt.data,
&rt.domain,
&user,
&messages,
&rt.config,
&identifier,
);
if let Ok(result) = result {
if result.reset_watchdog {
reset_push_watchdog(rt, &user, &identifier);
}
});
for (to, payload) in result.deliveries {
enqueue_push_route(rt, to, payload);
}
}
}
}
}
@@ -565,7 +557,7 @@ fn reset_push_watchdog(rt: &Runtime, user: &str, identifier: &str) {
"smacks_hibernation_time",
300,
));
for session in rt.hub.all() {
for session in rt.hub.for_owners(&[&owner]) {
let mut s = session.lock().unwrap();
if s.owner == owner && s.push_identifier.as_deref() == Some(identifier) {
s.first_important_push(timeout);
@@ -932,6 +924,7 @@ pub async fn serve(path: &Path) -> Result<()> {
"durable-sessions-loaded",
&[("count", &hub.all().len().to_string())],
);
let (push_routes, push_route_rx) = mpsc::channel(1024);
let runtime = Runtime {
_data_guard: data_guard,
domain: domain.clone(),
@@ -963,9 +956,14 @@ pub async fn serve(path: &Path) -> Result<()> {
uploads: Arc::new(uploads),
started: Instant::now(),
push: Default::default(),
push_routes,
stopping: Default::default(),
federation,
};
let push_worker = PushRouteWorker(tokio::spawn(run_push_routes(
runtime.clone(),
push_route_rx,
)));
let interfaces = c
.option(&domain, "interfaces")
.and_then(|v| v.as_array())
@@ -1317,6 +1315,7 @@ pub async fn serve(path: &Path) -> Result<()> {
);
}
tasks.abort_all();
push_worker.0.abort();
Ok(())
}
fn bind_listener(addr: std::net::SocketAddr) -> Result<TcpListener> {
@@ -1350,9 +1349,14 @@ async fn csi_send<S: AsyncWrite + Unpin>(
let output = if let Some(session) = session {
let mut s = session.lock().unwrap();
let owner = s.owner.clone();
let output = s.csi.output(xml.into(), &owner, Instant::now());
s.checkpoint()?;
output
let output = s.csi.pending_output(xml.into(), &owner, Instant::now());
if output.durable_changed()
&& let Err(error) = s.checkpoint()
{
output.rollback(&mut s.csi);
return Err(error);
}
output.commit()
} else {
vec![xml.into()]
};
@@ -1367,9 +1371,14 @@ async fn csi_activity<S: AsyncWrite + Unpin>(
) -> Result<()> {
let output = if let Some(s) = session {
let mut s = s.lock().unwrap();
let output = s.csi.activity(Instant::now());
s.checkpoint()?;
output
let output = s.csi.pending_activity(Instant::now());
if output.durable_changed()
&& let Err(error) = s.checkpoint()
{
output.rollback(&mut s.csi);
return Err(error);
}
output.commit()
} else {
Vec::new()
};
@@ -2433,6 +2442,17 @@ fn pep_iq_metadata(
Some(fields)
}
// Ordinary account traffic must not wait for MUC archive maintenance or room
// checkpoints. Component identity comes from immutable, validated configuration.
fn is_muc_destination(config: &Config, target: &str) -> bool {
let target = protocol::bare(target);
let domain = target.rsplit_once('@').map_or(target, |(_, domain)| domain);
config
.hosts
.get(domain)
.is_some_and(|host| host.component.as_deref() == Some("muc"))
}
fn dispatch_iq(
rt: &Runtime,
jid: &str,
@@ -2446,7 +2466,7 @@ fn dispatch_iq(
let typ = iq.attribute("type").unwrap_or("");
let target = iq.attribute("to").unwrap_or(&rt.domain);
let target_bare = protocol::bare(target);
{
if is_muc_destination(&rt.config, target) {
let mut rooms = rt.mucs.lock().unwrap();
for muc in rooms.values_mut() {
if let Some(result) = muc.handle(jid, iq)? {
@@ -2875,15 +2895,7 @@ impl<S: AsyncRead + Unpin> AsyncRead for ActivityReader<'_, S> {
result
}
}
fn configure_bound_session(
rt: &Runtime,
s: &SharedSession,
client_id: Option<String>,
csi_inactive: bool,
) -> Result<()> {
s.lock().unwrap().ack_limit =
number(&rt.config, &rt.domain, "smacks_max_unacked", 100) as usize;
s.lock().unwrap().client_id = client_id.clone();
fn configure_csi_buffer(rt: &Runtime, buffer: &mut crate::csi::Buffer) {
if rt.enabled("csi_simple") {
let limit = number(&rt.config, &rt.domain, "csi_queue_size", 256) as usize;
let delay = period(
@@ -2901,13 +2913,25 @@ fn configure_bound_session(
.filter_map(|v| v.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default();
s.lock()
.unwrap()
.csi
.configure(limit, Duration::from_secs_f64(delay), payloads);
.unwrap_or_else(|| {
crate::csi::DEFAULT_IMPORTANT_PAYLOADS
.iter()
.map(|value| (*value).to_owned())
.collect()
});
buffer.configure(limit, Duration::from_secs_f64(delay), payloads);
}
}
fn configure_bound_session(
rt: &Runtime,
s: &SharedSession,
client_id: Option<String>,
csi_inactive: bool,
) -> Result<()> {
let mut session = s.lock().unwrap();
session.ack_limit = number(&rt.config, &rt.domain, "smacks_max_unacked", 100) as usize;
session.client_id = client_id;
configure_csi_buffer(rt, &mut session.csi);
session.csi.set_inactive(csi_inactive);
session.checkpoint()?;
Ok(())
@@ -2978,7 +3002,14 @@ async fn restore_muc_after_resume<S: AsyncWrite + Unpin>(
rt: &Runtime,
ctx: &ConnectionLog,
) -> Result<()> {
let full = session.lock().unwrap().jid.clone();
let full = {
let mut state = session.lock().unwrap();
// Restored slots may contain the previous release's policy; current
// explicit configuration/defaults apply equally to bind and resume.
configure_csi_buffer(rt, &mut state.csi);
state.checkpoint()?;
state.jid.clone()
};
let results = {
let mut rooms = rt.mucs.lock().unwrap();
rooms
@@ -3084,15 +3115,11 @@ async fn authenticated_transport<S: AsyncRead + AsyncWrite + Unpin>(
}
}
if rt.enabled("mam") {
let mut first = None;
let mut last = None;
crate::archive::scan_store(&rt.data, &rt.domain, Some(&user), "archive", |entry| {
if first.is_none() {
first = Some((entry.id.clone(), entry.when));
}
last = Some((entry.id, entry.when));
Ok(())
})?;
let edges = crate::archive::edges_store(&rt.data, &rt.domain, Some(&user), "archive")?;
let (first, last) = match edges {
Some((first, last)) => (Some((first.id, first.when)), Some((last.id, last.when))),
None => (None, None),
};
bound.push_str("<metadata xmlns='urn:xmpp:mam:2'>");
for (element, edge) in [("start", first), ("end", last)] {
if let Some((id, when)) = edge {
@@ -3275,6 +3302,7 @@ async fn authenticated_transport<S: AsyncRead + AsyncWrite + Unpin>(
);
ctx.event(Level::Debug, "stanza-received", &[("kind", kind)]);
let handled_session = session.clone();
let processing_started = Instant::now();
async {
if kind == "iq" {
let typ = n.attribute("type").unwrap_or("");
@@ -3434,7 +3462,7 @@ async fn authenticated_transport<S: AsyncRead + AsyncWrite + Unpin>(
apply(stream, session.as_ref(), &rt, result).await?;
} else {
let full = jid.as_deref().context("binding required")?;
let muc_result = {
let muc_result = if n.attribute("to").is_some_and(|to| is_muc_destination(&rt.config, to)) {
let mut rooms = rt.mucs.lock().unwrap();
let mut found = None;
for muc in rooms.values_mut() {
@@ -3444,6 +3472,8 @@ async fn authenticated_transport<S: AsyncRead + AsyncWrite + Unpin>(
}
}
found
} else {
None
};
if let Some(result) = muc_result {
apply(stream, session.as_ref(), &rt, result).await?;
@@ -3652,7 +3682,7 @@ async fn authenticated_transport<S: AsyncRead + AsyncWrite + Unpin>(
&& n.tag_name().name() == "private"
})
{
for s in rt.hub.all() {
for s in rt.hub.for_owners(&[&owner, protocol::bare(to)]) {
let mut s = s.lock().unwrap();
if !s.carbons || s.jid == full || target_jids.contains(&s.jid) {
continue;
@@ -3675,20 +3705,11 @@ async fn authenticated_transport<S: AsyncRead + AsyncWrite + Unpin>(
}
}
}
if rt.enabled("cloud_notify")
&& typ != "error"
&& let Some(u) = &local
{
let doc = roxmltree::Document::parse(&xml)?;
let pushes = message_pushes(&rt, u, to, delivered, doc.root_element())?;
for (to, payload) in pushes {
let _ = route_delivery(&rt, &to, &payload).await;
}
}
// A push can wake the client immediately. Commit offline delivery
// before publishing its notification, just as for durable SM output.
if delivered == 0 && !matches!(typ, "error" | "headline") {
let queued = if let Some(u) = local {
crate::account_modules::offline_enqueue(&rt.data, &rt.domain, &u, &xml)?
let queued = if let Some(u) = &local {
crate::account_modules::offline_enqueue(&rt.data, &rt.domain, u, &xml)?
} else {
false
};
@@ -3696,6 +3717,17 @@ async fn authenticated_transport<S: AsyncRead + AsyncWrite + Unpin>(
emit(stream,session.as_ref(),&format!("<message xmlns='{CLIENT}' type='error' id='{}' from='{}' to='{}'><error type='cancel'><service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error></message>",escape(n.attribute("id").unwrap_or("")),escape(to),escape(full))).await?;
}
}
if rt.enabled("cloud_notify")
&& typ != "error"
&& let Some(u) = &local
{
let doc = roxmltree::Document::parse(&xml)?;
let pushes = message_pushes(&rt, u, to, delivered, doc.root_element())?;
for (to, payload) in pushes {
enqueue_push_route(&rt, to, payload);
}
}
}
Ok::<(), anyhow::Error>(())
}.await?;
@@ -3705,6 +3737,28 @@ async fn authenticated_transport<S: AsyncRead + AsyncWrite + Unpin>(
state.mark_handled()?;
}
}
if kind == "message" && processing_started.elapsed() >= Duration::from_millis(250) {
let route = n.attribute("to").map_or("implicit", |to| {
if is_muc_destination(&rt.config, to) {
"muc"
} else if rt.owner(to).is_some() {
"local"
} else {
"remote-or-component"
}
});
ctx.event(
Level::Warn,
"message-processing-slow",
&[
(
"duration_ms",
&processing_started.elapsed().as_millis().to_string(),
),
("route", route),
],
);
}
}
Ok(())
}
@@ -3734,6 +3788,53 @@ fn period(value: Option<&serde_json::Value>, default: Option<f64>) -> Option<f64
_ => default,
}
}
// The guard lives in serve rather than Runtime: Runtime clones inside queued
// routes must not keep their own worker (and the data-directory lock) alive.
struct PushRouteWorker(tokio::task::JoinHandle<()>);
impl Drop for PushRouteWorker {
fn drop(&mut self) {
self.0.abort();
}
}
fn enqueue_push_route(rt: &Runtime, to: String, xml: String) {
if let Err(error) = rt.push_routes.try_send((to, xml)) {
let reason = match error {
mpsc::error::TrySendError::Full(_) => "queue-full",
mpsc::error::TrySendError::Closed(_) => "worker-closed",
};
rt.logger.event(
Level::Warn,
"push",
None,
"notification-routing-failed",
&[("reason", reason)],
);
}
}
async fn run_push_routes(rt: Runtime, mut receiver: mpsc::Receiver<(String, String)>) {
let mut routes = tokio::task::JoinSet::new();
loop {
tokio::select! {
result = routes.join_next(), if !routes.is_empty() => {
if result.is_some_and(|result| result.is_err()) {
rt.logger.event(Level::Warn, "push", None,
"notification-routing-failed", &[("reason", "routing-task-failed")]);
}
}
next = receiver.recv(), if routes.len() < 8 => {
let Some((to, xml)) = next else { break };
let runtime = rt.clone();
routes.spawn(async move {
if route_delivery(&runtime, &to, &xml).await.is_err() {
runtime.logger.event(Level::Warn, "push", None,
"notification-routing-failed", &[("reason", "route-failed")]);
}
});
}
}
}
// Dropping JoinSet aborts in-flight routes, including on worker cancellation.
}
async fn route_delivery(rt: &Runtime, to: &str, xml: &str) -> Result<usize> {
let domain = protocol::bare(to)
.rsplit_once('@')
@@ -3836,7 +3937,7 @@ async fn incoming_federated(rt: Runtime, stanza: crate::federation::FederatedSta
}
return Ok(());
}
let muc_result = {
let muc_result = if is_muc_destination(&rt.config, to) {
let mut rooms = rt.mucs.lock().unwrap();
let mut out = None;
for room in rooms.values_mut() {
@@ -3846,6 +3947,8 @@ async fn incoming_federated(rt: Runtime, stanza: crate::federation::FederatedSta
}
}
out
} else {
None
};
if let Some(result) = muc_result {
for reply in result.replies {
@@ -3918,7 +4021,7 @@ async fn incoming_federated(rt: Runtime, stanza: crate::federation::FederatedSta
}
let notifications = message_pushes(&rt, &user, to, delivered, n)?;
for (to, xml) in notifications {
let _ = route_delivery(&rt, &to, &xml).await;
enqueue_push_route(&rt, to, xml);
}
}
Ok(())
+22 -2
View File
@@ -115,6 +115,10 @@ 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;
@@ -345,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();
@@ -383,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 {
+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());
}
+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()
+255
View File
@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""Measure real STARTTLS direct/MUC delivery with durable SM and CSI.
Usage: python3 tests/message_latency_network.py --binary target/release/rust-xmpp
Reports loopback median/p95/max, not a production latency guarantee. There are
no millisecond performance assertions: bounded delivery, exact counts, SM
counters and durable CSI buffering/urgent flush are the regression assertions.
The source executable is copied before testing so concurrent rebuilds are safe.
"""
import argparse
import asyncio
import collections
import datetime
import hashlib
import json
import pathlib
import shutil
import socket
import ssl
import statistics
import subprocess
import tempfile
import time
from muc_sm_load_network import Client, MUC, start
from muc_restart_network import stop
class TimedBodies(collections.Counter):
def __init__(self):
super().__init__()
self.arrivals = {}
def __setitem__(self, key, value):
self.arrivals.setdefault(key, time.perf_counter())
super().__setitem__(key, value)
def distribution(values):
values = sorted(value * 1000 for value in values)
return {'samples': len(values), 'median_ms': statistics.median(values),
'p95_ms': values[min(len(values) - 1, int(len(values) * .95))],
'max_ms': values[-1], 'samples_ms': values}
async def bounded(operation):
# A functional timeout, deliberately much wider than a performance budget.
return await asyncio.wait_for(operation, 20)
async def body(client, text):
await bounded(client.wait(lambda: client.bodies[text] == 1, text))
return client.bodies.arrivals[text]
def slot(root, client):
paths = list((root / 'data').rglob(client.token + '.json'))
assert len(paths) == 1, 'missing or duplicate durable SM record'
return json.loads(paths[0].read_text())
async def queued(root, client, text):
# Observe persisted queue state instead of assuming a short socket timeout
# proves buffering. Bob sends no input/acks that could flush CSI here.
deadline = time.monotonic() + 20
while time.monotonic() < deadline:
record = slot(root, client)
if any(text in xml for xml in record['csi']['queue']):
assert client.bodies[text] == 0, 'buffered message leaked onto wire'
return
assert client.bodies[text] == 0, 'inactive CSI failed to hold headline'
await asyncio.sleep(.01)
raise AssertionError('headline did not enter durable CSI queue')
async def csi_state(root, client, state):
# Alternate first so persisted state proves this exact control was handled,
# including resetting any temporary flushing window. A ping on the receiver
# would itself cause CSI activity and invalidate the buffering experiment.
for target in (state != 'inactive', state == 'inactive'):
await client.send(f"<{'inactive' if target else 'active'} xmlns='urn:xmpp:csi:0'/>")
deadline = time.monotonic() + 20
while slot(root, client)['csi']['inactive'] != target:
assert time.monotonic() < deadline, 'CSI state was not durably handled'
await asyncio.sleep(.001)
async def exercise(root, port, rounds, archive, history_records):
context = ssl.create_default_context(cafile=str(root / 'cert.pem'))
clients = []
report = {'mam_enabled': archive, 'sm_enabled': True, 'rounds': rounds,
'seeded_muc_archive_records': history_records if archive else 0}
try:
for number in range(2):
client = await bounded(Client.connect(number, port, context, 2))
clients.append(client)
client.bodies = TimedBodies()
# Explicit acks between samples avoid racing inbound ack activity
# with inactive-CSI assertions. SM remains enabled throughout.
client.auto_ack = False
await bounded(client.enable())
await client.stanza('<presence/>')
await bounded(client.fence(f'connected-{number}'))
await client.ack()
alice, bob = clients
room = 'latency@rooms.localhost'
for client in clients:
await client.stanza(f"<presence to='{room}/Nick-{client.number}'><x xmlns='{MUC}'><history maxstanzas='0'/></x></presence>")
await bounded(client.wait(lambda c=client: c.presences.get(f'{room}/Nick-{c.number}'), 'MUC self presence'))
assert client.presences[f'{room}/Nick-{client.number}'].get('type') != 'error'
for client in clients:
await bounded(client.fence(f'joined-{client.number}'))
await client.ack()
for state in ('active', 'inactive'):
measurements = collections.defaultdict(list)
for turn in range(rounds):
# Reset CSI immediately before each sample: an important output
# opens a configured temporary flushing window for later output.
await csi_state(root, bob, state)
text = f'{state}-direct-{turn}'
begun = time.perf_counter()
await alice.stanza(f"<message to='{bob.jid}' type='chat'><body>{text}</body></message>")
arrived = await body(bob, text)
measurements['direct_peer'].append(arrived - begun)
reply = text + '-reply'
await bob.stanza(f"<message to='{alice.jid}' type='chat'><body>{reply}</body></message>")
measurements['direct_application_rtt'].append(await body(alice, reply) - begun)
await csi_state(root, bob, state)
text = f'{state}-muc-{turn}'
begun = time.perf_counter()
await alice.stanza(f"<message to='{room}' type='groupchat'><body>{text}</body></message>")
echo, peer = await asyncio.gather(body(alice, text), body(bob, text))
measurements['muc_sender_echo'].append(echo - begun)
measurements['muc_peer'].append(peer - begun)
for client in clients:
await client.ack()
report[state] = {name: distribution(values) for name, values in measurements.items()}
# A low-priority headline is actually held; a normal chat body must
# release it in order even while the receiver remains CSI inactive.
await csi_state(root, bob, 'inactive')
held, urgent = 'queued-before-urgent', 'urgent-flush-body'
await alice.stanza(f"<message to='{bob.jid}' type='headline'><body>{held}</body></message>")
await bounded(alice.fence('headline-routed'))
await queued(root, bob, held)
begun = time.perf_counter()
await alice.stanza(f"<message to='{bob.jid}' type='chat'><body>{urgent}</body></message>")
held_at, urgent_at = await asyncio.gather(body(bob, held), body(bob, urgent))
assert held_at <= urgent_at, 'urgent flush reordered queued output'
report['urgent_flush'] = distribution([urgent_at - begun])
report['headline_durably_buffered'] = True
# Explicit activation must independently drain low-priority output.
await bob.ack()
await csi_state(root, bob, 'inactive')
held = 'queued-before-active'
await alice.stanza(f"<message to='{bob.jid}' type='headline'><body>{held}</body></message>")
await bounded(alice.fence('activation-headline-routed'))
await queued(root, bob, held)
await bob.send("<active xmlns='urn:xmpp:csi:0'/>")
await body(bob, held)
report['activation_drained_queue'] = True
for client in clients:
await bounded(client.fence(f'final-{client.number}'))
await client.ack()
await bounded(client.handled())
assert not client.errors, client.errors
assert all(count == 1 for count in client.bodies.values()), 'duplicate wire delivery'
archives = list((root / 'data' / 'localhost' / 'archive').glob('*.list'))
records = sum(sum(line.startswith('item(') for line in path.open()) for path in archives)
assert (records > 0) == archive, ('MAM toggle did not affect actual archive storage', records)
report['personal_archive_records'] = records
muc_archive = root / 'data' / 'rooms%2elocalhost' / 'muc_log' / 'latency.list'
muc_records = sum(line.startswith('item(') for line in muc_archive.open()) if muc_archive.exists() else 0
assert muc_records == (history_records + 2 * rounds if archive else 0), ('MUC archive seed/append mismatch', muc_records)
report['muc_archive_records'] = muc_records
report['wire_body_deliveries'] = sum(sum(client.bodies.values()) for client in clients)
assert report['wire_body_deliveries'] == rounds * 8 + 3
report['status'] = 'pass'
return report
finally:
await asyncio.gather(*(client.close_raw() for client in clients), return_exceptions=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', default='target/release/rust-xmpp')
parser.add_argument('--rounds', type=int, default=20)
parser.add_argument('--history-records', type=int, nargs='+', default=[0],
help='MUC native archive sizes to seed before starting each isolated server')
parser.add_argument('--output', help='optional JSON measurement report')
args = parser.parse_args()
assert args.rounds > 0
assert all(count >= 0 for count in args.history_records)
with tempfile.TemporaryDirectory(prefix='xmpp-message-latency-') as directory:
work = pathlib.Path(directory)
binary = work / 'rust-xmpp'
shutil.copy2(pathlib.Path(args.binary).resolve(), binary)
report = {'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(),
'scope': 'loopback STARTTLS; isolated fixture; no production latency claim', 'cases': []}
cases = [(False, 0)] + [(True, count) for count in args.history_records]
for archive, history_records in cases:
root = work / (f'mam-on-{history_records}' if archive else 'mam-off')
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',
'-addext', 'basicConstraints=critical,CA:FALSE', '-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]
modules = ['tls', 'saslauth', 'smacks', 'csi_simple', 'ping', 'disco']
if archive:
modules.append('mam')
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': modules, 'default_archive_policy': True, 'archive_expires_after': 'never',
'ssl': {'key': str(root/'key.pem'), 'certificate': str(root/'cert.pem')}},
'hosts': {'localhost': {'options': {'authentication': 'internal_hashed', 'password_hash': 'SHA-256'}},
'rooms.localhost': {'component': 'muc', 'options': {'muc_room_locking': False,
'modules_enabled': ['muc_mam'] if archive else [],
'muc_log_expires_after': 604800}}}}))
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)
if archive and history_records:
# Native Prosody `.list` fixture, matching archive_mam.rs. All
# timestamps are recent, so retention scans must retain them.
# Seeding occurs before any server opens this isolated store.
archive_path = root / 'data' / 'rooms%2elocalhost' / 'muc_log' / 'latency.list'
archive_path.parent.mkdir(parents=True, exist_ok=True)
now = time.time()
stamp = datetime.datetime.fromtimestamp(now, datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
with archive_path.open('w') as output:
for number in range(history_records):
output.write("item({name='message';attr={xmlns='jabber:client';type='groupchat';"
f"from='latency@rooms.localhost/Seed';stamp='{stamp}'" + "};"
f"key='seed-{number}';when={now};with='message<groupchat';"
"{name='body';attr={};'seed history'}});\n")
with (root/'server.log').open('w+') as log:
server = start(str(binary), config, port, log)
try:
report['cases'].append(asyncio.run(exercise(root, port, args.rounds, archive, history_records)))
finally:
stop(server)
result = json.dumps(report, indent=2) + '\n'
if args.output:
pathlib.Path(args.output).write_text(result)
print(result, end='')
if __name__ == '__main__':
main()
+146
View File
@@ -0,0 +1,146 @@
use rust_xmpp::{archive, muc::Muc, protocol::ModuleResult, storage};
use std::{fs, path::Path, time::SystemTime};
fn send(muc: &mut Muc, xml: &str) -> ModuleResult {
let doc = roxmltree::Document::parse(xml).unwrap();
muc.handle("alice@test/a", doc.root_element())
.unwrap()
.unwrap()
}
fn room(muc: &mut Muc, name: &str) {
send(
muc,
&format!(
"<presence to='{name}@rooms.test/Alice'><x xmlns='http://jabber.org/protocol/muc'/></presence>"
),
);
send(
muc,
&format!(
"<iq type='set' to='{name}@rooms.test'><query xmlns='http://jabber.org/protocol/muc#owner'><x xmlns='jabber:x:data' type='submit'/></query></iq>"
),
);
}
fn chat(muc: &mut Muc) -> ModuleResult {
send(
muc,
"<message to='r@rooms.test' type='groupchat'><body>Fresh</body></message>",
)
}
fn mark(path: &Path) -> SystemTime {
let marker = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
fs::File::options()
.write(true)
.open(path)
.unwrap()
.set_times(fs::FileTimes::new().set_modified(marker))
.unwrap();
fs::metadata(path).unwrap().modified().unwrap()
}
#[test]
fn groupchat_skips_native_rewrites_and_archive_retention_scan() {
let temp = tempfile::tempdir().unwrap();
let mut muc = Muc::new(temp.path().into(), "rooms.test".into());
muc.set_defaults(serde_json::json!({"persistent":true,"archiving":true}));
muc.set_archive_retention(Some(60.0));
room(&mut muc, "r");
room(&mut muc, "other");
let config = storage::path(temp.path(), "rooms.test", Some("r"), "config");
let other = storage::path(temp.path(), "rooms.test", Some("other"), "config");
let index = storage::path(temp.path(), "rooms.test", None, "persistent");
let journal = storage::path(temp.path(), "rooms.test", None, "rust_muc_checkpoint");
let config_time = mark(&config);
let other_time = mark(&other);
let index_time = mark(&index);
let journal_time = mark(&journal);
// An unreadable historic record makes any accidental full archive scan fail.
// Appending a fresh record needs only the existing file length, never its XML.
let log = storage::path(temp.path(), "rooms.test", Some("r"), "muc_log").with_extension("list");
fs::create_dir_all(log.parent().unwrap()).unwrap();
let historic = b"item(this is intentionally invalid historic archive data);\n";
fs::write(&log, historic).unwrap();
let delivered = chat(&mut muc);
assert_eq!(delivered.deliveries.len(), 1);
let contents = fs::read(&log).unwrap();
assert!(contents.starts_with(historic));
assert!(contents.len() > historic.len());
assert!(archive::load_store(temp.path(), "rooms.test", Some("r"), "muc_log").is_err());
for (path, before) in [
(&config, config_time),
(&other, other_time),
(&index, index_time),
] {
assert_eq!(fs::metadata(path).unwrap().modified().unwrap(), before);
}
assert_ne!(
fs::metadata(&journal).unwrap().modified().unwrap(),
journal_time
);
assert!(fs::read_to_string(journal).unwrap().contains("Fresh"));
}
#[test]
fn native_exports_repair_missing_files_and_export_changed_config() {
let temp = tempfile::tempdir().unwrap();
let mut muc = Muc::new(temp.path().into(), "rooms.test".into());
muc.set_defaults(serde_json::json!({"persistent":true}));
room(&mut muc, "r");
room(&mut muc, "other");
let config = storage::path(temp.path(), "rooms.test", Some("r"), "config");
let other = storage::path(temp.path(), "rooms.test", Some("other"), "config");
let index = storage::path(temp.path(), "rooms.test", None, "persistent");
let original = fs::read(&config).unwrap();
fs::remove_file(&config).unwrap();
fs::remove_file(&index).unwrap();
assert_eq!(chat(&mut muc).deliveries.len(), 1);
assert_eq!(fs::read(&config).unwrap(), original);
let recovered = storage::load(temp.path(), "rooms.test", None, "persistent")
.unwrap()
.unwrap();
assert_eq!(
recovered.get("r@rooms.test"),
Some(&storage::LuaValue::Bool(true))
);
assert_eq!(
recovered.get("other@rooms.test"),
Some(&storage::LuaValue::Bool(true))
);
let other_time = mark(&other);
let index_time = mark(&index);
let result = send(
&mut muc,
"<iq type='set' to='r@rooms.test'><query xmlns='http://jabber.org/protocol/muc#owner'><x xmlns='jabber:x:data' type='submit'><field var='muc#roomconfig_roomname'><value>Updated</value></field></x></query></iq>",
);
assert!(result.replies[0].contains("result"));
assert!(fs::read_to_string(config).unwrap().contains("Updated"));
assert_eq!(fs::metadata(other).unwrap().modified().unwrap(), other_time);
assert_eq!(
fs::metadata(&index).unwrap().modified().unwrap(),
index_time
);
// Export failures are nonfatal after the authoritative room commit, and
// exports must be retried even if no configuration changed since failure.
fs::remove_file(&index).unwrap();
fs::create_dir(&index).unwrap();
assert_eq!(chat(&mut muc).deliveries.len(), 1);
fs::remove_dir(&index).unwrap();
assert_eq!(chat(&mut muc).deliveries.len(), 1);
let recovered = storage::load(temp.path(), "rooms.test", None, "persistent")
.unwrap()
.unwrap();
assert_eq!(
recovered.get("r@rooms.test"),
Some(&storage::LuaValue::Bool(true))
);
assert_eq!(
recovered.get("other@rooms.test"),
Some(&storage::LuaValue::Bool(true))
);
}
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""Separate personal-chat wire delivery, handled SM ACK and client receipt timing.
Native personal MAM/roster fixtures, real loopback STARTTLS, durable SM/CSI.
A forced CSI dwell is reported explicitly and is not a server performance sample.
The default exercises both the runtime defaults (urgent encrypted/receipt payloads)
and an explicit empty importance list (native policy). Use --expect-held for the
old binary; --csi-policy default limits a baseline run to its default policy.
"""
import argparse
import asyncio
import collections
import hashlib
import json
import pathlib
import shutil
import socket
import ssl
import subprocess
import tempfile
import time
import xml.etree.ElementTree as ET
from message_latency_network import bounded, csi_state, distribution, slot
from muc_sm_load_network import Client, CLIENT, SM, start
from muc_restart_network import stop
class ObservedReader:
"""Observe complete wire stanzas while retaining the existing client's parser."""
def __init__(self, reader, client):
self.reader, self.client = reader, client
self.parser = ET.XMLPullParser(events=('start', 'end'))
self.parser.feed("<root xmlns='jabber:client'>")
self.root = next(self.parser.read_events())[1]
self.depth = 0
def __getattr__(self, name):
return getattr(self.reader, name)
async def read(self, size):
chunk = await self.reader.read(size)
self.parser.feed(chunk)
for event, node in self.parser.read_events():
self.depth += 1 if event == 'start' else -1
if event == 'end' and self.depth == 0:
now = time.perf_counter()
if node.tag == f'{{{CLIENT}}}message' and node.get('id'):
self.client.message_times[node.get('id')].append(now)
if node.tag == f'{{{SM}}}a':
self.client.ack_times[int(node.get('h'))] = now
self.root.remove(node)
return chunk
class MeasuredClient(Client):
def __init__(self, *args):
super().__init__(*args)
self.message_times = collections.defaultdict(list)
self.ack_times = {}
self.reader = ObservedReader(self.reader, self)
async def seen(client, identifier):
await bounded(client.wait(lambda: client.message_times[identifier], identifier))
assert len(client.message_times[identifier]) == 1
return client.message_times[identifier][0]
async def send_measured(sender, receiver, identifier, payload, typ='chat'):
begun = time.perf_counter()
await sender.stanza(f"<message to='{receiver.jid}' type='{typ}' id='{identifier}'>{payload}</message>")
await bounded(sender.handled())
ack_at = sender.ack_times[sender.inputs]
return begun, ack_at
async def exercise(root, port, args, policy):
clients = []
try:
context = ssl.create_default_context(cafile=str(root/'cert.pem'))
for n in range(2):
client = await bounded(MeasuredClient.connect(n, port, context, 2))
clients.append(client)
client.auto_ack = False
await bounded(client.enable())
await client.stanza('<presence/>')
await bounded(client.fence(f'connected-{n}'))
await client.ack()
alice, bob = clients
results = {}
for state in ('active', 'inactive'):
values = collections.defaultdict(list)
for turn in range(args.rounds):
await csi_state(root, alice, 'active')
await csi_state(root, bob, state)
identifier = f'{state}-body-{turn}'
begun, ack_at = await send_measured(alice, bob, identifier, '<body>personal test</body>')
delivery = await seen(bob, identifier)
receipt_id = identifier + '-receipt'
# Receipt generation belongs to the receiving client; the server
# does not synthesize XEP-0184 receipts on SM acknowledgement.
receipt_sent = time.perf_counter()
await bob.stanza(f"<message to='{alice.jid}' type='chat' id='{receipt_id}'><received xmlns='urn:xmpp:receipts' id='{identifier}'/></message>")
receipt = await seen(alice, receipt_id)
values['delivery'].append(delivery-begun)
values['sender_sm_handled'].append(ack_at-begun)
values['client_receipt_end_to_end'].append(receipt-begun)
values['receipt_return_leg'].append(receipt-receipt_sent)
for client in clients:
await client.ack()
results[state] = {name: distribution(v) for name, v in values.items()}
payloads = {
'receipt_only': "<received xmlns='urn:xmpp:receipts' id='earlier-message'/>",
'omemo_legacy_only': "<encrypted xmlns='eu.siacs.conversations.axolotl'><header sid='1'><iv>AA==</iv></header><payload>AA==</payload></encrypted>",
'omemo_2_only': "<encrypted xmlns='urn:xmpp:omemo:2'><header sid='1'><iv>AA==</iv></header><payload>AA==</payload></encrypted>",
'omemo_with_eme': "<encrypted xmlns='eu.siacs.conversations.axolotl'><payload>AA==</payload></encrypted><encryption xmlns='urn:xmpp:eme:0' namespace='eu.siacs.conversations.axolotl'/>",
}
payloads.update({f'marker_{name}':
f"<{name} xmlns='urn:xmpp:chat-markers:0' id='earlier-message'/>"
for name in ('received', 'displayed', 'acknowledged')})
payloads['low_priority_headline'] = '<body>held low priority</body>'
for name, payload in payloads.items():
for state in ('active', 'inactive'):
await csi_state(root, alice, 'active')
await csi_state(root, bob, state)
identifier = name + '-' + state
begun, ack_at = await send_measured(alice, bob, identifier, payload,
'headline' if name == 'low_priority_headline' else 'chat')
deadline = time.monotonic() + 20
while True:
buffered = any(identifier in xml for xml in slot(root, bob)['csi']['queue'])
if buffered or bob.message_times[identifier]:
break
assert time.monotonic() < deadline, identifier
await asyncio.sleep(.001)
expect_held = state == 'inactive' and (
name == 'low_priority_headline' or (
name != 'omemo_with_eme' and (policy == 'native' or args.expect_held)))
assert buffered == expect_held, (policy, identifier, buffered, expect_held)
activation = None
if buffered:
assert not bob.message_times[identifier]
await asyncio.sleep(args.csi_dwell)
assert not bob.message_times[identifier], 'CSI unexpectedly emitted held payload'
activation = time.perf_counter()
await bob.send("<active xmlns='urn:xmpp:csi:0'/>")
delivered = await seen(bob, identifier)
results[identifier] = {'delivery_ms': (delivered-begun)*1000,
'sender_sm_handled_ms': (ack_at-begun)*1000,
'durably_csi_buffered': buffered,
'forced_dwell_seconds': args.csi_dwell if buffered else 0,
'after_activation_ms': (delivered-activation)*1000 if activation else None}
for client in clients:
await client.ack()
await csi_state(root, bob, 'active')
# Keep receiving/draining but deliberately withhold recipient ACKs.
for n in range(args.backlog):
await alice.stanza(f"<message to='{bob.jid}' type='chat' id='backlog-{n}'><body>unacked backlog</body></message>")
await seen(bob, f'backlog-{n}')
if args.backlog:
await seen(bob, f'backlog-{args.backlog-1}')
await bounded(alice.handled())
pending = len(slot(root, bob)['pending'])
begun, ack_at = await send_measured(alice, bob, 'after-backlog', '<body>after backlog</body>')
delivery = await seen(bob, 'after-backlog')
results['sm_backlog'] = {'seeded_messages': args.backlog, 'durable_pending_before_sample': pending,
'delivery_ms': (delivery-begun)*1000, 'sender_sm_handled_ms': (ack_at-begun)*1000}
for client in clients:
await client.ack()
assert not client.errors, client.errors
results['status'] = 'pass'
return results
finally:
await asyncio.gather(*(c.close_raw() for c in clients), return_exceptions=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', default='target/release/rust-xmpp')
parser.add_argument('--rounds', type=int, default=3)
parser.add_argument('--history-records', type=int, default=100000)
parser.add_argument('--roster-sizes', type=int, nargs='+', default=[0, 5000, 10000])
parser.add_argument('--backlog', type=int, default=500)
parser.add_argument('--csi-dwell', type=float, default=2)
parser.add_argument('--csi-policy', choices=('default', 'native', 'both'), default='both',
help='native explicitly configures csi_important_payloads=[]; both verifies override')
parser.add_argument('--expect-held', action='store_true',
help='baseline binaries: expect native holds even when option is absent')
parser.add_argument('--output')
args = parser.parse_args()
assert args.rounds > 0 and args.history_records >= 0
assert all(count >= 0 for count in args.roster_sizes)
assert 0 <= args.backlog < 4000 and args.csi_dwell >= 0
with tempfile.TemporaryDirectory(prefix='xmpp-personal-latency-') as directory:
work = pathlib.Path(directory)
binary = work/'rust-xmpp'
shutil.copy2(pathlib.Path(args.binary).resolve(), binary)
report = {'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(),
'scope': 'loopback STARTTLS; fixture only; forced CSI dwell explicitly separated', 'cases': []}
policies = ('default', 'native') if args.csi_policy == 'both' else (args.csi_policy,)
for roster_size, policy in ((n, p) for n in args.roster_sizes for p in policies):
root = work/f'roster-{roster_size}-{policy}'
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', 'csi_simple', 'ping', 'mam', 'roster'],
'default_archive_policy': 'roster' if roster_size else True, 'archive_expires_after': 'never',
'ssl': {'key': str(root/'key.pem'), 'certificate': str(root/'cert.pem')}},
'hosts': {'localhost': {'options': {'authentication': 'internal_hashed', 'password_hash': 'SHA-256'}}}}))
if policy == 'native':
configured = json.loads(config.read_text())
configured['global']['csi_important_payloads'] = []
config.write_text(json.dumps(configured))
for n in range(2):
subprocess.run([str(binary), 'create-user', str(config), f'load-{n}', '--password-stdin'],
input='load-test-password\n', text=True, check=True, capture_output=True)
archive = root/'data'/'localhost'/'archive'/f'load%2d{n}.list'
archive.parent.mkdir(parents=True, exist_ok=True)
now = time.time()
with archive.open('w') as output:
for i in range(args.history_records):
output.write("item({name='message';attr={xmlns='jabber:client';type='chat'};"
f"key='seed-{i}';when={now};with='load-{1-n}@localhost';"
"{name='body';attr={};'native personal history'}});\n")
if roster_size:
roster = root/'data'/'localhost'/'roster'/f'load%2d{n}.dat'
roster.parent.mkdir(parents=True, exist_ok=True)
with roster.open('w') as output:
output.write("return { [false]={version=1};\n")
output.write(f"['load-{1-n}@localhost']={{subscription='none'}};\n")
for i in range(roster_size-1):
output.write(f"['contact-{i}@example.invalid']={{subscription='none'}};\n")
output.write('}\n')
with (root/'server.log').open('w+') as log:
server = start(str(binary), config, port, log)
try:
results = asyncio.run(exercise(root, port, args, policy))
counts = [sum(line.startswith('item(') for line in path.open())
for path in sorted((root/'data'/'localhost'/'archive').glob('*.list'))]
expected = args.history_records + 4*args.rounds + 10 + args.backlog + 1
assert counts == [expected, expected], (counts, expected)
results['final_personal_mam_records_per_user'] = counts
results.update({'csi_policy': policy, 'baseline_expect_held': args.expect_held,
'roster_contacts_per_user': roster_size,
'seeded_personal_mam_records_per_user': args.history_records,
'archive_policy': 'roster' if roster_size else 'always'})
report['cases'].append(results)
if args.output:
pathlib.Path(args.output).write_text(json.dumps(report, indent=2)+'\n')
print(f'personal latency: roster={roster_size} csi={policy} pass', flush=True)
finally:
stop(server)
result = json.dumps(report, indent=2)+'\n'
if args.output:
pathlib.Path(args.output).write_text(result)
print(result, end='')
if __name__ == '__main__':
main()
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""Personal delivery must not await a stalled external XEP-0357 gateway.
Real c2s and s2s STARTTLS; Bob hibernates or goes offline with registered push. The
account domain is chat.localhost and the external push service is localhost,
which is not a configured local host. Its standard s2s endpoint is a local
fixture on 127.0.0.1:5269; the special-use localhost name avoids external DNS.
After verified TLS it reads the server's stream but never sends features.
--expect-blocked demonstrates the old bug in a bounded two-second observation;
the default asserts both durable deliveries and Alice's handled SM ACK while
the real gateway transport remains stalled. This is a local regression fixture,
not an APNs/FCM or production-client validation.
"""
import argparse
import asyncio
import hashlib
import json
import os
import pathlib
import shutil
import socket
import ssl
import subprocess
import tempfile
import time
from message_latency_network import slot
import muc_sm_load_network as network
from muc_sm_load_network import SM, start
PUSH = 'urn:xmpp:push:0'
GATEWAY = 'localhost'
DOMAIN = 'chat.localhost'
network.STREAM = network.STREAM.replace("to='localhost'", f"to='{DOMAIN}'")
class Client(network.Client):
def __init__(self, *args):
super().__init__(*args)
self.jid = self.jid.replace('@localhost/', f'@{DOMAIN}/')
async def fence(self, identifier):
await self.stanza(f"<iq type='get' to='{DOMAIN}' id='{identifier}'><ping xmlns='urn:xmpp:ping'/></iq>")
result = await self.wait(lambda: self.iqs.pop(identifier, None), identifier)
assert result.get('type') == 'result'
def openssl(root, *args):
subprocess.run(['openssl', *args], cwd=root, check=True, capture_output=True)
def certificates(root):
openssl(root, 'req', '-x509', '-newkey', 'ec', '-pkeyopt',
'ec_paramgen_curve:P-256', '-nodes', '-days', '1', '-subj',
'/CN=Push latency fixture CA', '-addext',
'basicConstraints=critical,CA:TRUE', '-keyout', 'ca.key', '-out', 'ca.pem')
openssl(root, 'req', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256',
'-nodes', '-subj', '/CN=localhost', '-keyout', 'key.pem', '-out', 'server.csr')
(root/'extensions').write_text('basicConstraints=critical,CA:FALSE\n'
'keyUsage=critical,digitalSignature\nextendedKeyUsage=serverAuth,clientAuth\n'
'subjectAltName=DNS:localhost,DNS:chat.localhost\n')
openssl(root, 'x509', '-req', '-in', 'server.csr', '-CA', 'ca.pem',
'-CAkey', 'ca.key', '-CAcreateserial', '-days', '1', '-extfile',
'extensions', '-out', 'cert.pem')
async def poll(predicate, description, timeout=5):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return
await asyncio.sleep(.01)
raise AssertionError(description)
class Blackhole:
def __init__(self, root):
self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
self.context.load_cert_chain(root/'cert.pem', root/'key.pem')
self.seen = asyncio.Event()
self.tasks = set()
self.writers = set()
self.errors = []
self.closed = False
self.snapshot = lambda: None
self.pending_at_stream = None
async def accept(self, reader, writer):
self.tasks.add(asyncio.current_task())
self.writers.add(writer)
try:
stream = await reader.readuntil(b'>')
if stream.startswith(b'<?xml'):
stream = await reader.readuntil(b'>')
assert b'stream:stream' in stream and GATEWAY.encode() in stream
header = ("<stream:stream xmlns='jabber:server' "
"xmlns:stream='http://etherx.jabber.org/streams' "
"from='localhost' to='chat.localhost' version='1.0'>")
writer.write((header + "<stream:features><starttls "
"xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required/>"
"</starttls></stream:features>").encode())
await writer.drain()
assert b'starttls' in await reader.readuntil(b'>')
writer.write(b"<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
await writer.drain()
await writer.start_tls(self.context)
stream = await reader.readuntil(b'>')
if stream.startswith(b'<?xml'):
stream = await reader.readuntil(b'>')
assert b'stream:stream' in stream and GATEWAY.encode() in stream
writer.write(header.encode())
await writer.drain()
self.pending_at_stream = self.snapshot()
self.seen.set()
# Keep transport open; features are never sent and no route completes.
await reader.read()
self.closed = True
except asyncio.CancelledError:
raise
except Exception as error:
self.errors.append(repr(error))
finally:
writer.close()
self.writers.discard(writer)
self.tasks.discard(asyncio.current_task())
async def close(self):
for task in list(self.tasks):
task.cancel()
await asyncio.gather(*self.tasks, return_exceptions=True)
for writer in list(self.writers):
writer.close()
async def exercise(root, binary, config, port, args, log):
gateway = Blackhole(root)
try:
listener = await asyncio.start_server(gateway.accept, '127.0.0.1', 5269)
except OSError as error:
raise RuntimeError(f'push fixture needs an unused {GATEWAY}:5269: {error}') from error
clients = []
server = None
try:
# rustls-native-certs uses this explicit fixture trust bundle; identity
# verification still checks the remote JID's DNS SAN during real TLS.
previous = os.environ.get('SSL_CERT_FILE')
os.environ['SSL_CERT_FILE'] = str(root/'ca.pem')
try:
server = start(str(binary), config, port, log)
finally:
if previous is None:
os.environ.pop('SSL_CERT_FILE', None)
else:
os.environ['SSL_CERT_FILE'] = previous
context = ssl.create_default_context(cafile=str(root/'ca.pem'))
for number in range(2):
client = await asyncio.wait_for(Client.connect(number, port, context, 2), 10)
clients.append(client)
client.auto_ack = False
if number == 0 or args.recipient == 'hibernating':
await client.enable()
await client.stanza('<presence/>')
await client.fence(f'ready-{number}')
if client.token:
await client.handled()
await client.ack()
alice, bob = clients
await bob.stanza(f"<iq type='set' to='{bob.jid}' id='push-enable'>"
f"<enable xmlns='{PUSH}' jid='{GATEWAY}' node='phone'/></iq>")
response = await bob.wait(lambda: bob.iqs.get('push-enable'), 'push registration')
assert response.get('type') == 'result'
if bob.token:
await bob.handled()
await bob.ack()
await poll(lambda: bool(slot(root, bob).get('push_identifier')), 'missing durable push binding')
else:
registration = list((root/'data').rglob('cloud_notify/*.dat'))
assert len(registration) == 1 and 'phone' in registration[0].read_text(), 'missing native push registration'
await bob.close_raw()
mode = 'hibernating' if bob.token else 'removed'
await poll(lambda: any('session-disconnected' in line and mode in line
and bob.jid in line for line in (root/'server.log').read_text().splitlines()),
f'Bob did not enter {mode} state')
if not bob.token:
assert len(list((root/'data').rglob('rust_smacks/*.json'))) == 1, 'offline Bob unexpectedly has an SM slot'
def pending_state():
if bob.token:
pending = slot(root, bob)['pending']
else:
pending = [path.read_text() for path in (root/'data').rglob('offline/*.list')]
return {identifier: any(identifier in stanza for stanza in pending)
for identifier in ('push-first', 'push-second')}
gateway.snapshot = pending_state
before = alice.inputs
began = time.perf_counter()
for identifier in ('push-first', 'push-second'):
await alice.stanza(f"<message to='{bob.jid}' type='chat' id='{identifier}'>"
f"<body>{identifier}</body></message>")
await alice.send(f"<r xmlns='{SM}'/>")
# Observe for the fixed interval even if a faulty implementation emits an
# early ACK. Durable pending state and a real blocked gateway are required.
await asyncio.sleep(args.bound)
present = pending_state()
acknowledgements = [int(node.get('h')) for node in alice.controls[f'{{{SM}}}a']]
seen_at_bound = gateway.seen.is_set()
assert not gateway.errors and not gateway.closed, (gateway.errors, gateway.closed)
if not args.expect_blocked or bob.token:
assert present['push-first'], 'first message was not durably delivered before push'
if args.expect_blocked:
assert not present['push-second'], 'old blocking behavior was not reproduced'
assert not acknowledgements, acknowledgements
assert slot(root, alice)['handled'] == before, 'first message marked handled before blocked push'
else:
assert present['push-second'], 'second message blocked behind external push'
assert acknowledgements == [alice.inputs], acknowledgements
assert slot(root, alice)['handled'] == alice.inputs
assert not alice.errors, alice.errors
assert gateway.seen.is_set(), f'no real post-TLS push attempt: {gateway.errors}'
assert not gateway.errors and not gateway.closed, (gateway.errors, gateway.closed)
if not args.expect_blocked:
assert gateway.pending_at_stream['push-first'], 'push started before first durable recipient delivery'
elif not bob.token:
assert not present['push-first'], 'old offline ordering bug was not reproduced'
assert not gateway.pending_at_stream['push-first']
return {'status': 'reproduced-blocking' if args.expect_blocked else 'pass',
'recipient_state': args.recipient,
'durable_pending_at_gateway_stream': gateway.pending_at_stream,
'observation_seconds': args.bound, 'elapsed_seconds': time.perf_counter()-began,
'gateway_post_tls_stream_observed': gateway.seen.is_set(),
'gateway_post_tls_stream_at_bound': seen_at_bound,
'blocked_stage': 'post-tls-features' if gateway.seen.is_set() else 'external-route-before-tcp',
'durable_pending': present,
'sender_sm_ack_h': acknowledgements, 'sender_inputs': alice.inputs,
'sender_handled_before': before}
finally:
# Kill before closing the blackhole: releasing it could complete the old
# awaited route and would invalidate this bounded reproducer.
if server is not None:
server.kill()
server.wait(timeout=5)
listener.close()
await listener.wait_closed()
await gateway.close()
await asyncio.gather(*(client.close_raw() for client in clients), return_exceptions=True)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--binary', default='target/release/rust-xmpp')
parser.add_argument('--expect-blocked', action='store_true')
parser.add_argument('--recipient', choices=('hibernating', 'offline'), default='hibernating')
parser.add_argument('--bound', type=float, default=2)
parser.add_argument('--output')
args = parser.parse_args()
assert 0 < args.bound < 10, 'observation must precede the 30-second federation timeout'
with tempfile.TemporaryDirectory(prefix='xmpp-push-latency-') as directory:
root = pathlib.Path(directory)
binary = root/'rust-xmpp'
shutil.copy2(pathlib.Path(args.binary).resolve(), binary)
certificates(root)
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', 'cloud_notify', 's2s'],
'ssl': {'key': str(root/'key.pem'), 'certificate': str(root/'cert.pem')}},
'hosts': {DOMAIN: {'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)
with (root/'server.log').open('w+') as log:
try:
report = asyncio.run(exercise(root, binary, config, port, args, log))
except BaseException:
log.seek(0)
print(log.read())
raise
report['binary_version'] = subprocess.run([str(binary), '--version'],
check=True, capture_output=True, text=True).stdout.strip()
report['binary_sha256'] = hashlib.sha256(binary.read_bytes()).hexdigest()
report['scope'] = 'loopback TLS fixture; stalled external XMPP push service; no APNs/FCM'
result = json.dumps(report, indent=2)+'\n'
if args.output:
pathlib.Path(args.output).write_text(result)
print(result, end='')
if __name__ == '__main__':
main()
+62
View File
@@ -0,0 +1,62 @@
use rust_xmpp::sessions::Hub;
use std::{sync::mpsc, thread, time::Duration};
use tokio::sync::mpsc as async_mpsc;
#[test]
fn unrelated_busy_session_does_not_block_account_routing_or_carbons_selection() {
let hub = Hub::default();
let (a_tx, _a_rx) = async_mpsc::channel(8);
let (b_tx, mut b_rx) = async_mpsc::channel(8);
let a = hub
.bind("alice@x/r", "a", a_tx, Duration::from_secs(30))
.unwrap();
let b = hub
.bind("bob@x/r", "b", b_tx, Duration::from_secs(30))
.unwrap();
b.lock().unwrap().available = true;
let held = a.lock().unwrap();
let (done, result) = mpsc::channel();
let worker = thread::spawn(move || {
assert_eq!(hub.message_targets("bob@x").len(), 1);
assert_eq!(hub.for_owners(&["bob@x", "bob@x"]).len(), 1);
assert_eq!(hub.deliver("bob@x", "bare"), 1);
assert_eq!(hub.deliver("bob@x/r", "full"), 1);
done.send(()).unwrap();
});
let completed_while_locked = result.recv_timeout(Duration::from_secs(2)).is_ok();
drop(held);
worker.join().unwrap();
assert!(
completed_while_locked,
"routing waited on unrelated account's session lock"
);
assert_eq!(b_rx.try_recv().unwrap(), "bare");
assert_eq!(b_rx.try_recv().unwrap(), "full");
}
#[test]
fn duplicate_acknowledgement_does_not_rewrite_durable_slot() {
let dir = tempfile::tempdir().unwrap();
let hub = Hub::persistent(dir.path()).unwrap();
let (tx, _rx) = async_mpsc::channel(8);
let session = hub
.bind("bob@x/r", "b", tx, Duration::from_secs(30))
.unwrap();
let mut state = session.lock().unwrap();
state.enable(true).unwrap();
let path = dir
.path()
.join(format!("{}.json", state.resume_id.as_deref().unwrap()));
let marker = std::time::UNIX_EPOCH + Duration::from_secs(1_000_000);
std::fs::File::options()
.write(true)
.open(&path)
.unwrap()
.set_times(std::fs::FileTimes::new().set_modified(marker))
.unwrap();
state.ack_requested = true;
state.acknowledge(0).unwrap();
assert!(!state.ack_requested);
assert_eq!(std::fs::metadata(path).unwrap().modified().unwrap(), marker);
assert!(state.acknowledge(1).is_err());
}