- Исправлена задержка доставки 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). - Личные сообщения и 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:
1 parent
b798b3f9b7
commit
e9329ccbb2
48 files changed
+11073
-235
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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 после загрузки репозитория и настройки
|
||||
|
||||
@@ -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).
|
||||
@@ -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
@@ -1111,7 +1111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-xmpp"
|
||||
version = "0.8.0"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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).
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 .` по-прежнему
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+1641
-35
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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());
|
||||
}
|
||||
Reference in new issue
Block a user